package httpx import ( "net" "net/http" "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 (or a raw TCP peer address) // is loopback. It gates the setup-mode check (SPEC-003 §7, D-025) and therefore // FAILS CLOSED in the other direction: an empty or unparseable value is NOT // loopback. func IsLoopbackIP(ip string) bool { loopback, known := classifyHost(ip) return known && loopback } // SetupRequestAllowed reports whether a request may be served while the // instance is in setup mode (SPEC-003 §7, D-025 revised 2026-10-15): the TCP // PEER (the connection's RemoteAddr) must be loopback AND the request must // carry NO `X-Forwarded-For` — i.e. the direct SSH tunnel. Anything else, // including a request relayed by a trusted proxy, is refused (the door answers // 404 before dispatch). // // Both tests are required: a proxy on the same host presents a loopback peer, // so the peer alone is not enough — the ABSENCE of `X-Forwarded-For` is what // proves the request did not traverse a proxy. An empty/unparseable peer fails // closed. NB: this must be evaluated on the RAW request, before ClientIP() SETS // the header (A6). func SetupRequestAllowed(r *http.Request) bool { if strings.TrimSpace(r.Header.Get("X-Forwarded-For")) != "" { return false } return IsLoopbackIP(r.RemoteAddr) } // 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 }