github.com/weaveworks/common@v0.0.0-20230728070032-dd9e68f319d5/middleware/source_ips.go (about)

     1  package middleware
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"net/http"
     7  	"regexp"
     8  	"strings"
     9  )
    10  
    11  // Parts copied and changed from gorilla mux proxy_headers.go
    12  
    13  var (
    14  	// De-facto standard header keys.
    15  	xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
    16  	xRealIP       = http.CanonicalHeaderKey("X-Real-IP")
    17  )
    18  
    19  var (
    20  	// RFC7239 defines a new "Forwarded: " header designed to replace the
    21  	// existing use of X-Forwarded-* headers.
    22  	// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
    23  	forwarded = http.CanonicalHeaderKey("Forwarded")
    24  	// Allows for a sub-match of the first value after 'for=' to the next
    25  	// comma, semi-colon or space. The match is case-insensitive.
    26  	forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)`)
    27  )
    28  
    29  // SourceIPExtractor extracts the source IPs from a HTTP request
    30  type SourceIPExtractor struct {
    31  	// The header to search for
    32  	header string
    33  	// A regex that extracts the IP address from the header.
    34  	// It should contain at least one capturing group the first of which will be returned.
    35  	regex *regexp.Regexp
    36  }
    37  
    38  // NewSourceIPs creates a new SourceIPs
    39  func NewSourceIPs(header, regex string) (*SourceIPExtractor, error) {
    40  	if (header == "" && regex != "") || (header != "" && regex == "") {
    41  		return nil, fmt.Errorf("either both a header field and a regex have to be given or neither")
    42  	}
    43  	re, err := regexp.Compile(regex)
    44  	if err != nil {
    45  		return nil, fmt.Errorf("invalid regex given")
    46  	}
    47  
    48  	return &SourceIPExtractor{
    49  		header: header,
    50  		regex:  re,
    51  	}, nil
    52  }
    53  
    54  // extractHost returns the Host IP address without any port information
    55  func extractHost(address string) string {
    56  	hostIP := net.ParseIP(address)
    57  	if hostIP != nil {
    58  		return hostIP.String()
    59  	}
    60  	var err error
    61  	hostStr, _, err := net.SplitHostPort(address)
    62  	if err != nil {
    63  		// Invalid IP address, just return it so it shows up in the logs
    64  		return address
    65  	}
    66  	return hostStr
    67  }
    68  
    69  // Get returns any source addresses we can find in the request, comma-separated
    70  func (sips SourceIPExtractor) Get(req *http.Request) string {
    71  	fwd := extractHost(sips.getIP(req))
    72  	if fwd == "" {
    73  		if req.RemoteAddr == "" {
    74  			return ""
    75  		}
    76  		return extractHost(req.RemoteAddr)
    77  	}
    78  	// If RemoteAddr is empty just return the header
    79  	if req.RemoteAddr == "" {
    80  		return fwd
    81  	}
    82  	remoteIP := extractHost(req.RemoteAddr)
    83  	if fwd == remoteIP {
    84  		return remoteIP
    85  	}
    86  	// If both a header and RemoteAddr are present return them both, stripping off any port info from the RemoteAddr
    87  	return fmt.Sprintf("%v, %v", fwd, remoteIP)
    88  }
    89  
    90  // getIP retrieves the IP from the RFC7239 Forwarded headers,
    91  // X-Real-IP and X-Forwarded-For (in that order) or from the
    92  // custom regex.
    93  func (sips SourceIPExtractor) getIP(r *http.Request) string {
    94  	var addr string
    95  
    96  	// Use the custom regex only if it was setup
    97  	if sips.header != "" {
    98  		hdr := r.Header.Get(sips.header)
    99  		if hdr == "" {
   100  			return ""
   101  		}
   102  		allMatches := sips.regex.FindAllStringSubmatch(hdr, 1)
   103  		if len(allMatches) == 0 {
   104  			return ""
   105  		}
   106  		firstMatch := allMatches[0]
   107  		// Check there is at least 1 submatch
   108  		if len(firstMatch) < 2 {
   109  			return ""
   110  		}
   111  		return firstMatch[1]
   112  	}
   113  
   114  	if fwd := r.Header.Get(forwarded); fwd != "" {
   115  		// match should contain at least two elements if the protocol was
   116  		// specified in the Forwarded header. The first element will always be
   117  		// the 'for=' capture, which we ignore. In the case of multiple IP
   118  		// addresses (for=8.8.8.8, 8.8.4.4,172.16.1.20 is valid) we only
   119  		// extract the first, which should be the client IP.
   120  		if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 {
   121  			// IPv6 addresses in Forwarded headers are quoted-strings. We strip
   122  			// these quotes.
   123  			addr = strings.Trim(match[1], `"`)
   124  		}
   125  	} else if fwd := r.Header.Get(xRealIP); fwd != "" {
   126  		// X-Real-IP should only contain one IP address (the client making the
   127  		// request).
   128  		addr = fwd
   129  	} else if fwd := strings.ReplaceAll(r.Header.Get(xForwardedFor), " ", ""); fwd != "" {
   130  		// Only grab the first (client) address. Note that '192.168.0.1,
   131  		// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
   132  		// the first may represent forwarding proxies earlier in the chain.
   133  		s := strings.Index(fwd, ",")
   134  		if s == -1 {
   135  			s = len(fwd)
   136  		}
   137  		addr = fwd[:s]
   138  	}
   139  
   140  	return addr
   141  }