Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,168 @@
package clientip
import (
"net"
"net/http"
"strings"
"go.uber.org/zap"
)
// Extractor provides secure client IP address extraction
// CWE-348: Prevents X-Forwarded-For header spoofing by validating trusted proxies
type Extractor struct {
trustedProxies []*net.IPNet
logger *zap.Logger
}
// NewExtractor creates a new IP extractor with trusted proxy configuration
// trustedProxyCIDRs should contain CIDR blocks of trusted reverse proxies
// Example: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
func NewExtractor(trustedProxyCIDRs []string, logger *zap.Logger) (*Extractor, error) {
var trustedProxies []*net.IPNet
for _, cidr := range trustedProxyCIDRs {
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
logger.Error("failed to parse trusted proxy CIDR",
zap.String("cidr", cidr),
zap.Error(err))
return nil, err
}
trustedProxies = append(trustedProxies, ipNet)
}
logger.Info("client IP extractor initialized",
zap.Int("trusted_proxy_ranges", len(trustedProxies)))
return &Extractor{
trustedProxies: trustedProxies,
logger: logger.Named("client-ip-extractor"),
}, nil
}
// NewDefaultExtractor creates an extractor with no trusted proxies
// This is safe for direct connections but will ignore X-Forwarded-For headers
func NewDefaultExtractor(logger *zap.Logger) *Extractor {
logger.Warn("client IP extractor initialized with NO trusted proxies - X-Forwarded-For will be ignored")
return &Extractor{
trustedProxies: []*net.IPNet{},
logger: logger.Named("client-ip-extractor"),
}
}
// Extract extracts the real client IP address from the HTTP request
// CWE-348: Secure implementation that prevents header spoofing
func (e *Extractor) Extract(r *http.Request) string {
// Step 1: Get the immediate connection's remote address
remoteAddr := r.RemoteAddr
// Remove port from RemoteAddr (format: "IP:port" or "[IPv6]:port")
remoteIP := e.stripPort(remoteAddr)
// Step 2: Parse the remote IP
parsedRemoteIP := net.ParseIP(remoteIP)
if parsedRemoteIP == nil {
e.logger.Warn("failed to parse remote IP address",
zap.String("remote_addr", remoteAddr))
return remoteIP // Return as-is if we can't parse it
}
// Step 3: Check if the immediate connection is from a trusted proxy
if !e.isTrustedProxy(parsedRemoteIP) {
// NOT from a trusted proxy - do NOT trust X-Forwarded-For header
// This prevents clients from spoofing their IP by setting the header
e.logger.Debug("remote IP is not a trusted proxy, using RemoteAddr",
zap.String("remote_ip", remoteIP))
return remoteIP
}
// Step 4: Remote IP is trusted, check X-Forwarded-For header
// Format: "client, proxy1, proxy2" (leftmost is original client)
xff := r.Header.Get("X-Forwarded-For")
if xff == "" {
// No X-Forwarded-For header, use RemoteAddr
e.logger.Debug("no X-Forwarded-For header from trusted proxy",
zap.String("remote_ip", remoteIP))
return remoteIP
}
// Step 5: Parse X-Forwarded-For header
// Take the FIRST IP (leftmost) which should be the original client
ips := strings.Split(xff, ",")
if len(ips) == 0 {
e.logger.Debug("empty X-Forwarded-For header",
zap.String("remote_ip", remoteIP))
return remoteIP
}
// Get the first IP and trim whitespace
clientIP := strings.TrimSpace(ips[0])
// Step 6: Validate the client IP
parsedClientIP := net.ParseIP(clientIP)
if parsedClientIP == nil {
e.logger.Warn("invalid IP in X-Forwarded-For header",
zap.String("xff", xff),
zap.String("client_ip", clientIP))
return remoteIP // Fall back to RemoteAddr
}
e.logger.Debug("extracted client IP from X-Forwarded-For",
zap.String("client_ip", clientIP),
zap.String("remote_proxy", remoteIP),
zap.String("xff_chain", xff))
return clientIP
}
// ExtractOrDefault extracts the client IP or returns a default value
func (e *Extractor) ExtractOrDefault(r *http.Request, defaultIP string) string {
ip := e.Extract(r)
if ip == "" {
return defaultIP
}
return ip
}
// isTrustedProxy checks if an IP is in the trusted proxy list
func (e *Extractor) isTrustedProxy(ip net.IP) bool {
for _, ipNet := range e.trustedProxies {
if ipNet.Contains(ip) {
return true
}
}
return false
}
// stripPort removes the port from an address string
// Handles both IPv4 (192.168.1.1:8080) and IPv6 ([::1]:8080) formats
func (e *Extractor) stripPort(addr string) string {
// For IPv6, check for bracket format [IP]:port
if strings.HasPrefix(addr, "[") {
// IPv6 format: [::1]:8080
if idx := strings.LastIndex(addr, "]:"); idx != -1 {
return addr[1:idx] // Extract IP between [ and ]
}
// Malformed IPv6 address
return addr
}
// For IPv4, split on last colon
if idx := strings.LastIndex(addr, ":"); idx != -1 {
return addr[:idx]
}
// No port found
return addr
}
// GetTrustedProxyCount returns the number of configured trusted proxy ranges
func (e *Extractor) GetTrustedProxyCount() int {
return len(e.trustedProxies)
}
// HasTrustedProxies returns true if any trusted proxies are configured
func (e *Extractor) HasTrustedProxies() bool {
return len(e.trustedProxies) > 0
}

View file

@ -0,0 +1,19 @@
package clientip
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config"
)
// ProvideExtractor provides a client IP extractor configured from the application config
func ProvideExtractor(cfg *config.Config, logger *zap.Logger) (*Extractor, error) {
// If no trusted proxies configured, use default (no X-Forwarded-For trust)
if len(cfg.Security.TrustedProxies) == 0 {
logger.Info("no trusted proxies configured - X-Forwarded-For headers will be ignored for security")
return NewDefaultExtractor(logger), nil
}
// Create extractor with trusted proxies
return NewExtractor(cfg.Security.TrustedProxies, logger)
}