Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

encode: Enhancements to content negotiation, serving precompressed content #4045

Merged
merged 10 commits into from
Mar 30, 2021
66 changes: 66 additions & 0 deletions caddytest/integration/caddyfile_adapt/encode_options.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
:80

encode gzip zstd {
minimum_length 256
prefer zstd gzip
match {
status 2xx 4xx 500
header Content-Type text/*
header Content-Type application/json*
header Content-Type application/javascript*
header Content-Type application/xhtml+xml*
header Content-Type application/atom+xml*
header Content-Type application/rss+xml*
header Content-Type image/svg+xml*
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"encodings": {
"gzip": {},
"zstd": {}
},
"handler": "encode",
"match": {
"headers": {
"Content-Type": [
"text/*",
"application/json*",
"application/javascript*",
"application/xhtml+xml*",
"application/atom+xml*",
"application/rss+xml*",
"image/svg+xml*"
]
},
"status_code": [
2,
4,
500
]
},
"minimum_length": 256,
"prefer": [
"zstd",
"gzip"
]
}
]
}
]
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
:80

file_server {
precompressed zstd br gzip
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"precompressed": {
"br": {},
"gzip": {},
"zstd": {}
},
"precompressed_order": [
"zstd",
"br",
"gzip"
]
}
]
}
]
}
}
}
}
}
7 changes: 7 additions & 0 deletions caddytest/integration/caddyfile_adapt_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package integration

import (
jsonMod "encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
"testing"
Expand Down Expand Up @@ -39,6 +42,10 @@ func TestCaddyfileAdaptToJSON(t *testing.T) {
// replace windows newlines in the json with unix newlines
json = winNewlines.ReplaceAllString(json, "\n")

// replace os-specific default path for file_server's hide field
replacePath, _ := jsonMod.Marshal(fmt.Sprint(".", string(filepath.Separator), "Caddyfile"))
json = strings.ReplaceAll(json, `"./Caddyfile"`, string(replacePath))

// run the test
ok := caddytest.CompareAdapt(t, filename, caddyfile, "caddyfile", json)
if !ok {
Expand Down
31 changes: 31 additions & 0 deletions modules/caddyhttp/encode/brotli/brotli_precompressed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package caddybrotli

import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
)

func init() {
caddy.RegisterModule(BrotliPrecompressed{})
}

// BrotliPrecompressed provides the file extension for files precompressed with brotli encoding.
type BrotliPrecompressed struct{}

// CaddyModule returns the Caddy module information.
func (BrotliPrecompressed) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.precompressed.br",
New: func() caddy.Module { return new(BrotliPrecompressed) },
}
}

// AcceptEncoding returns the name of the encoding as
// used in the Accept-Encoding request headers.
func (BrotliPrecompressed) AcceptEncoding() string { return "br" }

// Suffix returns the filename suffix of precompressed files.
func (BrotliPrecompressed) Suffix() string { return ".br" }

// Interface guards
var _ encode.Precompressed = (*BrotliPrecompressed)(nil)
139 changes: 122 additions & 17 deletions modules/caddyhttp/encode/caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package encode

import (
"fmt"
"net/http"
"strconv"
"strings"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
Expand All @@ -40,21 +42,31 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// encode [<matcher>] <formats...> {
// gzip [<level>]
// gzip [<level>]
// zstd
// minimum_length <length>
// prefer <formats...>
// # response matcher block
// match {
// status <code...>
// header <field> [<value>]
// }
// # or response matcher single line syntax
// match [header <field> [<value>]] | [status <code...>]
// }
//
// 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)
Expand All @@ -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 <field> [<value>]
// status <code...>
// }
//
// Or, single line syntax:
//
// match [header <field> [<value>]] | [status <code...>]
//
func (enc *Encode) parseNamedResponseMatcher(d *caddyfile.Dispenser, matchers map[string]caddyhttp.ResponseMatcher) error {
francislavoie marked this conversation as resolved.
Show resolved Hide resolved
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)
Loading