router.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  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. "github.com/imgproxy/imgproxy/v3/config"
  10. )
  11. const (
  12. xRequestIDHeader = "X-Request-ID"
  13. xAmznRequestContextHeader = "x-amzn-request-context"
  14. )
  15. var (
  16. requestIDRe = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`)
  17. )
  18. type RouteHandler func(string, http.ResponseWriter, *http.Request)
  19. type route struct {
  20. Method string
  21. Prefix string
  22. Handler RouteHandler
  23. Exact bool
  24. }
  25. type Router struct {
  26. prefix string
  27. healthRoutes []string
  28. faviconRoute string
  29. Routes []*route
  30. HealthHandler RouteHandler
  31. }
  32. func (r *route) isMatch(req *http.Request) bool {
  33. if r.Method != req.Method {
  34. return false
  35. }
  36. if r.Exact {
  37. return req.URL.Path == r.Prefix
  38. }
  39. return strings.HasPrefix(req.URL.Path, r.Prefix)
  40. }
  41. func New(prefix string) *Router {
  42. healthRoutes := []string{prefix + "/health"}
  43. if len(config.HealthCheckPath) > 0 {
  44. healthRoutes = append(healthRoutes, prefix+config.HealthCheckPath)
  45. }
  46. return &Router{
  47. prefix: prefix,
  48. healthRoutes: healthRoutes,
  49. faviconRoute: prefix + "/favicon.ico",
  50. Routes: make([]*route, 0),
  51. }
  52. }
  53. func (r *Router) Add(method, prefix string, handler RouteHandler, exact bool) {
  54. // Don't add routes with empty prefix
  55. if len(r.prefix+prefix) == 0 {
  56. return
  57. }
  58. r.Routes = append(
  59. r.Routes,
  60. &route{Method: method, Prefix: r.prefix + prefix, Handler: handler, Exact: exact},
  61. )
  62. }
  63. func (r *Router) GET(prefix string, handler RouteHandler, exact bool) {
  64. r.Add(http.MethodGet, prefix, handler, exact)
  65. }
  66. func (r *Router) OPTIONS(prefix string, handler RouteHandler, exact bool) {
  67. r.Add(http.MethodOptions, prefix, handler, exact)
  68. }
  69. func (r *Router) HEAD(prefix string, handler RouteHandler, exact bool) {
  70. r.Add(http.MethodHead, prefix, handler, exact)
  71. }
  72. func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
  73. req, timeoutCancel := startRequestTimer(req)
  74. defer timeoutCancel()
  75. rw = newTimeoutResponse(rw)
  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. LogResponse(reqID, req, 404, newRouteNotDefinedError(req.URL.Path))
  129. rw.Header().Set("Content-Type", "text/plain")
  130. rw.WriteHeader(404)
  131. rw.Write([]byte{' '})
  132. }
  133. func replaceRemoteAddr(req *http.Request, ip string) {
  134. _, port, err := net.SplitHostPort(req.RemoteAddr)
  135. if err != nil {
  136. port = "80"
  137. }
  138. req.RemoteAddr = net.JoinHostPort(strings.TrimSpace(ip), port)
  139. }