gamo.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. package main
  2. import (
  3. "crypto/hmac"
  4. "crypto/sha1"
  5. "encoding/hex"
  6. "flag"
  7. "fmt"
  8. "io"
  9. "math"
  10. "net/http"
  11. "os"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "github.com/Jeffail/tunny"
  16. "github.com/discord/lilliput"
  17. "github.com/google/uuid"
  18. "github.com/sirupsen/logrus"
  19. )
  20. const (
  21. DEFAULT_BIND = "127.0.0.1:8081"
  22. DEFAULT_KEY = "0x24FEEDFACEDEADBEEFCAFE"
  23. MAX_CONTENT_LENGTH = 5242880
  24. MAX_DIMENSIONS = 8912
  25. OUTPUT_BUFFER_SIZE = 10 * 1024 * 1024
  26. )
  27. type imageOpsWorker struct {
  28. ops *lilliput.ImageOps
  29. }
  30. type imageOpsPayload struct {
  31. decoder lilliput.Decoder
  32. options *lilliput.ImageOptions
  33. }
  34. type imageOpsResult struct {
  35. result []byte
  36. err error
  37. }
  38. func (w *imageOpsWorker) Process(payload interface{}) interface{} {
  39. log.Debug("Allocating memory for transformation")
  40. input := payload.(*imageOpsPayload)
  41. output := make([]byte, OUTPUT_BUFFER_SIZE)
  42. output, err := w.ops.Transform(input.decoder, input.options, output)
  43. return &imageOpsResult{
  44. result: output,
  45. err: err,
  46. }
  47. }
  48. func (w *imageOpsWorker) BlockUntilReady() {
  49. //
  50. }
  51. func (w *imageOpsWorker) Interrupt() {
  52. //
  53. }
  54. func (w *imageOpsWorker) Terminate() {
  55. log.Debug("Shutting down worker")
  56. w.ops.Close()
  57. }
  58. func newImageOpsWorker() *imageOpsWorker {
  59. log.Debug("Initializing worker")
  60. return &imageOpsWorker{
  61. ops: lilliput.NewImageOps(MAX_DIMENSIONS),
  62. }
  63. }
  64. var (
  65. configListenAddr string
  66. configSharedKey string
  67. configDimensions string
  68. )
  69. var EncodeOptions = map[string]map[int]int{
  70. ".jpeg": {lilliput.JpegQuality: 85},
  71. ".png": {lilliput.PngCompression: 7},
  72. ".webp": {lilliput.WebpQuality: 85},
  73. }
  74. func nextRequestID() string {
  75. return uuid.New().String()
  76. }
  77. var log = logrus.New()
  78. func main() {
  79. log.Out = os.Stdout
  80. log.Level = logrus.DebugLevel
  81. flag.StringVar(&configListenAddr, "bind", DEFAULT_BIND, "Bind address")
  82. flag.StringVar(&configSharedKey, "key", DEFAULT_KEY, "Shared HMAC secret")
  83. flag.StringVar(&configDimensions, "dimensions", "", "Which target sizes besides the original one will be accessible (comma-separated)")
  84. flag.Parse()
  85. listenAddr := configListenAddr
  86. sharedKey := []byte(configSharedKey)
  87. dimensionsMap := map[int64]bool{}
  88. log.Info("Welcome to Gamo, the image proxy and optimization server")
  89. log.Info(fmt.Sprintf("Starting on %s...", listenAddr))
  90. if len(configDimensions) > 0 {
  91. log.Info("With dimensions: ", configDimensions)
  92. for _, x := range strings.Split(configDimensions, ",") {
  93. parsedDimensions, err := strconv.ParseInt(x, 10, 0)
  94. if err != nil {
  95. log.Fatal("Unrecognized value in dimensions: ", x)
  96. }
  97. dimensionsMap[parsedDimensions] = true
  98. }
  99. } else {
  100. log.Info("Without resizing")
  101. }
  102. pool := tunny.New(2, func() tunny.Worker {
  103. return newImageOpsWorker()
  104. })
  105. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  106. requestID := nextRequestID()
  107. requestLog := log.WithFields(logrus.Fields{"request-id": requestID})
  108. w.Header().Set("X-Request-Id", requestID)
  109. segments := strings.Split(r.URL.Path, "/")
  110. dimensions := MAX_DIMENSIONS
  111. if len(segments) < 3 {
  112. http.Error(w, "Not found", http.StatusNotFound)
  113. return
  114. }
  115. encodedMAC, encodedImageURL := segments[1], segments[2]
  116. if len(segments) >= 4 {
  117. parsedDimensions, err := strconv.ParseInt(segments[3], 10, 0)
  118. if err != nil || !dimensionsMap[parsedDimensions] {
  119. http.Error(w, "Not found", http.StatusNotFound)
  120. return
  121. }
  122. dimensions = int(parsedDimensions)
  123. }
  124. imageURL, err := hex.DecodeString(encodedImageURL)
  125. if err != nil {
  126. http.Error(w, "Bad request", http.StatusBadRequest)
  127. return
  128. }
  129. messageMAC, err := hex.DecodeString(encodedMAC)
  130. if err != nil {
  131. http.Error(w, "Bad request", http.StatusBadRequest)
  132. return
  133. }
  134. mac := hmac.New(sha1.New, sharedKey)
  135. mac.Write(imageURL)
  136. expectedMAC := mac.Sum(nil)
  137. if hmac.Equal(messageMAC, expectedMAC) {
  138. requestLog = requestLog.WithFields(logrus.Fields{"url": string(imageURL)})
  139. resp, err := http.Get(string(imageURL))
  140. if err != nil {
  141. requestLog.Error(fmt.Sprintf("Error performing request: %s", err))
  142. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  143. return
  144. }
  145. defer resp.Body.Close()
  146. contentLength, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 0)
  147. if contentLength > MAX_CONTENT_LENGTH {
  148. requestLog.Error("Image exceeds length limit")
  149. http.Error(w, "Bad request", http.StatusBadRequest)
  150. return
  151. }
  152. originalImage, err := io.ReadAll(resp.Body)
  153. if err != nil {
  154. requestLog.Error(fmt.Sprintf("Error reading response body: %s", err))
  155. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  156. return
  157. }
  158. decoder, err := lilliput.NewDecoder(originalImage)
  159. originalImage = nil
  160. if err != nil {
  161. requestLog.Error(fmt.Sprintf("Error decoding image: %s", err))
  162. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  163. return
  164. }
  165. defer decoder.Close()
  166. header, err := decoder.Header()
  167. if err != nil {
  168. requestLog.Error(fmt.Sprintf("Error reading image header: %s", err))
  169. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  170. return
  171. }
  172. outputFormat := "." + strings.ToLower(decoder.Description())
  173. outputPixels := dimensions * dimensions
  174. var (
  175. outputWidth int
  176. outputHeight int
  177. )
  178. if (header.Width() * header.Height()) > outputPixels {
  179. outputWidth = int(math.Round(math.Sqrt(float64(outputPixels) * (float64(header.Width()) / float64(header.Height())))))
  180. outputHeight = int(math.Round(math.Sqrt(float64(outputPixels) * (float64(header.Height()) / float64(header.Width())))))
  181. } else {
  182. outputWidth = header.Width()
  183. outputHeight = header.Height()
  184. }
  185. resizeOptions := &lilliput.ImageOptions{
  186. FileType: outputFormat,
  187. Width: outputWidth,
  188. Height: outputHeight,
  189. ResizeMethod: lilliput.ImageOpsResize,
  190. NormalizeOrientation: true,
  191. EncodeOptions: EncodeOptions[outputFormat],
  192. }
  193. result := pool.Process(&imageOpsPayload{
  194. decoder: decoder,
  195. options: resizeOptions,
  196. }).(*imageOpsResult)
  197. if result.err != nil {
  198. requestLog.Error(fmt.Sprintf("Error transforming image: %s", err))
  199. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  200. return
  201. }
  202. outputImage := result.result
  203. w.Header().Set("Content-Length", strconv.FormatInt(int64(len(outputImage)), 10))
  204. w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
  205. w.Header().Set("Cache-Control", "public, max-age=31536000")
  206. w.Header().Set("Expires", time.Now().Add(31536000*time.Second).In(time.UTC).Format("Mon, 02 Jan 2006 15:04:05 GMT"))
  207. w.Header().Set("Vary", "Accept-Encoding")
  208. w.Header().Set("Etag", fmt.Sprintf("%d-%x", len(outputImage), sha1.Sum(outputImage)))
  209. if r.Method != "HEAD" {
  210. _, err = w.Write(outputImage)
  211. if err != nil {
  212. requestLog.Error(fmt.Sprintf("Error writing response: %s", err))
  213. }
  214. }
  215. } else {
  216. http.Error(w, "Unauthorized", http.StatusUnauthorized)
  217. }
  218. })
  219. log.Fatal(http.ListenAndServe(listenAddr, nil))
  220. }