router.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. package router
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net"
  6. "net/http"
  7. "regexp"
  8. "strings"
  9. nanoid "github.com/matoous/go-nanoid/v2"
  10. "github.com/imgproxy/imgproxy/v3/config"
  11. "github.com/imgproxy/imgproxy/v3/ierrors"
  12. )
  13. const (
  14. xRequestIDHeader = "X-Request-ID"
  15. xAmznRequestContextHeader = "x-amzn-request-context"
  16. )
  17. var (
  18. requestIDRe = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`)
  19. )
  20. type RouteHandler func(string, http.ResponseWriter, *http.Request)
  21. type route struct {
  22. Method string
  23. Prefix string
  24. Handler RouteHandler
  25. Exact bool
  26. }
  27. type Router struct {
  28. prefix string
  29. healthRoutes []string
  30. faviconRoute string
  31. Routes []*route
  32. HealthHandler RouteHandler
  33. }
  34. func (r *route) isMatch(req *http.Request) bool {
  35. if r.Method != req.Method {
  36. return false
  37. }
  38. if r.Exact {
  39. return req.URL.Path == r.Prefix
  40. }
  41. return strings.HasPrefix(req.URL.Path, r.Prefix)
  42. }
  43. func New(prefix string) *Router {
  44. healthRoutes := []string{prefix + "/health"}
  45. if len(config.HealthCheckPath) > 0 {
  46. healthRoutes = append(healthRoutes, prefix+config.HealthCheckPath)
  47. }
  48. return &Router{
  49. prefix: prefix,
  50. healthRoutes: healthRoutes,
  51. faviconRoute: prefix + "/favicon.ico",
  52. Routes: make([]*route, 0),
  53. }
  54. }
  55. func (r *Router) Add(method, prefix string, handler RouteHandler, exact bool) {
  56. // Don't add routes with empty prefix
  57. if len(r.prefix+prefix) == 0 {
  58. return
  59. }
  60. r.Routes = append(
  61. r.Routes,
  62. &route{Method: method, Prefix: r.prefix + prefix, Handler: handler, Exact: exact},
  63. )
  64. }
  65. func (r *Router) GET(prefix string, handler RouteHandler, exact bool) {
  66. r.Add(http.MethodGet, prefix, handler, exact)
  67. }
  68. func (r *Router) OPTIONS(prefix string, handler RouteHandler, exact bool) {
  69. r.Add(http.MethodOptions, prefix, handler, exact)
  70. }
  71. func (r *Router) HEAD(prefix string, handler RouteHandler, exact bool) {
  72. r.Add(http.MethodHead, prefix, handler, exact)
  73. }
  74. func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
  75. req, timeoutCancel := startRequestTimer(req)
  76. defer timeoutCancel()
  77. rw = newTimeoutResponse(rw)
  78. reqID := req.Header.Get(xRequestIDHeader)
  79. if len(reqID) == 0 || !requestIDRe.MatchString(reqID) {
  80. if lambdaContextVal := req.Header.Get(xAmznRequestContextHeader); len(lambdaContextVal) > 0 {
  81. var lambdaContext struct {
  82. RequestID string `json:"requestId"`
  83. }
  84. err := json.Unmarshal([]byte(lambdaContextVal), &lambdaContext)
  85. if err == nil && len(lambdaContext.RequestID) > 0 {
  86. reqID = lambdaContext.RequestID
  87. }
  88. }
  89. }
  90. if len(reqID) == 0 || !requestIDRe.MatchString(reqID) {
  91. reqID, _ = nanoid.New()
  92. }
  93. rw.Header().Set("Server", "imgproxy")
  94. rw.Header().Set(xRequestIDHeader, reqID)
  95. if req.Method == http.MethodGet {
  96. if r.HealthHandler != nil {
  97. for _, healthRoute := range r.healthRoutes {
  98. if req.URL.Path == healthRoute {
  99. r.HealthHandler(reqID, rw, req)
  100. return
  101. }
  102. }
  103. }
  104. if req.URL.Path == r.faviconRoute {
  105. // TODO: Add a real favicon maybe?
  106. rw.Header().Set("Content-Type", "text/plain")
  107. rw.WriteHeader(404)
  108. // Write a single byte to make AWS Lambda happy
  109. rw.Write([]byte{' '})
  110. return
  111. }
  112. }
  113. if ip := req.Header.Get("CF-Connecting-IP"); len(ip) != 0 {
  114. replaceRemoteAddr(req, ip)
  115. } else if ip := req.Header.Get("X-Forwarded-For"); len(ip) != 0 {
  116. if index := strings.Index(ip, ","); index > 0 {
  117. ip = ip[:index]
  118. }
  119. replaceRemoteAddr(req, ip)
  120. } else if ip := req.Header.Get("X-Real-IP"); len(ip) != 0 {
  121. replaceRemoteAddr(req, ip)
  122. }
  123. LogRequest(reqID, req)
  124. for _, rr := range r.Routes {
  125. if rr.isMatch(req) {
  126. rr.Handler(reqID, rw, req)
  127. return
  128. }
  129. }
  130. LogResponse(reqID, req, 404, ierrors.New(404, fmt.Sprintf("Route for %s is not defined", req.URL.Path), "Not found"))
  131. rw.Header().Set("Content-Type", "text/plain")
  132. rw.WriteHeader(404)
  133. rw.Write([]byte{' '})
  134. }
  135. func replaceRemoteAddr(req *http.Request, ip string) {
  136. _, port, err := net.SplitHostPort(req.RemoteAddr)
  137. if err != nil {
  138. port = "80"
  139. }
  140. req.RemoteAddr = net.JoinHostPort(strings.TrimSpace(ip), port)
  141. }