Serve /api/ on the public short host so callers use piha.lc / beta.piha.li.
CI / test (pull_request) Successful in 4s
CI / test (pull_request) Successful in 4s
Closes #7.
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
"""Bearer token auth for /api/. The lock that keeps a public API host closed."""
|
||||
"""Bearer token auth for /api/. The lock that keeps minting closed on the public host."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"""Keep the short domain and Django admin off the public API hostname."""
|
||||
"""Host split: public short domain serves redirects + /api/; admin stays local."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404, HttpRequest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _normalize_host(host: str) -> str:
|
||||
return host.split(":")[0].lower().rstrip(".")
|
||||
@@ -33,11 +37,16 @@ def is_admin_host(host: str) -> bool:
|
||||
return _host_in(host, list(getattr(settings, "SHORT_ADMIN_HOSTS", []) or []))
|
||||
|
||||
|
||||
class HostSplitMiddleware:
|
||||
"""Short host = redirects only. API host = /api/ (Bearer). Admin = local only.
|
||||
def serves_api(host: str) -> bool:
|
||||
"""Short domain and extra API hosts both serve /api/ (Bearer)."""
|
||||
return is_api_host(host) or is_public_host(host)
|
||||
|
||||
A public DNS name may be listed in SHORT_API_HOSTS. Auth, not the network,
|
||||
keeps /api/ closed: missing/wrong Bearer is 401; empty token list is 503.
|
||||
|
||||
class HostSplitMiddleware:
|
||||
"""One public host: GET /<code> and /api/ (Bearer). Admin = local only.
|
||||
|
||||
Auth, not a second DNS name, keeps /api/ closed: missing/wrong Bearer is 401;
|
||||
empty token list is 503.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
@@ -61,8 +70,13 @@ class HostSplitMiddleware:
|
||||
return self.get_response(request)
|
||||
|
||||
if path.startswith("/api/"):
|
||||
# Short redirect hostname never serves the API, even if mis-listed.
|
||||
if is_public_host(host) or not is_api_host(host):
|
||||
if not serves_api(host):
|
||||
logger.warning(
|
||||
"blocked /api/ host=%s public=%s api=%s",
|
||||
host,
|
||||
is_public_host(host),
|
||||
is_api_host(host),
|
||||
)
|
||||
raise Http404()
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
+23
-30
@@ -21,7 +21,7 @@ SETTINGS = dict(
|
||||
SHORT_DOMAIN="piha.lc",
|
||||
PUBLIC_SHORT_URL="https://piha.lc",
|
||||
SHORT_PUBLIC_HOSTS=["piha.lc"],
|
||||
SHORT_API_HOSTS=["testserver", "localhost", "127.0.0.1", "shortener.example.com"],
|
||||
SHORT_API_HOSTS=["testserver", "localhost", "127.0.0.1", "piha.lc"],
|
||||
SHORT_ADMIN_HOSTS=["localhost", "127.0.0.1"],
|
||||
SHORT_ALLOWED_HOSTS=["mkdrealtor.com"],
|
||||
CLICK_IP_PEPPER="test-pepper-not-the-secret-key",
|
||||
@@ -88,7 +88,7 @@ class AuthTests(TestCase):
|
||||
|
||||
@override_settings(**SETTINGS)
|
||||
class HostSplitTests(TestCase):
|
||||
def test_public_host_api_404_even_with_bearer(self):
|
||||
def test_short_host_with_bearer_201(self):
|
||||
response = self.client.post(
|
||||
"/api/links/",
|
||||
data=json.dumps({"target_url": "https://mkdrealtor.com/x"}),
|
||||
@@ -96,63 +96,56 @@ class HostSplitTests(TestCase):
|
||||
HTTP_AUTHORIZATION=AUTH,
|
||||
HTTP_HOST="piha.lc",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@override_settings(
|
||||
SHORT_API_HOSTS=[
|
||||
"testserver",
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"shortener.example.com",
|
||||
"piha.lc",
|
||||
]
|
||||
)
|
||||
def test_short_host_never_serves_api_even_if_also_listed_as_api(self):
|
||||
response = self.client.post(
|
||||
"/api/links/",
|
||||
data=json.dumps({"target_url": "https://mkdrealtor.com/x"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=AUTH,
|
||||
HTTP_HOST="piha.lc",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
def test_public_host_admin_404(self):
|
||||
response = self.client.get("/admin/", HTTP_HOST="piha.lc")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_public_api_host_without_bearer_401(self):
|
||||
def test_short_host_without_bearer_401(self):
|
||||
response = self.client.post(
|
||||
"/api/links/",
|
||||
data=json.dumps({"target_url": "https://mkdrealtor.com/x"}),
|
||||
content_type="application/json",
|
||||
HTTP_HOST="shortener.example.com",
|
||||
HTTP_HOST="piha.lc",
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertEqual(response["WWW-Authenticate"], "Bearer")
|
||||
|
||||
def test_public_api_host_wrong_token_401(self):
|
||||
def test_short_host_wrong_token_401(self):
|
||||
response = self.client.post(
|
||||
"/api/links/",
|
||||
data=json.dumps({"target_url": "https://mkdrealtor.com/x"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION="Bearer monica:wrong-secret",
|
||||
HTTP_HOST="shortener.example.com",
|
||||
HTTP_HOST="piha.lc",
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_public_api_host_valid_bearer_201(self):
|
||||
@override_settings(
|
||||
SHORT_API_HOSTS=["testserver", "localhost", "127.0.0.1"],
|
||||
)
|
||||
def test_short_host_serves_api_even_if_not_in_api_hosts(self):
|
||||
response = self.client.post(
|
||||
"/api/links/",
|
||||
data=json.dumps({"target_url": "https://mkdrealtor.com/x"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=AUTH,
|
||||
HTTP_HOST="shortener.example.com",
|
||||
HTTP_HOST="piha.lc",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
def test_public_api_host_admin_404(self):
|
||||
response = self.client.get("/admin/", HTTP_HOST="shortener.example.com")
|
||||
@override_settings(
|
||||
ALLOWED_HOSTS=[*SETTINGS["ALLOWED_HOSTS"], "other.example.com"]
|
||||
)
|
||||
def test_allowed_host_not_public_or_api_404(self):
|
||||
response = self.client.post(
|
||||
"/api/links/",
|
||||
data=json.dumps({"target_url": "https://mkdrealtor.com/x"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=AUTH,
|
||||
HTTP_HOST="other.example.com",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_healthz_on_public_and_api(self):
|
||||
|
||||
Reference in New Issue
Block a user