router.go 3.8 KB

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