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).
46 lines
1.4 KiB
Go
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
|
|
}
|