router.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  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. reqID := req.Header.Get(xRequestIDHeader)
  78. if len(reqID) == 0 || !requestIDRe.MatchString(reqID) {
  79. if lambdaContextVal := req.Header.Get(xAmznRequestContextHeader); len(lambdaContextVal) > 0 {
  80. var lambdaContext struct {
  81. RequestID string `json:"requestId"`
  82. }
  83. err := json.Unmarshal([]byte(lambdaContextVal), &lambdaContext)
  84. if err == nil && len(lambdaContext.RequestID) > 0 {
  85. reqID = lambdaContext.RequestID
  86. }
  87. }
  88. }
  89. if len(reqID) == 0 || !requestIDRe.MatchString(reqID) {
  90. reqID, _ = nanoid.New()
  91. }
  92. rw.Header().Set("Server", "imgproxy")
  93. rw.Header().Set(xRequestIDHeader, reqID)
  94. if req.Method == http.MethodGet {
  95. if r.HealthHandler != nil {
  96. for _, healthRoute := range r.healthRoutes {
  97. if req.URL.Path == healthRoute {
  98. r.HealthHandler(reqID, rw, req)
  99. return
  100. }
  101. }
  102. }
  103. if req.URL.Path == r.faviconRoute {
  104. // TODO: Add a real favicon maybe?
  105. rw.Header().Set("Content-Type", "text/plain")
  106. rw.WriteHeader(404)
  107. // Write a single byte to make AWS Lambda happy
  108. rw.Write([]byte{' '})
  109. return
  110. }
  111. }
  112. if ip := req.Header.Get("CF-Connecting-IP"); len(ip) != 0 {
  113. replaceRemoteAddr(req, ip)
  114. } else if ip := req.Header.Get("X-Forwarded-For"); len(ip) != 0 {
  115. if index := strings.Index(ip, ","); index > 0 {
  116. ip = ip[:index]
  117. }
  118. replaceRemoteAddr(req, ip)
  119. } else if ip := req.Header.Get("X-Real-IP"); len(ip) != 0 {
  120. replaceRemoteAddr(req, ip)
  121. }
  122. LogRequest(reqID, req)
  123. for _, rr := range r.Routes {
  124. if rr.isMatch(req) {
  125. rr.Handler(reqID, rw, req)
  126. return
  127. }
  128. }
  129. LogResponse(reqID, req, 404, ierrors.New(404, fmt.Sprintf("Route for %s is not defined", req.URL.Path), "Not found"))
  130. rw.Header().Set("Content-Type", "text/plain")
  131. rw.WriteHeader(404)
  132. rw.Write([]byte{' '})
  133. }
  134. func replaceRemoteAddr(req *http.Request, ip string) {
  135. _, port, err := net.SplitHostPort(req.RemoteAddr)
  136. if err != nil {
  137. port = "80"
  138. }
  139. req.RemoteAddr = net.JoinHostPort(strings.TrimSpace(ip), port)
  140. }