From f35a7fa466ffb06c38dcb3216e30c13aa8e14ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Br=C3=BCheim?= Date: Tue, 30 Mar 2021 02:47:19 +0200 Subject: encode,staticfiles: Content negotiation, precompressed files (#4045) * encode: implement prefer setting * encode: minimum_length configurable via caddyfile * encode: configurable content-types which to encode * file_server: support precompressed files * encode: use ReponseMatcher for conditional encoding of content * linting error & documentation of encode.PrecompressedOrder * encode: allow just one response matcher also change the namespace of the encoders back, I accidently changed to precompressed >.> default matchers include a * to match to any charset, that may be appended * rounding of the PR * added integration tests for new caddyfile directives * improved various doc strings (punctuation and typos) * added json tag for file_server precompress order and encode matcher * file_server: add vary header, remove accept-ranges when serving precompressed files * encode: move Suffix implementation to precompressed modules --- modules/caddyhttp/encode/caddyfile.go | 139 +++++++++++++++++++++++++++++----- 1 file changed, 122 insertions(+), 17 deletions(-) (limited to 'modules/caddyhttp/encode/caddyfile.go') diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go index 2f11ca0..c45f159 100644 --- a/modules/caddyhttp/encode/caddyfile.go +++ b/modules/caddyhttp/encode/caddyfile.go @@ -15,7 +15,9 @@ package encode import ( - "fmt" + "net/http" + "strconv" + "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -40,21 +42,31 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // encode [] { -// gzip [] +// gzip [] // zstd +// minimum_length +// prefer +// # response matcher block +// match { +// status +// header [] +// } +// # or response matcher single line syntax +// match [header []] | [status ] // } // // Specifying the formats on the first line will use those formats' defaults. func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + responseMatchers := make(map[string]caddyhttp.ResponseMatcher) for d.Next() { for _, arg := range d.RemainingArgs() { mod, err := caddy.GetModule("http.encoders." + arg) if err != nil { - return fmt.Errorf("finding encoder module '%s': %v", mod, err) + return d.Errf("finding encoder module '%s': %v", mod, err) } encoding, ok := mod.New().(Encoding) if !ok { - return fmt.Errorf("module %s is not an HTTP encoding", mod) + return d.Errf("module %s is not an HTTP encoding", mod) } if enc.EncodingsRaw == nil { enc.EncodingsRaw = make(caddy.ModuleMap) @@ -63,25 +75,118 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } for d.NextBlock(0) { - name := d.Val() - modID := "http.encoders." + name - unm, err := caddyfile.UnmarshalModule(d, modID) - if err != nil { - return err - } - encoding, ok := unm.(Encoding) - if !ok { - return fmt.Errorf("module %s is not an HTTP encoding; is %T", modID, unm) + switch d.Val() { + case "minimum_length": + if !d.NextArg() { + return d.ArgErr() + } + minLength, err := strconv.Atoi(d.Val()) + if err != nil { + return err + } + enc.MinLength = minLength + case "prefer": + var encs []string + for d.NextArg() { + encs = append(encs, d.Val()) + } + if len(encs) == 0 { + return d.ArgErr() + } + enc.Prefer = encs + case "match": + err := enc.parseNamedResponseMatcher(d.NewFromNextSegment(), responseMatchers) + if err != nil { + return err + } + matcher := responseMatchers["match"] + enc.Matcher = &matcher + default: + name := d.Val() + modID := "http.encoders." + name + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + encoding, ok := unm.(Encoding) + if !ok { + return d.Errf("module %s is not an HTTP encoding; is %T", modID, unm) + } + if enc.EncodingsRaw == nil { + enc.EncodingsRaw = make(caddy.ModuleMap) + } + enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil) } - if enc.EncodingsRaw == nil { - enc.EncodingsRaw = make(caddy.ModuleMap) - } - enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil) } } return nil } +// Parse the tokens of a named response matcher. +// +// match { +// header [] +// status +// } +// +// Or, single line syntax: +// +// match [header []] | [status ] +// +func (enc *Encode) parseNamedResponseMatcher(d *caddyfile.Dispenser, matchers map[string]caddyhttp.ResponseMatcher) error { + for d.Next() { + definitionName := d.Val() + + if _, ok := matchers[definitionName]; ok { + return d.Errf("matcher is defined more than once: %s", definitionName) + } + + matcher := caddyhttp.ResponseMatcher{} + for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { + switch d.Val() { + case "header": + if matcher.Headers == nil { + matcher.Headers = http.Header{} + } + + // reuse the header request matcher's unmarshaler + headerMatcher := caddyhttp.MatchHeader(matcher.Headers) + err := headerMatcher.UnmarshalCaddyfile(d.NewFromNextSegment()) + if err != nil { + return err + } + + matcher.Headers = http.Header(headerMatcher) + case "status": + if matcher.StatusCode == nil { + matcher.StatusCode = []int{} + } + + args := d.RemainingArgs() + if len(args) == 0 { + return d.ArgErr() + } + + for _, arg := range args { + if len(arg) == 3 && strings.HasSuffix(arg, "xx") { + arg = arg[:1] + } + statusNum, err := strconv.Atoi(arg) + if err != nil { + return d.Errf("bad status value '%s': %v", arg, err) + } + matcher.StatusCode = append(matcher.StatusCode, statusNum) + } + default: + return d.Errf("unrecognized response matcher %s", d.Val()) + } + } + + matchers[definitionName] = matcher + } + return nil +} + // Interface guard var _ caddyfile.Unmarshaler = (*Encode)(nil) -- cgit v1.2.3