util.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. package server
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "mime"
  8. "net/http"
  9. "net/netip"
  10. "regexp"
  11. "strings"
  12. "heckel.io/ntfy/v2/util"
  13. )
  14. var (
  15. mimeDecoder mime.WordDecoder
  16. priorityHeaderIgnoreRegex = regexp.MustCompile(`^u=\d,\s*(i|\d)$|^u=\d$`)
  17. )
  18. func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
  19. value := strings.ToLower(readParam(r, names...))
  20. if value == "" {
  21. return defaultValue
  22. }
  23. return toBool(value)
  24. }
  25. func isBoolValue(value string) bool {
  26. return value == "1" || value == "yes" || value == "true" || value == "0" || value == "no" || value == "false"
  27. }
  28. func toBool(value string) bool {
  29. return value == "1" || value == "yes" || value == "true"
  30. }
  31. func readCommaSeparatedParam(r *http.Request, names ...string) (params []string) {
  32. paramStr := readParam(r, names...)
  33. if paramStr != "" {
  34. params = make([]string, 0)
  35. for _, s := range util.SplitNoEmpty(paramStr, ",") {
  36. params = append(params, strings.TrimSpace(s))
  37. }
  38. }
  39. return params
  40. }
  41. func readParam(r *http.Request, names ...string) string {
  42. value := readHeaderParam(r, names...)
  43. if value != "" {
  44. return value
  45. }
  46. return readQueryParam(r, names...)
  47. }
  48. func readHeaderParam(r *http.Request, names ...string) string {
  49. for _, name := range names {
  50. value := strings.TrimSpace(maybeDecodeHeader(name, r.Header.Get(name)))
  51. if value != "" {
  52. return value
  53. }
  54. }
  55. return ""
  56. }
  57. func readQueryParam(r *http.Request, names ...string) string {
  58. for _, name := range names {
  59. value := r.URL.Query().Get(strings.ToLower(name))
  60. if value != "" {
  61. return strings.TrimSpace(value)
  62. }
  63. }
  64. return ""
  65. }
  66. func extractIPAddress(r *http.Request, behindProxy bool, proxyClientIPHeader string) netip.Addr {
  67. logr(r).Debug("Starting IP extraction")
  68. remoteAddr := r.RemoteAddr
  69. logr(r).Debug("RemoteAddr: %s", remoteAddr)
  70. addrPort, err := netip.ParseAddrPort(remoteAddr)
  71. ip := addrPort.Addr()
  72. if err != nil {
  73. logr(r).Warn("Failed to parse RemoteAddr as AddrPort: %v", err)
  74. ip, err = netip.ParseAddr(remoteAddr)
  75. if err != nil {
  76. ip = netip.IPv4Unspecified()
  77. logr(r).Error("Failed to parse RemoteAddr as IP: %v, defaulting to 0.0.0.0", err)
  78. }
  79. }
  80. // Log initial IP before further processing
  81. logr(r).Debug("Initial IP after RemoteAddr parsing: %s", ip)
  82. if proxyClientIPHeader != "" {
  83. logr(r).Debug("Using ProxyClientIPHeader: %s", proxyClientIPHeader)
  84. if customHeaderIP := r.Header.Get(proxyClientIPHeader); customHeaderIP != "" {
  85. logr(r).Debug("Custom header %s value: %s", proxyClientIPHeader, customHeaderIP)
  86. realIP, err := netip.ParseAddr(customHeaderIP)
  87. if err != nil {
  88. logr(r).Error("Invalid IP in %s header: %s, error: %v", proxyClientIPHeader, customHeaderIP, err)
  89. } else {
  90. logr(r).Debug("Successfully parsed IP from custom header: %s", realIP)
  91. ip = realIP
  92. }
  93. } else {
  94. logr(r).Warn("Custom header %s is empty or missing", proxyClientIPHeader)
  95. }
  96. } else if behindProxy {
  97. logr(r).Debug("No ProxyClientIPHeader set, checking X-Forwarded-For")
  98. if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
  99. logr(r).Debug("X-Forwarded-For value: %s", xff)
  100. ips := util.SplitNoEmpty(xff, ",")
  101. realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr)))
  102. if err != nil {
  103. logr(r).Error("Invalid IP in X-Forwarded-For header: %s, error: %v", xff, err)
  104. } else {
  105. logr(r).Debug("Successfully parsed IP from X-Forwarded-For: %s", realIP)
  106. ip = realIP
  107. }
  108. } else {
  109. logr(r).Debug("X-Forwarded-For header is empty or missing")
  110. }
  111. } else {
  112. logr(r).Debug("Behind proxy is false, skipping proxy headers")
  113. }
  114. // Final resolved IP
  115. logr(r).Debug("Final resolved IP: %s", ip)
  116. return ip
  117. }
  118. func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) {
  119. obj, err := util.UnmarshalJSONWithLimit[T](r, limit, allowEmpty)
  120. if errors.Is(err, util.ErrUnmarshalJSON) {
  121. return nil, errHTTPBadRequestJSONInvalid
  122. } else if errors.Is(err, util.ErrTooLargeJSON) {
  123. return nil, errHTTPEntityTooLargeJSONBody
  124. } else if err != nil {
  125. return nil, err
  126. }
  127. return obj, nil
  128. }
  129. func withContext(r *http.Request, ctx map[contextKey]any) *http.Request {
  130. c := r.Context()
  131. for k, v := range ctx {
  132. c = context.WithValue(c, k, v)
  133. }
  134. return r.WithContext(c)
  135. }
  136. func fromContext[T any](r *http.Request, key contextKey) (T, error) {
  137. t, ok := r.Context().Value(key).(T)
  138. if !ok {
  139. return t, fmt.Errorf("cannot find key %v in request context", key)
  140. }
  141. return t, nil
  142. }
  143. // maybeDecodeHeader decodes the given header value if it is MIME encoded, e.g. "=?utf-8?q?Hello_World?=",
  144. // or returns the original header value if it is not MIME encoded. It also calls maybeIgnoreSpecialHeader
  145. // to ignore new HTTP "Priority" header.
  146. func maybeDecodeHeader(name, value string) string {
  147. decoded, err := mimeDecoder.DecodeHeader(value)
  148. if err != nil {
  149. return maybeIgnoreSpecialHeader(name, value)
  150. }
  151. return maybeIgnoreSpecialHeader(name, decoded)
  152. }
  153. // maybeIgnoreSpecialHeader ignores new HTTP "Priority" header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority)
  154. //
  155. // Cloudflare (and potentially other providers) add this to requests when forwarding to the backend (ntfy),
  156. // so we just ignore it. If the "Priority" header is set to "u=*, i" or "u=*" (by Cloudflare), the header will be ignored.
  157. // Returning an empty string will allow the rest of the logic to continue searching for another header (x-priority, prio, p),
  158. // or in the Query parameters.
  159. func maybeIgnoreSpecialHeader(name, value string) string {
  160. if strings.ToLower(name) == "priority" && priorityHeaderIgnoreRegex.MatchString(strings.TrimSpace(value)) {
  161. return ""
  162. }
  163. return value
  164. }