diff --git a/.gitignore b/.gitignore index 0155138..fff7749 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ *.sql *.sqlite3 *.deb +*.ini +!*.sample.ini diff --git a/backend.sample.ini b/backend.sample.ini new file mode 100644 index 0000000..42669d7 --- /dev/null +++ b/backend.sample.ini @@ -0,0 +1,9 @@ +[general] +domain = wg.example.com +database = database.sqlite3 +users = users.txt +#port = 8084 + +[jwt] +secret = key +#invalidate_before = 1970-01-01T00:00:00Z diff --git a/pkg/deb.sh b/pkg/deb.sh index aa9ce6f..96ab1e5 100755 --- a/pkg/deb.sh +++ b/pkg/deb.sh @@ -8,6 +8,12 @@ mkdir -p build/ mkdir -p build/usr/bin/ cp src/elwig-backend build/usr/bin +mkdir -p build/lib/systemd/system/ +cp pkg/elwig-backend.service build/lib/systemd/system + +mkdir -p build/etc/elwig +cp backend.sample.ini build/etc/elwig/backend.ini + mkdir -p build/DEBIAN/ echo -n "\ Package: elwig-backend @@ -17,11 +23,15 @@ Priority: optional Essential: no Architecture: all Depends: python3 -Installed-Size: $(du -ks build/usr/bin/ | cut -f 1) +Installed-Size: $(du -ks build/usr/bin/ build/etc/ build/lib/systemd/system/ | cut -f 1 | paste -sd+ | bc) Maintainer: Lorenz Stechauner Homepage: https://git.necronda.net/winzer/elwig-backend Description: Local backend for Elwig's REST API " > build/DEBIAN/control +echo -n "\ +/etc/elwig/backend.ini +" > build/DEBIAN/conffiles +cp pkg/postinst pkg/prerm pkg/postrm build/DEBIAN chmod 0755 build/usr/bin/* sudo chown root:root -R build/ diff --git a/pkg/elwig-backend.service b/pkg/elwig-backend.service new file mode 100644 index 0000000..bf3d8bf --- /dev/null +++ b/pkg/elwig-backend.service @@ -0,0 +1,13 @@ +[Unit] +Description=Elwig backend +After=network.target + +[Service] +RestartSec=5s +Type=simple +User=nobody +ExecStart=/usr/bin/elwig-backend /etc/elwig/backend.ini +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/pkg/postinst b/pkg/postinst new file mode 100755 index 0000000..22f81a0 --- /dev/null +++ b/pkg/postinst @@ -0,0 +1,25 @@ +#!/bin/sh +set -e +if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then + # This will only remove masks created by d-s-h on package removal. + deb-systemd-helper unmask 'elwig-backend.service' >/dev/null || true + # was-enabled defaults to true, so new installations run enable. + if deb-systemd-helper --quiet was-enabled 'elwig-backend.service'; then + # Enables the unit on first installation, creates new + # symlinks on upgrades if the unit file has changed. + deb-systemd-helper enable 'elwig-backend.service' >/dev/null || true + else + # Update the statefile to add new symlinks (if any), which need to be + # cleaned up on purge. Also remove old symlinks. + deb-systemd-helper update-state 'elwig-backend.service' >/dev/null || true + fi + if [ -d /run/systemd/system ]; then + systemctl --system daemon-reload >/dev/null || true + if [ -n "$2" ]; then + _dh_action=restart + else + _dh_action=start + fi + deb-systemd-invoke $_dh_action 'elwig-backend.service' >/dev/null || true + fi +fi diff --git a/pkg/postrm b/pkg/postrm new file mode 100755 index 0000000..b14733e --- /dev/null +++ b/pkg/postrm @@ -0,0 +1,16 @@ +#!/bin/sh +set -e +if [ -d /run/systemd/system ]; then + systemctl --system daemon-reload >/dev/null || true +fi +if [ "$1" = "remove" ]; then + if [ -x "/usr/bin/deb-systemd-helper" ]; then + deb-systemd-helper mask 'elwig-backend.service' >/dev/null || true + fi +fi +if [ "$1" = "purge" ]; then + if [ -x "/usr/bin/deb-systemd-helper" ]; then + deb-systemd-helper purge 'elwig-backend.service' >/dev/null || true + deb-systemd-helper unmask 'elwig-backend.service' >/dev/null || true + fi +fi diff --git a/pkg/prerm b/pkg/prerm new file mode 100755 index 0000000..1d494e9 --- /dev/null +++ b/pkg/prerm @@ -0,0 +1,5 @@ +#!/bin/sh +set -e +if [ -d /run/systemd/system ] && [ "$1" = remove ]; then + deb-systemd-invoke stop 'elwig-backend.service' >/dev/null || true +fi diff --git a/src/elwig-backend b/src/elwig-backend index 87490e7..d01b31c 100755 --- a/src/elwig-backend +++ b/src/elwig-backend @@ -4,6 +4,8 @@ from __future__ import annotations from typing import Callable, Optional from http.server import BaseHTTPRequestHandler, HTTPServer +import configparser +import os.path import argparse import datetime import time @@ -18,7 +20,7 @@ import hashlib import hmac -VERSION: str = '0.0.4' +VERSION: str = '0.0.6' CNX: sqlite3.Cursor USER_FILE: str @@ -176,7 +178,7 @@ class ElwigApi(BaseHTTPRequestHandler): self.send(f'{{"message":{jdmp(message)}}}\n', status_code=status_code) def see_other(self, url: str) -> None: - self.send(f'{{"url": {jdmp(url)}}}\n', status_code=303, url=url) + self.send(f'{{"url":{jdmp(url)}}}\n', status_code=303, url=url) def authorize(self) -> tuple[str, str, str]: auth = self.headers.get('Authorization') @@ -488,29 +490,29 @@ def main() -> None: sqlite3.register_adapter(datetime.time, lambda t: t.strftime('%H:%M:%S')) parser = argparse.ArgumentParser() - parser.add_argument('db', type=str, metavar='DB') - parser.add_argument('jwt_file', type=str, metavar='JWT-FILE') - parser.add_argument('user_file', type=str, metavar='USER-FILE') - parser.add_argument('-p', '--port', type=int, default=8080) + parser.add_argument('config_file', type=str, metavar='CONFIG_FILE') + parser.add_argument('-p', '--port', type=int) args = parser.parse_args() - jwt_file = args.jwt_file - with open(jwt_file, 'rb') as file: - iss, dt, JWT_SECRET = file.readline().strip().split(b':', 2) - JWT_ISSUER = iss.decode('utf-8') - JWT_INVALIDATE_BEFORE = int(datetime.datetime.fromisoformat(dt.decode('ascii')).timestamp()) if dt else 0 + config = configparser.ConfigParser() + config.read(args.config_file) + JWT_ISSUER = config['general']['domain'] + JWT_INVALIDATE_BEFORE = int(datetime.datetime.fromisoformat(config['jwt'].get('invalidate_before', '1970-01-01T00:00:00Z')).timestamp()) + JWT_SECRET = config['jwt']['secret'].encode('utf-8') - USER_FILE = args.user_file + USER_FILE = os.path.join(os.path.dirname(os.path.abspath(args.config_file)), config['general']['users']) with open(USER_FILE, 'r') as file: for line in file: (u, r, i, p) = line.strip().split(':', 3) JWT_USER_INVALIDATE_BEFORE[u.strip()] = int(datetime.datetime.fromisoformat(i).timestamp()) if i else 0 - CNX = sqlite3.connect(f'file:{args.db}?mode=ro', uri=True) + db_file = os.path.join(os.path.dirname(os.path.abspath(args.config_file)), config['general']['database']) + CNX = sqlite3.connect(f'file:{db_file}?mode=ro', uri=True) CNX.create_function('REGEXP', 2, sqlite_regexp, deterministic=True) - server = HTTPServer(('localhost', args.port), ElwigApi) - print(f'Listening on http://localhost:{args.port}') + port = args.port or int(config['general'].get('port', '8084')) + server = HTTPServer(('localhost', port), ElwigApi) + print(f'Listening on http://localhost:{port}') try: server.serve_forever() except InterruptedError | KeyboardInterrupt: