NextMyDay/internal/core/httpx/scheme.go
Claus Lohmar c7229e23ff feat(setup): gate setup with the config toggle and an optional code
D-045 — the setup gate is now `config.setup`, not the peer/SSH tunnel:

- setup: true      -> /setup serves the wizard (the code form first when
                      `setup_code` is set, via the existing D-044 gate);
                      GET / points at the wizard; every other route -> 404.
- setup: false + no admin -> LOCKED: GET / and /setup render an "enable
                      setup" boilerplate; every other route -> 404; the
                      wizard is not claimable.
- completion       -> writes `setup: false` and creates the admin.

`setup_code` is now OPTIONAL: blank => Gate.Required() false => the toggle
alone admits; set => required (reusing the unlock cookie/fingerprint/
redaction/rate-limit). The peer/tunnel gate is retired: `httpx.
SetupRequestAllowed` and `httpx.IsLoopbackIP` are deleted; the XFF/XFP
handling is kept for the client IP and the Secure/__Host- cookies + HSTS
(A6/A11/D-043). The !bootstrapped() fail-safe becomes the locked predicate,
latched at startup.

New door seams: Deps.LockedMode and SetupGate.{Required,CodeForm,Landing,
Locked}; the Gate renders three first-run pages. The startup banner states
the mode and prints the code only when required.

Tests cover the three states, the boilerplate, the code optionality and the
unlock mechanics; live-smoked end to end (locked / wizard / code form /
full bootstrap -> setup: false).
2026-10-15 06:47:33 +00:00

47 lines
1.7 KiB
Go

package httpx
import (
"context"
"net/http"
"strings"
)
// secureKey carries the per-request "served over https" decision on the request
// context. The door resolves it ONCE (RequestIsHTTPS) and every cookie writer
// reads it back, so the session cookie's `Secure` flag, the `__Host-` prefix,
// and HSTS all agree on one value (D-043).
type secureKey struct{}
// WithSecure records whether the request is served over https (D-043). The door
// sets it before dispatching; a request that never passed through the door has
// no value and is treated as plain http.
func WithSecure(ctx context.Context, secure bool) context.Context {
return context.WithValue(ctx, secureKey{}, secure)
}
// SecureFromRequest reports whether the request is served over https, as
// resolved by the door (D-043). The zero value is false — plain http — which is
// what keeps a plain-http bootstrap working.
func SecureFromRequest(r *http.Request) bool {
v, _ := r.Context().Value(secureKey{}).(bool)
return v
}
// RequestIsHTTPS reports whether r reached the app over https (D-043): either
// the connection itself is TLS, or a TRUSTED proxy terminated TLS and said so
// via `X-Forwarded-Proto`. A header from an untrusted peer is ignored, so a
// client cannot spoof `Secure` cookies onto a plain-http connection. The
// leftmost token is the original client-facing scheme.
func RequestIsHTTPS(r *http.Request, trusted TrustedProxies) bool {
if r.TLS != nil {
return true
}
if !trusted.Contains(remoteIP(r.RemoteAddr)) {
return false
}
proto := r.Header.Get("X-Forwarded-Proto")
if i := strings.IndexByte(proto, ','); i >= 0 {
proto = proto[:i]
}
return strings.EqualFold(strings.TrimSpace(proto), "https")
}