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 = C.UNKNOWN imageTypeJPEG = C.JPEG imageTypePNG = C.PNG imageTypeWEBP = C.WEBP imageTypeGIF = C.GIF ) var imageTypes = map[string]imageType{ "jpeg": imageTypeJPEG, "jpg": imageTypeJPEG, "png": imageTypePNG, "webp": imageTypeWEBP, "gif": imageTypeGIF, } 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 color 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 Gravity gravityOptions Enlarge bool Format imageType Flatten bool Background color Blur float32 Sharpen float32 Watermark watermarkOptions UsedPresets []string } const ( imageURLCtxKey = ctxKey("imageUrl") processingOptionsCtxKey = ctxKey("processingOptions") ) var ( errInvalidURLEncoding = errors.New("Invalid url encoding") errInvalidPath = errors.New("Invalid path") errInvalidImageURL = errors.New("Invalid image url") errResultingImageFormatIsNotSupported = errors.New("Resulting image format is not supported") ) 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 colorFromHex(hexcolor string) (color, error) { c := color{} 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 (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 decodeURL(parts []string) (string, string, error) { var extension string urlParts := strings.Split(strings.Join(parts, ""), ".") if len(urlParts) > 2 { return "", "", errInvalidURLEncoding } if len(urlParts) == 2 { extension = urlParts[1] } url, err := base64.RawURLEncoding.DecodeString(urlParts[0]) if err != nil { return "", "", errInvalidURLEncoding } return string(url), extension, nil } 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 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 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 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 "gravity", "g": if err := applyGravityOption(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 } 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(acceptHeader string) (*processingOptions, error) { var err error po := processingOptions{ Resize: resizeFit, Width: 0, Height: 0, Gravity: gravityOptions{Type: gravityCenter}, Enlarge: false, Format: imageTypeJPEG, Blur: 0, Sharpen: 0, Watermark: watermarkOptions{Opacity: 1, Replicate: false, Gravity: gravityCenter}, UsedPresets: make([]string, 0, len(conf.Presets)), } if (conf.EnableWebpDetection || conf.EnforceWebp) && strings.Contains(acceptHeader, "image/webp") { po.Format = imageTypeWEBP } if _, ok := conf.Presets["default"]; ok { err = applyPresetOption(&po, []string{"default"}) } return &po, err } func parsePathAdvanced(parts []string, acceptHeader string) (string, *processingOptions, error) { po, err := defaultProcessingOptions(acceptHeader) 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 string(url), po, nil } func parsePathSimple(parts []string, acceptHeader string) (string, *processingOptions, error) { var err error if len(parts) < 6 { return "", nil, errInvalidPath } po, err := defaultProcessingOptions(acceptHeader) 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 string(url), po, nil } func parsePath(ctx context.Context, r *http.Request) (context.Context, error) { path := r.URL.Path parts := strings.Split(strings.TrimPrefix(path, "/"), "/") var acceptHeader string if h, ok := r.Header["Accept"]; ok { acceptHeader = h[0] } 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, err } } var imageURL string var po *processingOptions var err error if _, ok := resizeTypes[parts[1]]; ok { imageURL, po, err = parsePathSimple(parts[1:], acceptHeader) } else { imageURL, po, err = parsePathAdvanced(parts[1:], acceptHeader) } if err != nil { return ctx, err } if _, err = url.ParseRequestURI(imageURL); err != nil { return ctx, errInvalidImageURL } ctx = context.WithValue(ctx, imageURLCtxKey, imageURL) ctx = context.WithValue(ctx, processingOptionsCtxKey, po) return ctx, err } func getImageURL(ctx context.Context) string { return ctx.Value(imageURLCtxKey).(string) } func getProcessingOptions(ctx context.Context) *processingOptions { return ctx.Value(processingOptionsCtxKey).(*processingOptions) }