Files
member-console/internal/server/domains.go
T
cgalo5758 8e3c68c6be Make UI surfaces honestly reflect system state
- Add deployment-name branding to titles, mastheads, and OG tags
- Share one grant delivery-state query with lineage across grants
  surfaces
- Show pool status/usage, org owners, and config readiness
- Make billing views projection-aware with recency and sync vocabulary
- Guard FedWiki creation without domains and render route-aware 404s
2026-08-23 01:45:52 -05:00

86 lines
3.9 KiB
Go

package server
import (
"context"
"log/slog"
"net/http"
"strings"
"git.coopcloud.tech/wiki-cafe/member-console/internal/dnsname"
)
// DomainAuthorizer answers the serving-authorization question behind
// GET /domains/ask: does a servable placement exist at exactly this FQDN?
// Implementations perform an exact-match lookup only — no ancestor or
// wildcard reasoning (spec: domain-authorization). The FedWiki integration
// supplies today's implementation; a future domains registry replaces it
// behind this same interface without changing the HTTP contract.
type DomainAuthorizer interface {
AuthorizeDomain(ctx context.Context, fqdn string) (bool, error)
}
// DomainAskHandler implements the on-demand-TLS ask contract (e.g. Caddy's
// on_demand_tls.ask, which appends ?domain=<fqdn> to the configured URL):
// 200 authorizes certificate issuance for the queried name, any other
// status refuses it. The endpoint is unauthenticated by design —
// TLS-terminating proxies send no credentials — so deployments expose it
// on an internal network only; existence of a name is not sensitive
// (issued certificates are public in Certificate Transparency logs).
//
// The name's shape is checked before anything is looked up. The route is
// unauthenticated, so a malformed parameter must cost a regexp rather than a
// database round trip. A malformed name shares the empty parameter's 400
// rather than the 404 a well-formed-but-unserved name gets: both are
// defects in the request itself, and neither says anything about which
// names exist. Every other status stays as specified — 200 authorizes,
// 404 refuses, 500 on a lookup error.
//
// The endpoint is called both by TLS-terminating proxies (Caddy's
// on_demand_tls.ask, mid-handshake, no Accept header) and, occasionally, by
// a person's browser following a stale or hand-edited link. tmpl renders the
// malformed-request cases as a styled page for the latter (spec:
// domain-authorization); the machine-facing contract — status code and
// plain-text body — is unchanged for everyone else. tmpl may be nil (e.g.
// in tests that only assert status codes), which always takes the
// machine-facing branch.
func DomainAskHandler(authorizer DomainAuthorizer, logger *slog.Logger, tmpl *SafeTemplates) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
domain := dnsname.Normalize(r.URL.Query().Get("domain"))
if domain == "" {
askMalformed(w, r, tmpl, "domain parameter required",
"This link is missing its domain. Ask whoever gave you the link to include a domain, or contact your operator.")
return
}
if msg := dnsname.ValidateExternalFQDN(domain, nil); msg != "" {
askMalformed(w, r, tmpl, "domain parameter malformed",
"This link's domain isn't valid. Ask whoever gave you the link to check it, or contact your operator.")
return
}
ok, err := authorizer.AuthorizeDomain(r.Context(), domain)
if err != nil {
logger.Error("domain authorization check failed",
slog.String("domain", domain), slog.Any("error", err))
http.Error(w, "authorization check failed", http.StatusInternalServerError)
return
}
if !ok {
http.Error(w, "domain not served here", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
}
}
// askMalformed answers a malformed /domains/ask request: status 400 always
// (the machine-facing contract never changes), with the body styled for a
// browser and left as the original plain-text message for everyone else. A
// browser is recognized by an HTML-accepting Accept header — the on-demand
// TLS proxy that is this endpoint's primary caller sends none.
func askMalformed(w http.ResponseWriter, r *http.Request, tmpl *SafeTemplates, machineMsg, humanMsg string) {
if tmpl != nil && strings.Contains(r.Header.Get("Accept"), "text/html") {
tmpl.RenderErrorPage(w, r, http.StatusBadRequest, humanMsg)
return
}
http.Error(w, machineMsg, http.StatusBadRequest)
}