feat: X-Host-Only
This commit is contained in:
+2
-1
@@ -68,4 +68,5 @@ docker-run docker run
|
|||||||
- [x] detect root certificate change and update server
|
- [x] detect root certificate change and update server
|
||||||
- [x] detect tokens change and update token_manager
|
- [x] detect tokens change and update token_manager
|
||||||
- [x] proper doc
|
- [x] proper doc
|
||||||
|
- [x] X-Host-Only
|
||||||
|
- [ ] Fix certbot on base path with upgrade server
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ options:
|
|||||||
PUT /{page}/
|
PUT /{page}/
|
||||||
X-Token (your API token)
|
X-Token (your API token)
|
||||||
X-Host (optional host as entrypoint)
|
X-Host (optional host as entrypoint)
|
||||||
|
X-Host-Only (optional host as entrypoint)
|
||||||
(body with tar data)
|
(body with tar data)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -121,14 +122,24 @@ tar -czC dist . | curl -X PUT \
|
|||||||
--data-binary @- \
|
--data-binary @- \
|
||||||
https://stapler-host/my-project/
|
https://stapler-host/my-project/
|
||||||
|
|
||||||
# make stapler server identifiers myproject.example.com as /my-project/
|
# make stapler server identifiers myproject.example.com and /my-project/
|
||||||
tar -czC dist . | curl -X PUT \
|
tar -czC dist . | curl -X PUT \
|
||||||
--data-binary @- \
|
--data-binary @- \
|
||||||
-H 'X-Token: <TOKEN>' \
|
-H 'X-Token: <TOKEN>' \
|
||||||
-H 'X-Host: myproject.example.com' \
|
-H 'X-Host: myproject.example.com' \
|
||||||
https://stapler-host/my-project/
|
https://stapler-host/my-project/
|
||||||
|
|
||||||
|
# make stapler server identifiers myproject.example.com only
|
||||||
|
tar -czC dist . | curl -X PUT \
|
||||||
|
--data-binary @- \
|
||||||
|
-H 'X-Token: <TOKEN>' \
|
||||||
|
-H 'X-Host-Only: myproject.example.com' \
|
||||||
|
https://stapler-host/my-project/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Creating/updating comes with `X-Host` for enabling host-based resolving but you can also use `X-Host-Only` to do the same thing but disable listing as /path/.
|
||||||
|
|
||||||
### Create/update page with redirect
|
### Create/update page with redirect
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
@@ -136,6 +147,7 @@ PUT /{page}/
|
|||||||
X-Token (your API token)
|
X-Token (your API token)
|
||||||
X-Redirect (redirection target)
|
X-Redirect (redirection target)
|
||||||
X-Host (optional host as entrypoint)
|
X-Host (optional host as entrypoint)
|
||||||
|
X-Host-Only (optional host as entrypoint)
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -160,6 +172,7 @@ PUT /{page}/
|
|||||||
X-Token (your API token)
|
X-Token (your API token)
|
||||||
X-Proxy (proxy target)
|
X-Proxy (proxy target)
|
||||||
X-Host (optional host as entrypoint)
|
X-Host (optional host as entrypoint)
|
||||||
|
X-Host-Only (optional host as entrypoint)
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ class DataDir:
|
|||||||
file_path.chmod(chmod)
|
file_path.chmod(chmod)
|
||||||
self.logger.debug("Wrote %s", file_path)
|
self.logger.debug("Wrote %s", file_path)
|
||||||
|
|
||||||
|
def remove_file(self, path: str, file_name: str) -> None:
|
||||||
|
if self.exists(path):
|
||||||
|
file_path = self.root_path / path / file_name
|
||||||
|
if file_path.is_file():
|
||||||
|
file_path.unlink()
|
||||||
|
self.logger.debug("Removed %s", file_path)
|
||||||
|
|
||||||
def get_file(self, path: str, file_name: str) -> str | None:
|
def get_file(self, path: str, file_name: str) -> str | None:
|
||||||
if self.exists(path):
|
if self.exists(path):
|
||||||
file_path = self.root_path / path / file_name
|
file_path = self.root_path / path / file_name
|
||||||
|
|||||||
+53
-19
@@ -17,7 +17,6 @@ from . import PKG_VERSION, STAPLER_ASCII, logs
|
|||||||
from .data_dir import DataDir
|
from .data_dir import DataDir
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from .cert_manager import CertManager
|
|
||||||
from .page import Page
|
from .page import Page
|
||||||
from .params import Parameters
|
from .params import Parameters
|
||||||
from .registry import Registry
|
from .registry import Registry
|
||||||
@@ -223,6 +222,7 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
|
|||||||
AUTHORIZED_PATHS: typing.ClassVar[list[str]] = ["/favicon.ico"]
|
AUTHORIZED_PATHS: typing.ClassVar[list[str]] = ["/favicon.ico"]
|
||||||
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"
|
||||||
REDIRECT_HEADER = "X-Redirect"
|
REDIRECT_HEADER = "X-Redirect"
|
||||||
PROXY_HEADER = "X-Proxy"
|
PROXY_HEADER = "X-Proxy"
|
||||||
|
|
||||||
@@ -232,7 +232,6 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
|
|||||||
*args: typing.Any,
|
*args: typing.Any,
|
||||||
params: Parameters,
|
params: Parameters,
|
||||||
registry: Registry,
|
registry: Registry,
|
||||||
cert_manager: CertManager,
|
|
||||||
token_manager: TokenManager,
|
token_manager: TokenManager,
|
||||||
**kwargs: dict[str, typing.Any],
|
**kwargs: dict[str, typing.Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -241,10 +240,10 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
|
|||||||
self.data_dir: DataDir = DataDir(params.data_dir)
|
self.data_dir: DataDir = DataDir(params.data_dir)
|
||||||
self.max_size_bytes: int = params.max_size_bytes
|
self.max_size_bytes: int = params.max_size_bytes
|
||||||
self.registry: Registry = registry
|
self.registry: Registry = registry
|
||||||
self.cert_manager: CertManager = cert_manager
|
|
||||||
self.certbot_www: str = os.path.realpath(params.certbot_www)
|
self.certbot_www: str = os.path.realpath(params.certbot_www)
|
||||||
self.__token: str | None = None
|
self.__token: str | None = None
|
||||||
self.__target_host: str | None = None
|
self.__target_host: str | None = None
|
||||||
|
self.__target_host_only: str | None = None
|
||||||
self.__target_redirect: str | None = None
|
self.__target_redirect: str | None = None
|
||||||
self.__target_proxy: str | None = None
|
self.__target_proxy: str | None = None
|
||||||
super().__init__(*args, directory=params.data_dir, **kwargs, params=params) # ty:ignore[unknown-argument]
|
super().__init__(*args, directory=params.data_dir, **kwargs, params=params) # ty:ignore[unknown-argument]
|
||||||
@@ -260,14 +259,34 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
|
|||||||
return len(self.token) > 0
|
return len(self.token) > 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_host(self) -> str:
|
def request_host(self) -> str:
|
||||||
if self.__target_host is None:
|
if self.__target_host is None:
|
||||||
self.__target_host = self._get_header(self.HOST_HEADER).lower()
|
self.__target_host = self._get_header(self.HOST_HEADER).lower()
|
||||||
return self.__target_host
|
return self.__target_host
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_request_host(self) -> bool:
|
||||||
|
return len(self.request_host) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request_host_only(self) -> str:
|
||||||
|
if self.__target_host_only is None:
|
||||||
|
self.__target_host_only = self._get_header(self.HOST_ONLY_HEADER).lower()
|
||||||
|
return self.__target_host_only
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_request_host_only(self) -> bool:
|
||||||
|
return len(self.request_host_only) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_host(self) -> str:
|
||||||
|
if self.has_request_host:
|
||||||
|
return self.request_host
|
||||||
|
return self.request_host_only
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_target_host(self) -> bool:
|
def has_target_host(self) -> bool:
|
||||||
return len(self.target_host) > 0
|
return self.has_request_host or self.has_request_host_only
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_redirect(self) -> str:
|
def target_redirect(self) -> str:
|
||||||
@@ -310,31 +329,24 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
|
|||||||
return None
|
return None
|
||||||
if (path := self.__check_update_request()) is None:
|
if (path := self.__check_update_request()) is None:
|
||||||
return None
|
return None
|
||||||
if self.has_target_host and not self.__valid_host(self.target_host):
|
if not self.__check_put_headers():
|
||||||
return self.send_error(
|
return None
|
||||||
http.HTTPStatus.BAD_REQUEST, "Invalid requested host"
|
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
self.has_target_host
|
self.has_target_host
|
||||||
and (page := self.registry.get_from_host(self.target_host)) is not None
|
and (page := self.registry.get_from_host(self.target_host)) is not None
|
||||||
and page.path != path
|
and page.path != path
|
||||||
):
|
):
|
||||||
return self.send_error(http.HTTPStatus.FORBIDDEN, "Host already taken")
|
return self.send_error(http.HTTPStatus.FORBIDDEN, "Host already taken")
|
||||||
if self.has_target_proxy and self.has_target_redirect:
|
|
||||||
return self.send_error(
|
|
||||||
http.HTTPStatus.BAD_REQUEST,
|
|
||||||
f"Cannot use {self.PROXY_HEADER} with {self.REDIRECT_HEADER}",
|
|
||||||
)
|
|
||||||
if self.has_target_redirect:
|
if self.has_target_redirect:
|
||||||
self._update_redirect(path)
|
self._update_redirect(path)
|
||||||
elif self.has_target_proxy:
|
elif self.has_target_proxy:
|
||||||
self._update_proxy(path)
|
self._update_proxy(path)
|
||||||
else:
|
else:
|
||||||
self._update_extract(path)
|
self._update_extract(path)
|
||||||
if self.has_target_host and self.cert_manager.create_or_update(
|
if self.has_request_host:
|
||||||
self.target_host
|
|
||||||
):
|
|
||||||
self.registry.set_host(path, self.target_host)
|
self.registry.set_host(path, self.target_host)
|
||||||
|
if self.has_request_host_only:
|
||||||
|
self.registry.set_host_only(path, self.target_host)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def do_POST(self) -> None:
|
def do_POST(self) -> None:
|
||||||
@@ -485,6 +497,24 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
|
|||||||
return None
|
return None
|
||||||
return sub_path
|
return sub_path
|
||||||
|
|
||||||
|
def __check_put_headers(self) -> bool:
|
||||||
|
if self.has_request_host and self.has_request_host_only:
|
||||||
|
self.send_error(
|
||||||
|
http.HTTPStatus.BAD_REQUEST,
|
||||||
|
f"Cannot use {self.HOST_ONLY_HEADER} with {self.HOST_HEADER}",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
if self.has_target_host and not self.__valid_host(self.target_host):
|
||||||
|
self.send_error(http.HTTPStatus.BAD_REQUEST, "Invalid requested host")
|
||||||
|
return False
|
||||||
|
if self.has_target_proxy and self.has_target_redirect:
|
||||||
|
self.send_error(
|
||||||
|
http.HTTPStatus.BAD_REQUEST,
|
||||||
|
f"Cannot use {self.PROXY_HEADER} with {self.REDIRECT_HEADER}",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def __get_path(self, path: str, regex: re.Pattern) -> str | None:
|
def __get_path(self, path: str, regex: re.Pattern) -> str | None:
|
||||||
if (match := regex.match(path.lower())) is not None:
|
if (match := regex.match(path.lower())) is not None:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
@@ -498,8 +528,12 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
|
|||||||
|
|
||||||
def __get_page(self, src_path: str) -> Page | None:
|
def __get_page(self, src_path: str) -> Page | None:
|
||||||
if self.host == self.default_host:
|
if self.host == self.default_host:
|
||||||
if path := self.__get_path(src_path, self.GET_PATH_REGEX):
|
if (
|
||||||
return self.registry.get_from_path(path)
|
(path := self.__get_path(src_path, self.GET_PATH_REGEX))
|
||||||
|
and (page := self.registry.get_from_path(path)) is not None
|
||||||
|
and not page.host_only
|
||||||
|
):
|
||||||
|
return page
|
||||||
return None
|
return None
|
||||||
return self.registry.get_from_host(self.host)
|
return self.registry.get_from_host(self.host)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class Page:
|
|||||||
path: str
|
path: str
|
||||||
with_index: bool = False
|
with_index: bool = False
|
||||||
host: str | None = None
|
host: str | None = None
|
||||||
|
host_only: bool = False
|
||||||
token_hash: str | None = None
|
token_hash: str | None = None
|
||||||
redirect: str | None = None
|
redirect: str | None = None
|
||||||
proxy: str | None = None
|
proxy: str | None = None
|
||||||
@@ -20,4 +21,6 @@ class Page:
|
|||||||
out += f" (proxy: {self.proxy})"
|
out += f" (proxy: {self.proxy})"
|
||||||
elif not self.with_index:
|
elif not self.with_index:
|
||||||
out += " (no index)"
|
out += " (no index)"
|
||||||
|
if self.host_only:
|
||||||
|
out += " (host only)"
|
||||||
return out
|
return out
|
||||||
|
|||||||
+24
-7
@@ -16,6 +16,7 @@ class Registry:
|
|||||||
]
|
]
|
||||||
|
|
||||||
HOST_FILE = ".host"
|
HOST_FILE = ".host"
|
||||||
|
HOST_ONLY_FILE = ".host_only"
|
||||||
TOKEN_FILE = ".token" # noqa: S105
|
TOKEN_FILE = ".token" # noqa: S105
|
||||||
REDIRECT_FILE = ".redirect"
|
REDIRECT_FILE = ".redirect"
|
||||||
PROXY_FILE = ".proxy"
|
PROXY_FILE = ".proxy"
|
||||||
@@ -34,22 +35,38 @@ class Registry:
|
|||||||
return [p.host for p in self.pages.values() if p.host is not None]
|
return [p.host for p in self.pages.values() if p.host is not None]
|
||||||
|
|
||||||
def add(self, path: str) -> None:
|
def add(self, path: str) -> None:
|
||||||
|
host = self.data_dir.get_file(path, self.HOST_FILE)
|
||||||
|
host_only = self.data_dir.get_file(path, self.HOST_ONLY_FILE)
|
||||||
self.pages[path] = Page(
|
self.pages[path] = Page(
|
||||||
path,
|
path=path,
|
||||||
self.data_dir.has_index(path),
|
with_index=self.data_dir.has_index(path),
|
||||||
self.data_dir.get_file(path, self.HOST_FILE),
|
host=host if host is not None else host_only,
|
||||||
self.data_dir.get_file(path, self.TOKEN_FILE),
|
host_only=host_only is not None,
|
||||||
self.data_dir.get_file(path, self.REDIRECT_FILE),
|
token_hash=self.data_dir.get_file(path, self.TOKEN_FILE),
|
||||||
self.data_dir.get_file(path, self.PROXY_FILE),
|
redirect=self.data_dir.get_file(path, self.REDIRECT_FILE),
|
||||||
|
proxy=self.data_dir.get_file(path, self.PROXY_FILE),
|
||||||
)
|
)
|
||||||
self.logger.info("Updated %s", self.pages[path])
|
self.logger.info("Updated %s", self.pages[path])
|
||||||
|
|
||||||
def set_host(self, path: str, host: str) -> None:
|
def set_host(self, path: str, host: str) -> None:
|
||||||
if path in self.pages and self.pages[path].host != host:
|
if path in self.pages and (
|
||||||
|
self.pages[path].host != host or self.pages[path].host_only
|
||||||
|
):
|
||||||
self.data_dir.set_file(path, self.HOST_FILE, host)
|
self.data_dir.set_file(path, self.HOST_FILE, host)
|
||||||
|
self.data_dir.remove_file(path, self.HOST_ONLY_FILE)
|
||||||
self.pages[path].host = host
|
self.pages[path].host = host
|
||||||
self.logger.debug("Updated %s", self.pages[path])
|
self.logger.debug("Updated %s", self.pages[path])
|
||||||
|
|
||||||
|
def set_host_only(self, path: str, host: str) -> None:
|
||||||
|
if path in self.pages and (
|
||||||
|
self.pages[path].host != host or not self.pages[path].host_only
|
||||||
|
):
|
||||||
|
self.data_dir.set_file(path, self.HOST_ONLY_FILE, host)
|
||||||
|
self.data_dir.remove_file(path, self.HOST_FILE)
|
||||||
|
self.pages[path].host = host
|
||||||
|
self.pages[path].host_only = True
|
||||||
|
self.logger.debug("Updated %s", self.pages[path])
|
||||||
|
|
||||||
def set_token_hash(self, path: str, token_hash: str) -> None:
|
def set_token_hash(self, path: str, token_hash: str) -> None:
|
||||||
if path in self.pages and self.pages[path].token_hash != token_hash:
|
if path in self.pages and self.pages[path].token_hash != token_hash:
|
||||||
self.data_dir.set_file(path, self.TOKEN_FILE, token_hash, 0o600)
|
self.data_dir.set_file(path, self.TOKEN_FILE, token_hash, 0o600)
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ class StaplerServer:
|
|||||||
*args,
|
*args,
|
||||||
params=self.params,
|
params=self.params,
|
||||||
registry=self.registry,
|
registry=self.registry,
|
||||||
cert_manager=self.cert_manager,
|
|
||||||
token_manager=self.token_manager,
|
token_manager=self.token_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,21 @@ class TestDataDir(BaseTestCase):
|
|||||||
assert not (self.tmp_path / "test_1").exists()
|
assert not (self.tmp_path / "test_1").exists()
|
||||||
assert not (self.tmp_path / "test_1" / ".value").exists()
|
assert not (self.tmp_path / "test_1" / ".value").exists()
|
||||||
|
|
||||||
|
def test_remove_file_do_nothing(self) -> None:
|
||||||
|
self.__create_path("test_1")
|
||||||
|
self.data_dir.remove_file("test_1", ".value")
|
||||||
|
assert not (self.tmp_path / "test_1" / ".value").exists()
|
||||||
|
|
||||||
|
def test_remove_file_ok(self) -> None:
|
||||||
|
self.__create_path("test_1", {".value": "test_value\nother_line"})
|
||||||
|
self.data_dir.remove_file("test_1", ".value")
|
||||||
|
assert not (self.tmp_path / "test_1" / ".value").exists()
|
||||||
|
|
||||||
|
def test_remove_file_invalid_path(self) -> None:
|
||||||
|
self.data_dir.remove_file("test_1", ".value")
|
||||||
|
assert not (self.tmp_path / "test_1").exists()
|
||||||
|
assert not (self.tmp_path / "test_1" / ".value").exists()
|
||||||
|
|
||||||
def test_remove(self) -> None:
|
def test_remove(self) -> None:
|
||||||
self.__create_path("test_1")
|
self.__create_path("test_1")
|
||||||
self.data_dir.remove("test_1")
|
self.data_dir.remove("test_1")
|
||||||
|
|||||||
+79
-31
@@ -122,7 +122,6 @@ class TestRequestHandler(BaseHandlerTestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.get_tmp_dir()
|
self.get_tmp_dir()
|
||||||
self.registry = self.new_mock()
|
self.registry = self.new_mock()
|
||||||
self.cert_manager = self.new_mock()
|
|
||||||
self.token_manager = self.new_mock()
|
self.token_manager = self.new_mock()
|
||||||
self.certbot_www = self.tmp_path / "certbot_www"
|
self.certbot_www = self.tmp_path / "certbot_www"
|
||||||
self.data_dir = self.new_mock()
|
self.data_dir = self.new_mock()
|
||||||
@@ -146,7 +145,6 @@ class TestRequestHandler(BaseHandlerTestCase):
|
|||||||
data_dir=self.get_tmp_dir(), certbot_www=str(self.certbot_www)
|
data_dir=self.get_tmp_dir(), certbot_www=str(self.certbot_www)
|
||||||
),
|
),
|
||||||
registry=self.registry,
|
registry=self.registry,
|
||||||
cert_manager=self.cert_manager,
|
|
||||||
token_manager=self.token_manager,
|
token_manager=self.token_manager,
|
||||||
)
|
)
|
||||||
handler.address_string = lambda: "127.0.0.1" # ty:ignore[invalid-assignment]
|
handler.address_string = lambda: "127.0.0.1" # ty:ignore[invalid-assignment]
|
||||||
@@ -285,6 +283,44 @@ class TestRequestHandler(BaseHandlerTestCase):
|
|||||||
):
|
):
|
||||||
handler.do_PUT()
|
handler.do_PUT()
|
||||||
|
|
||||||
|
def test_do_put_invalid_host_only(self) -> None:
|
||||||
|
handler = self._get_handler(
|
||||||
|
"/path", {"X-Token": "secret", "X-Host-Only": "invalid_host"}
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
self.mock_call(self.token_manager.is_valid, ["secret"], True), # noqa: FBT003
|
||||||
|
self.mock_call(
|
||||||
|
self.token_manager.is_valid_for_path,
|
||||||
|
["secret", "path"],
|
||||||
|
True, # noqa: FBT003
|
||||||
|
),
|
||||||
|
self.expects_error(
|
||||||
|
handler, http.HTTPStatus.BAD_REQUEST, "Invalid requested host"
|
||||||
|
),
|
||||||
|
self.seal_mocks(),
|
||||||
|
):
|
||||||
|
handler.do_PUT()
|
||||||
|
|
||||||
|
def test_do_put_invalid_host_and_host_only(self) -> None:
|
||||||
|
handler = self._get_handler(
|
||||||
|
"/path", {"X-Token": "secret", "X-Host": "host", "X-Host-Only": "host"}
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
self.mock_call(self.token_manager.is_valid, ["secret"], True), # noqa: FBT003
|
||||||
|
self.mock_call(
|
||||||
|
self.token_manager.is_valid_for_path,
|
||||||
|
["secret", "path"],
|
||||||
|
True, # noqa: FBT003
|
||||||
|
),
|
||||||
|
self.expects_error(
|
||||||
|
handler,
|
||||||
|
http.HTTPStatus.BAD_REQUEST,
|
||||||
|
"Cannot use X-Host-Only with X-Host",
|
||||||
|
),
|
||||||
|
self.seal_mocks(),
|
||||||
|
):
|
||||||
|
handler.do_PUT()
|
||||||
|
|
||||||
def test_do_put_invalid_host_for_path(self) -> None:
|
def test_do_put_invalid_host_for_path(self) -> None:
|
||||||
handler = self._get_handler(
|
handler = self._get_handler(
|
||||||
"/path", {"X-Token": "secret", "X-Host": "example.com"}
|
"/path", {"X-Token": "secret", "X-Host": "example.com"}
|
||||||
@@ -404,31 +440,6 @@ class TestRequestHandler(BaseHandlerTestCase):
|
|||||||
):
|
):
|
||||||
handler.do_PUT()
|
handler.do_PUT()
|
||||||
|
|
||||||
def test_do_put_extract_with_host_fail_init(self) -> None:
|
|
||||||
handler = self._get_handler(
|
|
||||||
"/path",
|
|
||||||
{"X-Token": "secret", "Content-Length": "1", "X-Host": "example.com"},
|
|
||||||
)
|
|
||||||
handler.rfile.write(b"\0")
|
|
||||||
with (
|
|
||||||
self.mock_call(self.token_manager.is_valid, ["secret"], True), # noqa: FBT003
|
|
||||||
self.mock_call(
|
|
||||||
self.token_manager.is_valid_for_path,
|
|
||||||
["secret", "path"],
|
|
||||||
True, # noqa: FBT003
|
|
||||||
),
|
|
||||||
self.mock_call(self.registry.get_from_host, ["example.com"], Page("path")),
|
|
||||||
self.mock_call_unchecked(self.data_dir.extract_tar_bytes),
|
|
||||||
self.mock_call(self.registry.add, ["path"]),
|
|
||||||
self.mock_call(self.token_manager.set_token, ["path", "secret"]),
|
|
||||||
self.mock_call(self.cert_manager.create_or_update, ["example.com"], False), # noqa: FBT003
|
|
||||||
self.expects_status_only(
|
|
||||||
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
|
|
||||||
),
|
|
||||||
self.seal_mocks(),
|
|
||||||
):
|
|
||||||
handler.do_PUT()
|
|
||||||
|
|
||||||
def test_do_put_extract_with_host(self) -> None:
|
def test_do_put_extract_with_host(self) -> None:
|
||||||
handler = self._get_handler(
|
handler = self._get_handler(
|
||||||
"/path",
|
"/path",
|
||||||
@@ -446,7 +457,6 @@ class TestRequestHandler(BaseHandlerTestCase):
|
|||||||
self.mock_call_unchecked(self.data_dir.extract_tar_bytes),
|
self.mock_call_unchecked(self.data_dir.extract_tar_bytes),
|
||||||
self.mock_call(self.registry.add, ["path"]),
|
self.mock_call(self.registry.add, ["path"]),
|
||||||
self.mock_call(self.token_manager.set_token, ["path", "secret"]),
|
self.mock_call(self.token_manager.set_token, ["path", "secret"]),
|
||||||
self.mock_call(self.cert_manager.create_or_update, ["example.com"], True), # noqa: FBT003
|
|
||||||
self.mock_call(self.registry.set_host, ["path", "example.com"]),
|
self.mock_call(self.registry.set_host, ["path", "example.com"]),
|
||||||
self.expects_status_only(
|
self.expects_status_only(
|
||||||
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
|
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
|
||||||
@@ -523,7 +533,6 @@ class TestRequestHandler(BaseHandlerTestCase):
|
|||||||
self.mock_call(self.registry.get_from_host, ["example.com"], Page("path")),
|
self.mock_call(self.registry.get_from_host, ["example.com"], Page("path")),
|
||||||
self.mock_call(self.registry.set_redirect, ["path", "https://example.com"]),
|
self.mock_call(self.registry.set_redirect, ["path", "https://example.com"]),
|
||||||
self.mock_call(self.token_manager.set_token, ["path", "secret"]),
|
self.mock_call(self.token_manager.set_token, ["path", "secret"]),
|
||||||
self.mock_call(self.cert_manager.create_or_update, ["example.com"], True), # noqa: FBT003
|
|
||||||
self.mock_call(self.registry.set_host, ["path", "example.com"]),
|
self.mock_call(self.registry.set_host, ["path", "example.com"]),
|
||||||
self.expects_status_only(
|
self.expects_status_only(
|
||||||
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
|
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
|
||||||
@@ -532,6 +541,33 @@ class TestRequestHandler(BaseHandlerTestCase):
|
|||||||
):
|
):
|
||||||
handler.do_PUT()
|
handler.do_PUT()
|
||||||
|
|
||||||
|
def test_do_put_redirect_with_host_only(self) -> None:
|
||||||
|
handler = self._get_handler(
|
||||||
|
"/path",
|
||||||
|
{
|
||||||
|
"X-Token": "secret",
|
||||||
|
"X-Redirect": "https://example.com",
|
||||||
|
"X-Host-Only": "example.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
self.mock_call(self.token_manager.is_valid, ["secret"], True), # noqa: FBT003
|
||||||
|
self.mock_call(
|
||||||
|
self.token_manager.is_valid_for_path,
|
||||||
|
["secret", "path"],
|
||||||
|
True, # noqa: FBT003
|
||||||
|
),
|
||||||
|
self.mock_call(self.registry.get_from_host, ["example.com"], Page("path")),
|
||||||
|
self.mock_call(self.registry.set_redirect, ["path", "https://example.com"]),
|
||||||
|
self.mock_call(self.token_manager.set_token, ["path", "secret"]),
|
||||||
|
self.mock_call(self.registry.set_host_only, ["path", "example.com"]),
|
||||||
|
self.expects_status_only(
|
||||||
|
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
|
||||||
|
),
|
||||||
|
self.seal_mocks(),
|
||||||
|
):
|
||||||
|
handler.do_PUT()
|
||||||
|
|
||||||
def test_do_put_proxy_with_content(self) -> None:
|
def test_do_put_proxy_with_content(self) -> None:
|
||||||
handler = self._get_handler(
|
handler = self._get_handler(
|
||||||
"/path",
|
"/path",
|
||||||
@@ -600,7 +636,6 @@ class TestRequestHandler(BaseHandlerTestCase):
|
|||||||
self.mock_call(self.registry.get_from_host, ["example.com"], Page("path")),
|
self.mock_call(self.registry.get_from_host, ["example.com"], Page("path")),
|
||||||
self.mock_call(self.registry.set_proxy, ["path", "https://example.com"]),
|
self.mock_call(self.registry.set_proxy, ["path", "https://example.com"]),
|
||||||
self.mock_call(self.token_manager.set_token, ["path", "secret"]),
|
self.mock_call(self.token_manager.set_token, ["path", "secret"]),
|
||||||
self.mock_call(self.cert_manager.create_or_update, ["example.com"], True), # noqa: FBT003
|
|
||||||
self.mock_call(self.registry.set_host, ["path", "example.com"]),
|
self.mock_call(self.registry.set_host, ["path", "example.com"]),
|
||||||
self.expects_status_only(
|
self.expects_status_only(
|
||||||
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
|
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
|
||||||
@@ -626,7 +661,6 @@ class TestRequestHandler(BaseHandlerTestCase):
|
|||||||
["secret", "path"],
|
["secret", "path"],
|
||||||
True, # noqa: FBT003
|
True, # noqa: FBT003
|
||||||
),
|
),
|
||||||
self.mock_call(self.registry.get_from_host, ["example.com"], Page("path")),
|
|
||||||
self.expects_status_only(
|
self.expects_status_only(
|
||||||
handler,
|
handler,
|
||||||
http.HTTPStatus.BAD_REQUEST,
|
http.HTTPStatus.BAD_REQUEST,
|
||||||
@@ -1106,6 +1140,20 @@ class TestRequestHandler(BaseHandlerTestCase):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_translate_path_default_host_only(self) -> None:
|
||||||
|
handler = self._get_handler()
|
||||||
|
with (
|
||||||
|
self.mock_call(
|
||||||
|
self.registry.get_from_path, ["path"], Page("path", host_only=True)
|
||||||
|
),
|
||||||
|
self.patch("http.server.SimpleHTTPRequestHandler.translate_path", count=0),
|
||||||
|
self.seal_mocks(),
|
||||||
|
):
|
||||||
|
self.assertEqual(
|
||||||
|
handler.translate_path("/path/index.html"),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
def test_translate_path_default_host_not_found(self) -> None:
|
def test_translate_path_default_host_not_found(self) -> None:
|
||||||
handler = self._get_handler()
|
handler = self._get_handler()
|
||||||
with (
|
with (
|
||||||
|
|||||||
@@ -27,3 +27,9 @@ class TestPage(BaseTestCase):
|
|||||||
str(Page("test_1", proxy="https://example.com")),
|
str(Page("test_1", proxy="https://example.com")),
|
||||||
"/test_1/ (proxy: https://example.com)",
|
"/test_1/ (proxy: https://example.com)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_repr_with_host_only(self) -> None:
|
||||||
|
self.assertEqual(
|
||||||
|
str(Page("test_1", with_index=True, host_only=True)),
|
||||||
|
"/test_1/ (host only)",
|
||||||
|
)
|
||||||
|
|||||||
@@ -28,20 +28,24 @@ class TestRegistry(BaseTestCase):
|
|||||||
self.data_dir.get_file,
|
self.data_dir.get_file,
|
||||||
[
|
[
|
||||||
["test_1", Registry.HOST_FILE],
|
["test_1", Registry.HOST_FILE],
|
||||||
|
["test_1", Registry.HOST_ONLY_FILE],
|
||||||
["test_1", Registry.TOKEN_FILE],
|
["test_1", Registry.TOKEN_FILE],
|
||||||
["test_1", Registry.REDIRECT_FILE],
|
["test_1", Registry.REDIRECT_FILE],
|
||||||
["test_1", Registry.PROXY_FILE],
|
["test_1", Registry.PROXY_FILE],
|
||||||
["test_2", Registry.HOST_FILE],
|
["test_2", Registry.HOST_FILE],
|
||||||
|
["test_2", Registry.HOST_ONLY_FILE],
|
||||||
["test_2", Registry.TOKEN_FILE],
|
["test_2", Registry.TOKEN_FILE],
|
||||||
["test_2", Registry.REDIRECT_FILE],
|
["test_2", Registry.REDIRECT_FILE],
|
||||||
["test_2", Registry.PROXY_FILE],
|
["test_2", Registry.PROXY_FILE],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
None,
|
||||||
"test_1_host",
|
"test_1_host",
|
||||||
"test_1_token",
|
"test_1_token",
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
"test_2_token",
|
"test_2_token",
|
||||||
"test_2_redirect",
|
"test_2_redirect",
|
||||||
None,
|
None,
|
||||||
@@ -58,6 +62,7 @@ class TestRegistry(BaseTestCase):
|
|||||||
"test_1",
|
"test_1",
|
||||||
True, # noqa: FBT003
|
True, # noqa: FBT003
|
||||||
"test_1_host",
|
"test_1_host",
|
||||||
|
True, # noqa: FBT003
|
||||||
"test_1_token",
|
"test_1_token",
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
@@ -68,6 +73,7 @@ class TestRegistry(BaseTestCase):
|
|||||||
"test_2",
|
"test_2",
|
||||||
False, # noqa: FBT003
|
False, # noqa: FBT003
|
||||||
None,
|
None,
|
||||||
|
False, # noqa: FBT003
|
||||||
"test_2_token",
|
"test_2_token",
|
||||||
"test_2_redirect",
|
"test_2_redirect",
|
||||||
),
|
),
|
||||||
@@ -98,10 +104,30 @@ class TestRegistry(BaseTestCase):
|
|||||||
self.mock_call(
|
self.mock_call(
|
||||||
self.data_dir.set_file, ["test_1", Registry.HOST_FILE, "new_value"]
|
self.data_dir.set_file, ["test_1", Registry.HOST_FILE, "new_value"]
|
||||||
),
|
),
|
||||||
|
self.mock_call(
|
||||||
|
self.data_dir.remove_file, ["test_1", Registry.HOST_ONLY_FILE]
|
||||||
|
),
|
||||||
self.seal_mocks(),
|
self.seal_mocks(),
|
||||||
):
|
):
|
||||||
self.registry.set_host("test_1", "new_value")
|
self.registry.set_host("test_1", "new_value")
|
||||||
self.assertEqual(self.registry.pages["test_1"].host, "new_value")
|
self.assertEqual(self.registry.pages["test_1"].host, "new_value")
|
||||||
|
assert not self.registry.pages["test_1"].host_only
|
||||||
|
|
||||||
|
def test_set_host_only(self) -> None:
|
||||||
|
self.registry.pages["test_1"] = Page(
|
||||||
|
"test_1",
|
||||||
|
host="test_1_host",
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
self.mock_call(
|
||||||
|
self.data_dir.set_file, ["test_1", Registry.HOST_ONLY_FILE, "new_value"]
|
||||||
|
),
|
||||||
|
self.mock_call(self.data_dir.remove_file, ["test_1", Registry.HOST_FILE]),
|
||||||
|
self.seal_mocks(),
|
||||||
|
):
|
||||||
|
self.registry.set_host_only("test_1", "new_value")
|
||||||
|
self.assertEqual(self.registry.pages["test_1"].host, "new_value")
|
||||||
|
assert self.registry.pages["test_1"].host_only
|
||||||
|
|
||||||
def test_set_token_hash(self) -> None:
|
def test_set_token_hash(self) -> None:
|
||||||
self.registry.pages["test_1"] = Page(
|
self.registry.pages["test_1"] = Page(
|
||||||
|
|||||||
Reference in New Issue
Block a user