mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
117 lines
3.9 KiB
Go
117 lines
3.9 KiB
Go
package admin
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"log/slog"
|
||
|
|
"net"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
// This file gives the first-run setup endpoint the same proxy-aware client
|
||
|
|
// IP resolution every other session-creating path uses
|
||
|
|
// (api.clientIPWithProxies, used by register/login/AdminIPRestrict/rate
|
||
|
|
// limiting). admin cannot import api — api imports admin, and that would be
|
||
|
|
// a cycle — so the algorithm is reproduced here rather than shared. Keep it
|
||
|
|
// in lockstep with api/middleware.go's clientIPWithProxies/parseCIDRList/
|
||
|
|
// ipInNets if that logic ever changes (OC-0274).
|
||
|
|
|
||
|
|
// setupParseCIDRList parses CIDR strings into networks, skipping invalid
|
||
|
|
// entries with a warning — a misconfigured entry must not take the server
|
||
|
|
// down. Called once per handler at construction (see handleSetup), never on
|
||
|
|
// the request path.
|
||
|
|
func setupParseCIDRList(cidrs []string) []*net.IPNet {
|
||
|
|
nets := make([]*net.IPNet, 0, len(cidrs))
|
||
|
|
for _, c := range cidrs {
|
||
|
|
_, n, err := net.ParseCIDR(c)
|
||
|
|
if err != nil {
|
||
|
|
slog.Warn("ignoring invalid CIDR entry (use address/prefix notation, e.g. 10.0.0.1/32)",
|
||
|
|
"cidr", c, "error", err)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
nets = append(nets, n)
|
||
|
|
}
|
||
|
|
return nets
|
||
|
|
}
|
||
|
|
|
||
|
|
// setupIPInNets reports whether ipStr (a plain IP, no port) falls inside any
|
||
|
|
// of the parsed networks.
|
||
|
|
func setupIPInNets(ipStr string, nets []*net.IPNet) bool {
|
||
|
|
ip := net.ParseIP(ipStr)
|
||
|
|
if ip == nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
for _, n := range nets {
|
||
|
|
if n.Contains(ip) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// setupClientIP returns the real client IP for the first-run setup endpoint,
|
||
|
|
// used both as the rate-limit bucket key and as the session IP recorded for
|
||
|
|
// the new Owner account.
|
||
|
|
//
|
||
|
|
// Security model (identical to api.clientIPWithProxies):
|
||
|
|
// - Always parse the actual connecting address from r.RemoteAddr.
|
||
|
|
// - Only honour X-Forwarded-For/X-Real-IP when the connecting address
|
||
|
|
// matches one of trustedNets. Otherwise a client could forge its IP to
|
||
|
|
// bypass the setup rate limit.
|
||
|
|
// - If trustedNets is empty (no trusted_proxies configured, or no running
|
||
|
|
// config at all — the legacy/test construction path), RemoteAddr is
|
||
|
|
// always used.
|
||
|
|
func setupClientIP(r *http.Request, trustedNets []*net.IPNet) string {
|
||
|
|
remoteHost, _, err := net.SplitHostPort(r.RemoteAddr)
|
||
|
|
if err != nil {
|
||
|
|
// RemoteAddr without a port (e.g. Unix socket or test stub) — use as-is.
|
||
|
|
remoteHost = r.RemoteAddr
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(trustedNets) == 0 {
|
||
|
|
return remoteHost
|
||
|
|
}
|
||
|
|
|
||
|
|
if !setupIPInNets(remoteHost, trustedNets) {
|
||
|
|
return remoteHost
|
||
|
|
}
|
||
|
|
|
||
|
|
// Prefer X-Forwarded-For when coming from a trusted proxy. Walk from the
|
||
|
|
// RIGHT and skip entries that are themselves trusted proxies — the first
|
||
|
|
// non-trusted, valid address is the real client. Taking the leftmost
|
||
|
|
// entry would trust a client-supplied value (a client can prepend a
|
||
|
|
// spoofed IP that the proxy then appends to).
|
||
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||
|
|
parts := strings.Split(xff, ",")
|
||
|
|
leftmostValid := ""
|
||
|
|
for i := len(parts) - 1; i >= 0; i-- {
|
||
|
|
candidate := strings.TrimSpace(parts[i])
|
||
|
|
if candidate == "" || net.ParseIP(candidate) == nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
leftmostValid = candidate
|
||
|
|
if setupIPInNets(candidate, trustedNets) {
|
||
|
|
continue // our own proxy hop, keep walking left
|
||
|
|
}
|
||
|
|
return candidate
|
||
|
|
}
|
||
|
|
// Every entry fell inside trustedNets — a config that covers client
|
||
|
|
// networks too. Falling back to RemoteAddr here would collapse every
|
||
|
|
// client behind the proxy into one rate-limit/session-IP bucket, so
|
||
|
|
// use the furthest-upstream hop instead — the best distinct per-client
|
||
|
|
// value available under such a config.
|
||
|
|
if leftmostValid != "" {
|
||
|
|
return leftmostValid
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fall back to X-Real-IP only when X-Forwarded-For was absent or wholly
|
||
|
|
// unusable. Still validate it — this header is untrustworthy on its own.
|
||
|
|
if xri := strings.TrimSpace(r.Header.Get("X-Real-IP")); xri != "" {
|
||
|
|
if net.ParseIP(xri) != nil {
|
||
|
|
return xri
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return remoteHost
|
||
|
|
}
|