1package httphelper
2
3import (
4 "net"
5 "net/http"
6 "strconv"
7 "strings"
8)
9
10// LocalhostNets contains IPv4 and IPv6 loopback ranges.
11var LocalhostNets = ParseTrustedProxies([]string{"127.0.0.0/8", "::1"})
12
13// ParseTrustedProxies converts a list of IP addresses and CIDR ranges to [net.IPNet].
14// It accepts both single IPs (e.g., "10.0.0.1") and CIDR notation (e.g., "10.0.0.0/8").
15// Single IPs are converted to /32 (IPv4) or /128 (IPv6) CIDR blocks.
16func ParseTrustedProxies(proxies []string) []*net.IPNet {
17 var nets []*net.IPNet
18 for _, proxy := range proxies {
19 proxy = strings.TrimSpace(proxy)
20 if proxy == "" {
21 continue
22 }
23
24 // Try parsing as CIDR
25 _, ipNet, err := net.ParseCIDR(proxy)
26 if err == nil {
27 nets = append(nets, ipNet)
28 continue
29 }
30 // Try parsing as single IP
31 ip := net.ParseIP(proxy)
32 if ip != nil {
33 // Convert single IP to /32 or /128 CIDR
34 bits := 32
35 if ip.To4() == nil {
36 bits = 128
37 }
38 nets = append(nets, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
39 }
40 }
41 return nets
42}
43
44// IsTrustedProxy reports whether the given IP address falls within any of the
45// trusted networks. Returns false if trustedNets is empty or the IP cannot be parsed.
46func IsTrustedProxy(ip string, trustedNets []*net.IPNet) bool {
47 if len(trustedNets) == 0 {
48 return false
49 }
50 parsedIP := net.ParseIP(ip)
51 if parsedIP == nil {
52 return false
53 }
54 for _, ipNet := range trustedNets {
55 if ipNet.Contains(parsedIP) {
56 return true
57 }
58 }
59 return false
60}
61
62// IsTrustedOrLocal reports whether an IP belongs to localhost or the given
63// trusted proxy networks.
64func IsTrustedOrLocal(ip string, proxyNets []*net.IPNet) bool {
65 return IsTrustedProxy(ip, LocalhostNets) || IsTrustedProxy(ip, proxyNets)
66}
67
68// ExtractRemoteIP returns the IP portion of an [http.Request.RemoteAddr],
69// stripping the port. When RemoteAddr lacks a port (e.g. Unix sockets or
70// certain proxy setups), it is returned as-is.
71func ExtractRemoteIP(remoteAddr string) string {
72 host, _, err := net.SplitHostPort(remoteAddr)
73 if err != nil {
74 return remoteAddr
75 }
76 return host
77}
78
79// IsTrustedSource reports whether the request's remote address is in trustedNets.
80// When RemoteAddr lacks a port, IPv6 brackets are stripped before checking.
81func IsTrustedSource(r *http.Request, trustedNets []*net.IPNet) bool {
82 if len(trustedNets) == 0 {
83 return false
84 }
85 host, _, err := net.SplitHostPort(r.RemoteAddr)
86 if err != nil {
87 // RemoteAddr may be a bare IP without a port (e.g. from Unix sockets
88 // or certain proxy setups). Fall back to using it directly.
89 host = strings.TrimSuffix(strings.TrimPrefix(r.RemoteAddr, "["), "]")
90 }
91 return IsTrustedProxy(host, trustedNets)
92}
93
94// HTTPSAuthority converts an incoming Host header plus httpPort/tlsPort into
95// the correct HTTPS authority (host or host:port).
96//
97// Rules:
98// - No port in Host: omit TLS port (public reverse-proxy assumed on :443).
99// - Host port == httpPort: map to tlsPort.
100// - Any other explicit port: preserve it.
101func HTTPSAuthority(host string, httpPort, tlsPort int) string {
102 hostname, portStr, err := net.SplitHostPort(host)
103 if err != nil {
104 // No port in Host header — omit TLS port from URL.
105 return host
106 }
107
108 port, err := strconv.Atoi(portStr)
109 if err == nil && port == httpPort {
110 return net.JoinHostPort(hostname, strconv.Itoa(tlsPort))
111 }
112
113 // Unknown explicit port — preserve it.
114 return net.JoinHostPort(hostname, portStr)
115}