diff options
Diffstat (limited to 'hosts/surtr/email/ccert-policy-server')
3 files changed, 104 insertions, 0 deletions
diff --git a/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__init__.py b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__init__.py | |||
diff --git a/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py new file mode 100644 index 00000000..f481090c --- /dev/null +++ b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py | |||
@@ -0,0 +1,92 @@ | |||
1 | from systemd.daemon import listen_fds | ||
2 | from sdnotify import SystemdNotifier | ||
3 | from socketserver import StreamRequestHandler, ThreadingMixIn | ||
4 | from systemd_socketserver import SystemdSocketServer | ||
5 | import sys | ||
6 | from threading import Thread | ||
7 | from psycopg_pool import ConnectionPool | ||
8 | from psycopg.rows import namedtuple_row | ||
9 | |||
10 | import logging | ||
11 | |||
12 | |||
13 | class PolicyHandler(StreamRequestHandler): | ||
14 | def handle(self): | ||
15 | logger.debug('Handling new connection...') | ||
16 | |||
17 | self.args = dict() | ||
18 | |||
19 | line = None | ||
20 | while line := self.rfile.readline().removesuffix(b'\n'): | ||
21 | if b'=' not in line: | ||
22 | break | ||
23 | |||
24 | key, val = line.split(sep=b'=', maxsplit=1) | ||
25 | self.args[key.decode()] = val.decode() | ||
26 | |||
27 | logger.info('Connection parameters: %s', self.args) | ||
28 | |||
29 | allowed = False | ||
30 | with self.server.db_pool.connection() as conn: | ||
31 | local, domain = self.args['sender'].split(sep='@', maxsplit=1) | ||
32 | extension = None | ||
33 | if '+' in local: | ||
34 | local, extension = local.split(sep='+', maxsplit=1) | ||
35 | |||
36 | logger.debug('Parsed address: %s', {'local': local, 'extension': extension, 'domain': domain}) | ||
37 | |||
38 | with conn.cursor() as cur: | ||
39 | cur.row_factory = namedtuple_row | ||
40 | cur.execute('SELECT "mailbox"."mailbox" as "user", "local", "extension", "domain" FROM "mailbox" INNER JOIN "mailbox_mapping" ON "mailbox".id = "mailbox_mapping"."mailbox" WHERE "mailbox"."mailbox" = %(user)s AND ("local" = %(local)s OR "local" IS NULL) AND ("extension" = %(extension)s OR "extension" IS NULL) AND "domain" = %(domain)s', params = {'user': self.args['ccert_subject'], 'local': local, 'extension': extension if extension is not None else '', 'domain': domain}, prepare=True) | ||
41 | for record in cur: | ||
42 | logger.debug('Received result: %s', record) | ||
43 | allowed = True | ||
44 | |||
45 | action = '550 5.7.0 Sender address not authorized for current user' | ||
46 | if allowed: | ||
47 | action = 'DUNNO' | ||
48 | |||
49 | logger.info('Reached verdict: %s', {'allowed': allowed, 'action': action}) | ||
50 | self.wfile.write(f'action={action}\n\n'.encode()) | ||
51 | |||
52 | class ThreadedSystemdSocketServer(ThreadingMixIn, SystemdSocketServer): | ||
53 | def __init__(self, fd, RequestHandlerClass): | ||
54 | super().__init__(fd, RequestHandlerClass) | ||
55 | |||
56 | self.db_pool = ConnectionPool(min_size=1) | ||
57 | self.db_pool.wait() | ||
58 | |||
59 | def main(): | ||
60 | global logger | ||
61 | logger = logging.getLogger(__name__) | ||
62 | console_handler = logging.StreamHandler() | ||
63 | console_handler.setFormatter( logging.Formatter('[%(levelname)s](%(name)s): %(message)s') ) | ||
64 | if sys.stderr.isatty(): | ||
65 | console_handler.setFormatter( logging.Formatter('%(asctime)s [%(levelname)s](%(name)s): %(message)s') ) | ||
66 | logger.addHandler(console_handler) | ||
67 | logger.setLevel(logging.DEBUG) | ||
68 | |||
69 | # log uncaught exceptions | ||
70 | def log_exceptions(type, value, tb): | ||
71 | global logger | ||
72 | |||
73 | logger.error(value) | ||
74 | sys.__excepthook__(type, value, tb) # calls default excepthook | ||
75 | |||
76 | sys.excepthook = log_exceptions | ||
77 | |||
78 | fds = listen_fds() | ||
79 | servers = [ThreadedSystemdSocketServer(fd, PolicyHandler) for fd in fds] | ||
80 | |||
81 | if servers: | ||
82 | for server in servers: | ||
83 | Thread(name=f'Server for fd{server.fileno()}', target=server.serve_forever).start() | ||
84 | else: | ||
85 | return 2 | ||
86 | |||
87 | SystemdNotifier().notify('READY=1') | ||
88 | |||
89 | return 0 | ||
90 | |||
91 | if __name__ == '__main__': | ||
92 | sys.exit(main()) | ||
diff --git a/hosts/surtr/email/ccert-policy-server/setup.py b/hosts/surtr/email/ccert-policy-server/setup.py new file mode 100644 index 00000000..d8eb415a --- /dev/null +++ b/hosts/surtr/email/ccert-policy-server/setup.py | |||
@@ -0,0 +1,12 @@ | |||
1 | from setuptools import setup, find_packages | ||
2 | |||
3 | setup( | ||
4 | name = 'ccert-policy-server', | ||
5 | version = '0.0.0', | ||
6 | packages = ['ccert_policy_server'], | ||
7 | entry_points = { | ||
8 | 'console_scripts': [ | ||
9 | 'ccert-policy-server=ccert_policy_server.__main__:main' | ||
10 | ], | ||
11 | }, | ||
12 | ) | ||