2017-06-27 11:00:33 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-02-25 14:26:51 +02:00
|
|
|
"bytes"
|
2018-09-10 08:17:00 +02:00
|
|
|
"context"
|
2017-07-01 23:25:08 +02:00
|
|
|
"crypto/subtle"
|
2017-06-27 11:00:33 +02:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2019-01-10 07:49:17 +02:00
|
|
|
"net/url"
|
|
|
|
"path/filepath"
|
2019-05-06 10:42:30 +02:00
|
|
|
"regexp"
|
2018-10-25 13:31:31 +02:00
|
|
|
"strings"
|
2017-07-03 06:08:47 +02:00
|
|
|
"time"
|
2018-03-15 18:58:11 +02:00
|
|
|
|
|
|
|
nanoid "github.com/matoous/go-nanoid"
|
2019-02-25 14:26:51 +02:00
|
|
|
"github.com/valyala/fasthttp"
|
2017-06-27 11:00:33 +02:00
|
|
|
)
|
|
|
|
|
2019-01-10 07:49:17 +02:00
|
|
|
const (
|
|
|
|
contextDispositionFilenameFallback = "image"
|
2019-05-06 10:42:30 +02:00
|
|
|
xRequestIDHeader = "X-Request-ID"
|
2019-01-10 07:49:17 +02:00
|
|
|
)
|
2018-10-25 13:31:31 +02:00
|
|
|
|
2018-10-05 17:17:36 +02:00
|
|
|
var (
|
|
|
|
mimes = map[imageType]string{
|
|
|
|
imageTypeJPEG: "image/jpeg",
|
|
|
|
imageTypePNG: "image/png",
|
|
|
|
imageTypeWEBP: "image/webp",
|
2018-11-08 12:34:21 +02:00
|
|
|
imageTypeGIF: "image/gif",
|
2018-12-02 15:02:19 +02:00
|
|
|
imageTypeICO: "image/x-icon",
|
2018-10-05 17:17:36 +02:00
|
|
|
}
|
2017-07-05 23:09:41 +02:00
|
|
|
|
2019-01-10 07:49:17 +02:00
|
|
|
contentDispositionsFmt = map[imageType]string{
|
|
|
|
imageTypeJPEG: "inline; filename=\"%s.jpg\"",
|
|
|
|
imageTypePNG: "inline; filename=\"%s.png\"",
|
|
|
|
imageTypeWEBP: "inline; filename=\"%s.webp\"",
|
|
|
|
imageTypeGIF: "inline; filename=\"%s.gif\"",
|
|
|
|
imageTypeICO: "inline; filename=\"%s.ico\"",
|
2018-11-01 16:34:28 +02:00
|
|
|
}
|
|
|
|
|
2018-10-05 17:17:36 +02:00
|
|
|
authHeaderMust []byte
|
2017-06-27 11:00:33 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
healthPath = []byte("/health")
|
|
|
|
|
2018-10-25 13:31:31 +02:00
|
|
|
imgproxyIsRunningMsg = []byte("imgproxy is running")
|
2018-10-05 22:29:55 +02:00
|
|
|
|
|
|
|
errInvalidMethod = newError(422, "Invalid request method", "Method doesn't allowed")
|
|
|
|
errInvalidSecret = newError(403, "Invalid secret", "Forbidden")
|
2018-10-05 17:17:36 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
responseGzipPool *gzipPool
|
2019-05-06 10:42:30 +02:00
|
|
|
|
|
|
|
requestIDRe = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`)
|
2019-01-17 10:51:19 +02:00
|
|
|
)
|
2018-10-25 13:31:31 +02:00
|
|
|
|
|
|
|
type httpHandler struct {
|
|
|
|
sem chan struct{}
|
|
|
|
}
|
2018-09-10 08:17:00 +02:00
|
|
|
|
2018-10-25 13:31:31 +02:00
|
|
|
func newHTTPHandler() *httpHandler {
|
|
|
|
return &httpHandler{make(chan struct{}, conf.Concurrency)}
|
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func startServer() *fasthttp.Server {
|
|
|
|
handler := newHTTPHandler()
|
|
|
|
|
|
|
|
server := &fasthttp.Server{
|
|
|
|
Name: "imgproxy",
|
|
|
|
Handler: handler.ServeHTTP,
|
|
|
|
Concurrency: conf.MaxClients,
|
|
|
|
ReadTimeout: time.Duration(conf.ReadTimeout) * time.Second,
|
2018-09-10 08:17:00 +02:00
|
|
|
}
|
|
|
|
|
2019-01-17 10:51:19 +02:00
|
|
|
if conf.GZipCompression > 0 {
|
|
|
|
responseGzipPool = newGzipPool(conf.Concurrency)
|
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
if conf.ETagEnabled {
|
|
|
|
eTagCalcPool = newEtagPool(conf.Concurrency)
|
|
|
|
}
|
|
|
|
|
2018-09-10 08:17:00 +02:00
|
|
|
go func() {
|
2019-01-11 17:01:48 +02:00
|
|
|
logNotice("Starting server at %s", conf.Bind)
|
2019-02-25 14:26:51 +02:00
|
|
|
if err := server.ListenAndServe(conf.Bind); err != nil {
|
2019-01-11 17:01:48 +02:00
|
|
|
logFatal(err.Error())
|
2018-10-05 17:17:36 +02:00
|
|
|
}
|
2018-09-10 08:17:00 +02:00
|
|
|
}()
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
return server
|
2018-09-10 08:17:00 +02:00
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func shutdownServer(s *fasthttp.Server) {
|
2019-01-11 17:01:48 +02:00
|
|
|
logNotice("Shutting down the server...")
|
2019-02-25 14:26:51 +02:00
|
|
|
s.Shutdown()
|
2018-09-10 08:17:00 +02:00
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func writeCORS(rctx *fasthttp.RequestCtx) {
|
2018-04-26 13:22:31 +02:00
|
|
|
if len(conf.AllowOrigin) > 0 {
|
2019-02-25 14:26:51 +02:00
|
|
|
rctx.Response.Header.Set("Access-Control-Allow-Origin", conf.AllowOrigin)
|
2019-05-06 12:57:44 +02:00
|
|
|
rctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
2018-04-26 13:22:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-10 07:49:17 +02:00
|
|
|
func contentDisposition(imageURL string, imgtype imageType) string {
|
|
|
|
url, err := url.Parse(imageURL)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Sprintf(contentDispositionsFmt[imgtype], contextDispositionFilenameFallback)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, filename := filepath.Split(url.Path)
|
|
|
|
if len(filename) == 0 {
|
|
|
|
return fmt.Sprintf(contentDispositionsFmt[imgtype], contextDispositionFilenameFallback)
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf(contentDispositionsFmt[imgtype], strings.TrimSuffix(filename, filepath.Ext(filename)))
|
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func respondWithImage(ctx context.Context, reqID string, rctx *fasthttp.RequestCtx, data []byte) {
|
2018-10-05 18:20:29 +02:00
|
|
|
po := getProcessingOptions(ctx)
|
2018-04-26 14:17:08 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
rctx.SetStatusCode(200)
|
2018-04-26 14:17:08 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
rctx.Response.Header.Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat))
|
|
|
|
rctx.Response.Header.Set("Cache-Control", fmt.Sprintf("max-age=%d, public", conf.TTL))
|
|
|
|
rctx.Response.Header.Set("Content-Type", mimes[po.Format])
|
|
|
|
rctx.Response.Header.Set("Content-Disposition", contentDisposition(getImageURL(ctx), po.Format))
|
2019-01-17 15:00:33 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
addVaryHeader(rctx)
|
2018-10-25 13:31:31 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
if conf.GZipCompression > 0 && rctx.Request.Header.HasAcceptEncoding("gzip") {
|
|
|
|
gz := responseGzipPool.Get(rctx)
|
2019-01-30 12:31:00 +02:00
|
|
|
defer responseGzipPool.Put(gz)
|
2019-01-09 14:41:00 +02:00
|
|
|
|
2019-01-17 10:51:19 +02:00
|
|
|
gz.Write(data)
|
|
|
|
gz.Close()
|
2017-07-03 06:08:47 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
rctx.Response.Header.Set("Content-Encoding", "gzip")
|
2019-01-09 14:41:00 +02:00
|
|
|
} else {
|
2019-02-25 14:26:51 +02:00
|
|
|
rctx.SetBody(data)
|
2019-01-09 14:41:00 +02:00
|
|
|
}
|
2018-10-25 13:31:31 +02:00
|
|
|
|
2019-01-11 17:01:48 +02:00
|
|
|
logResponse(reqID, 200, fmt.Sprintf("Processed in %s: %s; %+v", getTimerSince(ctx), getImageURL(ctx), po))
|
2017-06-27 11:00:33 +02:00
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func addVaryHeader(rctx *fasthttp.RequestCtx) {
|
|
|
|
vary := make([]string, 0, 5)
|
2019-01-17 15:00:33 +02:00
|
|
|
|
|
|
|
if conf.EnableWebpDetection || conf.EnforceWebp {
|
|
|
|
vary = append(vary, "Accept")
|
|
|
|
}
|
|
|
|
|
|
|
|
if conf.GZipCompression > 0 {
|
|
|
|
vary = append(vary, "Accept-Encoding")
|
|
|
|
}
|
|
|
|
|
|
|
|
if conf.EnableClientHints {
|
|
|
|
vary = append(vary, "DPR", "Viewport-Width", "Width")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(vary) > 0 {
|
2019-02-25 14:26:51 +02:00
|
|
|
rctx.Response.Header.Set("Vary", strings.Join(vary, ", "))
|
2019-01-17 15:00:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func respondWithError(reqID string, rctx *fasthttp.RequestCtx, err *imgproxyError) {
|
2019-01-11 17:01:48 +02:00
|
|
|
logResponse(reqID, err.StatusCode, err.Message)
|
2017-06-27 11:00:33 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
rctx.SetStatusCode(err.StatusCode)
|
|
|
|
rctx.SetBodyString(err.PublicMessage)
|
2017-07-01 23:25:08 +02:00
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func respondWithOptions(reqID string, rctx *fasthttp.RequestCtx) {
|
2019-01-11 17:01:48 +02:00
|
|
|
logResponse(reqID, 200, "Respond with options")
|
2019-02-25 14:26:51 +02:00
|
|
|
rctx.SetStatusCode(200)
|
2018-04-26 13:22:31 +02:00
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func respondWithNotModified(reqID string, rctx *fasthttp.RequestCtx) {
|
|
|
|
logResponse(reqID, 304, "Not modified")
|
|
|
|
rctx.SetStatusCode(304)
|
2018-11-20 14:53:44 +02:00
|
|
|
}
|
|
|
|
|
2019-05-06 10:42:30 +02:00
|
|
|
func generateRequestID(rctx *fasthttp.RequestCtx) (reqID string) {
|
|
|
|
reqIDb := rctx.Request.Header.Peek(xRequestIDHeader)
|
|
|
|
|
|
|
|
if len(reqIDb) > 0 && requestIDRe.Match(reqIDb) {
|
|
|
|
reqID = string(reqIDb)
|
|
|
|
} else {
|
|
|
|
reqID, _ = nanoid.Nanoid()
|
|
|
|
}
|
|
|
|
|
|
|
|
rctx.Response.Header.Set(xRequestIDHeader, reqID)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-10-05 17:17:36 +02:00
|
|
|
func prepareAuthHeaderMust() []byte {
|
|
|
|
if len(authHeaderMust) == 0 {
|
2018-10-05 18:10:17 +02:00
|
|
|
authHeaderMust = []byte(fmt.Sprintf("Bearer %s", conf.Secret))
|
2017-07-03 11:36:37 +02:00
|
|
|
}
|
2017-07-01 23:25:08 +02:00
|
|
|
|
2018-10-05 17:17:36 +02:00
|
|
|
return authHeaderMust
|
2017-07-04 16:05:53 +02:00
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func checkSecret(rctx *fasthttp.RequestCtx) bool {
|
2018-10-05 17:17:36 +02:00
|
|
|
if len(conf.Secret) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return subtle.ConstantTimeCompare(
|
2019-02-25 14:26:51 +02:00
|
|
|
rctx.Request.Header.Peek("Authorization"),
|
2018-10-05 17:17:36 +02:00
|
|
|
prepareAuthHeaderMust(),
|
|
|
|
) == 1
|
2017-07-04 16:05:53 +02:00
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func requestCtxToRequest(rctx *fasthttp.RequestCtx) *http.Request {
|
|
|
|
if r, ok := rctx.UserValue("httpRequest").(*http.Request); ok {
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
reqURL, _ := url.Parse(rctx.Request.URI().String())
|
|
|
|
|
|
|
|
r := &http.Request{
|
|
|
|
Method: http.MethodGet, // Only GET is supported
|
|
|
|
URL: reqURL,
|
|
|
|
Proto: "HTTP/1.0",
|
|
|
|
ProtoMajor: 1,
|
|
|
|
ProtoMinor: 0,
|
|
|
|
Header: make(http.Header),
|
|
|
|
Body: http.NoBody,
|
|
|
|
Host: reqURL.Host,
|
|
|
|
RequestURI: reqURL.RequestURI(),
|
|
|
|
RemoteAddr: rctx.RemoteAddr().String(),
|
|
|
|
}
|
|
|
|
|
|
|
|
rctx.Request.Header.VisitAll(func(key, value []byte) {
|
|
|
|
r.Header.Add(string(key), string(value))
|
|
|
|
})
|
|
|
|
|
|
|
|
rctx.SetUserValue("httpRequest", r)
|
|
|
|
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2018-10-25 13:31:31 +02:00
|
|
|
func (h *httpHandler) lock() {
|
|
|
|
h.sem <- struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *httpHandler) unlock() {
|
|
|
|
<-h.sem
|
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
func (h *httpHandler) ServeHTTP(rctx *fasthttp.RequestCtx) {
|
2019-05-06 10:42:30 +02:00
|
|
|
reqID := generateRequestID(rctx)
|
2018-03-15 18:58:11 +02:00
|
|
|
|
2017-10-04 21:44:58 +02:00
|
|
|
defer func() {
|
2018-11-14 15:41:16 +02:00
|
|
|
if rerr := recover(); rerr != nil {
|
|
|
|
if err, ok := rerr.(error); ok {
|
2019-02-25 14:26:51 +02:00
|
|
|
reportError(err, requestCtxToRequest(rctx))
|
2018-11-14 15:41:16 +02:00
|
|
|
|
2018-11-20 14:53:44 +02:00
|
|
|
if ierr, ok := err.(*imgproxyError); ok {
|
2019-02-25 14:26:51 +02:00
|
|
|
respondWithError(reqID, rctx, ierr)
|
2018-11-14 15:41:16 +02:00
|
|
|
} else {
|
2019-02-25 14:26:51 +02:00
|
|
|
respondWithError(reqID, rctx, newUnexpectedError(err, 4))
|
2018-11-14 15:41:16 +02:00
|
|
|
}
|
2017-10-04 21:44:58 +02:00
|
|
|
} else {
|
2018-11-14 15:41:16 +02:00
|
|
|
panic(rerr)
|
2017-10-04 21:44:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
2017-07-04 16:05:53 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
logRequest(reqID, rctx)
|
2018-04-26 13:22:31 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
writeCORS(rctx)
|
2018-04-26 13:22:31 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
if rctx.Request.Header.IsOptions() {
|
|
|
|
respondWithOptions(reqID, rctx)
|
2018-04-26 13:22:31 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
if !rctx.Request.Header.IsGet() {
|
2018-10-05 22:29:55 +02:00
|
|
|
panic(errInvalidMethod)
|
2018-04-26 13:22:31 +02:00
|
|
|
}
|
|
|
|
|
2019-04-11 16:07:09 +02:00
|
|
|
if bytes.Equal(rctx.RequestURI(), healthPath) {
|
2019-02-25 14:26:51 +02:00
|
|
|
rctx.SetStatusCode(200)
|
|
|
|
rctx.SetBody(imgproxyIsRunningMsg)
|
2018-11-16 12:07:30 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
if !checkSecret(rctx) {
|
2019-01-17 15:01:15 +02:00
|
|
|
panic(errInvalidSecret)
|
|
|
|
}
|
|
|
|
|
2018-10-25 15:24:34 +02:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
if newRelicEnabled {
|
|
|
|
var newRelicCancel context.CancelFunc
|
2019-02-25 14:26:51 +02:00
|
|
|
ctx, newRelicCancel = startNewRelicTransaction(ctx, requestCtxToRequest(rctx))
|
2018-10-25 15:24:34 +02:00
|
|
|
defer newRelicCancel()
|
|
|
|
}
|
|
|
|
|
2018-10-29 14:04:47 +02:00
|
|
|
if prometheusEnabled {
|
|
|
|
prometheusRequestsTotal.Inc()
|
|
|
|
defer startPrometheusDuration(prometheusRequestDuration)()
|
|
|
|
}
|
|
|
|
|
2018-10-25 13:31:31 +02:00
|
|
|
h.lock()
|
|
|
|
defer h.unlock()
|
2017-07-03 06:08:47 +02:00
|
|
|
|
2018-10-25 15:24:34 +02:00
|
|
|
ctx, timeoutCancel := startTimer(ctx, time.Duration(conf.WriteTimeout)*time.Second)
|
2018-10-05 17:17:36 +02:00
|
|
|
defer timeoutCancel()
|
2018-03-19 10:58:52 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
ctx, err := parsePath(ctx, rctx)
|
2017-06-27 11:00:33 +02:00
|
|
|
if err != nil {
|
2018-11-20 14:53:44 +02:00
|
|
|
panic(err)
|
2017-06-27 11:00:33 +02:00
|
|
|
}
|
|
|
|
|
2018-10-05 17:17:36 +02:00
|
|
|
ctx, downloadcancel, err := downloadImage(ctx)
|
|
|
|
defer downloadcancel()
|
2017-06-27 11:00:33 +02:00
|
|
|
if err != nil {
|
2018-10-25 15:24:34 +02:00
|
|
|
if newRelicEnabled {
|
|
|
|
sendErrorToNewRelic(ctx, err)
|
|
|
|
}
|
2018-10-29 14:04:47 +02:00
|
|
|
if prometheusEnabled {
|
|
|
|
incrementPrometheusErrorsTotal("download")
|
|
|
|
}
|
2018-11-20 14:53:44 +02:00
|
|
|
panic(err)
|
2017-06-27 11:00:33 +02:00
|
|
|
}
|
|
|
|
|
2018-10-05 17:17:36 +02:00
|
|
|
checkTimeout(ctx)
|
2017-10-04 21:44:58 +02:00
|
|
|
|
2018-10-05 18:20:29 +02:00
|
|
|
if conf.ETagEnabled {
|
2019-02-25 14:26:51 +02:00
|
|
|
eTag, etagcancel := calcETag(ctx)
|
|
|
|
defer etagcancel()
|
|
|
|
|
|
|
|
rctx.Response.Header.SetBytesV("ETag", eTag)
|
2018-02-26 11:41:37 +02:00
|
|
|
|
2019-04-11 16:07:09 +02:00
|
|
|
if bytes.Equal(eTag, rctx.Request.Header.Peek("If-None-Match")) {
|
2019-02-25 14:26:51 +02:00
|
|
|
respondWithNotModified(reqID, rctx)
|
2018-11-20 14:53:44 +02:00
|
|
|
return
|
2018-10-05 18:20:29 +02:00
|
|
|
}
|
|
|
|
}
|
2018-02-26 10:45:52 +02:00
|
|
|
|
2018-10-05 17:17:36 +02:00
|
|
|
checkTimeout(ctx)
|
2018-02-26 11:41:37 +02:00
|
|
|
|
2019-01-17 10:51:19 +02:00
|
|
|
imageData, processcancel, err := processImage(ctx)
|
|
|
|
defer processcancel()
|
2017-06-27 11:00:33 +02:00
|
|
|
if err != nil {
|
2018-10-25 15:24:34 +02:00
|
|
|
if newRelicEnabled {
|
|
|
|
sendErrorToNewRelic(ctx, err)
|
|
|
|
}
|
2018-10-29 14:04:47 +02:00
|
|
|
if prometheusEnabled {
|
|
|
|
incrementPrometheusErrorsTotal("processing")
|
|
|
|
}
|
2018-11-20 14:53:44 +02:00
|
|
|
panic(err)
|
2017-06-27 11:00:33 +02:00
|
|
|
}
|
|
|
|
|
2018-10-05 17:17:36 +02:00
|
|
|
checkTimeout(ctx)
|
2017-10-04 21:44:58 +02:00
|
|
|
|
2019-02-25 14:26:51 +02:00
|
|
|
respondWithImage(ctx, reqID, rctx, imageData)
|
2017-06-27 11:00:33 +02:00
|
|
|
}
|