NextMyDay/internal/core/httpx/loopback.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

46 lines
1.4 KiB
Go

package httpx
import (
"net"
"strings"
)
// IsLoopbackHost reports whether a request Host ("host" or "host:port") names a
// loopback origin.
//
// It gates the passkey nudge (SPEC-006 §8): a passkey registered at
// `127.0.0.1` binds the Relying Party ID `127.0.0.1` and is UNUSABLE at the
// public domain (O-13/D-041), so the nudge is suppressed on loopback. An
// empty/unknown host counts as loopback — fail closed, so an origin we cannot
// classify never triggers the nudge.
func IsLoopbackHost(hostport string) bool {
loopback, known := classifyHost(hostport)
return !known || loopback
}
// classifyHost reports whether hostport ("host", "host:port", or a bare IP) is
// loopback, and whether it could be classified at all.
//
// "Unknown" is only an empty/blank value: a NAME that is not `localhost` is
// known — and it is not loopback.
func classifyHost(hostport string) (loopback, known bool) {
host := strings.TrimSpace(hostport)
if host == "" {
return false, false
}
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
host = strings.Trim(strings.TrimSpace(host), "[]")
if host == "" {
return false, false
}
if strings.EqualFold(host, "localhost") || strings.EqualFold(host, "localhost.") {
return true, true
}
if ip := net.ParseIP(host); ip != nil {
return ip.IsLoopback(), true
}
// A name we cannot map to loopback: known, and not loopback.
return false, true
}