diff --git a/config.go b/config.go index a6d73cff..e2e5ccbe 100644 --- a/config.go +++ b/config.go @@ -131,6 +131,7 @@ type config struct { EnableWebpDetection bool EnforceWebp bool + EnableClientHints bool Key []byte Salt []byte @@ -221,6 +222,7 @@ func init() { boolEnvConfig(&conf.EnableWebpDetection, "IMGPROXY_ENABLE_WEBP_DETECTION") boolEnvConfig(&conf.EnforceWebp, "IMGPROXY_ENFORCE_WEBP") + boolEnvConfig(&conf.EnableClientHints, "IMGPROXY_ENABLE_CLIENT_HINTS") hexEnvConfig(&conf.Key, "IMGPROXY_KEY") hexEnvConfig(&conf.Salt, "IMGPROXY_SALT") diff --git a/docs/configuration.md b/docs/configuration.md index 0817a8ca..809b2dbe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -69,7 +69,15 @@ imgproxy can use the `Accept` HTTP header to detect if the browser supports WebP When WebP support detection is enabled, please take care to configure your CDN or caching proxy to take the `Accept` HTTP header into account while caching. -**Warning**: Headers cannot be signed. This means that an attacker can bypass your CDN cache by changing the `Accept` HTTP header. Have this in mind when configuring your production caching setup. +**Warning**: Headers cannot be signed. This means that an attacker can bypass your CDN cache by changing the `Accept` HTTP headers. Have this in mind when configuring your production caching setup. + +## Client Hints support + +imgproxy can use the `Width` or `Viewport-Width` HTTP header to determine the width of the image container using Client Hints when the width argument is ommited. + +* `IMGPROXY_ENABLE_CLIENT_HINTS`: enables Client Hints support when the width is ommited for automatic responsive images . Read [here](https://developers.google.com/web/updates/2015/09/automating-resource-selection-with-client-hints) details about Client Hints. + +**Warning**: Headers cannot be signed. This means that an attacker can bypass your CDN cache by changing the `Width` or `Viewport-Width` HTTP headers. Have this in mind when configuring your production caching setup. ### Watermark diff --git a/processing_options.go b/processing_options.go index 30733e40..db3486e1 100644 --- a/processing_options.go +++ b/processing_options.go @@ -30,6 +30,12 @@ const ( imageTypeGIF = imageType(C.GIF) ) +type processingHeaders struct { + Accept string + Width string + ViewportWidth string +} + var imageTypes = map[string]imageType{ "jpeg": imageTypeJPEG, "jpg": imageTypeJPEG, @@ -685,7 +691,7 @@ func parseURLOptions(opts []string) (urlOptions, []string) { return parsed, rest } -func defaultProcessingOptions(acceptHeader string) (*processingOptions, error) { +func defaultProcessingOptions(headers *processingHeaders) (*processingOptions, error) { var err error po := processingOptions{ @@ -702,10 +708,19 @@ func defaultProcessingOptions(acceptHeader string) (*processingOptions, error) { UsedPresets: make([]string, 0, len(conf.Presets)), } - if (conf.EnableWebpDetection || conf.EnforceWebp) && strings.Contains(acceptHeader, "image/webp") { + 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 _, ok := conf.Presets["default"]; ok { err = applyPresetOption(&po, []string{"default"}) } @@ -713,8 +728,8 @@ func defaultProcessingOptions(acceptHeader string) (*processingOptions, error) { return &po, err } -func parsePathAdvanced(parts []string, acceptHeader string) (string, *processingOptions, error) { - po, err := defaultProcessingOptions(acceptHeader) +func parsePathAdvanced(parts []string, headers *processingHeaders) (string, *processingOptions, error) { + po, err := defaultProcessingOptions(headers) if err != nil { return "", po, err } @@ -739,14 +754,14 @@ func parsePathAdvanced(parts []string, acceptHeader string) (string, *processing return url, po, nil } -func parsePathSimple(parts []string, acceptHeader string) (string, *processingOptions, error) { +func parsePathSimple(parts []string, headers *processingHeaders) (string, *processingOptions, error) { var err error if len(parts) < 6 { return "", nil, errInvalidPath } - po, err := defaultProcessingOptions(acceptHeader) + po, err := defaultProcessingOptions(headers) if err != nil { return "", po, err } @@ -787,11 +802,6 @@ 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 } @@ -801,15 +811,19 @@ func parsePath(ctx context.Context, r *http.Request) (context.Context, error) { return ctx, err } } + headers := &processingHeaders{} + headers.Accept = r.Header.Get("Accept") + headers.Width = r.Header.Get("Width") + headers.ViewportWidth = r.Header.Get("Viewport-Width") var imageURL string var po *processingOptions var err error if _, ok := resizeTypes[parts[1]]; ok { - imageURL, po, err = parsePathSimple(parts[1:], acceptHeader) + imageURL, po, err = parsePathSimple(parts[1:], headers) } else { - imageURL, po, err = parsePathAdvanced(parts[1:], acceptHeader) + imageURL, po, err = parsePathAdvanced(parts[1:], headers) } if err != nil {