helpers.go raw

   1  package app
   2  
   3  import (
   4  	"net/http"
   5  	"strings"
   6  
   7  	"next.orly.dev/pkg/lol/log"
   8  )
   9  
  10  // GetRemoteFromReq retrieves the originating IP address of the client from
  11  // an HTTP request, considering standard and non-standard proxy headers.
  12  //
  13  // # Parameters
  14  //
  15  //   - r: The HTTP request object containing details of the client and
  16  //     routing information.
  17  //
  18  // # Return Values
  19  //
  20  //   - rr: A string value representing the IP address of the originating
  21  //     remote client.
  22  //
  23  // # Expected behaviour
  24  //
  25  // The function first checks for the standardized "Forwarded" header (RFC 7239)
  26  // to identify the original client IP. If that isn't available, it falls back to
  27  // the "X-Forwarded-For" header. If both headers are absent, it defaults to
  28  // using the request's RemoteAddr.
  29  //
  30  // For the "Forwarded" header, it extracts the client IP from the "for"
  31  // parameter. For the "X-Forwarded-For" header, if it contains one IP, it
  32  // returns that. If it contains two IPs, it returns the second.
  33  func GetRemoteFromReq(r *http.Request) (rr string) {
  34  	// First check for the standardized Forwarded header (RFC 7239)
  35  	forwarded := r.Header.Get("Forwarded")
  36  	if forwarded != "" {
  37  		// Parse the Forwarded header which can contain multiple parameters
  38  		//
  39  		// Format:
  40  		//
  41  		// 	Forwarded: by=<identifier>;for=<identifier>;host=<host>;proto=<http|https>
  42  		parts := strings.Split(forwarded, ";")
  43  		for _, part := range parts {
  44  			part = strings.TrimSpace(part)
  45  			if strings.HasPrefix(part, "for=") {
  46  				// Extract the client IP from the "for" parameter
  47  				forValue := strings.TrimPrefix(part, "for=")
  48  				// Remove quotes if present
  49  				forValue = strings.Trim(forValue, "\"")
  50  				// Handle IPv6 addresses which are enclosed in square brackets
  51  				forValue = strings.Trim(forValue, "[]")
  52  				return forValue
  53  			}
  54  		}
  55  	}
  56  	// If the Forwarded header is not available or doesn't contain "for"
  57  	// parameter, fall back to X-Forwarded-For
  58  	rem := r.Header.Get("X-Forwarded-For")
  59  	if rem == "" {
  60  		rr = r.RemoteAddr
  61  	} else {
  62  		// X-Forwarded-For can be comma-separated or space-separated;
  63  		// use the first (client) IP for comma-separated, or handle
  64  		// space-separated format from some proxies.
  65  		parts := strings.Split(rem, ",")
  66  		if len(parts) > 1 {
  67  			rr = strings.TrimSpace(parts[0])
  68  		} else {
  69  			splitted := strings.Split(rem, " ")
  70  			rr = strings.TrimSpace(splitted[0])
  71  		}
  72  	}
  73  	return
  74  }
  75  
  76  // LogProxyInfo logs comprehensive proxy information for debugging
  77  func LogProxyInfo(r *http.Request, prefix string) {
  78  	proxyHeaders := map[string]string{
  79  		"X-Forwarded-For":   r.Header.Get("X-Forwarded-For"),
  80  		"X-Real-IP":         r.Header.Get("X-Real-IP"),
  81  		"X-Forwarded-Proto": r.Header.Get("X-Forwarded-Proto"),
  82  		"X-Forwarded-Host":  r.Header.Get("X-Forwarded-Host"),
  83  		"X-Forwarded-Port":  r.Header.Get("X-Forwarded-Port"),
  84  		"Forwarded":         r.Header.Get("Forwarded"),
  85  		"Host":              r.Header.Get("Host"),
  86  		"User-Agent":        r.Header.Get("User-Agent"),
  87  	}
  88  
  89  	var info []string
  90  	for header, value := range proxyHeaders {
  91  		if value != "" {
  92  			info = append(info, header+":"+value)
  93  		}
  94  	}
  95  
  96  	if len(info) > 0 {
  97  		log.T.F("%s proxy info: %s", prefix, strings.Join(info, " "))
  98  	}
  99  }
 100