%PDF- %PDF-
| Direktori : /opt/floodGuard/ |
| Current File : //opt/floodGuard/floodGuard.py |
import os
import os.path
from os.path import exists
import traceback
import socket
import subprocess
from subprocess import Popen, PIPE, check_output
import time
from datetime import datetime
### DOKU
# https://confluence.sec.dogado.net/display/WUM/floodGuard
# GIT: https://git.sec.dogado.net/mpa/floodguard
# Version 20221019-01
### default config
DEBUG=False
MAILSPERRUN=50
WAITTIMESEC=60
LOGFILE='/var/log/floodGuard.log' # default file for log
PATH=os.path.dirname(os.path.realpath(__file__)) + '/'
hostname=socket.gethostname() # needed to validate if the mail is incoming (only valid for mails mailbox@hostname) # WUM-2821
### function for logging
def Log(message):
msg = ""
try:
msg = str(datetime.now()) + ": " + message
f=open(LOGFILE, "a")
print (msg)
f.write(msg + "\n")
f.close()
except:
print ("Logfile " + LOGFILE + " not writable! continue. Message was: " + msg)
pass # if log cant be written then the software should proceed without log
def Debug(message):
if DEBUG:
Log("DEBUG: " + message)
### read configfile
if os.path.isfile('/etc/floodGuard.conf'): # no file to work with
f=open('/etc/floodGuard.conf', 'r')
if f.mode != 'r':
Log ("can not open holded.list for reading.")
f.Close()
else:
lines = f.readlines()
Log("read configfile")
for line in lines:
try:
obj = line.split()
flag = obj[0].lower()
value = obj[1]
if flag == "debug":
DEBUG = value.lower()=="true"
elif flag == "mailsperrun":
MAILSPERRUN = int(value)
elif flag == "waittimesec":
WAITTIMESEC = int(value)
elif flag == "logfile":
LOGFILE = value
elif flag == "python3bin":
continue
else:
Log("error by reading of config. Flag " + flag + " not recognised")
except:
Log("error by reading of config. Flag " + flag + " raised error")
### cleanup and minvalues
if os.path.exists(PATH + 'stop'):
os.remove(PATH + 'stop')
if WAITTIMESEC < 10:
WAITTIMESEC = 10
if MAILSPERRUN < 1:
MAILSPERRUN = 1
internalcyclecount = int(WAITTIMESEC / 2)
### MAINPROGRAM
def main():
Log("start floodGuard")
Log("getting domainlist (if it is a plesk-server)")
domainlist = Domainlist()
domainlistrefresh = 0;
maillist = Maillist()
### read old stand from a possibly crashed run
if os.path.isfile(PATH + 'holded.list'): # no file to work with
f=open(PATH + "holded.list", "r")
if f.mode != 'r':
Log ("can not open holded.list for reading.")
f.Close()
else:
lines = f.readlines()
Debug("holded.list has " + str(len(lines)) + " lines")
# analyse files
mailsfromfile = []
for line in lines:
data = line.split()
ID = data[1]
sender = data[0]
mailsfromfile.append(Mail(ID, sender, True))
# create list of all currently holded mailIDs
output=None
err=None
exit_code=None
try:
process = Popen(["mailq |awk '{print $1}' | grep '!' | tr -d '!'"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = process.communicate()
exit_code = process.wait()
except OSError as e:
Log("calling mailq caused an systemerror: " + e.strerror)
if exit_code != 0:
Log ("a softerror occured during mailq. error: " + str(err))
else:
output = output.decode("utf-8").splitlines()
# check which IDs from file are still in mailq hold
if output != None: # if no output is set, skip
if len(mailsfromfile) > 0 and len(output) > 0: # check only if both lists are not empty
for m in mailsfromfile:
for i in output:
if i == m.GetID(): # mail from holded.list is also in mailq holding
Debug("mail with ID " + i + " is valid (it exists)")
maillist.AddMail(m)
break # found - no need to search longer for this combination
f.close()
Log("restarting with " + str(maillist.CountMails()) + " currently holded ids from " + str(maillist.CountSenders()) + " senders.")
running=True
# data contains a list of all currently holded mails (from previews floodGuard-sessions)
while running:
domainlistrefresh += 1
if domainlistrefresh >= 10:
domainlistrefresh = 0
domainlist.refresh()
maillist = refresh_maillist(maillist,domainlist)
# at this point all mails that are holded by the software and still exists (as holded) plus all 'free' mails are in the maillist-object - sorted by senders
Log ("Currently mails in processing: " + str(maillist.CountMails()))
# check if user has less than 20 Mails in sendqueue and 0 holded --> remove user from monitoring
if maillist.CountSenders() > 0:
for s in maillist.GetSenders():
if s.CountMails(False) <= MAILSPERRUN and s.CountMails(True) == 0:
#Log ("Sender " + s.GetSendername() + " has no more mails that have to be watched. Remove sender from watchlist.")
maillist.RemoveSender(s)
if maillist.CountSenders() > 0:
for s in maillist.GetSenders():
if s.CountMails(False) > MAILSPERRUN:
while s.CountMails(False) >= MAILSPERRUN: # Hold mails until is reached
s.HoldNext() # hold next mail
else:
if s.CountMails(True) > 0:
while s.CountMails(False) < MAILSPERRUN:
if s.ReleaseNext() == False: # release mail - if there is no more mail then break
break
if s.CountMails(True) == 0:
maillist.RemoveSender(s)
else:
maillist.RemoveSender(s)
# added 20200724 - floodguard writes down which ids are not holded from him
try:
process = Popen(["mailq |awk '{print $1}' | grep '!' | tr -d '!'"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = process.communicate()
exit_code = process.wait()
except OSError as e:
Log("calling mailq caused an systemerror: " + e.strerror + "skipping creation of holded_external.list")
if exit_code != 0:
Log ("a softerror occured during mailq. error: " + str(err))
else:
output = output.decode("utf-8").splitlines()
try:
f=open(PATH + 'holded_external.list', "w")
if f.mode != 'w':
Log ("can not open holded_external.list for writing. continue")
else:
for i in output:
mail = maillist.GetMail(i)
if mail == None: # mailid is not in maillist
# count this
f.write(str(i) + '\n')
else:
if not mail.GetHolded(): # mail is in maillist and is not holded by the software
#and this
f.write(str(i) + '\n')
f.close()
except:
Log ("something went wrong by opening holded.list - continue")
# after
if maillist.CountSenders() > 0:
# save all sender with their mails
try:
f=open(PATH + 'holded.list', "w")
if f.mode != 'w':
Log ("can not open holded.list for writing. no backup from memory created. Please do not stop the software until the error is fixed")
else:
# write all lines to holded.list like <sender> <Mail-ID>
for s in maillist.GetSenders():
for m in s.GetAllMails():
if m.GetHolded() == True:
f.write(s.GetSendername() + ' ' + m.GetID() + '\n')
f.close()
except:
Log ("something went wrong by opening holded.list - continue")
else:
# delete holded.list
if os.path.exists(PATH + 'holded.list'):
os.remove(PATH + 'holded.list')
for s in maillist.GetSenders():
c = s.CountMails(True)
if c > 0:
Log ("Sender " + s.GetSendername() + " has " + str(c) + " mails that are hold back from floodGuard.")
# processing finished - waiting
for x in range(1, internalcyclecount):
if os.path.exists(PATH + 'stop'):
os.remove(PATH + 'stop')
Log("floodGuard stopped.")
running = False
break
else:
time.sleep(2)
# END from while loop
############################################################################################################################################################
def ReleaseMail(id):
Debug("release " + id)
return postsuper("-H", id)
def HoldMail(id):
Debug("hold " + id)
return postsuper("-h", id)
def postsuper(parameter, id):
FNULL = open(os.devnull, 'w')
process = Popen(["postsuper", parameter, id], stdout=FNULL, stderr=FNULL)
process.communicate()
exit_code = process.wait()
Debug(">> postsuper " + parameter + " " + id + " returns " + str(exit_code))
return exit_code == 0
def refresh_maillist(mlist,whitelist):
# create new list from this both lists
new = Maillist()
# get all mails from mlist that are holded
for s in mlist.GetSenders():
for m in s.GetAllMails():
if m.GetHolded():
new.AddMail(m)
# get all mails from mailq that are in processing
err=None
output=None
exit_code=None
try:
process = Popen(["mailq"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = process.communicate()
exit_code = process.wait()
except OSError as e:
Log("calling mailq caused an systemerror: " + e.strerror)
return []
if exit_code != 0:
Log ("a softerror occured during mailq. error: " + str(err.decode("utf-8")))
return []
Debug("mailq output " + str(len(output)) + " chars")
output = output.decode("utf-8").splitlines()
Debug("get_mailq has " + str(len(output)) + " lines to analyse")
skipping = False
currentMail = None
for rawline in output:
line = rawline.strip()
if skipping:
if line == "": # skip textblock (until a line is empty)
currentMail = None; # should there be an mail in processig remove it
skipping = False
continue
else:
continue
if line == "Mail queue is empty": # nothing to analyse - return
return new # empty maillist
if line == "": # end ob mailblock reached. If there is a mail then add it to list
if currentMail != None:
# check if it is an incomming mail ( set recv contains hostname)
internal = True
for r in currentMail.GetReceivers():
if hostname not in r and not whitelist.contains(r): # if hostname not in mailaddrss and whitelist does not know the mailaddr then its not internal
internal = False
if not internal:
Debug("adding mail " + currentMail.GetID() + " to global list. from: " + currentMail.GetSender() + " to: " + ','.join(currentMail.GetReceivers()))
new.AddMail(currentMail)
else:
Debug("mail " + currentMail.GetID() + " has only local receivers. skip it.")
currentMail = None
continue
firstchar = line[0] # first character of the given line
if firstchar == '-' or firstchar == '(': # or firstchar == ' ': # message from other server (like 'can not connect') - ignore it
continue
values = line.split()
if (len(values)) > 1: # this is the startline of a mailblock
try: # try to analyse the line
id = None
v1 = values[0] # id
if v1.endswith('!'): # waiting - also this is the start of a mail
Debug("found mail " + str(v1[:-1]) + " in mailq with status hold - skipping")
skipping = True
continue # mail is holded (either by this program or another - In both cases ignore it!)
else: # active [ ] or processing [*]
if v1.endswith('*'):
id = v1[:-1]
Debug("found mail " + id + " in mailq with status processing")
else:
id = v1
Debug("found mail " + id + " in mailq with status waiting")
sender = values[6]
currentMail = Mail(id, sender, False) # create mail from the first line of the block
except Exception as e: # something went wrong by analising the line
Log("ERROR: FLOODGUARD CRASHED while analysing the mailq startline from one mail: " + str(e))
Log("the line where it crashed was: " + line)
Log(str(traceback.format_exc()))
# dump the mailq for debugging
process = Popen(["mailq"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = process.communicate()
exit_code = process.wait()
output = output.decode("utf-8").splitlines()
Log("mailq output at this time:")
for s in output:
Log(s)
Log("I ignore this error and go on...")
pass
else: # there is only one item in this line - must be an receive-address
recv = values[0]
Debug("add receiver " + recv + " to mail " + currentMail.GetID())
currentMail.AddReceiver(recv)
return new
# holds a list of all local domains (excluding subdomains from $hostname - these are filtered out through another rule - no need to do more work than nessesary)
class Domainlist:
# set(str) domains
domains = set() # create empty set
def __init__(self):
self.refresh()
def refresh(self):
if exists("/sbin/plesk"): # this is a plesk server - get domainlist
try:
rawout = subprocess.Popen("/sbin/plesk bin domain --list|sed 's/^*.//g'|sort -u|grep -v $(hostname)$", shell=True, stdout=subprocess.PIPE)
lines = rawout.stdout.read().decode().splitlines()
if len(lines) > 0:
self.domains = set() # clear list for rebuild (so old entries can expire)
for line in lines:
self.domains.add(line.strip()) # add every domain to the set. to future-dev: here you can add checks if you want to filter some out
Log("refreshed domainlist. Got " + str(len(self.domains)) + " domains from plesk.")
except Exception as e: # something went wrong
Log("This is a plesk server, but something went wrong by getting the domainlist: " + str(e))
self.domains = set() # go ahead with empty set.
else: # no plesk
self.domains = set()
# can get a domain or a mailaddr and checks if the domain(or addr) is in the list
def contains(self, string):
if "@" in string: # got a mailaddr
domain = string[string.rfind("@") + 1:] # cut after last occurence of @ and get last part (domainpart)
return domain in self.domains
else: # must be a domain
return string in self.domains
class Maillist:
# set(Sender) senderlist
def __init__(self):
self.senderlist = set()
def SenderExists(self, sendername):
for s in self.sender:
if s.GetSendername() == sendername:
return True
return False
# adds a new mail to the sender. If the sender does not exists it will be created.
def AddMail(self, mail):
for s in self.senderlist:
if mail.GetSender() == s.GetSendername():
s.AddMail(mail)
return
# if he comes here the sender is new
s = Sender(mail.GetSender())
s.AddMail(mail)
self.senderlist.add(s)
return
def RemoveSender(self, sendername):
for s in self.senderlist:
if s.GetSendername() == sendername:
self.senderlist.discard(s)
return
# if sender does not exist than there is nothing to do - return
return
# sum of all mailds of all senders regardless of their status
def CountMails(self):
c=0
for s in self.senderlist:
c = c + s.CountAllMails()
return c
def CountSenders(self):
return len(self.senderlist)
def GetSenders(self):
return self.senderlist
def GetMail(self, ID):
for s in self.senderlist:
for m in s.GetAllMails():
if m.GetID() == ID:
return m
return None
class Sender:
# str sendername # will be set by init
# set(Mail) mails
def __init__(self, sendername):
self.sendername = sendername
self.mails = set()
# add a mail to this sender. If the mailid exists it will be overwritten
def AddMail(self, mail):
for m in self.mails:
if m.GetID() == mail.GetID():
# m = mail
return
self.mails.add(mail)
def Remove(self, mail):
for m in self.mails:
if m.GetID() == mail.GetID():
# self.mails.remove(m)
self.mails.discard(m)
def CountAllMails(self):
return len(self.mails)
def CountMails(self, holdstatus): # holdstatus is True or False
c=0
for m in self.mails:
if m.GetHolded() == holdstatus:
c+=1
return c
def HoldNext(self):
for m in self.mails:
if m.GetHolded() == False:
HoldMail(m.GetID())
m.SetHolded(True)
return
return
def ReleaseNext(self):
for m in self.mails:
if m.GetHolded() == True:
ReleaseMail(m.GetID())
m.SetHolded(False)
return True
return False
def GetAllMails(self):
return self.mails
def GetSendername(self):
return self.sendername
class Mail:
# str id
# str sender
## set(str) receiver
# bool holded
def __init__(self, id, sender, holded):
self.id = id
self.sender = sender
self.holded = holded
self.receiver = set()
def SetHolded(self, holded):
self.holded = holded
def GetHolded(self):
return self.holded
def GetID(self):
return self.id
def GetSender(self):
return self.sender
def AddReceiver(self, recv):
self.receiver.add(recv)
def GetReceivers(self):
return self.receiver
if __name__ == "__main__":
try:
main()
except Exception as e: # something went terribly wrong
Log("ERROR: FLOODGUARD CRASHED: " + str(e))
Log(str(traceback.format_exc()))
# dump the mailq for debugging
process = Popen(["mailq"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = process.communicate()
exit_code = process.wait()
output = output.decode("utf-8").splitlines()
Log("mailq output at this time:")
for s in output:
Log(s)
Log("I am dieing now...")