headers.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. package internal
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "net"
  8. "net/http"
  9. "net/netip"
  10. "strings"
  11. "github.com/TecharoHQ/anubis"
  12. "github.com/sebest/xff"
  13. )
  14. type realIPKey struct{}
  15. func RealIP(r *http.Request) (netip.Addr, bool) {
  16. result, ok := r.Context().Value(realIPKey{}).(netip.Addr)
  17. return result, ok
  18. }
  19. // TODO: move into config
  20. type XFFComputePreferences struct {
  21. StripPrivate bool
  22. StripLoopback bool
  23. StripCGNAT bool
  24. StripLLU bool
  25. Flatten bool
  26. }
  27. var CGNat = netip.MustParsePrefix("100.64.0.0/10")
  28. // UnchangingCache sets the Cache-Control header to cache a response for 1 year if
  29. // and only if the application is compiled in "release" mode by Docker.
  30. func UnchangingCache(next http.Handler) http.Handler {
  31. //goland:noinspection GoBoolExpressions
  32. if anubis.Version == "devel" {
  33. return next
  34. }
  35. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  36. w.Header().Set("Cache-Control", "public, max-age=31536000")
  37. next.ServeHTTP(w, r)
  38. })
  39. }
  40. // CustomXRealIPHeader sets the X-Real-IP header to the value of a
  41. // different header.
  42. // Used in environments where the upstream proxy sets the request's
  43. // origin IP in a custom header.
  44. func CustomRealIPHeader(customRealIPHeaderValue string, next http.Handler) http.Handler {
  45. if customRealIPHeaderValue == "" {
  46. slog.Debug("skipping middleware, customRealIPHeaderValue is empty")
  47. return next
  48. }
  49. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  50. r.Header.Set("X-Real-IP", r.Header.Get(customRealIPHeaderValue))
  51. next.ServeHTTP(w, r)
  52. })
  53. }
  54. // RemoteXRealIP sets the X-Real-Ip header to the request's real IP if
  55. // the setting is enabled by the user.
  56. func RemoteXRealIP(useRemoteAddress bool, bindNetwork string, next http.Handler) http.Handler {
  57. if !useRemoteAddress {
  58. slog.Debug("skipping middleware, useRemoteAddress is empty")
  59. return next
  60. }
  61. if bindNetwork == "unix" {
  62. // For local sockets there is no real remote address but the localhost
  63. // address should be sensible.
  64. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  65. r.Header.Set("X-Real-Ip", "127.0.0.1")
  66. next.ServeHTTP(w, r)
  67. })
  68. }
  69. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  70. host, _, err := net.SplitHostPort(r.RemoteAddr)
  71. if err != nil {
  72. panic(err) // this should never happen
  73. }
  74. r.Header.Set("X-Real-Ip", host)
  75. if addr, err := netip.ParseAddr(host); err == nil {
  76. r = r.WithContext(context.WithValue(r.Context(), realIPKey{}, addr))
  77. }
  78. next.ServeHTTP(w, r)
  79. })
  80. }
  81. // XForwardedForToXRealIP sets the X-Real-Ip header based on the contents
  82. // of the X-Forwarded-For header.
  83. func XForwardedForToXRealIP(next http.Handler) http.Handler {
  84. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  85. if xffHeader := r.Header.Get("X-Forwarded-For"); r.Header.Get("X-Real-Ip") == "" && xffHeader != "" {
  86. ip := xff.Parse(xffHeader)
  87. slog.Debug("setting X-Real-Ip from X-Forwarded-For", "to", ip, "x-forwarded-for", xffHeader)
  88. r.Header.Set("X-Real-Ip", ip)
  89. if addr, err := netip.ParseAddr(ip); err == nil {
  90. r = r.WithContext(context.WithValue(r.Context(), realIPKey{}, addr))
  91. }
  92. }
  93. next.ServeHTTP(w, r)
  94. })
  95. }
  96. // XForwardedForUpdate sets or updates the X-Forwarded-For header, adding
  97. // the known remote address to an existing chain if present
  98. func XForwardedForUpdate(stripPrivate bool, next http.Handler) http.Handler {
  99. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  100. defer next.ServeHTTP(w, r)
  101. pref := XFFComputePreferences{
  102. StripPrivate: stripPrivate,
  103. StripLoopback: true,
  104. StripCGNAT: true,
  105. Flatten: true,
  106. StripLLU: true,
  107. }
  108. remoteAddr := r.RemoteAddr
  109. origXFFHeader := r.Header.Get("X-Forwarded-For")
  110. if remoteAddr == "@" {
  111. // remote is a unix socket
  112. // do not touch chain
  113. return
  114. }
  115. xffHeaderString, err := computeXFFHeader(remoteAddr, origXFFHeader, pref)
  116. if err != nil {
  117. slog.Debug("computing X-Forwarded-For header failed", "err", err)
  118. return
  119. }
  120. if len(xffHeaderString) == 0 {
  121. r.Header.Del("X-Forwarded-For")
  122. } else {
  123. r.Header.Set("X-Forwarded-For", xffHeaderString)
  124. }
  125. })
  126. }
  127. var (
  128. ErrCantSplitHostParse = errors.New("internal: unable to net.SplitHostParse")
  129. ErrCantParseRemoteIP = errors.New("internal: unable to parse remote IP")
  130. )
  131. func computeXFFHeader(remoteAddr string, origXFFHeader string, pref XFFComputePreferences) (string, error) {
  132. remoteIP, _, err := net.SplitHostPort(remoteAddr)
  133. if err != nil {
  134. return "", fmt.Errorf("%w: %w", ErrCantSplitHostParse, err)
  135. }
  136. parsedRemoteIP, err := netip.ParseAddr(remoteIP)
  137. if err != nil {
  138. return "", fmt.Errorf("%w: %w", ErrCantParseRemoteIP, err)
  139. }
  140. origForwardedList := make([]string, 0, 4)
  141. if origXFFHeader != "" {
  142. origForwardedList = strings.Split(origXFFHeader, ",")
  143. for i := range origForwardedList {
  144. origForwardedList[i] = strings.TrimSpace(origForwardedList[i])
  145. }
  146. }
  147. origForwardedList = append(origForwardedList, parsedRemoteIP.String())
  148. forwardedList := make([]string, 0, len(origForwardedList))
  149. // this behavior is equivalent to
  150. // ingress-nginx "compute-full-forwarded-for"
  151. // https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#compute-full-forwarded-for
  152. //
  153. // this would be the correct place to strip and/or flatten this list
  154. //
  155. // strip - iterate backwards and eliminate configured trusted IPs
  156. // flatten - only return the last element to avoid spoofing confusion
  157. //
  158. // many applications handle this in different ways, but
  159. // generally they'd be expected to do these two things on
  160. // their own end to find the first non-spoofed IP
  161. for i := len(origForwardedList) - 1; i >= 0; i-- {
  162. segmentIP, err := netip.ParseAddr(strings.TrimSpace(origForwardedList[i]))
  163. if err != nil {
  164. // can't assess this element, so the remainder of the chain
  165. // can't be trusted. not a fatal error, since anyone can
  166. // spoof an XFF header
  167. slog.Debug("failed to parse XFF segment", "err", err)
  168. break
  169. }
  170. if pref.StripPrivate && segmentIP.IsPrivate() {
  171. continue
  172. }
  173. if pref.StripLoopback && segmentIP.IsLoopback() {
  174. continue
  175. }
  176. if pref.StripLLU && segmentIP.IsLinkLocalUnicast() {
  177. continue
  178. }
  179. if pref.StripCGNAT && CGNat.Contains(segmentIP) {
  180. continue
  181. }
  182. forwardedList = append([]string{segmentIP.String()}, forwardedList...)
  183. }
  184. var xffHeaderString string
  185. if len(forwardedList) == 0 {
  186. xffHeaderString = ""
  187. return xffHeaderString, nil
  188. }
  189. if pref.Flatten {
  190. xffHeaderString = forwardedList[len(forwardedList)-1]
  191. } else {
  192. xffHeaderString = strings.Join(forwardedList, ",")
  193. }
  194. return xffHeaderString, nil
  195. }
  196. // NoStoreCache sets the Cache-Control header to no-store for the response.
  197. func NoStoreCache(next http.Handler) http.Handler {
  198. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  199. w.Header().Set("Cache-Control", "no-store")
  200. next.ServeHTTP(w, r)
  201. })
  202. }
  203. // NoBrowsing prevents directory browsing by returning a 404 for any request that ends with a "/".
  204. func NoBrowsing(next http.Handler) http.Handler {
  205. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  206. if strings.HasSuffix(r.URL.Path, "/") {
  207. http.NotFound(w, r)
  208. return
  209. }
  210. next.ServeHTTP(w, r)
  211. })
  212. }