package main import ( "bytes" "context" "crypto/subtle" "fmt" "log" "net/http" "time" nanoid "github.com/matoous/go-nanoid" "github.com/valyala/fasthttp" ) var ( mimes = map[imageType]string{ imageTypeJPEG: "image/jpeg", imageTypePNG: "image/png", imageTypeWEBP: "image/webp", } authHeaderMust []byte healthPath = []byte("/health") serverMutex mutex errInvalidMethod = newError(422, "Invalid request method", "Method doesn't allowed") errInvalidSecret = newError(403, "Invalid secret", "Forbidden") ) func startServer() *fasthttp.Server { serverMutex = newMutex(conf.Concurrency) s := &fasthttp.Server{ Name: "imgproxy", Handler: serveHTTP, Concurrency: conf.MaxClients, ReadTimeout: time.Duration(conf.ReadTimeout) * time.Second, } go func() { log.Printf("Starting server at %s\n", conf.Bind) if err := s.ListenAndServe(conf.Bind); err != nil { log.Fatalln(err) } }() return s } func shutdownServer(s *fasthttp.Server) { log.Println("Shutting down the server...") s.Shutdown() } func logResponse(status int, msg string) { var color int if status >= 500 { color = 31 } else if status >= 400 { color = 33 } else { color = 32 } log.Printf("|\033[7;%dm %d \033[0m| %s\n", color, status, msg) } func writeCORS(rctx *fasthttp.RequestCtx) { if len(conf.AllowOrigin) > 0 { rctx.Request.Header.Set("Access-Control-Allow-Origin", conf.AllowOrigin) rctx.Request.Header.Set("Access-Control-Allow-Methods", "GET, OPTIONs") } } func respondWithImage(ctx context.Context, reqID string, rctx *fasthttp.RequestCtx, data []byte) { rctx.SetStatusCode(200) po := getProcessingOptions(ctx) rctx.SetContentType(mimes[po.Format]) rctx.Response.Header.Set("Cache-Control", fmt.Sprintf("max-age=%d, public", conf.TTL)) rctx.Response.Header.Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat)) if conf.GZipCompression > 0 && rctx.Request.Header.HasAcceptEncodingBytes([]byte("gzip")) { rctx.Response.Header.Set("Content-Encoding", "gzip") gzipData(data, rctx) } else { rctx.SetBody(data) } logResponse(200, fmt.Sprintf("[%s] Processed in %s: %s; %+v", reqID, getTimerSince(ctx), getImageURL(ctx), po)) } func respondWithError(reqID string, rctx *fasthttp.RequestCtx, err imgproxyError) { logResponse(err.StatusCode, fmt.Sprintf("[%s] %s", reqID, err.Message)) rctx.SetStatusCode(err.StatusCode) rctx.SetBodyString(err.PublicMessage) } func respondWithOptions(reqID string, rctx *fasthttp.RequestCtx) { logResponse(200, fmt.Sprintf("[%s] Respond with options", reqID)) rctx.SetStatusCode(200) } func prepareAuthHeaderMust() []byte { if len(authHeaderMust) == 0 { authHeaderMust = []byte(fmt.Sprintf("Bearer %s", conf.Secret)) } return authHeaderMust } func checkSecret(rctx *fasthttp.RequestCtx) bool { if len(conf.Secret) == 0 { return true } return subtle.ConstantTimeCompare( rctx.Request.Header.Peek("Authorization"), prepareAuthHeaderMust(), ) == 1 } func serveHTTP(rctx *fasthttp.RequestCtx) { reqID, _ := nanoid.Nanoid() defer func() { if r := recover(); r != nil { if err, ok := r.(imgproxyError); ok { respondWithError(reqID, rctx, err) } else { respondWithError(reqID, rctx, newUnexpectedError(r.(error), 4)) } } }() log.Printf("[%s] %s: %s\n", reqID, rctx.Method(), rctx.Path()) writeCORS(rctx) if rctx.Request.Header.IsOptions() { respondWithOptions(reqID, rctx) return } if !rctx.IsGet() { panic(errInvalidMethod) } if !checkSecret(rctx) { panic(errInvalidSecret) } serverMutex.Lock() defer serverMutex.Unock() if bytes.Equal(rctx.Path(), healthPath) { rctx.SetStatusCode(200) rctx.SetBodyString("imgproxy is running") return } ctx, timeoutCancel := startTimer(time.Duration(conf.WriteTimeout) * time.Second) defer timeoutCancel() ctx, err := parsePath(ctx, rctx) if err != nil { panic(newError(404, err.Error(), "Invalid image url")) } ctx, downloadcancel, err := downloadImage(ctx) defer downloadcancel() if err != nil { panic(newError(404, err.Error(), "Image is unreachable")) } checkTimeout(ctx) if conf.ETagEnabled { eTag := calcETag(ctx) rctx.Response.Header.SetBytesV("ETag", eTag) if bytes.Equal(eTag, rctx.Request.Header.Peek("If-None-Match")) { panic(errNotModified) } } checkTimeout(ctx) imageData, err := processImage(ctx) if err != nil { panic(newError(500, err.Error(), "Error occurred while processing image")) } checkTimeout(ctx) respondWithImage(ctx, reqID, rctx, imageData) }