server.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. package main
  2. import (
  3. "compress/gzip"
  4. "crypto/subtle"
  5. "encoding/base64"
  6. "errors"
  7. "fmt"
  8. "log"
  9. "net/http"
  10. "net/url"
  11. "strconv"
  12. "strings"
  13. "time"
  14. nanoid "github.com/matoous/go-nanoid"
  15. )
  16. var mimes = map[imageType]string{
  17. JPEG: "image/jpeg",
  18. PNG: "image/png",
  19. WEBP: "image/webp",
  20. }
  21. type httpHandler struct {
  22. sem chan struct{}
  23. }
  24. func newHTTPHandler() *httpHandler {
  25. return &httpHandler{make(chan struct{}, conf.Concurrency)}
  26. }
  27. func parsePath(r *http.Request) (string, processingOptions, error) {
  28. var po processingOptions
  29. var err error
  30. path := r.URL.Path
  31. parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
  32. if len(parts) < 7 {
  33. return "", po, errors.New("Invalid path")
  34. }
  35. token := parts[0]
  36. if err = validatePath(token, strings.TrimPrefix(path, fmt.Sprintf("/%s", token))); err != nil {
  37. return "", po, err
  38. }
  39. if r, ok := resizeTypes[parts[1]]; ok {
  40. po.Resize = r
  41. } else {
  42. return "", po, fmt.Errorf("Invalid resize type: %s", parts[1])
  43. }
  44. if po.Width, err = strconv.Atoi(parts[2]); err != nil {
  45. return "", po, fmt.Errorf("Invalid width: %s", parts[2])
  46. }
  47. if po.Height, err = strconv.Atoi(parts[3]); err != nil {
  48. return "", po, fmt.Errorf("Invalid height: %s", parts[3])
  49. }
  50. if g, ok := gravityTypes[parts[4]]; ok {
  51. po.Gravity = g
  52. } else {
  53. return "", po, fmt.Errorf("Invalid gravity: %s", parts[4])
  54. }
  55. po.Enlarge = parts[5] != "0"
  56. filenameParts := strings.Split(strings.Join(parts[6:], ""), ".")
  57. if len(filenameParts) < 2 {
  58. po.Format = imageTypes["jpg"]
  59. } else if f, ok := imageTypes[filenameParts[1]]; ok {
  60. po.Format = f
  61. } else {
  62. return "", po, fmt.Errorf("Invalid image format: %s", filenameParts[1])
  63. }
  64. if !vipsTypeSupportSave[po.Format] {
  65. return "", po, errors.New("Resulting image type not supported")
  66. }
  67. filename, err := base64.RawURLEncoding.DecodeString(filenameParts[0])
  68. if err != nil {
  69. return "", po, errors.New("Invalid filename encoding")
  70. }
  71. return string(filename), po, nil
  72. }
  73. func logResponse(status int, msg string) {
  74. var color int
  75. if status >= 500 {
  76. color = 31
  77. } else if status >= 400 {
  78. color = 33
  79. } else {
  80. color = 32
  81. }
  82. log.Printf("|\033[7;%dm %d \033[0m| %s\n", color, status, msg)
  83. }
  84. func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, data []byte, imgURL string, po processingOptions, duration time.Duration) {
  85. gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && conf.GZipCompression > 0
  86. rw.Header().Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat))
  87. rw.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, public", conf.TTL))
  88. rw.Header().Set("Content-Type", mimes[po.Format])
  89. if gzipped {
  90. rw.Header().Set("Content-Encoding", "gzip")
  91. }
  92. rw.WriteHeader(200)
  93. if gzipped {
  94. gz, _ := gzip.NewWriterLevel(rw, conf.GZipCompression)
  95. gz.Write(data)
  96. gz.Close()
  97. } else {
  98. rw.Write(data)
  99. }
  100. logResponse(200, fmt.Sprintf("[%s] Processed in %s: %s; %+v", reqID, duration, imgURL, po))
  101. }
  102. func respondWithError(reqID string, rw http.ResponseWriter, err imgproxyError) {
  103. logResponse(err.StatusCode, fmt.Sprintf("[%s] %s", reqID, err.Message))
  104. rw.WriteHeader(err.StatusCode)
  105. rw.Write([]byte(err.PublicMessage))
  106. }
  107. func checkSecret(s string) bool {
  108. if len(conf.Secret) == 0 {
  109. return true
  110. }
  111. return strings.HasPrefix(s, "Bearer ") && subtle.ConstantTimeCompare([]byte(strings.TrimPrefix(s, "Bearer ")), []byte(conf.Secret)) == 1
  112. }
  113. func (h *httpHandler) lock() {
  114. h.sem <- struct{}{}
  115. }
  116. func (h *httpHandler) unlock() {
  117. <-h.sem
  118. }
  119. func (h *httpHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
  120. reqID, _ := nanoid.Nanoid()
  121. log.Printf("[%s] GET: %s\n", reqID, r.URL.RequestURI())
  122. defer func() {
  123. if r := recover(); r != nil {
  124. if err, ok := r.(imgproxyError); ok {
  125. respondWithError(reqID, rw, err)
  126. } else {
  127. respondWithError(reqID, rw, newUnexpectedError(r.(error), 4))
  128. }
  129. }
  130. }()
  131. if !checkSecret(r.Header.Get("Authorization")) {
  132. panic(invalidSecretErr)
  133. }
  134. h.lock()
  135. defer h.unlock()
  136. if r.URL.Path == "/health" {
  137. rw.WriteHeader(200)
  138. rw.Write([]byte("imgproxy is running"))
  139. return
  140. }
  141. t := startTimer(time.Duration(conf.WriteTimeout)*time.Second, "Processing")
  142. imgURL, procOpt, err := parsePath(r)
  143. if err != nil {
  144. panic(newError(404, err.Error(), "Invalid image url"))
  145. }
  146. if _, err = url.ParseRequestURI(imgURL); err != nil {
  147. panic(newError(404, err.Error(), "Invalid image url"))
  148. }
  149. b, imgtype, err := downloadImage(imgURL)
  150. if err != nil {
  151. panic(newError(404, err.Error(), "Image is unreachable"))
  152. }
  153. t.Check()
  154. if conf.ETagEnabled {
  155. eTag := calcETag(b, &procOpt)
  156. rw.Header().Set("ETag", eTag)
  157. if eTag == r.Header.Get("If-None-Match") {
  158. panic(notModifiedErr)
  159. }
  160. }
  161. t.Check()
  162. b, err = processImage(b, imgtype, procOpt, t)
  163. if err != nil {
  164. panic(newError(500, err.Error(), "Error occurred while processing image"))
  165. }
  166. t.Check()
  167. respondWithImage(reqID, r, rw, b, imgURL, procOpt, t.Since())
  168. }