aboutsummaryrefslogtreecommitdiff
path: root/gerboweb/deploy/notification_proxy.py
diff options
context:
space:
mode:
Diffstat (limited to 'gerboweb/deploy/notification_proxy.py')
-rw-r--r--gerboweb/deploy/notification_proxy.py179
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()
-