From e7fc80821231f62187638a07bf6bed497c35129e Mon Sep 17 00:00:00 2001 From: klemek Date: Sun, 12 Apr 2026 00:09:07 +0200 Subject: [PATCH] feat: page specific host --- README.md | 7 +++++-- src/handler.py | 18 +++++++++++++++--- src/page.py | 19 +++++++++++++++++++ src/registry.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/server.py | 14 ++++++++++---- 5 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 src/page.py create mode 100644 src/registry.py diff --git a/README.md b/README.md index cac6c25..da6f613 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,11 @@ curl -X DELETE \ - [x] PUT gzip data into /data/xxx - [x] DELETE request - [x] max file size -- [ ] CNAME in /data/xxx can be translated as host in GET / -- [ ] header to setup CNAME file instead of in archive +- [x] .host in /data/xxx can be translated as host in GET / +- [ ] header to setup .host file instead of in archive +- [ ] log visits (and store accross sessions) +- [ ] deliver visits in /page/visits +- [ ] ignore gitignore/.host etc at root - [ ] cerbot install in container + path env/arg - [ ] redirect /.well-known/acme-challenge to specific path - [ ] certbot/self-signed create/renew in specific dir diff --git a/src/handler.py b/src/handler.py index d96a583..c7dfc5b 100644 --- a/src/handler.py +++ b/src/handler.py @@ -6,24 +6,32 @@ import io import os import shutil -from . import project, params +from . import project, params, registry class StaplerRequestHandler(http.server.SimpleHTTPRequestHandler): protocol_version = "HTTP/2.0" server_version = "StaplerServer/" + project.get_version() - def __init__(self, *args, params: params.Parameters, **kwargs): + def __init__( + self, *args, params: params.Parameters, registry: registry.Registry, **kwargs + ): self.default_host = params.host self.token = params.token self.data_dir = params.data_dir self.max_size_bytes = params.max_size_bytes + self.registry = registry super().__init__(*args, directory=params.data_dir, **kwargs) def list_directory(self, *_, **__): """Disable default directory listing""" self.send_error(http.HTTPStatus.NOT_FOUND, "File not found") + def translate_path(self, path: str) -> str: + if (page := self.registry.get_from_host(self.get_host())) is not None: + path = f"/{page.path}" + path + return super().translate_path(path) + def do_GET(self): if self.path == "/" and self.get_host() == self.default_host: return self.server_index() @@ -53,6 +61,7 @@ class StaplerRequestHandler(http.server.SimpleHTTPRequestHandler): except Exception as e: return self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, str(e)) self.send_status_only(http.HTTPStatus.CREATED, f"Resource /{sub_path}/ updated") + self.registry.add(sub_path) def do_DELETE(self): if self.headers["X-Token"] != self.token: @@ -60,13 +69,16 @@ class StaplerRequestHandler(http.server.SimpleHTTPRequestHandler): if (sub_path := self.get_subpath()) is None: return self.send_error(http.HTTPStatus.BAD_REQUEST, "Invalid path") target_path = os.path.join(self.data_dir, sub_path) + if not os.path.exists(target_path): + return self.send_error(http.HTTPStatus.NOT_FOUND, "Not found") try: shutil.rmtree(target_path) except Exception as e: return self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, str(e)) self.send_status_only( - http.HTTPStatus.NO_CONTENT, f"Resource /{sub_path}/ deleted" + http.HTTPStatus.NO_CONTENT, f"Resource /{sub_path}/ removed" ) + self.registry.remove(sub_path) def get_subpath(self) -> str | None: if (match := re.match(r"^\/(\w+)\/$", self.path)) is not None: diff --git a/src/page.py b/src/page.py new file mode 100644 index 0000000..67e104e --- /dev/null +++ b/src/page.py @@ -0,0 +1,19 @@ +import dataclasses + + +@dataclasses.dataclass +class Page: + path: str + with_index: bool + host: str | None = None + + def get_url_path(self) -> str: + return f"/{self.path}/" + + def __repr__(self) -> str: + out = self.get_url_path() + if self.host is not None: + out += f" ({self.host})" + if not self.with_index: + out += " (no index)" + return out diff --git a/src/registry.py b/src/registry.py new file mode 100644 index 0000000..4c29fef --- /dev/null +++ b/src/registry.py @@ -0,0 +1,47 @@ +import os + +from . import params, page + + +class Registry: + def __init__(self, params: params.Parameters): + self.pages: dict[str, page.Page] = {} + self.data_dir = params.data_dir + self.prefix = f"http://{params.host}:{params.port}" + + def load_pages(self): + self.pages = {} + for path in os.listdir(self.data_dir): + self.add(path) + + def add(self, path: str): + real_path = os.path.join(self.data_dir, path) + if os.path.isdir(real_path): + self.pages[path] = page.Page( + path, self.__has_index(path), self.__get_host(path) + ) + print("Updated: " + self.prefix + str(self.pages[path])) + + def __has_index(self, path: str) -> bool: + path_index = os.path.join(self.data_dir, path, "index.html") + return os.path.exists(path_index) and os.path.isfile(path_index) + + def __get_host(self, path: str) -> str | None: + path_host = os.path.join(self.data_dir, path, ".host") + if os.path.exists(path_host) and os.path.isfile(path_host): + try: + with open(path_host) as host_file: + return host_file.read().split("\n")[0].strip() + except Exception: + pass + return None + + def remove(self, path: str): + page = self.pages[path] + del self.pages[path] + print("Removed: " + self.prefix + str(page)) + + def get_from_host(self, host: str) -> page.Page | None: + for p in self.pages.values(): + if p.host == host: + return p diff --git a/src/server.py b/src/server.py index ae9b078..d28f430 100644 --- a/src/server.py +++ b/src/server.py @@ -1,19 +1,25 @@ import http.server -from . import params, handler +from . import params, handler, registry class StaplerServer: def __init__(self, params: params.Parameters): self.default_host = params.host + self.registry = registry.Registry(params) + self.params = params self.server = http.server.ThreadingHTTPServer( (params.bind, params.port), - lambda req, client, server: handler.StaplerRequestHandler( - req, client, server, params=params - ), + self.request_handler, + ) + + def request_handler(self, *args) -> http.server.BaseHTTPRequestHandler: + return handler.StaplerRequestHandler( + *args, params=self.params, registry=self.registry ) def start(self): + self.registry.load_pages() print( f"{handler.StaplerRequestHandler.server_version} serving on http://{self.default_host}:{self.server.server_port}..." )