A Python port of the Invisible Internet Project (I2P)
at main 83 lines 2.3 kB view raw
1"""Eepsite static file server. 2 3Serves static files from a docroot directory. Bound to localhost, 4accessed via I2PTunnel's HTTP server tunnel which forwards I2P 5connections to this server. 6 7Ported from Jetty eepsite configuration in Java I2P. 8""" 9 10from __future__ import annotations 11 12import mimetypes 13from dataclasses import dataclass, field 14from pathlib import Path 15 16# Ensure common types are registered 17mimetypes.init() 18 19_CONTENT_TYPES = { 20 ".html": "text/html", 21 ".htm": "text/html", 22 ".css": "text/css", 23 ".js": "application/javascript", 24 ".json": "application/json", 25 ".png": "image/png", 26 ".jpg": "image/jpeg", 27 ".jpeg": "image/jpeg", 28 ".gif": "image/gif", 29 ".svg": "image/svg+xml", 30 ".ico": "image/x-icon", 31 ".txt": "text/plain", 32 ".xml": "application/xml", 33} 34 35_DEFAULT_INDEX = "index.html" 36 37 38@dataclass 39class EepsiteConfig: 40 """Configuration for the eepsite server.""" 41 host: str = "127.0.0.1" 42 port: int = 7658 43 docroot: Path = field(default_factory=lambda: Path.home() / ".i2p-python" / "eepsite" / "docroot") 44 45 46class EepsiteServer: 47 """Static file server for personal .i2p websites.""" 48 49 def __init__(self, config: EepsiteConfig) -> None: 50 self._config = config 51 self._docroot = config.docroot.resolve() 52 53 def resolve_path(self, request_path: str) -> Path | None: 54 """Resolve a URL path to a file in the docroot. 55 56 Returns None if the file doesn't exist or path traversal is detected. 57 """ 58 # Normalize the path 59 clean = request_path.lstrip("/") 60 if not clean: 61 clean = _DEFAULT_INDEX 62 63 target = (self._docroot / clean).resolve() 64 65 # Block path traversal 66 try: 67 target.relative_to(self._docroot) 68 except ValueError: 69 return None 70 71 # If it's a directory, try index.html 72 if target.is_dir(): 73 target = target / _DEFAULT_INDEX 74 75 if target.exists() and target.is_file(): 76 return target 77 return None 78 79 @staticmethod 80 def content_type(filename: str) -> str: 81 """Get the content type for a filename.""" 82 suffix = Path(filename).suffix.lower() 83 return _CONTENT_TYPES.get(suffix, "application/octet-stream")