Files
ocr_scanner/server.py
T

116 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Statischer Server mit Passwortschutz fuer die RAM-Sortierhilfe.
Liefert das gebaute Ergebnis aus dist/ aus, gibt aber ohne gueltiges Passwort
nichts heraus - weder HTML noch Skripte. Nutzt ausschliesslich die
Python-Standardbibliothek, damit keine zusaetzliche Abhaengigkeit entsteht.
Das Passwort kommt aus der Umgebungsvariable APP_PASSWORD. Fehlt sie, startet
der Server absichtlich nicht: Ein vergessener Eintrag soll auffallen, statt
stillschweigend eine ungeschuetzte Seite ins Netz zu stellen.
"""
import base64
import hmac
import os
import sys
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
# Absolut aufgeloest, damit es nicht darauf ankommt, aus welchem Verzeichnis
# die Hosting-Umgebung den Server startet.
SERVE_DIRECTORY = str(Path(__file__).resolve().parent / 'dist')
HEALTH_PATH = '/healthz'
REALM = 'RAM-Sortierhilfe'
def read_password():
"""Liest das Passwort aus der Umgebung. Fehlt es, wird der Start abgebrochen."""
password = os.environ.get('APP_PASSWORD', '')
if not password:
sys.stderr.write(
'FEHLER: Die Umgebungsvariable APP_PASSWORD ist nicht gesetzt.\n'
'Der Server startet nicht, damit die Seite nicht ungeschuetzt\n'
'erreichbar wird. Setze das Passwort in den VCH-Einstellungen.\n'
)
sys.exit(1)
return password
def build_handler(password):
"""Erzeugt einen Handler, der das Passwort per HTTP-Basic-Auth prueft."""
expected = base64.b64encode(f':{password}'.encode('utf-8'))
class ProtectedHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=SERVE_DIRECTORY, **kwargs)
def is_authorised(self):
"""Prueft nur das Passwort; der Benutzername darf beliebig sein."""
header = self.headers.get('Authorization', '')
if not header.startswith('Basic '):
return False
try:
decoded = base64.b64decode(header[6:].strip(), validate=True)
except (ValueError, TypeError):
return False
_, separator, supplied = decoded.partition(b':')
if not separator:
return False
# Zeitkonstanter Vergleich, damit die Antwortdauer nichts verraet.
return hmac.compare_digest(
base64.b64encode(b':' + supplied), expected
)
def demand_password(self):
self.send_response(401)
self.send_header('WWW-Authenticate', f'Basic realm="{REALM}", charset="UTF-8"')
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', '0')
self.end_headers()
def answer_health(self):
"""Lebenszeichen fuer die Ueberwachung - ohne Passwort, ohne Inhalt."""
body = b'ok'
self.send_response(200)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
if self.command != 'HEAD':
self.wfile.write(body)
def guard(self):
"""True, wenn die Anfrage weiterverarbeitet werden darf."""
if self.path.split('?', 1)[0] == HEALTH_PATH:
self.answer_health()
return False
if not self.is_authorised():
self.demand_password()
return False
return True
def do_GET(self):
if self.guard():
super().do_GET()
def do_HEAD(self):
if self.guard():
super().do_HEAD()
return ProtectedHandler
def main():
password = read_password()
port = int(os.environ.get('PORT', '8000'))
server = ThreadingHTTPServer(('0.0.0.0', port), build_handler(password))
sys.stderr.write(f'RAM-Sortierhilfe laeuft auf Port {port}, Zugang nur mit Passwort.\n')
try:
server.serve_forever()
except KeyboardInterrupt:
server.server_close()
if __name__ == '__main__':
main()