From a9244a70633b8e8c0d0246c981fed602708aabe1 Mon Sep 17 00:00:00 2001 From: DarthSim Date: Mon, 3 Jun 2019 23:02:46 +0600 Subject: [PATCH] Extract processing handler and imageType functions from server.go --- image_type.go | 96 +++++++++++++++++++ processing_handler.go | 149 ++++++++++++++++++++++++++++++ processing_options.go | 39 -------- server.go | 209 ++++-------------------------------------- 4 files changed, 263 insertions(+), 230 deletions(-) create mode 100644 image_type.go create mode 100644 processing_handler.go diff --git a/image_type.go b/image_type.go new file mode 100644 index 00000000..0c84446d --- /dev/null +++ b/image_type.go @@ -0,0 +1,96 @@ +package main + +/* +#cgo LDFLAGS: -s -w +#include "vips.h" +*/ +import "C" + +import ( + "path/filepath" + "fmt" + "net/url" + "strings" +) + +type imageType int + +const ( + imageTypeUnknown = imageType(C.UNKNOWN) + imageTypeJPEG = imageType(C.JPEG) + imageTypePNG = imageType(C.PNG) + imageTypeWEBP = imageType(C.WEBP) + imageTypeGIF = imageType(C.GIF) + imageTypeICO = imageType(C.ICO) + imageTypeSVG = imageType(C.SVG) + imageTypeHEIC = imageType(C.HEIC) + + contentDispositionFilenameFallback = "image" +) + +var ( + imageTypes = map[string]imageType{ + "jpeg": imageTypeJPEG, + "jpg": imageTypeJPEG, + "png": imageTypePNG, + "webp": imageTypeWEBP, + "gif": imageTypeGIF, + "ico": imageTypeICO, + "svg": imageTypeSVG, + "heic": imageTypeHEIC, + } + + mimes = map[imageType]string{ + imageTypeJPEG: "image/jpeg", + imageTypePNG: "image/png", + imageTypeWEBP: "image/webp", + imageTypeGIF: "image/gif", + imageTypeICO: "image/x-icon", + imageTypeHEIC: "image/heif", + } + + 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\"", + imageTypeHEIC: "inline; filename=\"%s.heic\"", + } +) + +func (it imageType) String() string { + for k, v := range imageTypes { + if v == it { + return k + } + } + return "" +} + +func (it imageType) Mime() string { + if mime, ok := mimes[it]; ok { + return mime + } else { + return "application/octet-stream" + } +} + +func (it imageType) ContentDisposition(imageURL string) string { + format, ok := contentDispositionsFmt[it] + if !ok { + return "inline" + } + + url, err := url.Parse(imageURL) + if err != nil { + return fmt.Sprintf(format, contentDispositionFilenameFallback) + } + + _, filename := filepath.Split(url.Path) + if len(filename) == 0 { + return fmt.Sprintf(format, contentDispositionFilenameFallback) + } + + return fmt.Sprintf(format, strings.TrimSuffix(filename, filepath.Ext(filename))) +} diff --git a/processing_handler.go b/processing_handler.go new file mode 100644 index 00000000..a31e5546 --- /dev/null +++ b/processing_handler.go @@ -0,0 +1,149 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + "time" +) + +var ( + responseGzipBufPool *bufPool + responseGzipPool *gzipPool + + processingSem chan struct{} + + headerVaryValue string +) + +func initProcessingHandler() { + processingSem = make(chan struct{}, conf.Concurrency) + + if conf.GZipCompression > 0 { + responseGzipBufPool = newBufPool("gzip", conf.Concurrency, conf.GZipBufferSize) + responseGzipPool = newGzipPool(conf.Concurrency) + } + + vary := make([]string, 0) + + 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") + } + + headerVaryValue = strings.Join(vary, ", ") +} + +func respondWithImage(ctx context.Context, reqID string, r *http.Request, rw http.ResponseWriter, data []byte) { + po := getProcessingOptions(ctx) + + rw.Header().Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat)) + rw.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, public", conf.TTL)) + rw.Header().Set("Content-Type", po.Format.Mime()) + rw.Header().Set("Content-Disposition", po.Format.ContentDisposition(getImageURL(ctx))) + + if len(headerVaryValue) > 0 { + rw.Header().Set("Vary", headerVaryValue) + } + + if conf.GZipCompression > 0 && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + buf := responseGzipBufPool.Get(0) + defer responseGzipBufPool.Put(buf) + + gz := responseGzipPool.Get(buf) + defer responseGzipPool.Put(gz) + + gz.Write(data) + gz.Close() + + rw.Header().Set("Content-Encoding", "gzip") + rw.Header().Set("Content-Length", strconv.Itoa(buf.Len())) + + rw.WriteHeader(200) + rw.Write(buf.Bytes()) + } else { + rw.Header().Set("Content-Length", strconv.Itoa(len(data))) + rw.WriteHeader(200) + rw.Write(data) + } + + logResponse(reqID, 200, fmt.Sprintf("Processed in %s: %s; %+v", getTimerSince(ctx), getImageURL(ctx), po)) +} + +func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) { + ctx := context.Background() + + if newRelicEnabled { + var newRelicCancel context.CancelFunc + ctx, newRelicCancel = startNewRelicTransaction(ctx, rw, r) + defer newRelicCancel() + } + + if prometheusEnabled { + prometheusRequestsTotal.Inc() + defer startPrometheusDuration(prometheusRequestDuration)() + } + + processingSem <- struct{}{} + defer func() { <-processingSem }() + + ctx, timeoutCancel := startTimer(ctx, time.Duration(conf.WriteTimeout)*time.Second) + defer timeoutCancel() + + ctx, err := parsePath(ctx, r) + if err != nil { + panic(err) + } + + ctx, downloadcancel, err := downloadImage(ctx) + defer downloadcancel() + if err != nil { + if newRelicEnabled { + sendErrorToNewRelic(ctx, err) + } + if prometheusEnabled { + incrementPrometheusErrorsTotal("download") + } + panic(err) + } + + checkTimeout(ctx) + + if conf.ETagEnabled { + eTag := calcETag(ctx) + rw.Header().Set("ETag", eTag) + + if eTag == r.Header.Get("If-None-Match") { + logResponse(reqID, 304, "Not modified") + rw.WriteHeader(304) + return + } + } + + checkTimeout(ctx) + + imageData, processcancel, err := processImage(ctx) + defer processcancel() + if err != nil { + if newRelicEnabled { + sendErrorToNewRelic(ctx, err) + } + if prometheusEnabled { + incrementPrometheusErrorsTotal("processing") + } + panic(err) + } + + checkTimeout(ctx) + + respondWithImage(ctx, reqID, r, rw, imageData) +} diff --git a/processing_options.go b/processing_options.go index e7278022..9b9e84aa 100644 --- a/processing_options.go +++ b/processing_options.go @@ -1,11 +1,5 @@ package main -/* -#cgo LDFLAGS: -s -w -#include "vips.h" -*/ -import "C" - import ( "context" "encoding/base64" @@ -20,19 +14,6 @@ import ( type urlOptions map[string][]string -type imageType int - -const ( - imageTypeUnknown = imageType(C.UNKNOWN) - imageTypeJPEG = imageType(C.JPEG) - imageTypePNG = imageType(C.PNG) - imageTypeWEBP = imageType(C.WEBP) - imageTypeGIF = imageType(C.GIF) - imageTypeICO = imageType(C.ICO) - imageTypeSVG = imageType(C.SVG) - imageTypeHEIC = imageType(C.HEIC) -) - type processingHeaders struct { Accept string Width string @@ -40,17 +21,6 @@ type processingHeaders struct { DPR string } -var imageTypes = map[string]imageType{ - "jpeg": imageTypeJPEG, - "jpg": imageTypeJPEG, - "png": imageTypePNG, - "webp": imageTypeWEBP, - "gif": imageTypeGIF, - "ico": imageTypeICO, - "svg": imageTypeSVG, - "heic": imageTypeHEIC, -} - type gravityType int const ( @@ -158,15 +128,6 @@ var ( errInvalidPath = newError(404, "Invalid path", msgInvalidURL) ) -func (it imageType) String() string { - for k, v := range imageTypes { - if v == it { - return k - } - } - return "" -} - func (gt gravityType) String() string { for k, v := range gravityTypes { if v == gt { diff --git a/server.go b/server.go index af241fcf..2bd4c1bd 100644 --- a/server.go +++ b/server.go @@ -6,47 +6,15 @@ import ( "fmt" "net" "net/http" - "net/url" - "path/filepath" - "strconv" - "strings" "time" "golang.org/x/net/netutil" ) -const ( - contextDispositionFilenameFallback = "image" -) - var ( - mimes = map[imageType]string{ - imageTypeJPEG: "image/jpeg", - imageTypePNG: "image/png", - imageTypeWEBP: "image/webp", - imageTypeGIF: "image/gif", - imageTypeICO: "image/x-icon", - imageTypeHEIC: "image/heif", - } - - 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\"", - imageTypeHEIC: "inline; filename=\"%s.heic\"", - } - imgproxyIsRunningMsg = []byte("imgproxy is running") - errInvalidMethod = newError(422, "Invalid request method", "Method doesn't allowed") errInvalidSecret = newError(403, "Invalid secret", "Forbidden") - - responseGzipBufPool *bufPool - responseGzipPool *gzipPool - - processingSem chan struct{} ) func buildRouter() *router { @@ -62,8 +30,6 @@ func buildRouter() *router { } func startServer() *http.Server { - processingSem = make(chan struct{}, conf.Concurrency) - l, err := net.Listen("tcp", conf.Bind) if err != nil { logFatal(err.Error()) @@ -76,10 +42,7 @@ func startServer() *http.Server { MaxHeaderBytes: 1 << 20, } - if conf.GZipCompression > 0 { - responseGzipBufPool = newBufPool("gzip", conf.Concurrency, conf.GZipBufferSize) - responseGzipPool = newGzipPool(conf.Concurrency) - } + initProcessingHandler() go func() { logNotice("Starting server at %s", conf.Bind) @@ -100,86 +63,6 @@ func shutdownServer(s *http.Server) { s.Shutdown(ctx) } -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))) -} - -func respondWithImage(ctx context.Context, reqID string, r *http.Request, rw http.ResponseWriter, data []byte) { - po := getProcessingOptions(ctx) - - rw.Header().Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat)) - rw.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, public", conf.TTL)) - rw.Header().Set("Content-Type", mimes[po.Format]) - rw.Header().Set("Content-Disposition", contentDisposition(getImageURL(ctx), po.Format)) - - addVaryHeader(rw) - - if conf.GZipCompression > 0 && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { - buf := responseGzipBufPool.Get(0) - defer responseGzipBufPool.Put(buf) - - gz := responseGzipPool.Get(buf) - defer responseGzipPool.Put(gz) - - gz.Write(data) - gz.Close() - - rw.Header().Set("Content-Encoding", "gzip") - rw.Header().Set("Content-Length", strconv.Itoa(buf.Len())) - - rw.WriteHeader(200) - rw.Write(buf.Bytes()) - } else { - rw.Header().Set("Content-Length", strconv.Itoa(len(data))) - rw.WriteHeader(200) - rw.Write(data) - } - - logResponse(reqID, 200, fmt.Sprintf("Processed in %s: %s; %+v", getTimerSince(ctx), getImageURL(ctx), po)) -} - -func addVaryHeader(rw http.ResponseWriter) { - vary := make([]string, 0) - - 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 { - rw.Header().Set("Vary", strings.Join(vary, ", ")) - } -} - -func respondWithError(reqID string, rw http.ResponseWriter, err *imgproxyError) { - logResponse(reqID, err.StatusCode, err.Message) - - rw.WriteHeader(err.StatusCode) - - if conf.DevelopmentErrorsMode { - rw.Write([]byte(err.Message)) - } else { - rw.Write([]byte(err.PublicMessage)) - } -} - func withCORS(h routeHandler) routeHandler { return func(reqID string, rw http.ResponseWriter, r *http.Request) { if len(conf.AllowOrigin) > 0 { @@ -202,7 +85,7 @@ func withSecret(h routeHandler) routeHandler { if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), authHeader) == 1 { h(reqID, rw, r) } else { - respondWithError(reqID, rw, errInvalidSecret) + panic(errInvalidSecret) } } } @@ -210,10 +93,23 @@ func withSecret(h routeHandler) routeHandler { func handlePanic(reqID string, rw http.ResponseWriter, r *http.Request, err error) { reportError(err, r) - if ierr, ok := err.(*imgproxyError); ok { - respondWithError(reqID, rw, ierr) + var ( + ierr *imgproxyError + ok bool + ) + + if ierr, ok = err.(*imgproxyError); !ok { + ierr = newUnexpectedError(err.Error(), 3) + } + + logResponse(reqID, ierr.StatusCode, ierr.Message) + + rw.WriteHeader(ierr.StatusCode) + + if conf.DevelopmentErrorsMode { + rw.Write([]byte(ierr.Message)) } else { - respondWithError(reqID, rw, newUnexpectedError(err.Error(), 3)) + rw.Write([]byte(ierr.PublicMessage)) } } @@ -227,72 +123,3 @@ func handleOptions(reqID string, rw http.ResponseWriter, r *http.Request) { logResponse(reqID, 200, "Respond with options") rw.WriteHeader(200) } - -func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) { - ctx := context.Background() - - if newRelicEnabled { - var newRelicCancel context.CancelFunc - ctx, newRelicCancel = startNewRelicTransaction(ctx, rw, r) - defer newRelicCancel() - } - - if prometheusEnabled { - prometheusRequestsTotal.Inc() - defer startPrometheusDuration(prometheusRequestDuration)() - } - - processingSem <- struct{}{} - defer func() { <-processingSem }() - - ctx, timeoutCancel := startTimer(ctx, time.Duration(conf.WriteTimeout)*time.Second) - defer timeoutCancel() - - ctx, err := parsePath(ctx, r) - if err != nil { - panic(err) - } - - ctx, downloadcancel, err := downloadImage(ctx) - defer downloadcancel() - if err != nil { - if newRelicEnabled { - sendErrorToNewRelic(ctx, err) - } - if prometheusEnabled { - incrementPrometheusErrorsTotal("download") - } - panic(err) - } - - checkTimeout(ctx) - - if conf.ETagEnabled { - eTag := calcETag(ctx) - rw.Header().Set("ETag", eTag) - - if eTag == r.Header.Get("If-None-Match") { - logResponse(reqID, 304, "Not modified") - rw.WriteHeader(304) - return - } - } - - checkTimeout(ctx) - - imageData, processcancel, err := processImage(ctx) - defer processcancel() - if err != nil { - if newRelicEnabled { - sendErrorToNewRelic(ctx, err) - } - if prometheusEnabled { - incrementPrometheusErrorsTotal("processing") - } - panic(err) - } - - checkTimeout(ctx) - - respondWithImage(ctx, reqID, r, rw, imageData) -}