fix: no message on bare curl

This commit is contained in:
2026-04-27 14:43:27 +02:00
committed by Kleπek
parent 6db1b561f0
commit 95514f16cb
3 changed files with 111 additions and 78 deletions
+14 -10
View File
@@ -112,33 +112,33 @@ PUT /{page}/
```bash
# create archive from 'dist' dir and upload it to /my-project/
tar -czC dist -f dist.tar.gz .
curl -v -X PUT \
curl -X PUT \
-H 'X-Token: <TOKEN>' \
--data-binary "@dist.tar.gz" \
https://stapler-host/my-project/
# same thing but one-liner
tar -czC dist . | curl -v -X PUT \
tar -czC dist . | curl -X PUT \
-H 'X-Token: <TOKEN>' \
--data-binary @- \
https://stapler-host/my-project/
# make stapler server identify myproject.example.com and /my-project/
tar -czC dist . | curl -v -X PUT \
tar -czC dist . | curl -X PUT \
--data-binary @- \
-H 'X-Token: <TOKEN>' \
-H 'X-Host: myproject.example.com' \
https://stapler-host/my-project/
# make stapler server identifiers myproject.example.com only
tar -czC dist . | curl -v -X PUT \
tar -czC dist . | curl -X PUT \
--data-binary @- \
-H 'X-Token: <TOKEN>' \
-H 'X-Host-Only: myproject.example.com' \
https://stapler-host/my-project/
# make a SPA site at /my-project/index.html
tar -czC dist . | curl -v -X PUT \
tar -czC dist . | curl -X PUT \
--data-binary @- \
-H 'X-Token: <TOKEN>' \
-H 'X-SPA: index.html' \
@@ -160,13 +160,13 @@ PUT /{page}/
```bash
# create /my-project/ that redirects to https://github.com/my-project
curl -v -X PUT \
curl -X PUT \
-H 'X-Token: <TOKEN>' \
-H 'X-Redirect: https://github.com/my-project' \
https://stapler-host/my-project/
# simple redirect from root host to www
curl -v -X PUT \
curl -X PUT \
-H 'X-Token: <TOKEN>' \
-H 'X-Proxy: https://www.my-website.com' \
-H 'X-Host: my-website.com' \
@@ -185,7 +185,7 @@ PUT /{page}/
```bash
# create /my-website/ that proxies to http://host.containers.internal:8000
curl -v -X PUT \
curl -X PUT \
-H 'X-Token: <TOKEN>' \
-H 'X-Proxy: http://host.containers.internal:8000' \
https://stapler-host/my-project/
@@ -200,7 +200,7 @@ DELETE /{page}/
```bash
# delete /my-project/
curl -v -X DELETE \
curl -X DELETE \
-H 'X-Token: <TOKEN>' \
https://stapler-host/my-project/
```
@@ -226,7 +226,11 @@ curl -v -X DELETE \
- name: Create archive
run: tar -czC dist -f dist.tar.gz .
- name: Deploy to Stapler server
run: curl -v -X PUT -H 'X-Token: ${{ secrets.STAPLER_TOKEN }}' -H 'X-Host: ${{ vars.TARGET_HOST }}' --data-binary "@dist.tar.gz" https://stapler-host/my-project/
run: |
curl -X PUT --data-binary "@dist.tar.gz" \
-H 'X-Token: ${{ secrets.STAPLER_TOKEN }}' \
-H 'X-Host: ${{ vars.TARGET_HOST }}' \
${{ vars.STAPLER_URL }}
```
### Redirecting hosts with DNS
+72 -57
View File
@@ -45,13 +45,25 @@ class BaseHandler(abc.ABC, http.server.BaseHTTPRequestHandler):
code: int,
message: str | None = None,
explain: str | None = None,
) -> None:
self.send_status(code, message, explain)
def send_status(
self,
code: int,
message: str | None = None,
explain: str | None = None,
) -> None:
shortmsg, longmsg = self.responses[code]
if message is None:
message = shortmsg
if explain is None:
explain = longmsg
if "text/" in self._get_header("Accept"):
if (
not self._has_header("Accept")
or self._get_header("Accept").startswith("*/")
or self._get_header("Accept").startswith("text/")
):
self.send_basic_body(
f"{code} {message}\n{explain}\n\n{self.server_signature()}",
code=code,
@@ -339,28 +351,26 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
def do_PUT(self) -> None:
self._pre_log_request()
if self._proxy_or_redirect():
return None
if (path := self.__check_update_request()) is None:
return None
if not self.__check_put_headers():
return None
if (
self.has_target_host
and (page := self.registry.get_from_host(self.target_host)) is not None
and page.path != path
):
return self.send_error(http.HTTPStatus.FORBIDDEN, "Host already taken")
return
if (path := self.__check_put_request()) is None:
return
if self.has_target_redirect:
self._update_redirect(path)
if not self._update_redirect(path):
return
elif self.has_target_proxy:
self._update_proxy(path)
else:
self._update_extract(path)
if not self._update_proxy(path):
return
elif not self._update_extract(path):
return
if self.has_request_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
self.send_status(
http.HTTPStatus.CREATED,
"Resource updated",
str(self.registry.get_from_path(path)),
)
def do_POST(self) -> None:
self.do_PUT() # be gentle on them
@@ -371,10 +381,15 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
def do_DELETE(self) -> None:
self._pre_log_request()
if self._proxy_or_redirect():
return None
return
if (path := self.__check_update_request()) is None:
return None
return self._update_remove(path)
return
if self._update_remove(path):
self.send_status(
http.HTTPStatus.OK,
f"Resource /{path}/ removed",
)
return
def do_CONNECT(self) -> None:
self._pre_log_request()
@@ -391,73 +406,64 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
if not self._proxy_or_redirect():
self.send_error(http.HTTPStatus.METHOD_NOT_ALLOWED)
def _update_extract(self, path: str) -> None:
def _update_extract(self, path: str) -> bool:
if self.in_size == 0:
return self.send_error(http.HTTPStatus.LENGTH_REQUIRED, "No body found")
self.send_error(http.HTTPStatus.LENGTH_REQUIRED, "No body found")
return False
if self.in_size > self.max_size_bytes:
return self.send_error(
self.send_error(
http.HTTPStatus.CONTENT_TOO_LARGE,
"Archive too large",
)
return False
try:
file_bytes = io.BytesIO(self.rfile.read(self.in_size))
self.data_dir.extract_tar_bytes(path, file_bytes)
except tarfile.TarError:
return self.send_error(http.HTTPStatus.BAD_REQUEST, "Invalid tar archive")
self.send_error(http.HTTPStatus.BAD_REQUEST, "Invalid tar archive")
return False
except Exception as e:
return self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, str(e))
self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, str(e))
return False
self.registry.add(path)
self.token_manager.set_token(path, self.token)
if self.has_target_spa:
self.registry.set_spa(path, self.target_spa)
self.send_status_only(
http.HTTPStatus.CREATED,
f"Resource /{path}/ updated",
)
return None
return True
def _update_redirect(self, path: str) -> None:
def _update_redirect(self, path: str) -> bool:
if self.in_size > 0:
return self.send_error(
self.send_error(
http.HTTPStatus.BAD_REQUEST,
f"No content must be sent with {self.REDIRECT_HEADER}",
)
return False
self.registry.set_redirect(path, self.target_redirect)
self.token_manager.set_token(path, self.token)
self.send_status_only(
http.HTTPStatus.CREATED,
f"Resource /{path}/ updated",
)
return None
return True
def _update_proxy(self, path: str) -> None:
def _update_proxy(self, path: str) -> bool:
if self.in_size > 0:
return self.send_error(
self.send_error(
http.HTTPStatus.BAD_REQUEST,
f"No content must be sent with {self.PROXY_HEADER}",
)
return False
self.registry.set_proxy(path, self.target_proxy)
self.token_manager.set_token(path, self.token)
self.send_status_only(
http.HTTPStatus.CREATED,
f"Resource /{path}/ updated",
)
return None
return True
def _update_remove(self, path: str) -> None:
def _update_remove(self, path: str) -> bool:
if not self.data_dir.exists(path):
self.send_error(http.HTTPStatus.NOT_FOUND, "Not found")
return None
return False
try:
self.data_dir.remove(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 /{path}/ removed",
)
self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, str(e))
return False
self.registry.remove(path)
return None
return True
def _proxy_or_redirect(self) -> bool:
if self.has_token or self.path.startswith(self.CERTBOT_CHALLENGE_PATH):
@@ -518,23 +524,32 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
return None
return sub_path
def __check_put_headers(self) -> bool:
def __check_put_request(self) -> str | None:
if (path := self.__check_update_request()) is None:
return None
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
return None
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
return None
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
return None
if (
self.has_target_host
and (page := self.registry.get_from_host(self.target_host)) is not None
and page.path != path
):
self.send_error(http.HTTPStatus.FORBIDDEN, "Host already taken")
return None
return path
def __get_path(self, path: str, regex: re.Pattern) -> str | None:
if (match := regex.match(path.lower())) is not None:
+25 -11
View File
@@ -136,6 +136,8 @@ class TestRequestHandler(BaseHandlerTestCase):
) -> RequestHandler:
if headers is None:
headers = {}
if "Accept" not in headers:
headers["Accept"] = "nothing"
with self.patch("http.server.BaseHTTPRequestHandler.__init__"):
handler = RequestHandler(
unittest.mock.MagicMock(),
@@ -433,8 +435,9 @@ class TestRequestHandler(BaseHandlerTestCase):
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.registry.get_from_path, ["path"]),
self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
handler, http.HTTPStatus.CREATED, "Resource updated"
),
self.seal_mocks(),
):
@@ -458,8 +461,9 @@ class TestRequestHandler(BaseHandlerTestCase):
self.mock_call(self.registry.add, ["path"]),
self.mock_call(self.token_manager.set_token, ["path", "secret"]),
self.mock_call(self.registry.set_host, ["path", "example.com"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
handler, http.HTTPStatus.CREATED, "Resource updated"
),
self.seal_mocks(),
):
@@ -481,8 +485,9 @@ class TestRequestHandler(BaseHandlerTestCase):
self.mock_call(self.registry.add, ["path"]),
self.mock_call(self.token_manager.set_token, ["path", "secret"]),
self.mock_call(self.registry.set_spa, ["path", "index.html"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
handler, http.HTTPStatus.CREATED, "Resource updated"
),
self.seal_mocks(),
):
@@ -530,8 +535,9 @@ class TestRequestHandler(BaseHandlerTestCase):
),
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.get_from_path, ["path"]),
self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
handler, http.HTTPStatus.CREATED, "Resource updated"
),
self.seal_mocks(),
):
@@ -557,8 +563,9 @@ class TestRequestHandler(BaseHandlerTestCase):
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, ["path", "example.com"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
handler, http.HTTPStatus.CREATED, "Resource updated"
),
self.seal_mocks(),
):
@@ -584,8 +591,9 @@ class TestRequestHandler(BaseHandlerTestCase):
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.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
handler, http.HTTPStatus.CREATED, "Resource updated"
),
self.seal_mocks(),
):
@@ -633,8 +641,9 @@ class TestRequestHandler(BaseHandlerTestCase):
),
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.registry.get_from_path, ["path"]),
self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
handler, http.HTTPStatus.CREATED, "Resource updated"
),
self.seal_mocks(),
):
@@ -660,8 +669,9 @@ class TestRequestHandler(BaseHandlerTestCase):
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.registry.set_host, ["path", "example.com"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated"
handler, http.HTTPStatus.CREATED, "Resource updated"
),
self.seal_mocks(),
):
@@ -783,9 +793,7 @@ class TestRequestHandler(BaseHandlerTestCase):
self.mock_call(self.data_dir.exists, ["path"], True), # noqa: FBT003
self.mock_call(self.data_dir.remove, ["path"]),
self.mock_call(self.registry.remove, ["path"]),
self.expects_error(
handler, http.HTTPStatus.NO_CONTENT, "Resource /path/ removed"
),
self.expects_error(handler, http.HTTPStatus.OK, "Resource /path/ removed"),
self.seal_mocks(),
):
handler.do_DELETE()
@@ -813,6 +821,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": None,
"headers": {
"Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "localhost",
"X-Forwarded-For": "127.0.0.1",
@@ -855,6 +864,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": b"hello",
"headers": {
"Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "localhost",
"X-Forwarded-For": "127.0.0.1",
@@ -897,6 +907,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": None,
"headers": {
"Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "localhost",
"X-Forwarded-For": "127.0.0.1",
@@ -930,6 +941,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": None,
"headers": {
"Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "localhost",
"X-Forwarded-For": "127.0.0.1",
@@ -972,6 +984,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": None,
"headers": {
"Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "localhost",
"X-Forwarded-For": "127.0.0.1",
@@ -1011,6 +1024,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": None,
"headers": {
"Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "host",
"X-Forwarded-For": "127.0.0.1",