diff options
Diffstat (limited to 'gerboweb/deploy/notification_proxy.py')
-rw-r--r-- | gerboweb/deploy/notification_proxy.py | 179 |
1 files changed, 0 insertions, 179 deletions
diff --git a/gerboweb/deploy/notification_proxy.py b/gerboweb/deploy/notification_proxy.py deleted file mode 100644 index 117f8e1..0000000 --- a/gerboweb/deploy/notification_proxy.py +++ /dev/null @@ -1,179 +0,0 @@ -import smtplib -import ssl -import email.utils -import hmac -from email.mime.text import MIMEText -from datetime import datetime -import time -import functools -import json -import binascii -import uwsgidecorators - -import sqlite3 - -from flask import Flask, request, abort - -app = Flask(__name__) -app.config.from_pyfile('config.py') - -db = sqlite3.connect(app.config['SQLITE_DB'], check_same_thread=False) -with db as conn: - conn.execute('''CREATE TABLE IF NOT EXISTS seqs_seen - (route_name TEXT PRIMARY KEY, - seq INTEGER)''') - conn.execute('''CREATE TABLE IF NOT EXISTS time_seen - (route_name TEXT PRIMARY KEY)''') - - conn.execute('''CREATE TABLE IF NOT EXISTS heartbeats_seen - (route_name TEXT PRIMARY KEY, - timestamp INTEGER, - notified INTEGER)''') - # Clear table on startup to avoid spurious notifications - conn.execute('''DELETE FROM heartbeats_seen''') - -mail_routes = {} - -def mail_route(name, receiver, secret): - def wrap(func): - global routes - mail_routes[name] = (receiver, func, secret) - return func - return wrap - - -def authenticate(route_name, secret, clock_delta_tolerance:'s'=120): - with db as conn: - if not request.is_json: - print('Rejecting notification: Incorrect content type') - abort(400) - - if not 'auth' in request.json and 'payload' in request.json: - print('Rejecting notification: signature or payload not found') - abort(400) - - if not isinstance(request.json['auth'], str): - print('Rejecting notification: signature is of incorrect type') - abort(400) - their_digest = binascii.unhexlify(request.json['auth']) - - our_digest = hmac.digest(secret.encode('utf-8'), request.json['payload'].encode('utf-8'), 'sha256') - if not hmac.compare_digest(their_digest, our_digest): - print('Rejecting notification: Incorrect signature') - abort(403) - - try: - payload = json.loads(request.json['payload']) - except: - print('Rejecting notification: Payload is not JSON') - abort(400) - - last_seqnum = conn.execute('SELECT seq FROM seqs_seen WHERE route_name = ?', (route_name,)).fetchone() or 0 - # We can check for seq here: Only an attacker with knowledge of the secret would be able to remove - # seq from a message. This means for a single key, only messages with or without seq may ever be used. - if 'seq' in payload: - seq = payload['seq'] - if not isinstance(seq, int): - print('Rejecting notification: seq of wrong type') - abort(400) - - if seq <= last_seqnum: - print('Rejecting notification: seq out of order') - abort(400) - - conn.execute('INSERT OR REPLACE INTO seqs_seen VALUES (?, ?)', (route_name, seq)) - - elif last_seqnum: - print('Rejecting notification: seq not included but past messages included seq') - abort(400) - - msg_time = None - if 'time' in payload: - msg_time = payload['time'] - if not isinstance(msg_time, int): - print('Rejecting notification: time of wrong type') - abort(400) - - if abs(msg_time - int(time.time())) > clock_delta_tolerance: - print('Rejecting notification: timestamp too far in the future or past') - abort(400) - - conn.execute('INSERT OR REPLACE INTO time_seen VALUES (?)', (route_name,)) - - elif conn.execute('SELECT * FROM time_seen WHERE route_name = ?', (route_name,)).fetchone(): - print('Rejecting notification: time not included but past messages included time') - abort(400) - - if msg_time is None: - msg_time = int(time.time()) - - return msg_time, payload['scope'], payload['d'] - -@mail_route('klingel', 'computerstuff@jaseg.de', app.config['SECRET_KLINGEL']) -def klingel(classification='somewhere', rms=None, capture=None, **kwargs): - return (f'It rang {classification}!', - f'rms={rms}\ncapture={capture}\nextra_args={kwargs}') - - -def send_mail(route_name, receiver, subject, body): - try: - context = ssl.create_default_context() - smtp = smtplib.SMTP_SSL(app.config['SMTP_HOST'], app.config['SMTP_PORT']) - smtp.login('apikey', app.config['SENDGRID_APIKEY']) - - sender = f'{route_name}@{app.config["DOMAIN"]}' - - msg = MIMEText(body) - msg['Subject'] = subject - msg['From'] = sender - msg['To'] = receiver - msg['Date'] = email.utils.formatdate() - - smtp.sendmail(sender, receiver, msg.as_string()) - finally: - smtp.quit() - -@app.route('/v1/notify/<route_name>', methods=['POST']) -def notify(route_name): - receiver, func, secret = mail_routes[route_name] - msg_time, scope, kwargs = authenticate(route_name, secret) - - if scope == 'default': - # Exceptions will yield a 500 error - subject, body = func(**kwargs) - send_mail(route_name, receiver, subject, body or 'empty message') - - elif scope == 'info': - send_mail(route_name, receiver, f'System info: {kwargs["info_msg"]}', f'Logged data: {kwargs}') - - elif scope == 'boot': - formatted = datetime.utcfromtimestamp(msg_time).isoformat() - send_mail(route_name, receiver, 'System startup', f'System powered up at {formatted}') - - elif scope == 'heartbeat': - with db as conn: - conn.execute('INSERT OR REPLACE INTO heartbeats_seen VALUES (?, ?, 0)', (route_name, int(time.time()))) - - elif scope == 'error': - print(f'Device error: {kwargs}') - - return 'success' - -@uwsgidecorators.timer(60) -def heartbeat_timer(_uwsgi_signum): - threshold = int(time.time()) - app.config['HEARTBEAT_TIMEOUT'] - with db as conn: - for route, ts in db.execute( - 'SELECT route_name, timestamp FROM heartbeats_seen WHERE timestamp <= ? AND notified == 0', - (threshold,)).fetchall(): - print(f'Heartbeat expired for {route}: {ts} < {threshold}') - - receiver, *_ = mail_routes[route] - last = datetime.utcfromtimestamp(ts).isoformat() - - send_mail(route, receiver, 'Heartbeat timeout', f'Last heartbeat at {last}') - db.execute('UPDATE heartbeats_seen SET notified = ? WHERE route_name = ?', (int(time.time()), route)) - -if __name__ == '__main__': - app.run() - |