A Python port of the Invisible Internet Project (I2P)
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")