Serve /api/ on piha.lc / beta.piha.li #8

Merged
westfarn merged 2 commits from feat/7-one-public-host into master 2026-08-30 17:27:40 -07:00
10 changed files with 115 additions and 113 deletions
+3 -3
View File
@@ -18,11 +18,11 @@ CONTACT_URL=https://aimloperations.com/contact
SHORT_DOMAIN=localhost:8005
# Origin printed in minted short_url (phones hit this). Local: this machine.
PUBLIC_SHORT_URL=http://127.0.0.1:8005
# Host values that only serve GET /<code> (no /api/).
# Host values that serve GET /<code> and /api/ (Bearer). Same public hostname.
SHORT_PUBLIC_HOSTS=piha.lc
# Host values that serve /api/ (Bearer required). May include a public DNS name.
# Extra Host values that also serve /api/ (localhost / docker).
SHORT_API_HOSTS=localhost,127.0.0.1,0.0.0.0,web,url-shortener
# Django admin — keep local. Do not add the public API hostname.
# Django admin — keep local. Do not add the public short hostname.
SHORT_ADMIN_HOSTS=localhost,127.0.0.1
# Named, rotatable tokens. This is what keeps /api/ closed on a public hostname.
# Generate: python -c "import secrets; print(secrets.token_urlsafe(32))"
+6 -4
View File
@@ -12,8 +12,8 @@
DJANGO_ENV=prod
DJANGO_DEBUG=false
DJANGO_SECRET_KEY=replace-with-a-long-random-secret
# Public short host AND public API hostname (plus docker names if used).
DJANGO_ALLOWED_HOSTS=piha.lc,shortener.aimloperations.com,url-shortener,web
# Public short host (plus docker names if used). One hostname for mint + redirects.
DJANGO_ALLOWED_HOSTS=piha.lc,url-shortener,web
# Shared external Postgres
DATABASE_URL=postgres://westfarn:replace-db-password@10.0.0.230:5432/url_shortener
@@ -24,7 +24,7 @@ WEB_PORT=8005
SHORT_DOMAIN=piha.lc
PUBLIC_SHORT_URL=https://piha.lc
SHORT_PUBLIC_HOSTS=piha.lc
SHORT_API_HOSTS=shortener.aimloperations.com,url-shortener,web
SHORT_API_HOSTS=piha.lc,url-shortener,web
SHORT_ADMIN_HOSTS=localhost,127.0.0.1
# Generate: python -c "import secrets; print(secrets.token_urlsafe(32))"
# This token is the only thing that authorizes minting. Treat it as a secret.
@@ -45,11 +45,13 @@ GUNICORN_BIND=0.0.0.0:8000
# DJANGO_ENV=beta
# DJANGO_DEBUG=false
# DJANGO_SECRET_KEY=replace-with-a-different-beta-secret
# DJANGO_ALLOWED_HOSTS=beta.piha.li,shortener-beta.aimloperations.com,url-shortener,web
# DJANGO_ALLOWED_HOSTS=beta.piha.li,url-shortener,web
# DATABASE_URL=postgres://westfarn:replace-db-password@10.0.0.230:5432/url_shortener_beta
# WEB_PORT=8015
# SHORT_DOMAIN=beta.piha.li
# PUBLIC_SHORT_URL=https://beta.piha.li
# SHORT_PUBLIC_HOSTS=beta.piha.li
# SHORT_API_HOSTS=beta.piha.li,url-shortener,web
# SHORT_ADMIN_HOSTS=localhost,127.0.0.1
# SHORTENER_API_TOKENS=monica:replace-with-a-different-token
# CLICK_IP_PEPPER=replace-with-a-different-pepper
+11 -12
View File
@@ -2,14 +2,13 @@
For other services (`monica_site`, later callers) that mint short links.
This service has two public hostnames. **Call the API host.** Never create links
on the short domain (`piha.lc` / `beta.piha.li`). That host
only serves `GET /` (landing) and `GET /<code>` (302). `/api/` there is **404**.
This service has one public hostname. Call it for minting **and** for
redirects. Prod: `https://piha.lc`. Beta: `https://beta.piha.li`.
| Host | Example | What you call |
|------|---------|----------------|
| API | `https://shortener.aimloperations.com` | `POST/GET /api/links/` |
| Short | `https://piha.lc` (beta: `https://beta.piha.li`) | phones only — `GET /<code>` |
| Prod | `https://piha.lc` | `POST /api/links/` and `GET /<code>` |
| Beta | `https://beta.piha.li` | same |
Local compose: API + redirects on `http://127.0.0.1:8005`.
@@ -53,7 +52,7 @@ mint without it.
allows `www.mkdrealtor.com`. `https` only.
5. Give the caller **only**:
- API origin (`SHORTENER_BASE_URL`)
- Origin (`SHORTENER_BASE_URL` — `https://piha.lc` or `https://beta.piha.li`)
- The full token string `name:secret` (they send it as Bearer)
Never put the token in git, logs, or the short URL.
@@ -66,11 +65,11 @@ Add to that apps env (not this repo):
```text
# Prod
SHORTENER_BASE_URL=https://shortener.aimloperations.com
SHORTENER_BASE_URL=https://piha.lc
SHORTENER_API_TOKEN=monica:<same-secret>
# Beta (when that host exists)
# SHORTENER_BASE_URL=https://shortener-beta.aimloperations.com
# Beta
# SHORTENER_BASE_URL=https://beta.piha.li
# SHORTENER_API_TOKEN=monica:<beta-secret>
# Local (this service via compose)
@@ -156,8 +155,8 @@ Content-Type: application/json
}
```
`short_url` is built from this services `PUBLIC_SHORT_URL` (the domain phones
hit). It is **not** the API host. Put `short_url` in SMS / email as-is.
`short_url` is built from this services `PUBLIC_SHORT_URL` (same origin you
called). Put `short_url` in SMS / email as-is.
### Idempotency `200`
@@ -286,4 +285,4 @@ def shorten(target_url: str, *, title: str = "", external_ref: str = "") -> str:
```
Use `short_url` in the message body. On `401`/`503`, fail the send — do not
fall back to pasting the API host into SMS.
fall back to pasting a long URL unless that is your explicit fallback.
+29 -35
View File
@@ -15,7 +15,7 @@ This document is the build spec. Implement in this repo. Do not fold the shorten
- Mint a short HTTPS URL that 302s to a long HTTPS URL.
- Public `GET /<code>` so SMS recipients can tap the link with no token.
- Create / list / disable **only** via a Bearer-authenticated API (own hostname; may be public DNS).
- Create / list / disable **only** via a Bearer-authenticated API on the public short host.
- Named, rotatable tokens so `monica_site` can be revoked without rotating every caller.
- Target-host allowlist so a stolen token cannot mint open redirects off the short domain.
- Click counts for campaign reporting.
@@ -38,7 +38,7 @@ Two surfaces. Mixing them is the failure mode.
| Surface | Who | Auth | Network |
|---------|-----|------|---------|
| `GET /<code>` | anyone with the SMS | **none** | public short hostname (NPM + TLS) |
| `/api/links/` (all methods) | `monica_site` and other trusted callers | **Bearer** | own hostname — **may be public DNS**. Token is the lock. |
| `/api/links/` (all methods) | `monica_site` and other trusted callers | **Bearer** | same public hostname. Token is the lock. |
### 2.1 Public redirect
@@ -60,30 +60,24 @@ working.
`DJANGO_SECRET_KEY` or any `monica_site` webhook secret.
- Do not log the `Authorization` header (Gunicorn/NPM access logs). Strip or disable it.
### 2.3 Two public hostnames (Bearer is the lock on /api/)
### 2.3 One public hostname (Bearer is the lock on /api/)
`/api/links/` may have its own public DNS record. Anyone who finds that URL still
`/api/links/` lives on `piha.lc` / `beta.piha.li`. Anyone who finds that URL still
cannot mint links without a configured Bearer token. Empty `SHORTENER_API_TOKENS`
**503**. Missing/wrong token → **401**. Do not add CORS `*` (server-to-server only).
```
Internet
─ NPM: SHORT_DOMAIN (piha.lc / beta.piha.li)
location ~*^/[a-z0-9]{4,8}$ → gunicorn (redirects only)
/api/* → 404 / drop
/admin/ → 404 / drop
└─ NPM: API hostname (e.g. shortener.aimloperations.com)
/api/links/ → gunicorn (Bearer required)
/admin/ → 404 / drop
─ NPM: SHORT_DOMAIN (piha.lc / beta.piha.li)
GET /<code> → gunicorn (redirects)
/api/links/ gunicorn (Bearer required)
/admin/ → 404 (Django)
```
- Public NPM for `SHORT_DOMAIN` must not proxy `/api/` or `/admin/`.
- Public NPM for the API hostname proxies `/api/` only. Do not expose `/admin/`.
- `monica_site` calls `SHORTENER_BASE_URL` (the API hostname), never the short
hostname, to create links.
- Django admin stays on `SHORT_ADMIN_HOSTS` (localhost). Not on the public API host.
- One NPM proxy host. `location /` → gunicorn. Django 404s `/admin/` and `/debug/`.
- `monica_site` calls `SHORTENER_BASE_URL` (`https://piha.lc` or `https://beta.piha.li`).
- Django admin stays on `SHORT_ADMIN_HOSTS` (localhost). Not on the public host.
### 2.4 Target allowlist
@@ -99,10 +93,11 @@ On create:
Even if NPM is misconfigured, the Django process must refuse the wrong surface:
- `request.get_host()` in `SHORT_PUBLIC_HOSTS` → redirect view only. `/api/` → 404.
- `request.get_host()` in `SHORT_API_HOSTS``/api/` (still Bearer). May be public DNS.
- `request.get_host()` in `SHORT_PUBLIC_HOSTS` → redirects **and** `/api/` (Bearer).
- `request.get_host()` in `SHORT_API_HOSTS``/api/` (still Bearer). Extra names
(localhost, docker) only.
- `request.get_host()` in `SHORT_ADMIN_HOSTS``/admin/` (localhost only by default).
- `/healthz/` allowed on both public hosts. No secrets in the body.
- `/healthz/` allowed on both. No secrets in the body.
---
@@ -120,7 +115,8 @@ Phone SMS: https://mkd.to/a3k9
Host header?
├─ public short host → GET /<code> 302 + Click row
└─ API host (own DNS) → /api/links/ (Bearer)
→ /api/links/ (Bearer)
└─ localhost / docker → /api/links/ (Bearer)
Postgres (own DATABASE_URL)
@@ -191,7 +187,7 @@ url_shortening_service/
services.py
urls.py
tests.py
admin.py # optional; only reachable on API host if enabled
admin.py # optional; localhost only
core/
views.py # healthz
```
@@ -353,12 +349,12 @@ Reject codes that do not match `^[a-z0-9]{4,8}$` with 404 (no extra work).
| `DJANGO_ENV` | `dev` / `beta` / `prod` |
| `DJANGO_SECRET_KEY` | Django signing; **not** an API token |
| `DJANGO_DEBUG` | false in prod |
| `DJANGO_ALLOWED_HOSTS` | public short host **and** API hostname |
| `DJANGO_ALLOWED_HOSTS` | public short host (plus docker names) |
| `DATABASE_URL` | Postgres; empty → SQLite |
| `SHORT_DOMAIN` | public hostname phones use (`piha.lc` prod, `beta.piha.li` beta) |
| `PUBLIC_SHORT_URL` | origin for minted URLs, e.g. `https://piha.lc` (no trailing slash) |
| `SHORT_PUBLIC_HOSTS` | comma list; Host values that only serve redirects |
| `SHORT_API_HOSTS` | comma list; Host values that serve `/api/` (public API DNS and/or docker name) |
| `SHORT_PUBLIC_HOSTS` | comma list; Host values that serve redirects **and** `/api/` |
| `SHORT_API_HOSTS` | comma list; extra Host values that serve `/api/` (localhost / docker) |
| `SHORT_ADMIN_HOSTS` | comma list; Host values that serve `/admin/` (default localhost only) |
| `SHORTENER_API_TOKENS` | `name:secret,name:secret` — required for API |
| `SHORT_ALLOWED_HOSTS` | allowlist for `target_url` hosts |
@@ -373,7 +369,7 @@ Reject codes that do not match `^[a-z0-9]{4,8}$` with 404 (no extra work).
### Caller (`monica_site`) — later, other repo
```text
SHORTENER_BASE_URL=https://shortener.aimloperations.com
SHORTENER_BASE_URL=https://piha.lc
SHORTENER_API_TOKEN=monica:<same-secret>
```
@@ -403,7 +399,7 @@ Same pattern as `monica_site`:
- `web`: build `.`, mount `./site`, `DJANGO_ENV=dev` → runserver
- **Do not** publish the host port to the LAN unless you are testing redirects.
Prefer `127.0.0.1:8005:8000` (container still listens on 8000).
- Internal API hostname: service name `web` (or `url-shortener`).
- Internal hostname: service name `web` (or `url-shortener`).
### `docker-compose.prod.yml`
@@ -424,12 +420,10 @@ Wait for DB → `migrate --noinput` → if `DJANGO_ENV=dev` then `runserver`, el
When the domains exist:
1. Public NPM proxy host = `SHORT_DOMAIN` → this containers `WEB_PORT`.
2. Proxy `/` (landing) and the code regex. `/api/`, `/admin/`, `/debug/` → 404.
2. Proxy `/` (landing), the code regex, and `/api/`. Django 404s `/admin/` and `/debug/`.
3. TLS like other apps.
4. Add `SHORT_DOMAIN` to `DJANGO_ALLOWED_HOSTS` and `SHORT_PUBLIC_HOSTS`.
4. Add `SHORT_DOMAIN` to `DJANGO_ALLOWED_HOSTS`, `SHORT_PUBLIC_HOSTS`, and `SHORT_API_HOSTS`.
5. `PUBLIC_SHORT_URL=https://<that-domain>`.
6. Second NPM proxy host = API hostname → same container. Proxy `/api/` only.
7. Add the API hostname to `DJANGO_ALLOWED_HOSTS` and `SHORT_API_HOSTS`.
CI (same split as `monica_site`, default branch `master`):
@@ -447,15 +441,15 @@ Django `TestCase` / `SimpleTestCase`. No live network.
- Auth: missing Bearer → 401; wrong token → 401; empty `SHORTENER_API_TOKENS` → 503;
matching named token → 201.
- Host split: request to short host `/api/links/` → 404 even with valid Bearer.
Public API host without Bearer → 401; valid Bearer → 201. `/admin/` 404 on API host.
- Host split: request to short host `/api/links/` without Bearer → 401; valid Bearer → 201.
Host not in public/API lists → 404. `/admin/` 404 on the public host.
- Allowlist: `https://mkdrealtor.com/x` ok; `http://…` 400; `https://evil.com` 400;
`javascript:alert(1)` 400.
- Redirect: active code → 302 to target; inactive/expired/unknown → 404.
- Redirect is 302, not 301.
- Click row created and `click_count` incremented.
- Idempotent create with same `target_url` + `external_ref`.
- `short_url` uses `PUBLIC_SHORT_URL`, not the API Host header.
- `short_url` uses `PUBLIC_SHORT_URL`.
- Code charset: generated codes only use the unambiguous alphabet.
---
@@ -488,7 +482,7 @@ cd site && uv run python manage.py migrate
uv run python manage.py runserver
```
Mint (API host / localhost):
Mint (same host / localhost):
```bash
curl -sS -X POST http://127.0.0.1:8005/api/links/ \
+18 -18
View File
@@ -1,8 +1,9 @@
# url_shortening_service
Django 6 URL shortener. `monica_site` (and other trusted callers) mint links over
Bearer auth on an **API hostname** (may be public DNS). SMS recipients hit
`GET /<code>` on the **short** domain and get a 302 to the long HTTPS URL.
Bearer auth on the **same** public host phones use. SMS recipients hit
`GET /<code>` on `piha.lc` (prod) / `beta.piha.li` (beta) and get a 302 to the
long HTTPS URL.
This service is standalone. Do not fold it into `monica_site`.
@@ -10,17 +11,16 @@ This service is standalone. Do not fold it into `monica_site`.
| Who | Path | Auth | Host |
|-----|------|------|------|
| Phone / public internet | `GET /<code>` | none | `SHORT_DOMAIN` (NPM + TLS) |
| Anyone | `GET /` | none | landing page on the short domain |
| `monica_site` | `/api/links/` | `Authorization: Bearer name:secret` | own DNS / NPM host — **not** the short domain |
| Phone / public internet | `GET /<code>` | none | `SHORT_DOMAIN` (`piha.lc` / `beta.piha.li`) |
| Anyone | `GET /` | none | landing page on that same host |
| `monica_site` | `/api/links/` | `Authorization: Bearer name:secret` | same host |
The API may be on the public internet. It is not open: every `/api/` request needs a
`/api/` is on the public internet. It is not open: every `/api/` request needs a
named Bearer token. No token / wrong token → **401**. No tokens configured → **503**.
`GET /<code>` never requires a token.
`/api/` and `/admin/` are 404 on the short domain. `/admin/` is also 404 on the
public API hostname (localhost only). `GET /debug/` is a mint form when
`DEBUG=true` and never on the short domain.
`/admin/` is 404 on the public host (localhost only). `GET /debug/` is a mint form
when `DEBUG=true` and never on the public short host.
## Local run
@@ -63,11 +63,11 @@ Deploy calls `server-infra/scripts/deploy.sh --app url_shortening_service`. Need
## Use case
1. Your site POSTs to this service with a Bearer token and a long HTTPS URL.
2. Response `201` includes `short_url` built from `PUBLIC_SHORT_URL` (not the API Host).
2. Response `201` includes `short_url` built from `PUBLIC_SHORT_URL`.
3. A person on the public internet opens that short URL.
4. This service 302s them to the long URL and increments `click_count`.
Mint (API host / localhost):
Mint (same host / localhost):
```bash
curl -sS -X POST http://127.0.0.1:8005/api/links/ \
@@ -89,7 +89,7 @@ Caller integration (onboard + `/api/links/`): **[API.md](API.md)**.
## Caller contract (`monica_site`, other repo)
```text
SHORTENER_BASE_URL=https://shortener.aimloperations.com
SHORTENER_BASE_URL=https://piha.lc
SHORTENER_API_TOKEN=monica:<same-secret-as-SHORTENER_API_TOKENS>
```
@@ -105,7 +105,7 @@ Content-Type: application/json
(exact or suffix, e.g. `mkdrealtor.com`). Same `target_url` + `external_ref` +
still-active link returns `200` with the existing row instead of a new code.
Do not call the public short hostname to create links.
Do not put the Bearer token on `GET /<code>`.
## Environment
@@ -123,9 +123,9 @@ short URL, logs, or git.
## Deploy notes
Two NPM hosts, same container `WEB_PORT` (default 8005 prod / 8015 beta):
One NPM host, container `WEB_PORT` (default 8005 prod / 8015 beta):
1. `SHORT_DOMAIN` (`piha.lc` prod / `beta.piha.li` beta) — `/` landing + `GET /[a-z0-9]{4,8}`.
Drop `/api/`, `/admin/`, `/debug/`.
2. API hostname — proxy `/api/` only. Drop `/admin/`. Add that Host to
`DJANGO_ALLOWED_HOSTS` and `SHORT_API_HOSTS`.
1. `SHORT_DOMAIN` (`piha.lc` prod / `beta.piha.li` beta) — `/` landing,
`GET /[a-z0-9]{4,8}`, and `/api/` (Bearer). Django 404s `/admin/` and `/debug/`
on this host. Add it to `DJANGO_ALLOWED_HOSTS`, `SHORT_PUBLIC_HOSTS`, and
`SHORT_API_HOSTS`.
+2 -2
View File
@@ -14,7 +14,7 @@
</div>
</div>
<div class="section" style="background: var(--surface-color);">
<div class="section">
<div class="container">
<h2 class="section-title">Why Piha?</h2>
<p style="text-align: center; max-width: 820px; margin: 0 auto 1.5rem; color: var(--text-muted); font-size: 1.1rem;">
@@ -27,7 +27,7 @@
</div>
</div>
<div class="section">
<div class="section" style="background: var(--surface-color);">
<div class="container">
<h2 class="section-title">A shortener you can hand to SMS</h2>
<p style="text-align: center; max-width: 820px; margin: 0 auto; color: var(--text-muted); font-size: 1.1rem;">
+1 -1
View File
@@ -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
+21 -7
View File
@@ -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
View File
@@ -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):
+1 -1
View File
@@ -184,7 +184,7 @@ SHORT_API_HOSTS = env_list(
"SHORT_API_HOSTS",
"localhost,127.0.0.1,0.0.0.0,testserver,web,url-shortener",
)
# Django admin — local/dev only. Never put the public API hostname here.
# Django admin — local/dev only. Never put the public short hostname here.
SHORT_ADMIN_HOSTS = env_list("SHORT_ADMIN_HOSTS", "localhost,127.0.0.1")
SHORTENER_API_TOKENS = parse_api_tokens(env("SHORTENER_API_TOKENS", "") or "")
SHORT_ALLOWED_HOSTS = env_list(