- 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
86 lines
3.9 KiB
Go
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)
|
|
}
|