Skip to content

Commit 8ad996e

Browse files
committed
Refactor CF SaaS backend and custom domain service for improved DNS handling
- Simplified the `CfSaasBackend` class by removing the `worker_origin` parameter and normalizing CNAME and delegation targets. - Enhanced the registration process to append ownership verification TXT records when returned by the CF API. - Introduced a new `dns_preflight` module to check CNAME propagation and detect Cloudflare DNS usage, preventing unnecessary CF API calls. - Updated `CustomDomainService` to enforce DNS preflight checks before domain registration. - Modified the domain verification method picker to prioritize CF HTTP DCV for custom domains. - Added unit tests for DNS preflight checks and custom domain resolution to ensure proper functionality and error handling.
1 parent bb47c3e commit 8ad996e

27 files changed

Lines changed: 654 additions & 545 deletions

app.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
StaticCacheHeadersMiddleware,
4545
configure_cors,
4646
)
47+
from middleware.tenant import TenantMiddleware
4748
from repositories.indexes import ensure_indexes
4849
from routes.api_v1 import router as api_v1_router
4950
from routes.auth import router as auth_router
@@ -239,7 +240,9 @@ async def docs(request: Request):
239240
app.add_middleware(
240241
MaxContentLengthMiddleware, max_content_length=settings.max_content_length
241242
)
242-
# 5. Request logging — innermost, logs all requests with request_id
243+
# 5. Tenant resolution — populates request.state.tenant from Host
244+
app.add_middleware(TenantMiddleware)
245+
# 6. Request logging — innermost, logs all requests with request_id
243246
app.add_middleware(RequestLoggingMiddleware)
244247

245248
# ── Error handlers + rate limiter ────────────────────────────────────

caddy/Caddyfile

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,12 @@ qr.spoo.me {
7979
}
8080
}
8181

82-
# CF SaaS fallback origin. CF polls this to keep the origin Active
83-
# (prereq for Custom Hostname registrations). mTLS via AOP required.
84-
proxy-fallback.spoo.me {
82+
# Custom-domain catch-all. CF SaaS terminates TLS at the edge, then
83+
# dispatches to this listener via the zone fallback_origin
84+
# (proxy-fallback.spoo.me) with SNI = proxy-fallback.spoo.me and Host
85+
# = customer fqdn. AOP client cert gates origin access; the app reads
86+
# the Host header to scope the lookup to the right tenant.
87+
:443 {
8588
tls /etc/caddy/origin.crt /etc/caddy/origin.key {
8689
protocols tls1.2 tls1.3
8790
client_auth {
@@ -98,23 +101,3 @@ proxy-fallback.spoo.me {
98101
header_up X-Forwarded-For {client_ip}
99102
}
100103
}
101-
102-
# CF Worker → origin hop for custom domains (see cloudflare-worker/).
103-
# Plain HTTP because Workers can't attach AOP. Shared secret gates auth;
104-
# Host gets rewritten from X-Forwarded-Host so the app builds absolute
105-
# URLs against the customer hostname.
106-
:80 {
107-
log
108-
header {
109-
-Server
110-
}
111-
@authed header X-Worker-Auth {env.WORKER_AUTH_SECRET}
112-
handle @authed {
113-
reverse_proxy app:8000 {
114-
header_up X-Real-IP {client_ip}
115-
header_up X-Forwarded-For {client_ip}
116-
header_up Host {http.request.header.X-Forwarded-Host}
117-
}
118-
}
119-
respond 403
120-
}

cloudflare-worker/README.md

Lines changed: 0 additions & 150 deletions
This file was deleted.

cloudflare-worker/deploy.sh

Lines changed: 0 additions & 25 deletions
This file was deleted.

cloudflare-worker/worker.js

Lines changed: 0 additions & 75 deletions
This file was deleted.

cloudflare-worker/wrangler.toml

Lines changed: 0 additions & 14 deletions
This file was deleted.

config.py

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ class CustomDomainSettings(BaseSettings):
151151
# is config, not abuse). Validators below fail container startup instead.
152152
max_per_user: int = Field(default=1, ge=1)
153153
create_attempts_per_day: int = Field(default=3, ge=1)
154-
verify_attempts_per_hour: int = Field(default=5, ge=1)
154+
# Generous because CF's own DCV cadence can take 5-15 min per probe;
155+
# legitimate users may poll many times during initial activation.
156+
verify_attempts_per_hour: int = Field(default=60, ge=1)
155157

156158
# Re-register cooldown after a user revokes their own domain — discourages
157159
# rapid hostname-cycling abuse against the LE rate limit. Zero allowed
@@ -187,32 +189,16 @@ class CustomDomainSettings(BaseSettings):
187189
# enabled (e.g. <random>.dcv.cloudflare.com). Customer adds
188190
# _acme-challenge.<fqdn> CNAME to <fqdn>.<this value> so CF can renew.
189191
cf_dcv_delegation_target: str = ""
190-
# Hostname CF SaaS dispatches per-Custom-Hostname traffic to. Must match
191-
# the Worker route pattern on the SaaS zone — Worker catches dispatched
192-
# traffic and proxies to the actual origin. Usually same as
193-
# cf_cname_target, kept separate so the user-facing CNAME and the
194-
# internal dispatch hostname can diverge if needed.
195-
cf_worker_origin: str = "customers.spoo.me"
196192
# Retry policy for CF API calls. Three attempts with exponential backoff.
197193
cf_api_max_retries: int = Field(default=3, ge=1)
198194
cf_api_initial_backoff_seconds: float = Field(default=1.0, gt=0)
199195

200196
@model_validator(mode="after")
201197
def _validate_cf_saas_config(self) -> CustomDomainSettings:
202-
# Normalise at config boundary so backend code can trust the value.
203-
self.cf_worker_origin = self.cf_worker_origin.strip().strip(".")
204-
if self.cf_zone_id:
205-
missing = []
206-
if not self.cf_api_token:
207-
missing.append("cf_api_token")
208-
if not self.cf_dcv_delegation_target:
209-
missing.append("cf_dcv_delegation_target")
210-
if not self.cf_worker_origin:
211-
missing.append("cf_worker_origin")
212-
if missing:
213-
raise ValueError(
214-
f"CF SaaS path requires {missing} when cf_zone_id is set."
215-
)
198+
if self.cf_zone_id and not self.cf_api_token:
199+
raise ValueError(
200+
"CF SaaS path requires cf_api_token when cf_zone_id is set."
201+
)
216202
return self
217203

218204

dependencies/wiring.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ def wire_services(app: FastAPI, settings: AppSettings, redis_client) -> None:
196196
custom_domain_repo=custom_domain_repo,
197197
cname_target=cd_settings.cf_cname_target,
198198
dcv_delegation_target=cd_settings.cf_dcv_delegation_target,
199-
worker_origin=cd_settings.cf_worker_origin,
200199
)
201200
# Same instance fills three protocol slots — wiring contract.
202201
verifiers = {
@@ -230,4 +229,7 @@ def wire_services(app: FastAPI, settings: AppSettings, redis_client) -> None:
230229
tenant_resolver=tenant_resolver,
231230
blocked_domain_repo=blocked_domain_repo,
232231
redis_client=redis_client,
232+
preflight_cname_target=cd_settings.cf_cname_target
233+
if cd_settings.cf_zone_id
234+
else None,
233235
)

0 commit comments

Comments
 (0)