1
0

server.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. package main
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/subtle"
  6. "fmt"
  7. "log"
  8. "net/http"
  9. "sync"
  10. "time"
  11. nanoid "github.com/matoous/go-nanoid"
  12. "github.com/valyala/fasthttp"
  13. )
  14. var (
  15. mimes = map[imageType]string{
  16. imageTypeJPEG: "image/jpeg",
  17. imageTypePNG: "image/png",
  18. imageTypeWEBP: "image/webp",
  19. }
  20. responseBufPool = sync.Pool{
  21. New: func() interface{} {
  22. return new(bytes.Buffer)
  23. },
  24. }
  25. authHeaderMust []byte
  26. healthRequestURI = []byte("/health")
  27. serverMutex mutex
  28. )
  29. func startServer() *fasthttp.Server {
  30. serverMutex = newMutex(conf.Concurrency)
  31. s := &fasthttp.Server{
  32. Name: "imgproxy",
  33. Handler: serveHTTP,
  34. Concurrency: conf.MaxClients,
  35. ReadTimeout: time.Duration(conf.ReadTimeout) * time.Second,
  36. }
  37. go func() {
  38. log.Printf("Starting server at %s\n", conf.Bind)
  39. if err := s.ListenAndServe(conf.Bind); err != nil {
  40. log.Fatalln(err)
  41. }
  42. }()
  43. return s
  44. }
  45. func shutdownServer(s *fasthttp.Server) {
  46. log.Println("Shutting down the server...")
  47. s.Shutdown()
  48. }
  49. func logResponse(status int, msg string) {
  50. var color int
  51. if status >= 500 {
  52. color = 31
  53. } else if status >= 400 {
  54. color = 33
  55. } else {
  56. color = 32
  57. }
  58. log.Printf("|\033[7;%dm %d \033[0m| %s\n", color, status, msg)
  59. }
  60. func writeCORS(rctx *fasthttp.RequestCtx) {
  61. if len(conf.AllowOrigin) > 0 {
  62. rctx.Request.Header.Set("Access-Control-Allow-Origin", conf.AllowOrigin)
  63. rctx.Request.Header.Set("Access-Control-Allow-Methods", "GET, OPTIONs")
  64. }
  65. }
  66. func respondWithImage(ctx context.Context, reqID string, rctx *fasthttp.RequestCtx, data []byte) {
  67. rctx.SetStatusCode(200)
  68. po := getprocessingOptions(ctx)
  69. rctx.SetContentType(mimes[po.Format])
  70. rctx.Response.Header.Set("Cache-Control", fmt.Sprintf("max-age=%d, public", conf.TTL))
  71. rctx.Response.Header.Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat))
  72. if conf.GZipCompression > 0 && rctx.Request.Header.HasAcceptEncodingBytes([]byte("gzip")) {
  73. rctx.Response.Header.Set("Content-Encoding", "gzip")
  74. gzipData(data, rctx)
  75. } else {
  76. rctx.SetBody(data)
  77. }
  78. logResponse(200, fmt.Sprintf("[%s] Processed in %s: %s; %+v", reqID, getTimerSince(ctx), getImageURL(ctx), po))
  79. }
  80. func respondWithError(reqID string, rctx *fasthttp.RequestCtx, err imgproxyError) {
  81. logResponse(err.StatusCode, fmt.Sprintf("[%s] %s", reqID, err.Message))
  82. rctx.SetStatusCode(err.StatusCode)
  83. rctx.SetBodyString(err.PublicMessage)
  84. }
  85. func respondWithOptions(reqID string, rctx *fasthttp.RequestCtx) {
  86. logResponse(200, fmt.Sprintf("[%s] Respond with options", reqID))
  87. rctx.SetStatusCode(200)
  88. }
  89. func prepareAuthHeaderMust() []byte {
  90. if len(authHeaderMust) == 0 {
  91. buf := bytes.NewBufferString("Bearer ")
  92. buf.WriteString(conf.Secret)
  93. authHeaderMust = []byte(conf.Secret)
  94. }
  95. return authHeaderMust
  96. }
  97. func checkSecret(rctx *fasthttp.RequestCtx) bool {
  98. if len(conf.Secret) == 0 {
  99. return true
  100. }
  101. return subtle.ConstantTimeCompare(
  102. rctx.Request.Header.Peek("Authorization"),
  103. prepareAuthHeaderMust(),
  104. ) == 1
  105. }
  106. func serveHTTP(rctx *fasthttp.RequestCtx) {
  107. reqID, _ := nanoid.Nanoid()
  108. defer func() {
  109. if r := recover(); r != nil {
  110. if err, ok := r.(imgproxyError); ok {
  111. respondWithError(reqID, rctx, err)
  112. } else {
  113. respondWithError(reqID, rctx, newUnexpectedError(r.(error), 4))
  114. }
  115. }
  116. }()
  117. log.Printf("[%s] %s: %s\n", reqID, rctx.Method(), rctx.RequestURI())
  118. writeCORS(rctx)
  119. if rctx.Request.Header.IsOptions() {
  120. respondWithOptions(reqID, rctx)
  121. return
  122. }
  123. if !rctx.IsGet() {
  124. panic(invalidMethodErr)
  125. }
  126. if !checkSecret(rctx) {
  127. panic(invalidSecretErr)
  128. }
  129. serverMutex.Lock()
  130. defer serverMutex.Unock()
  131. if bytes.Equal(rctx.RequestURI(), healthRequestURI) {
  132. rctx.SetStatusCode(200)
  133. rctx.SetBodyString("imgproxy is running")
  134. return
  135. }
  136. ctx, timeoutCancel := startTimer(time.Duration(conf.WriteTimeout) * time.Second)
  137. defer timeoutCancel()
  138. ctx, err := parsePath(ctx, rctx)
  139. if err != nil {
  140. panic(newError(404, err.Error(), "Invalid image url"))
  141. }
  142. ctx, downloadcancel, err := downloadImage(ctx)
  143. defer downloadcancel()
  144. if err != nil {
  145. panic(newError(404, err.Error(), "Image is unreachable"))
  146. }
  147. checkTimeout(ctx)
  148. // if conf.ETagEnabled {
  149. // eTag := calcETag(b, &procOpt)
  150. // rw.Header().Set("ETag", eTag)
  151. // if eTag == r.Header.Get("If-None-Match") {
  152. // panic(notModifiedErr)
  153. // }
  154. // }
  155. checkTimeout(ctx)
  156. imageData, err := processImage(ctx)
  157. if err != nil {
  158. panic(newError(500, err.Error(), "Error occurred while processing image"))
  159. }
  160. checkTimeout(ctx)
  161. respondWithImage(ctx, reqID, rctx, imageData)
  162. }