feat: scaffold app skeleton with config, ULID, health endpoints, compose stack (phase 1)
- internal .env loader (real env wins) + strictly validated config struct - ULID package: 48-bit ms timestamp + 80-bit crypto randomness, sortable - HTTP server with recovery/request-id/trusted-proxy/access-log middleware - /healthz, /readyz (pluggable checkers), /metricsz (counter registry) - multi-stage Dockerfile (alpine, non-root) + compose with healthchecks, app bound to 127.0.0.1:8787, mongo unpublished, graceful 15s shutdown Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// clientInfo is the resolved view of the requester after applying the
|
||||
// trusted-proxy policy. It feeds rate limiting, sessions, and the audit log.
|
||||
type clientInfo struct {
|
||||
IP netip.Addr
|
||||
Proto string // effective scheme: "http" or "https"
|
||||
Host string // effective host header
|
||||
}
|
||||
|
||||
func parsePeer(remoteAddr string) netip.Addr {
|
||||
if ap, err := netip.ParseAddrPort(remoteAddr); err == nil {
|
||||
return ap.Addr().Unmap()
|
||||
}
|
||||
if a, err := netip.ParseAddr(remoteAddr); err == nil {
|
||||
return a.Unmap()
|
||||
}
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
func isTrusted(a netip.Addr, trusted []netip.Prefix) bool {
|
||||
if !a.IsValid() {
|
||||
return false
|
||||
}
|
||||
for _, p := range trusted {
|
||||
if p.Contains(a) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveClient applies §12: X-Forwarded-For / X-Forwarded-Proto /
|
||||
// X-Forwarded-Host are honored only when the direct peer is inside
|
||||
// TRUSTED_PROXY_CIDRS; otherwise the headers are ignored entirely.
|
||||
//
|
||||
// The client IP is found by walking the X-Forwarded-For chain from the
|
||||
// rightmost entry (appended by our own proxy) towards the left, skipping
|
||||
// addresses that are themselves trusted proxies; the first untrusted address
|
||||
// is the real client. A malformed entry stops the walk (everything further
|
||||
// left is attacker-controllable).
|
||||
func resolveClient(r *http.Request, trusted []netip.Prefix, defaultProto string) clientInfo {
|
||||
peer := parsePeer(r.RemoteAddr)
|
||||
info := clientInfo{IP: peer, Proto: defaultProto, Host: r.Host}
|
||||
if len(trusted) == 0 || !isTrusted(peer, trusted) {
|
||||
return info
|
||||
}
|
||||
|
||||
var chain []string
|
||||
for _, h := range r.Header.Values("X-Forwarded-For") {
|
||||
for part := range strings.SplitSeq(h, ",") {
|
||||
if part = strings.TrimSpace(part); part != "" {
|
||||
chain = append(chain, part)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := len(chain) - 1; i >= 0; i-- {
|
||||
a := parsePeer(chain[i])
|
||||
if !a.IsValid() {
|
||||
break
|
||||
}
|
||||
info.IP = a
|
||||
if !isTrusted(a, trusted) {
|
||||
break
|
||||
}
|
||||
// a is itself a trusted proxy: keep walking left. If the whole chain
|
||||
// is trusted, the leftmost entry stands as the best available answer.
|
||||
}
|
||||
|
||||
switch p := strings.ToLower(strings.TrimSpace(r.Header.Get("X-Forwarded-Proto"))); p {
|
||||
case "http", "https":
|
||||
info.Proto = p
|
||||
}
|
||||
if h := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); h != "" {
|
||||
info.Host = h
|
||||
}
|
||||
return info
|
||||
}
|
||||
Reference in New Issue
Block a user