aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src
diff options
context:
space:
mode:
authorEgor Tensin <egor@tensin.name>2026-06-06 20:13:51 +0200
committerEgor Tensin <egor@tensin.name>2026-06-06 20:22:10 +0200
commit6775d40e9243e7144566a296acfacea275322fb8 (patch)
tree9b72611067dade15590d3858ab613cfba1ffce2e /src
parentformat files using Black (diff)
downloadvoid-6775d40e9243e7144566a296acfacea275322fb8.tar.gz
void-6775d40e9243e7144566a296acfacea275322fb8.zip
reorganize files
Diffstat (limited to 'src')
-rwxr-xr-xsrc/app.py154
l---------src/html1
-rwxr-xr-xsrc/server.py155
3 files changed, 310 insertions, 0 deletions
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 <Egor.Tensin@gmail.com>
+# 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 <Egor.Tensin@gmail.com>
+# 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()