package main /* #cgo LDFLAGS: -s -w #include "image_types.h" */ import "C" import ( "context" "encoding/base64" "errors" "fmt" "net/http" "net/url" "regexp" "strconv" "strings" ) 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) ) type processingHeaders struct { Accept string Width string ViewportWidth string DPR string } var imageTypes = map[string]imageType{ "jpeg": imageTypeJPEG, "jpg": imageTypeJPEG, "png": imageTypePNG, "webp": imageTypeWEBP, "gif": imageTypeGIF, "ico": imageTypeICO, "svg": imageTypeSVG, } type gravityType int const ( gravityCenter gravityType = iota gravityNorth gravityEast gravitySouth gravityWest gravityNorthWest gravityNorthEast gravitySouthWest gravitySouthEast gravitySmart gravityFocusPoint ) var gravityTypes = map[string]gravityType{ "ce": gravityCenter, "no": gravityNorth, "ea": gravityEast, "so": gravitySouth, "we": gravityWest, "nowe": gravityNorthWest, "noea": gravityNorthEast, "sowe": gravitySouthWest, "soea": gravitySouthEast, "sm": gravitySmart, "fp": gravityFocusPoint, } type gravityOptions struct { Type gravityType X, Y float64 } type resizeType int const ( resizeFit resizeType = iota resizeFill resizeCrop ) var resizeTypes = map[string]resizeType{ "fit": resizeFit, "fill": resizeFill, "crop": resizeCrop, } type rgbColor struct{ R, G, B uint8 } var hexColorRegex = regexp.MustCompile("^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$") const ( hexColorLongFormat = "%02x%02x%02x" hexColorShortFormat = "%1x%1x%1x" ) type watermarkOptions struct { Enabled bool Opacity float64 Replicate bool Gravity gravityType OffsetX int OffsetY int Scale float64 } type processingOptions struct { Resize resizeType Width int Height int Dpr float64 Gravity gravityOptions Enlarge bool Format imageType Quality int Flatten bool Background rgbColor Blur float32 Sharpen float32 CacheBuster string Watermark watermarkOptions UsedPresets []string } type applyOptionFunc func(po *processingOptions, args []string) error const ( imageURLCtxKey = ctxKey("imageUrl") processingOptionsCtxKey = ctxKey("processingOptions") urlTokenPlain = "plain" maxClientHintDPR = 8 msgForbidden = "Forbidden" msgInvalidURL = "Invalid URL" ) var ( errInvalidImageURL = errors.New("Invalid image url") errInvalidURLEncoding = errors.New("Invalid url encoding") errResultingImageFormatIsNotSupported = errors.New("Resulting image format is not supported") 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 { return k } } return "" } func (rt resizeType) String() string { for k, v := range resizeTypes { if v == rt { return k } } return "" } func (po *processingOptions) isPresetUsed(name string) bool { for _, usedName := range po.UsedPresets { if usedName == name { return true } } return false } func (po *processingOptions) presetUsed(name string) { po.UsedPresets = append(po.UsedPresets, name) } func colorFromHex(hexcolor string) (rgbColor, error) { c := rgbColor{} if !hexColorRegex.MatchString(hexcolor) { return c, fmt.Errorf("Invalid hex color: %s", hexcolor) } if len(hexcolor) == 3 { fmt.Sscanf(hexcolor, hexColorShortFormat, &c.R, &c.G, &c.B) c.R *= 17 c.G *= 17 c.B *= 17 } else { fmt.Sscanf(hexcolor, hexColorLongFormat, &c.R, &c.G, &c.B) } return c, nil } func decodeBase64URL(parts []string) (string, string, error) { var format string urlParts := strings.Split(strings.Join(parts, ""), ".") if len(urlParts) > 2 { return "", "", errInvalidURLEncoding } if len(urlParts) == 2 && len(urlParts[1]) > 0 { format = urlParts[1] } imageURL, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(urlParts[0], "=")) if err != nil { return "", "", errInvalidURLEncoding } fullURL := fmt.Sprintf("%s%s", conf.BaseURL, string(imageURL)) if _, err := url.ParseRequestURI(fullURL); err != nil { return "", "", errInvalidImageURL } return fullURL, format, nil } func decodePlainURL(parts []string) (string, string, error) { var format string urlParts := strings.Split(strings.Join(parts, "/"), "@") if len(urlParts) > 2 { return "", "", errInvalidURLEncoding } if len(urlParts) == 2 && len(urlParts[1]) > 0 { format = urlParts[1] } fullURL := fmt.Sprintf("%s%s", conf.BaseURL, urlParts[0]) if _, err := url.ParseRequestURI(fullURL); err == nil { return fullURL, format, nil } if unescaped, err := url.PathUnescape(urlParts[0]); err == nil { fullURL := fmt.Sprintf("%s%s", conf.BaseURL, unescaped) if _, err := url.ParseRequestURI(fullURL); err == nil { return fullURL, format, nil } } return "", "", errInvalidImageURL } func decodeURL(parts []string) (string, string, error) { if len(parts) == 0 { return "", "", errInvalidURLEncoding } if parts[0] == urlTokenPlain && len(parts) > 1 { return decodePlainURL(parts[1:]) } return decodeBase64URL(parts) } func applyWidthOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid width arguments: %v", args) } if w, err := strconv.Atoi(args[0]); err == nil && w >= 0 { po.Width = w } else { return fmt.Errorf("Invalid width: %s", args[0]) } return nil } func applyHeightOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid height arguments: %v", args) } if h, err := strconv.Atoi(args[0]); err == nil && po.Height >= 0 { po.Height = h } else { return fmt.Errorf("Invalid height: %s", args[0]) } return nil } func applyEnlargeOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid enlarge arguments: %v", args) } po.Enlarge = args[0] != "0" return nil } func applySizeOption(po *processingOptions, args []string) (err error) { if len(args) > 3 { return fmt.Errorf("Invalid size arguments: %v", args) } if len(args) >= 1 && len(args[0]) > 0 { if err = applyWidthOption(po, args[0:1]); err != nil { return } } if len(args) >= 2 && len(args[1]) > 0 { if err = applyHeightOption(po, args[1:2]); err != nil { return } } if len(args) == 3 && len(args[2]) > 0 { if err = applyEnlargeOption(po, args[2:3]); err != nil { return } } return nil } func applyResizingTypeOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid resizing type arguments: %v", args) } if r, ok := resizeTypes[args[0]]; ok { po.Resize = r } else { return fmt.Errorf("Invalid resize type: %s", args[0]) } return nil } func applyResizeOption(po *processingOptions, args []string) error { if len(args) > 4 { return fmt.Errorf("Invalid resize arguments: %v", args) } if len(args[0]) > 0 { if err := applyResizingTypeOption(po, args[0:1]); err != nil { return err } } if len(args) > 1 { if err := applySizeOption(po, args[1:]); err != nil { return err } } return nil } func applyDprOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid dpr arguments: %v", args) } if d, err := strconv.ParseFloat(args[0], 64); err == nil || (d > 0 && d != 1) { po.Dpr = d } else { return fmt.Errorf("Invalid dpr: %s", args[0]) } return nil } func applyGravityOption(po *processingOptions, args []string) error { if g, ok := gravityTypes[args[0]]; ok { po.Gravity.Type = g } else { return fmt.Errorf("Invalid gravity: %s", args[0]) } if po.Gravity.Type == gravityFocusPoint { if len(args) != 3 { return fmt.Errorf("Invalid gravity arguments: %v", args) } if x, err := strconv.ParseFloat(args[1], 64); err == nil && x >= 0 && x <= 1 { po.Gravity.X = x } else { return fmt.Errorf("Invalid gravity X: %s", args[1]) } if y, err := strconv.ParseFloat(args[2], 64); err == nil && y >= 0 && y <= 1 { po.Gravity.Y = y } else { return fmt.Errorf("Invalid gravity Y: %s", args[2]) } } else if len(args) > 1 { return fmt.Errorf("Invalid gravity arguments: %v", args) } return nil } func applyQualityOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid quality arguments: %v", args) } if q, err := strconv.Atoi(args[0]); err == nil && q > 0 && q <= 100 { po.Quality = q } else { return fmt.Errorf("Invalid quality: %s", args[0]) } return nil } func applyBackgroundOption(po *processingOptions, args []string) error { switch len(args) { case 1: if len(args[0]) == 0 { po.Flatten = false } else if c, err := colorFromHex(args[0]); err == nil { po.Flatten = true po.Background = c } else { return fmt.Errorf("Invalid background argument: %s", err) } case 3: po.Flatten = true if r, err := strconv.ParseUint(args[0], 10, 8); err == nil && r >= 0 && r <= 255 { po.Background.R = uint8(r) } else { return fmt.Errorf("Invalid background red channel: %s", args[0]) } if g, err := strconv.ParseUint(args[1], 10, 8); err == nil && g >= 0 && g <= 255 { po.Background.G = uint8(g) } else { return fmt.Errorf("Invalid background green channel: %s", args[1]) } if b, err := strconv.ParseUint(args[2], 10, 8); err == nil && b >= 0 && b <= 255 { po.Background.B = uint8(b) } else { return fmt.Errorf("Invalid background blue channel: %s", args[2]) } default: return fmt.Errorf("Invalid background arguments: %v", args) } return nil } func applyBlurOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid blur arguments: %v", args) } if b, err := strconv.ParseFloat(args[0], 32); err == nil || b >= 0 { po.Blur = float32(b) } else { return fmt.Errorf("Invalid blur: %s", args[0]) } return nil } func applySharpenOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid sharpen arguments: %v", args) } if s, err := strconv.ParseFloat(args[0], 32); err == nil || s >= 0 { po.Sharpen = float32(s) } else { return fmt.Errorf("Invalid sharpen: %s", args[0]) } return nil } func applyPresetOption(po *processingOptions, args []string) error { for _, preset := range args { if p, ok := conf.Presets[preset]; ok { if po.isPresetUsed(preset) { return fmt.Errorf("Recursive preset usage is detected: %s", preset) } po.presetUsed(preset) if err := applyProcessingOptions(po, p); err != nil { return err } } else { return fmt.Errorf("Unknown asset: %s", preset) } } return nil } func applyWatermarkOption(po *processingOptions, args []string) error { if len(args) > 7 { return fmt.Errorf("Invalid watermark arguments: %v", args) } if o, err := strconv.ParseFloat(args[0], 64); err == nil && o >= 0 && o <= 1 { po.Watermark.Enabled = o > 0 po.Watermark.Opacity = o } else { return fmt.Errorf("Invalid watermark opacity: %s", args[0]) } if len(args) > 1 && len(args[1]) > 0 { if args[1] == "re" { po.Watermark.Replicate = true } else if g, ok := gravityTypes[args[1]]; ok && g != gravityFocusPoint && g != gravitySmart { po.Watermark.Gravity = g } else { return fmt.Errorf("Invalid watermark position: %s", args[1]) } } if len(args) > 2 && len(args[2]) > 0 { if x, err := strconv.Atoi(args[2]); err == nil { po.Watermark.OffsetX = x } else { return fmt.Errorf("Invalid watermark X offset: %s", args[2]) } } if len(args) > 3 && len(args[3]) > 0 { if y, err := strconv.Atoi(args[3]); err == nil { po.Watermark.OffsetY = y } else { return fmt.Errorf("Invalid watermark Y offset: %s", args[3]) } } if len(args) > 4 && len(args[4]) > 0 { if s, err := strconv.ParseFloat(args[4], 64); err == nil && s >= 0 { po.Watermark.Scale = s } else { return fmt.Errorf("Invalid watermark scale: %s", args[4]) } } return nil } func applyFormatOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid format arguments: %v", args) } if conf.EnforceWebp && po.Format == imageTypeWEBP { // Webp is enforced and already set as format return nil } if f, ok := imageTypes[args[0]]; ok { po.Format = f } else { return fmt.Errorf("Invalid image format: %s", args[0]) } if !vipsTypeSupportSave[po.Format] { return errResultingImageFormatIsNotSupported } return nil } func applyCacheBusterOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid cache buster arguments: %v", args) } po.CacheBuster = args[0] return nil } func applyProcessingOption(po *processingOptions, name string, args []string) error { switch name { case "format", "f", "ext": if err := applyFormatOption(po, args); err != nil { return err } case "resize", "rs": if err := applyResizeOption(po, args); err != nil { return err } case "resizing_type", "rt": if err := applyResizingTypeOption(po, args); err != nil { return err } case "size", "s": if err := applySizeOption(po, args); err != nil { return err } case "width", "w": if err := applyWidthOption(po, args); err != nil { return err } case "height", "h": if err := applyHeightOption(po, args); err != nil { return err } case "enlarge", "el": if err := applyEnlargeOption(po, args); err != nil { return err } case "dpr": if err := applyDprOption(po, args); err != nil { return err } case "gravity", "g": if err := applyGravityOption(po, args); err != nil { return err } case "quality", "q": if err := applyQualityOption(po, args); err != nil { return err } case "background", "bg": if err := applyBackgroundOption(po, args); err != nil { return err } case "blur", "bl": if err := applyBlurOption(po, args); err != nil { return err } case "sharpen", "sh": if err := applySharpenOption(po, args); err != nil { return err } case "watermark", "wm": if err := applyWatermarkOption(po, args); err != nil { return err } case "preset", "pr": if err := applyPresetOption(po, args); err != nil { return err } case "cachebuster", "cb": if err := applyCacheBusterOption(po, args); err != nil { return err } default: return fmt.Errorf("Unknown processing option: %s", name) } return nil } func applyProcessingOptions(po *processingOptions, options urlOptions) error { for name, args := range options { if err := applyProcessingOption(po, name, args); err != nil { return err } } return nil } func parseURLOptions(opts []string) (urlOptions, []string) { parsed := make(urlOptions) urlStart := len(opts) + 1 for i, opt := range opts { args := strings.Split(opt, ":") if len(args) == 1 { urlStart = i break } parsed[args[0]] = args[1:] } var rest []string if urlStart < len(opts) { rest = opts[urlStart:] } else { rest = []string{} } return parsed, rest } func defaultProcessingOptions(headers *processingHeaders) (*processingOptions, error) { var err error po := processingOptions{ Resize: resizeFit, Width: 0, Height: 0, Gravity: gravityOptions{Type: gravityCenter}, Enlarge: false, Quality: conf.Quality, Format: imageTypeUnknown, Background: rgbColor{255, 255, 255}, Blur: 0, Sharpen: 0, Dpr: 1, Watermark: watermarkOptions{Opacity: 1, Replicate: false, Gravity: gravityCenter}, UsedPresets: make([]string, 0, len(conf.Presets)), } if (conf.EnableWebpDetection || conf.EnforceWebp) && strings.Contains(headers.Accept, "image/webp") { po.Format = imageTypeWEBP } if conf.EnableClientHints && len(headers.ViewportWidth) > 0 { if vw, err := strconv.Atoi(headers.ViewportWidth); err == nil { po.Width = vw } } if conf.EnableClientHints && len(headers.Width) > 0 { if w, err := strconv.Atoi(headers.Width); err == nil { po.Width = w } } if conf.EnableClientHints && len(headers.DPR) > 0 { if dpr, err := strconv.ParseFloat(headers.DPR, 64); err == nil || (dpr > 0 && dpr <= maxClientHintDPR) { po.Dpr = dpr } } if _, ok := conf.Presets["default"]; ok { err = applyPresetOption(&po, []string{"default"}) } return &po, err } func parsePathAdvanced(parts []string, headers *processingHeaders) (string, *processingOptions, error) { po, err := defaultProcessingOptions(headers) if err != nil { return "", po, err } options, urlParts := parseURLOptions(parts) if err := applyProcessingOptions(po, options); err != nil { return "", po, err } url, extension, err := decodeURL(urlParts) if err != nil { return "", po, err } if len(extension) > 0 { if err := applyFormatOption(po, []string{extension}); err != nil { return "", po, err } } return url, po, nil } func parsePathBasic(parts []string, headers *processingHeaders) (string, *processingOptions, error) { var err error if len(parts) < 6 { return "", nil, errInvalidPath } po, err := defaultProcessingOptions(headers) if err != nil { return "", po, err } po.Resize = resizeTypes[parts[0]] if err = applyWidthOption(po, parts[1:2]); err != nil { return "", po, err } if err = applyHeightOption(po, parts[2:3]); err != nil { return "", po, err } if err = applyGravityOption(po, strings.Split(parts[3], ":")); err != nil { return "", po, err } if err = applyEnlargeOption(po, parts[4:5]); err != nil { return "", po, err } url, extension, err := decodeURL(parts[5:]) if err != nil { return "", po, err } if len(extension) > 0 { if err := applyFormatOption(po, []string{extension}); err != nil { return "", po, err } } return url, po, nil } func parsePath(ctx context.Context, r *http.Request) (context.Context, error) { path := r.URL.Path parts := strings.Split(strings.TrimPrefix(path, "/"), "/") if len(parts) < 3 { return ctx, errInvalidPath } if !conf.AllowInsecure { if err := validatePath(parts[0], strings.TrimPrefix(path, fmt.Sprintf("/%s", parts[0]))); err != nil { return ctx, newError(403, err.Error(), msgForbidden) } } headers := &processingHeaders{ Accept: r.Header.Get("Accept"), Width: r.Header.Get("Width"), ViewportWidth: r.Header.Get("Viewport-Width"), DPR: r.Header.Get("DPR"), } var imageURL string var po *processingOptions var err error if _, ok := resizeTypes[parts[1]]; ok { imageURL, po, err = parsePathBasic(parts[1:], headers) } else { imageURL, po, err = parsePathAdvanced(parts[1:], headers) } if err != nil { return ctx, newError(404, err.Error(), msgInvalidURL) } ctx = context.WithValue(ctx, imageURLCtxKey, imageURL) ctx = context.WithValue(ctx, processingOptionsCtxKey, po) return ctx, nil } func getImageURL(ctx context.Context) string { return ctx.Value(imageURLCtxKey).(string) } func getProcessingOptions(ctx context.Context) *processingOptions { return ctx.Value(processingOptionsCtxKey).(*processingOptions) }