more code separation

This commit is contained in:
Dominik Chilla 2022-02-27 00:54:42 +01:00
parent 3f60e09a67
commit c93916cf19
7 changed files with 111 additions and 77 deletions

View File

@ -1,19 +1,14 @@
import Milter import Milter
import sys
import traceback import traceback
import string import string
import random import random
import re import re
import email.utils import email.utils
import authres import authres
from lam_config import LamConfig, LamConfigException from lam_globals import g_config, g_policy_backend
from lam_rex import rex_domain, rex_srs from lam_rex import g_rex_domain, g_rex_srs
from lam_logger import init_logger, log_debug, log_info, log_warning, log_error from lam_logger import log_debug, log_info, log_warning, log_error
from lam_policy import LamPolicyBackend, LamPolicyBackendException, LamSoftException, LamHardException from lam_exceptions import LamSoftException, LamHardException
# Globals...
g_config = None
g_policy_backend = None
class LdapAclMilter(Milter.Base): class LdapAclMilter(Milter.Base):
# Each new connection is handled in an own thread # Each new connection is handled in an own thread
@ -38,9 +33,6 @@ class LdapAclMilter(Milter.Base):
log_info(log_line) log_info(log_line)
elif kwargs['level'] == 'debug': elif kwargs['level'] == 'debug':
log_debug(log_line) log_debug(log_line)
else:
print("do_log(): invalid 'level' {}".format(kwargs['level']))
sys.exit(1)
def log_error(self, log_message): def log_error(self, log_message):
self.do_log(level='error', log_message=log_message) self.do_log(level='error', log_message=log_message)
def log_warn(self, log_message): def log_warn(self, log_message):
@ -132,7 +124,6 @@ class LdapAclMilter(Milter.Base):
# this may fail, if no x509 client certificate was used. # this may fail, if no x509 client certificate was used.
# postfix only passes this macro to milters if the TLS connection # postfix only passes this macro to milters if the TLS connection
# with the authenticating client was trusted in a x509 manner! # with the authenticating client was trusted in a x509 manner!
# http://postfix.1071664.n5.nabble.com/verification-levels-and-Milter-tp91634p91638.html
# Unfortunately, postfix only passes the CN-field of the subject/issuer DN :-/ # Unfortunately, postfix only passes the CN-field of the subject/issuer DN :-/
x509_subject = self.getsymval('{cert_subject}') x509_subject = self.getsymval('{cert_subject}')
if x509_subject != None: if x509_subject != None:
@ -169,14 +160,14 @@ class LdapAclMilter(Milter.Base):
# Strip out Simple Private Signature (PRVS) # Strip out Simple Private Signature (PRVS)
mailfrom = re.sub(r"^prvs=.{10}=", '', mailfrom) mailfrom = re.sub(r"^prvs=.{10}=", '', mailfrom)
# SRS (https://www.libsrs2.org/srs/srs.pdf) # SRS (https://www.libsrs2.org/srs/srs.pdf)
m_srs = rex_srs.match(mailfrom) m_srs = g_rex_srs.match(mailfrom)
if m_srs != None: if m_srs != None:
self.log_info("Found SRS-encoded envelope-sender: {}".format(mailfrom)) self.log_info("Found SRS-encoded envelope-sender: {}".format(mailfrom))
mailfrom = m_srs.group(2) + '@' + m_srs.group(1) mailfrom = m_srs.group(2) + '@' + m_srs.group(1)
self.log_info("SRS envelope-sender replaced with: {}".format(mailfrom)) self.log_info("SRS envelope-sender replaced with: {}".format(mailfrom))
self.env_from = mailfrom.lower() self.env_from = mailfrom.lower()
self.log_debug("5321.from={}".format(self.env_from)) self.log_debug("5321.from={}".format(self.env_from))
m = rex_domain.match(self.env_from) m = g_rex_domain.match(self.env_from)
if m == None: if m == None:
return self.milter_action( return self.milter_action(
action = 'reject', action = 'reject',
@ -224,7 +215,7 @@ class LdapAclMilter(Milter.Base):
if(hname.lower() == "From".lower()): if(hname.lower() == "From".lower()):
hdr_5322_from = email.utils.parseaddr(hval) hdr_5322_from = email.utils.parseaddr(hval)
self.hdr_from = hdr_5322_from[1].lower() self.hdr_from = hdr_5322_from[1].lower()
m = re.match(rex_domain, self.hdr_from) m = re.match(g_rex_domain, self.hdr_from)
if m is None: if m is None:
return self.milter_action( return self.milter_action(
action = 'reject', action = 'reject',
@ -329,31 +320,3 @@ class LdapAclMilter(Milter.Base):
# Clean up any external resources here. # Clean up any external resources here.
self.proto_stage = 'CLOSE' self.proto_stage = 'CLOSE'
return self.milter_action(action = 'continue') return self.milter_action(action = 'continue')
if __name__ == "__main__":
init_logger()
try:
g_config = LamConfig()
except LamConfigException as e:
log_info("A config error was raised: {}".format(e))
sys.exit(1)
try:
g_policy_backend = LamPolicyBackend(g_config)
except LamPolicyBackendException as e:
log_error("An backend init error was raised: {}".format(e))
sys.exit(1)
try:
timeout = 600
# Register to have the Milter factory create instances of your class:
Milter.factory = LdapAclMilter
# Tell the MTA which features we use
flags = Milter.ADDHDRS
Milter.set_flags(flags)
log_info("Starting {0}@socket: {1} in mode {2}".format(
g_config.milter_name, g_config.milter_socket, g_config.milter_mode
))
Milter.runmilter(g_config.milter_name, g_config.milter_socket, timeout, True)
log_info("Shutdown {}".format(g_config.milter_name))
except:
log_error("MAIN-EXCEPTION: {}".format(traceback.format_exc()))
sys.exit(1)

View File

@ -1,13 +1,8 @@
import re import re
import os import os
from lam_rex import rex_email
from lam_logger import log_info from lam_logger import log_info
from lam_exceptions import LamConfigException
class LamConfigException(Exception): from lam_rex import g_rex_email
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
class LamConfig(): class LamConfig():
def __init__(self): def __init__(self):
@ -73,7 +68,7 @@ class LamConfig():
# A blank separated list is expected # A blank separated list is expected
whitelisted_rcpts_str = os.environ['MILTER_WHITELISTED_RCPTS'] whitelisted_rcpts_str = os.environ['MILTER_WHITELISTED_RCPTS']
for whitelisted_rcpt in re.split(',|\s', whitelisted_rcpts_str): for whitelisted_rcpt in re.split(',|\s', whitelisted_rcpts_str):
if rex_email.match(whitelisted_rcpt) == None: if g_rex_email.match(whitelisted_rcpt) == None:
raise LamConfigException( raise LamConfigException(
"ENV[MILTER_WHITELISTED_RCPTS]: invalid email address: {}" "ENV[MILTER_WHITELISTED_RCPTS]: invalid email address: {}"
.format(whitelisted_rcpt) .format(whitelisted_rcpt)

20
app/lam_exceptions.py Normal file
View File

@ -0,0 +1,20 @@
class LamException(Exception):
def __init__(self, message="General exception message"):
self.message = message
def __str__(self) -> str:
return self.message
class LamInitException(LamException):
pass
class LamSoftException(LamException):
pass
class LamHardException(LamException):
pass
class LamPolicyBackendException(LamException):
pass
class LamConfigException(LamException):
pass

26
app/lam_globals.py Normal file
View File

@ -0,0 +1,26 @@
import sys
from lam_exceptions import (
LamInitException, LamPolicyBackendException, LamConfigException
)
from lam_config import LamConfig
from lam_logger import init_logger
from lam_policy_backend import LamPolicyBackend
init_logger()
g_config = None
try:
if g_config is None:
g_config = LamConfig()
except LamConfigException as e:
raise LamInitException(e.message) from e
# Instantiate the LDAP policy backend and
# establish a permanent connection with the LDAP server
# which will be reused on any milter connection
g_policy_backend = None
try:
if g_policy_backend is None:
g_policy_backend = LamPolicyBackend(g_config)
except LamPolicyBackendException as e:
raise LamInitException(e.message) from e

View File

@ -1,25 +1,13 @@
import re import re
from lam_logger import log_info, log_debug, log_error from lam_logger import log_info, log_debug, log_error
from lam_rex import rex_domain from lam_rex import g_rex_domain
from ldap3 import ( from ldap3 import (
Server, Connection, NONE, set_config_parameter Server, Connection, NONE, set_config_parameter
) )
from ldap3.core.exceptions import LDAPException from ldap3.core.exceptions import LDAPException
from lam_exceptions import (
class LamException(Exception): LamPolicyBackendException, LamHardException, LamSoftException
def __init__(self, message="General exception message"): )
self.message = message
def __str__(self) -> str:
return self.message
class LamSoftException(LamException):
pass
class LamHardException(LamException):
pass
class LamPolicyBackendException(LamException):
pass
class LamPolicyBackend(): class LamPolicyBackend():
def __init__(self, lam_config): def __init__(self, lam_config):
@ -28,11 +16,17 @@ class LamPolicyBackend():
try: try:
set_config_parameter("RESTARTABLE_SLEEPTIME", 2) set_config_parameter("RESTARTABLE_SLEEPTIME", 2)
set_config_parameter("RESTARTABLE_TRIES", 2) set_config_parameter("RESTARTABLE_TRIES", 2)
server = Server(self.config.ldap_server, get_info=NONE) server = Server(
self.config.ldap_server,
connect_timeout = 3,
get_info = NONE
)
self.ldap_conn = Connection(server, self.ldap_conn = Connection(server,
self.config.ldap_binddn, self.config.ldap_bindpw, self.config.ldap_binddn,
auto_bind=True, raise_exceptions=True, self.config.ldap_bindpw,
client_strategy='RESTARTABLE' auto_bind = True,
raise_exceptions = True,
client_strategy = 'RESTARTABLE'
) )
log_info("Connected to LDAP-server: {}".format(self.config.ldap_server)) log_info("Connected to LDAP-server: {}".format(self.config.ldap_server))
except LDAPException as e: except LDAPException as e:
@ -45,7 +39,7 @@ class LamPolicyBackend():
rcpt_addr = kwargs['rcpt_addr'] rcpt_addr = kwargs['rcpt_addr']
from_source = kwargs['from_source'] from_source = kwargs['from_source']
lam_session = kwargs['lam_session'] lam_session = kwargs['lam_session']
m = rex_domain.match(from_addr) m = g_rex_domain.match(from_addr)
if m == None: if m == None:
log_info("Could not determine domain of from={0}".format( log_info("Could not determine domain of from={0}".format(
from_addr from_addr
@ -53,7 +47,7 @@ class LamPolicyBackend():
raise LamSoftException() raise LamSoftException()
from_domain = m.group(1) from_domain = m.group(1)
log_debug("from_domain={}".format(from_domain)) log_debug("from_domain={}".format(from_domain))
m = rex_domain.match(rcpt_addr) m = g_rex_domain.match(rcpt_addr)
if m == None: if m == None:
raise LamHardException( raise LamHardException(
"Could not determine domain of rcpt={0}".format( "Could not determine domain of rcpt={0}".format(

View File

@ -1,6 +1,7 @@
import re import re
rex_domain = re.compile(r'^\S*@(\S+)$') # globaly used regex definitions
g_rex_domain = re.compile(r'^\S*@(\S+)$')
# http://emailregex.com/ -> Python # http://emailregex.com/ -> Python
rex_email = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)") g_rex_email = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")
rex_srs = re.compile(r"^SRS0=.+=.+=(\S+)=(\S+)\@.+$") g_rex_srs = re.compile(r"^SRS0=.+=.+=(\S+)=(\S+)\@.+$")

35
app/run_milter.py Normal file
View File

@ -0,0 +1,35 @@
import Milter
import sys
import traceback
from lam_exceptions import LamInitException
from lam_logger import log_info, log_error
try:
import lam_globals
except LamInitException as e:
log_error("Init exception: {}".format(e.message))
sys.exit(1)
from lam import LdapAclMilter
if __name__ == "__main__":
try:
timeout = 600
# Register to have the Milter factory create instances of your class:
Milter.factory = LdapAclMilter
# Tell the MTA which features we use
flags = Milter.ADDHDRS
Milter.set_flags(flags)
log_info("Starting {0}@socket: {1} in mode {2}".format(
lam_globals.g_config.milter_name,
lam_globals.g_config.milter_socket,
lam_globals.g_config.milter_mode
))
Milter.runmilter(
lam_globals.g_config.milter_name,
lam_globals.g_config.milter_socket,
timeout,
True
)
log_info("Shutdown {}".format(lam_globals.g_config.milter_name))
except:
log_error("MAIN-EXCEPTION: {}".format(traceback.format_exc()))
sys.exit(1)