fix: no message on bare curl
This commit is contained in:
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user