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 } // IsLoopbackIP reports whether a RESOLVED client IP is loopback. It gates the // per-request setup-mode check (SPEC-003 §7, H2) and therefore FAILS CLOSED in // the other direction: an empty or unparseable value is NOT loopback. // // A bind-time check alone is not enough (D-025): a reverse proxy on 127.0.0.1 // relaying remote clients makes `/setup` reachable past the bind check, so the // door must re-check every request. func IsLoopbackIP(ip string) bool { loopback, known := classifyHost(ip) return known && loopback } // classifyHost reports whether hostport ("host", "host:port", or a bare IP) is // loopback, and whether it could be classified at all. The two callers above // resolve "unknown" in opposite directions on purpose: each is fail-closed for // its own decision. // // "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 }