From 6775d40e9243e7144566a296acfacea275322fb8 Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Sat, 6 Jun 2026 20:13:51 +0200 Subject: reorganize files --- src/app.py | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/html | 1 + src/server.py | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 310 insertions(+) create mode 100755 src/app.py create mode 120000 src/html create mode 100755 src/server.py (limited to 'src') diff --git a/src/app.py b/src/app.py new file mode 100755 index 0000000..1655e00 --- /dev/null +++ b/src/app.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2022 Egor Tensin +# This file is part of the "void" project. +# For details, see https://github.com/egor-tensin/void. +# Distributed under the MIT License. + +import argparse +import cgi +from enum import Enum +import http.server +import os +import sys +from threading import Lock + + +class Response: + DEFAULT_STATUS = http.server.HTTPStatus.OK + + def __init__(self, body, status=None): + if status is None: + status = Response.DEFAULT_STATUS + self.status = status + self.body = body + + def headers(self): + yield 'Content-Type', 'text/html; charset=utf-8' + + def encode_body(self): + return self.body.encode(errors='replace') + + def write_as_cgi_script(self): + self.write_headers_as_cgi_script() + self.write_body_as_cgi_script() + + def write_headers_as_cgi_script(self): + print(f'{self.status.value} {self.status.phrase}') + for name, val in self.headers(): + print(f'{name}: {val}') + print() + + def write_body_as_cgi_script(self): + if self.body is not None: + print(self.body) + + def write_to_request_handler(self, handler): + handler.send_response(self.status) + self.write_headers_to_request_handler(handler) + self.write_body_to_request_handler(handler) + + def write_headers_to_request_handler(self, handler): + for name, val in self.headers(): + handler.send_header(name, val) + handler.end_headers() + + def write_body_to_request_handler(self, handler): + if self.body is not None: + handler.wfile.write(self.encode_body()) + + +class Void: + def __init__(self, backup_path): + self.lck = Lock() + self.cnt = 0 + self.backup_path = backup_path + if backup_path is not None: + self.backup_path = os.path.abspath(backup_path) + + def __enter__(self): + if self.backup_path is not None and os.path.exists(self.backup_path): + self.restore(self.backup_path) + return self + + def __exit__(self, type, value, traceback): + if self.backup_path is not None: + os.makedirs(os.path.dirname(self.backup_path), exist_ok=True) + self.save(self.backup_path) + + def write(self, fd): + with self.lck: + cnt = self.cnt + fd.write(str(cnt)) + + def read(self, fd): + src = fd.read() + cnt = int(src) + with self.lck: + self.cnt = cnt + + def save(self, path): + with open(path, 'w', encoding='utf-8') as fd: + self.write(fd) + + def restore(self, path): + with open(path, encoding='utf-8') as fd: + self.read(fd) + + def scream_once(self): + with self.lck: + self.cnt += 1 + cnt = self.cnt + return Response(str(cnt)) + + def get_numof_screams(self): + with self.lck: + cnt = self.cnt + return Response(str(cnt)) + + +class Request(Enum): + SCREAM_ONCE = 'scream' + HOW_MANY_SCREAMS = 'screams' + + def __str__(self): + return self.value + + @staticmethod + def from_http_path(path): + if not path or path[0] != '/': + raise ValueError('HTTP path must start with a forward slash /') + return Request(path[1:]) + + def process(self, void): + if self is Request.SCREAM_ONCE: + return void.scream_once() + if self is Request.HOW_MANY_SCREAMS: + return void.get_numof_screams() + raise NotImplementedError(f'unknown request: {self}') + + +def process_cgi_request(void): + params = cgi.FieldStorage() + what = params['what'].value + Request(what).process(void).write_as_cgi_script() + + +def parse_args(args=None): + if args is None: + args = sys.argv[1:] + parser = argparse.ArgumentParser() + parser.add_argument( + '-v', '--void', metavar='PATH', dest='backup', help='set path to the void' + ) + return parser.parse_args(args) + + +def main(args=None): + args = parse_args(args) + with Void(args.backup) as void: + process_cgi_request(void) + + +if __name__ == '__main__': + main() diff --git a/src/html b/src/html new file mode 120000 index 0000000..5eb7cf0 --- /dev/null +++ b/src/html @@ -0,0 +1 @@ +../html/ \ No newline at end of file diff --git a/src/server.py b/src/server.py new file mode 100755 index 0000000..0328d55 --- /dev/null +++ b/src/server.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2022 Egor Tensin +# This file is part of the "void" project. +# For details, see https://github.com/egor-tensin/void. +# Distributed under the MIT License. + +import argparse +from contextlib import contextmanager +import http.server +import os +import signal +import sys +from threading import Condition, Lock, Thread +import traceback + +from app import Request, Response, Void + +DEFAULT_PORT = 23666 +EXITING = False + + +def script_dir(): + return os.path.dirname(os.path.realpath(__file__)) + + +def default_html_dir(): + return os.path.join(script_dir(), 'html') + + +def set_exiting(): + global EXITING + with SignalHandler.LOCK: + EXITING = True + SignalHandler.CV.notify() + + +class SignalHandler: + LOCK = Lock() + CV = Condition(LOCK) + + def __init__(self, httpd): + self.httpd = httpd + self.thread = Thread(target=self.run) + + def __enter__(self): + self.thread.start() + return self + + def __exit__(self, type, value, traceback): + self.thread.join() + + def run(self): + with SignalHandler.CV: + while not EXITING: + SignalHandler.CV.wait() + self.httpd.shutdown() + + +def handle_sigterm(signum, frame): + print('\nSIGTERM received, exiting...') + set_exiting() + + +def handle_sigint(): + print('\nKeyboard interrupt received, exiting...') + set_exiting() + + +@contextmanager +def setup_signal_handlers(httpd): + handler_thread = SignalHandler(httpd) + with handler_thread: + signal.signal(signal.SIGTERM, handle_sigterm) + yield + # TODO: cleanup signal handlers? + + +class RequestHandler(http.server.SimpleHTTPRequestHandler): + VOID = None + + def address_string(self): + if 'x-forwarded-for' in self.headers: + return self.headers['x-forwarded-for'].split(',')[0].strip() + if 'x-real-ip' in self.headers: + return self.headers['x-real-ip'] + return super().address_string() + + def do_GET(self): + try: + request = Request.from_http_path(self.path) + except ValueError: + return super().do_GET() + try: + response = request.process(RequestHandler.VOID) + response.write_to_request_handler(self) + except: + status = http.server.HTTPStatus.INTERNAL_SERVER_ERROR + response = Response(traceback.format_exc(), status) + response.write_to_request_handler(self) + return + + +def make_server(port): + addr = ('', port) + server = http.server.HTTPServer + if sys.version_info >= (3, 7): + server = http.server.ThreadingHTTPServer + return server(addr, RequestHandler) + + +def parse_args(args=None): + if args is None: + args = sys.argv[1:] + parser = argparse.ArgumentParser() + parser.add_argument( + '-p', + '--port', + metavar='PORT', + type=int, + default=DEFAULT_PORT, + help='set port number', + ) + parser.add_argument( + '-d', + '--dir', + metavar='DIR', + default=default_html_dir(), + help='HTML directory path', + ) + parser.add_argument( + '-v', '--void', metavar='PATH', dest='backup', help='set path to the void' + ) + return parser.parse_args(args) + + +def main(args=None): + args = parse_args(args) + + # It's a failsafe; this script is only allowed to serve the directory it + # resides in. + os.chdir(args.dir) + + with Void(args.backup) as void: + RequestHandler.VOID = void + httpd = make_server(args.port) + with setup_signal_handlers(httpd): + try: + httpd.serve_forever() + except KeyboardInterrupt: + handle_sigint() + + +if __name__ == '__main__': + main() -- cgit v1.3