NextMyDay/internal/core/httpx/loopback.go
Claus Lohmar d11e14295b feat(core): add the store, identity plumbing and the door (SPEC-003/004)
- 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)
2026-10-14 10:53:51 +00:00

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()
}