%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /opt/floodGuard/
Upload File :
Create Path :
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...")

Zerion Mini Shell 1.0