- store: SQLite (WAL, foreign keys ON, CGO-free driver) + the embedded migration runner; each module's migrations are name-prefixed (D-012) - audit: the append-only audit log; the actor threads via context (R-1) - users: the user_id/GUID/email anchors (D-013) - config: the YAML settings file (D-035) + validation - httpx: cookies · loopback detection · trusted-proxy XFF · CSRF · security headers · redaction - middleware: correlation id · audit · rate limiting - mail: the invite/recovery mailer seam - server: the core DOOR — the resolution switch, the dispatch registry (RegisterModule), the role gates, /setup · enroll/login · recovery flows, the reserved `static` workflow, and the loopback-only bootstrap (D-025)
30 lines
853 B
Go
30 lines
853 B
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 is treated as loopback — fail closed, so an origin we
|
|
// cannot classify never triggers the nudge.
|
|
func IsLoopbackHost(hostport string) bool {
|
|
host := strings.TrimSpace(hostport)
|
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
|
host = h
|
|
}
|
|
host = strings.Trim(strings.TrimSpace(host), "[]")
|
|
if host == "" {
|
|
return true
|
|
}
|
|
if strings.EqualFold(host, "localhost") {
|
|
return true
|
|
}
|
|
ip := net.ParseIP(host)
|
|
return ip != nil && ip.IsLoopback()
|
|
}
|