3 Commits

Author SHA1 Message Date
klemek d9b559d13d chore: release 1.3.0
Python Lint CI / ruff (push) Successful in 2m51s
Docker CI / docker-build (push) Successful in 3m23s
Python Lint CI / ruff-format-check (push) Successful in 1m50s
Python Lint CI / ty (push) Successful in 3m33s
Python Test CI / coverage (push) Successful in 3m4s
2026-05-06 14:32:00 +02:00
klemek b0d98dd48b feat: add robots.txt and favicon by default 2026-05-06 14:31:33 +02:00
klemek 64f45e9779 fix: dont init certificates with self-signed by default 2026-05-06 14:17:34 +02:00
10 changed files with 33 additions and 24 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "stapler" name = "stapler"
version = "1.2.8" version = "1.3.0"
description = "Static pages as simple as a gzip file" description = "Static pages as simple as a gzip file"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [
+1 -3
View File
@@ -37,7 +37,7 @@ class CertManager:
self.with_certbot: bool = params.with_certbot self.with_certbot: bool = params.with_certbot
self.last_file_change: int | float = 0 self.last_file_change: int | float = 0
def init(self, hosts: list[str]) -> None: def init(self) -> None:
self.logger.debug("Initializing...") self.logger.debug("Initializing...")
if not self.certbot_www.exists(): if not self.certbot_www.exists():
self.certbot_www.mkdir(parents=True) self.certbot_www.mkdir(parents=True)
@@ -45,8 +45,6 @@ class CertManager:
if not self.self_signed_path.exists(): if not self.self_signed_path.exists():
self.self_signed_path.mkdir(parents=True) self.self_signed_path.mkdir(parents=True)
self.logger.debug("Created %s", self.self_signed_path) self.logger.debug("Created %s", self.self_signed_path)
for host in hosts:
self.init_cert(host)
def exists(self, host: str) -> bool: def exists(self, host: str) -> bool:
return self.__exists_certbot(host) or self.__exists_self_signed(host) return self.__exists_certbot(host) or self.__exists_self_signed(host)
+1 -1
View File
@@ -16,7 +16,7 @@ class DataDir:
] ]
PATH_REGEX = re.compile(r"^[\w-]+$") PATH_REGEX = re.compile(r"^[\w-]+$")
NEEDED_FILES: typing.ClassVar[list[str]] = ["favicon.ico"] NEEDED_FILES: typing.ClassVar[list[str]] = ["favicon.ico", "robots.txt"]
def __init__(self, root_path: str) -> None: def __init__(self, root_path: str) -> None:
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
+6 -1
View File
@@ -272,7 +272,7 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
UPDATE_PATH_REGEX = re.compile(r"^\/([\w-]+)\/?$") UPDATE_PATH_REGEX = re.compile(r"^\/([\w-]+)\/?$")
GET_PATH_REGEX = re.compile(r"^\/([\w-]+)($|\/)") GET_PATH_REGEX = re.compile(r"^\/([\w-]+)($|\/)")
HOST_PART_REGEX = re.compile(r"^([a-z0-9]|[a-z0-9][a-z0-9-]{,61}[a-z0-9])$") HOST_PART_REGEX = re.compile(r"^([a-z0-9]|[a-z0-9][a-z0-9-]{,61}[a-z0-9])$")
AUTHORIZED_PATHS: typing.ClassVar[list[str]] = ["/favicon.ico"] AUTHORIZED_PATHS: typing.ClassVar[list[str]] = ["/favicon.ico", "/robots.txt"]
TOKEN_HEADER = "X-Token" # noqa: S105 TOKEN_HEADER = "X-Token" # noqa: S105
HOST_HEADER = "X-Host" HOST_HEADER = "X-Host"
HOST_ONLY_HEADER = "X-Host-Only" HOST_ONLY_HEADER = "X-Host-Only"
@@ -542,6 +542,11 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
return super().translate_path(path) return super().translate_path(path)
return "" return ""
if self.host != self.default_host: if self.host != self.default_host:
if (
not (self.root_path / page.path / path).is_file()
and path in self.AUTHORIZED_PATHS
):
return super().translate_path(path)
path = f"/{page.path}" + path path = f"/{page.path}" + path
if pathlib.Path(path).name.startswith("."): # hidden files if pathlib.Path(path).name.startswith("."): # hidden files
return "" return ""
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
+2 -2
View File
@@ -50,7 +50,7 @@ class StaplerServer:
self.logger.info("Starting up...") self.logger.info("Starting up...")
self.registry.load_pages() self.registry.load_pages()
if self.params.with_certificates: if self.params.with_certificates:
self.cert_manager.init(self.__get_all_hosts()) self.cert_manager.init()
self.data_dir.init() self.data_dir.init()
self.token_manager.init() self.token_manager.init()
@@ -152,7 +152,7 @@ class StaplerServer:
self.logger.warning("Cannot renew without certificates") self.logger.warning("Cannot renew without certificates")
return 1 return 1
self.registry.load_pages() self.registry.load_pages()
self.cert_manager.init(self.__get_all_hosts()) self.cert_manager.init()
for host in self.__get_all_hosts(): for host in self.__get_all_hosts():
self.cert_manager.create_or_update(host) self.cert_manager.create_or_update(host)
return 0 return 0
+1 -9
View File
@@ -35,18 +35,10 @@ class TestRegistry(BaseTestCase):
self.patch("shutil.which", count=0), self.patch("shutil.which", count=0),
self.patch("subprocess.check_output", count=0), self.patch("subprocess.check_output", count=0),
): ):
self.cert_manager.init([]) self.cert_manager.init()
assert self.self_signed_path.is_dir() assert self.self_signed_path.is_dir()
assert self.certbot_www.is_dir() assert self.certbot_www.is_dir()
def test_init_with_hosts(self) -> None:
with (
self.patch("shutil.which", count=0),
self.patch("subprocess.check_output", count=0),
):
self._make_self_signed("example.com")
self.cert_manager.init(["example.com"])
def test_exists_self_signed(self) -> None: def test_exists_self_signed(self) -> None:
self._make_self_signed("example.com") self._make_self_signed("example.com")
assert self.cert_manager.exists("example.com") assert self.cert_manager.exists("example.com")
+15
View File
@@ -1192,6 +1192,21 @@ class TestRequestHandler(BaseHandlerTestCase):
None, None,
) )
def test_translate_path_with_host_favicon(self) -> None:
handler = self._get_handler(headers={"Host": "example.com"})
with (
self.mock_call(self.registry.get_from_host, ["example.com"], Page("path")),
self.patch_call(
"http.server.SimpleHTTPRequestHandler.translate_path",
["/favicon.ico"],
),
self.seal_mocks(),
):
self.assertEqual(
handler.translate_path("/favicon.ico"),
None,
)
def test_translate_path_default_host(self) -> None: def test_translate_path_default_host(self) -> None:
handler = self._get_handler() handler = self._get_handler()
with ( with (
+3 -6
View File
@@ -26,10 +26,8 @@ class TestStaplerServer(BaseTestCase):
def test_renew(self) -> None: def test_renew(self) -> None:
with ( with (
self.mock_call(self.registry.load_pages), self.mock_call(self.registry.load_pages),
self.mock_calls( self.mock_calls(self.registry.get_hosts, [[]], [["host_1"]]),
self.registry.get_hosts, [[], []], [["host_1"], ["host_1"]] self.mock_call(self.cert_manager.init),
),
self.mock_call(self.cert_manager.init, [["localhost", "host_1"]]),
self.mock_calls( self.mock_calls(
self.cert_manager.create_or_update, [["localhost"], ["host_1"]] self.cert_manager.create_or_update, [["localhost"], ["host_1"]]
), ),
@@ -70,8 +68,7 @@ class TestStaplerServer(BaseTestCase):
self.cert_manager.sni_callback = unittest.mock.Mock() self.cert_manager.sni_callback = unittest.mock.Mock()
with ( with (
self.mock_call(self.registry.load_pages), self.mock_call(self.registry.load_pages),
self.mock_call(self.registry.get_hosts, [], []), self.mock_call(self.cert_manager.init),
self.mock_call(self.cert_manager.init, [["localhost"]]),
self.mock_call(self.data_dir.init), self.mock_call(self.data_dir.init),
self.mock_call(self.token_manager.init), self.mock_call(self.token_manager.init),
self.patch("ssl.create_default_context", return_value=self.context_mock), self.patch("ssl.create_default_context", return_value=self.context_mock),
Generated
+1 -1
View File
@@ -212,7 +212,7 @@ wheels = [
[[package]] [[package]]
name = "stapler" name = "stapler"
version = "1.2.8" version = "1.3.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "requests" }, { name = "requests" },