diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cae0b9840..0c6846fc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: env: CGO_ENABLED: 0 run: | - go build -tags nobdger -trimpath -ldflags="-w -s" -v + go build -tags nobadger -trimpath -ldflags="-w -s" -v - name: Smoke test Caddy working-directory: ./cmd/caddy @@ -150,6 +150,7 @@ jobs: uses: actions/checkout@v4 - name: Run Tests run: | + set +e mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa # short sha is enough? diff --git a/.golangci.yml b/.golangci.yml index d144395db..74e3503c4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,9 @@ linters-settings: errcheck: - ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.* - ignoretests: true + exclude-functions: + - fmt.* + - (go.uber.org/zap/zapcore.ObjectEncoder).AddObject + - (go.uber.org/zap/zapcore.ObjectEncoder).AddArray gci: sections: - standard # Standard section: captures all standard packages. @@ -130,13 +132,14 @@ linters: run: # default concurrency is a available CPU number. # concurrency: 4 # explicitly omit this value to fully utilize available resources. - deadline: 5m + timeout: 5m issues-exit-code: 1 tests: false # output configuration options output: - format: 'colored-line-number' + formats: + - format: 'colored-line-number' print-issued-lines: true print-linter-name: true @@ -166,3 +169,6 @@ issues: - path: modules/logging/filters.go linters: - dupl + - path: _test\.go + linters: + - errcheck diff --git a/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest new file mode 100644 index 000000000..62bfd0cba --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest @@ -0,0 +1,36 @@ +:80 + +file_server browse { + sort size desc +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "handle": [ + { + "browse": {}, + "handler": "file_server", + "hide": [ + "./Caddyfile" + ], + "sort": [ + "size", + "desc" + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest new file mode 100644 index 000000000..ae5a6791e --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest @@ -0,0 +1,40 @@ +:8884 + +reverse_proxy 127.0.0.1:65535 { + health_uri /health + health_request_body "test body" +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "health_checks": { + "active": { + "body": "test body", + "uri": "/health" + } + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt b/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt new file mode 100644 index 000000000..75d7bfb87 --- /dev/null +++ b/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt @@ -0,0 +1,2 @@ +foo + diff --git a/caddytest/integration/testdata/foo_with_trailing_newline.txt b/caddytest/integration/testdata/foo_with_trailing_newline.txt new file mode 100644 index 000000000..257cc5642 --- /dev/null +++ b/caddytest/integration/testdata/foo_with_trailing_newline.txt @@ -0,0 +1 @@ +foo diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 746cf3da6..49d0321ef 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -74,6 +74,10 @@ func cmdStart(fl Flags) (int, error) { // sure by giving it some random bytes and having it echo // them back to us) cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) + // we should be able to run caddy in relative paths + if errors.Is(cmd.Err, exec.ErrDot) { + cmd.Err = nil + } if configFlag != "" { cmd.Args = append(cmd.Args, "--config", configFlag) } diff --git a/go.mod b/go.mod index f5559a8d9..e6dd928b9 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.7 github.com/mholt/acmez/v2 v2.0.1 github.com/prometheus/client_golang v1.19.1 - github.com/quic-go/quic-go v0.44.0 + github.com/quic-go/quic-go v0.46.0 github.com/smallstep/certificates v0.26.1 github.com/smallstep/nosql v0.6.1 github.com/smallstep/truststore v0.13.0 @@ -37,11 +37,11 @@ require ( go.uber.org/automaxprocs v1.5.3 go.uber.org/zap v1.27.0 go.uber.org/zap/exp v0.2.0 - golang.org/x/crypto v0.23.0 + golang.org/x/crypto v0.26.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 - golang.org/x/net v0.25.0 - golang.org/x/sync v0.7.0 - golang.org/x/term v0.20.0 + golang.org/x/net v0.28.0 + golang.org/x/sync v0.8.0 + golang.org/x/term v0.23.0 golang.org/x/time v0.5.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -123,7 +123,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pires/go-proxyproto v0.7.0 + github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect @@ -147,9 +147,9 @@ require ( go.step.sm/linkedca v0.20.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/sys v0.20.0 - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/sys v0.23.0 + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.34.1 // indirect howett.net/plist v1.0.0 // indirect diff --git a/go.sum b/go.sum index 63306efc1..5bf39b424 100644 --- a/go.sum +++ b/go.sum @@ -320,8 +320,8 @@ github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8P github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= -github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= -github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= +github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 h1:ct/vxNBgHpASQ4sT8NaBX9LtsEtluZqaUJydLG50U3E= +github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -339,8 +339,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0= -github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek= +github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y= +github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -510,8 +510,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw= golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= @@ -532,15 +532,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -567,8 +567,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -576,8 +576,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -588,8 +588,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -603,8 +603,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/ranges.go b/internal/ranges.go new file mode 100644 index 000000000..e9429e263 --- /dev/null +++ b/internal/ranges.go @@ -0,0 +1,14 @@ +package internal + +// PrivateRangesCIDR returns a list of private CIDR range +// strings, which can be used as a configuration shortcut. +func PrivateRangesCIDR() []string { + return []string{ + "192.168.0.0/16", + "172.16.0.0/12", + "10.0.0.0/8", + "127.0.0.1/8", + "fd00::/8", + "::1", + } +} diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index cf3d17b69..00e507277 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -266,6 +266,14 @@ func (rw *responseWriter) FlushError() error { // to rw.Write (see bug in #4314) return nil } + // also flushes the encoder, if any + // see: https://github.com/jjiang-stripe/caddy-slow-gzip + if rw.w != nil { + err := rw.w.Flush() + if err != nil { + return err + } + } //nolint:bodyclose return http.NewResponseController(rw.ResponseWriter).Flush() } @@ -475,6 +483,7 @@ type encodingPreference struct { type Encoder interface { io.WriteCloser Reset(io.Writer) + Flush() error // encoder by default buffers data to maximize compressing rate } // Encoding is a type which can create encoders of its kind diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go index 8b2b48e77..bd02c584f 100644 --- a/modules/caddyhttp/fileserver/browse.go +++ b/modules/caddyhttp/fileserver/browse.go @@ -206,11 +206,34 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs // browseApplyQueryParams applies query parameters to the listing. // It mutates the listing and may set cookies. func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseTemplateContext) { + var orderParam, sortParam string + + // The configs in Caddyfile have lower priority than Query params, + // so put it at first. + for idx, item := range fsrv.SortOptions { + // Only `sort` & `order`, 2 params are allowed + if idx >= 2 { + break + } + switch item { + case sortByName, sortByNameDirFirst, sortBySize, sortByTime: + sortParam = item + case sortOrderAsc, sortOrderDesc: + orderParam = item + } + } + layoutParam := r.URL.Query().Get("layout") - sortParam := r.URL.Query().Get("sort") - orderParam := r.URL.Query().Get("order") limitParam := r.URL.Query().Get("limit") offsetParam := r.URL.Query().Get("offset") + sortParamTmp := r.URL.Query().Get("sort") + if sortParamTmp != "" { + sortParam = sortParamTmp + } + orderParamTmp := r.URL.Query().Get("order") + if orderParamTmp != "" { + orderParam = orderParamTmp + } switch layoutParam { case "list", "grid", "": @@ -233,11 +256,11 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re // then figure out the order switch orderParam { case "": - orderParam = "asc" + orderParam = sortOrderAsc if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { orderParam = orderCookie.Value } - case "asc", "desc": + case sortOrderAsc, sortOrderDesc: http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil}) } diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go index 81e260c71..0251bc581 100644 --- a/modules/caddyhttp/fileserver/browsetplcontext.go +++ b/modules/caddyhttp/fileserver/browsetplcontext.go @@ -373,4 +373,7 @@ const ( sortByNameDirFirst = "namedirfirst" sortBySize = "size" sortByTime = "time" + + sortOrderAsc = "asc" + sortOrderDesc = "desc" ) diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go index f65695018..603a828c9 100644 --- a/modules/caddyhttp/fileserver/caddyfile.go +++ b/modules/caddyhttp/fileserver/caddyfile.go @@ -171,6 +171,17 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } fsrv.EtagFileExtensions = etagFileExtensions + case "sort": + for d.NextArg() { + dVal := d.Val() + switch dVal { + case sortByName, sortBySize, sortByTime, sortOrderAsc, sortOrderDesc: + fsrv.SortOptions = append(fsrv.SortOptions, dVal) + default: + return d.Errf("unknown sort option '%s'", dVal) + } + } + default: return d.Errf("unknown subdirective '%s'", d.Val()) } diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 3d7032804..48812cfec 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -153,6 +153,16 @@ type FileServer struct { // a 404 error. By default, this is false (disabled). PassThru bool `json:"pass_thru,omitempty"` + // Override the default sort. + // It includes the following options: + // - sort_by: name(default), namedirfirst, size, time + // - order: asc(default), desc + // eg.: + // - `sort time desc` will sort by time in descending order + // - `sort size` will sort by size in ascending order + // The first option must be `sort_by` and the second option must be `order` (if exists). + SortOptions []string `json:"sort,omitempty"` + // Selection of encoders to use to check for precompressed files. PrecompressedRaw caddy.ModuleMap `json:"precompressed,omitempty" caddy:"namespace=http.precompressed"` @@ -236,6 +246,22 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error { fsrv.precompressors[ae] = p } + // check sort options + for idx, sortOption := range fsrv.SortOptions { + switch idx { + case 0: + if sortOption != sortByName && sortOption != sortByNameDirFirst && sortOption != sortBySize && sortOption != sortByTime { + return fmt.Errorf("the first option must be one of the following: %s, %s, %s, %s, but got %s", sortByName, sortByNameDirFirst, sortBySize, sortByTime, sortOption) + } + case 1: + if sortOption != sortOrderAsc && sortOption != sortOrderDesc { + return fmt.Errorf("the second option must be one of the following: %s, %s, but got %s", sortOrderAsc, sortOrderDesc, sortOption) + } + default: + return fmt.Errorf("only max 2 sort options are allowed, but got %d", idx+1) + } + } + return nil } diff --git a/modules/caddyhttp/ip_matchers.go b/modules/caddyhttp/ip_matchers.go index 9101a0357..2e735cb69 100644 --- a/modules/caddyhttp/ip_matchers.go +++ b/modules/caddyhttp/ip_matchers.go @@ -29,6 +29,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/internal" ) // MatchRemoteIP matches requests by the remote IP address, @@ -79,7 +80,7 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.Err("the 'forwarded' option is no longer supported; use the 'client_ip' matcher instead") } if d.Val() == "private_ranges" { - m.Ranges = append(m.Ranges, PrivateRangesCIDR()...) + m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...) continue } m.Ranges = append(m.Ranges, d.Val()) @@ -173,7 +174,7 @@ func (m *MatchClientIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { for d.NextArg() { if d.Val() == "private_ranges" { - m.Ranges = append(m.Ranges, PrivateRangesCIDR()...) + m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...) continue } m.Ranges = append(m.Ranges, d.Val()) @@ -250,7 +251,9 @@ func (m MatchClientIP) Match(r *http.Request) bool { func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) { cidrs := []*netip.Prefix{} zones := []string{} + repl := caddy.NewReplacer() for _, str := range ranges { + str = repl.ReplaceAll(str, "") // Exclude the zone_id from the IP if strings.Contains(str, "%") { split := strings.Split(str, "%") diff --git a/modules/caddyhttp/ip_range.go b/modules/caddyhttp/ip_range.go index b1db25475..bfd76c14c 100644 --- a/modules/caddyhttp/ip_range.go +++ b/modules/caddyhttp/ip_range.go @@ -22,6 +22,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/internal" ) func init() { @@ -92,7 +93,7 @@ func (m *StaticIPRange) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } for d.NextArg() { if d.Val() == "private_ranges" { - m.Ranges = append(m.Ranges, PrivateRangesCIDR()...) + m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...) continue } m.Ranges = append(m.Ranges, d.Val()) @@ -121,22 +122,16 @@ func CIDRExpressionToPrefix(expr string) (netip.Prefix, error) { return prefix, nil } -// PrivateRangesCIDR returns a list of private CIDR range -// strings, which can be used as a configuration shortcut. -func PrivateRangesCIDR() []string { - return []string{ - "192.168.0.0/16", - "172.16.0.0/12", - "10.0.0.0/8", - "127.0.0.1/8", - "fd00::/8", - "::1", - } -} - // Interface guards var ( _ caddy.Provisioner = (*StaticIPRange)(nil) _ caddyfile.Unmarshaler = (*StaticIPRange)(nil) _ IPRangeSource = (*StaticIPRange)(nil) ) + +// PrivateRangesCIDR returns a list of private CIDR range +// strings, which can be used as a configuration shortcut. +// Note: this function is used at least by mholt/caddy-l4. +func PrivateRangesCIDR() []string { + return internal.PrivateRangesCIDR() +} diff --git a/modules/caddyhttp/proxyprotocol/listenerwrapper.go b/modules/caddyhttp/proxyprotocol/listenerwrapper.go index e0d9b86ce..e25fe02a6 100644 --- a/modules/caddyhttp/proxyprotocol/listenerwrapper.go +++ b/modules/caddyhttp/proxyprotocol/listenerwrapper.go @@ -50,7 +50,7 @@ type ListenerWrapper struct { // Policy definitions are here: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy FallbackPolicy Policy `json:"fallback_policy,omitempty"` - policy goproxy.PolicyFunc + policy goproxy.ConnPolicyFunc } // Provision sets up the listener wrapper. @@ -69,13 +69,14 @@ func (pp *ListenerWrapper) Provision(ctx caddy.Context) error { } pp.deny = append(pp.deny, ipnet) } - pp.policy = func(upstream net.Addr) (goproxy.Policy, error) { + + pp.policy = func(options goproxy.ConnPolicyOptions) (goproxy.Policy, error) { // trust unix sockets - if network := upstream.Network(); caddy.IsUnixNetwork(network) { + if network := options.Upstream.Network(); caddy.IsUnixNetwork(network) { return goproxy.USE, nil } ret := pp.FallbackPolicy - host, _, err := net.SplitHostPort(upstream.String()) + host, _, err := net.SplitHostPort(options.Upstream.String()) if err != nil { return goproxy.REJECT, err } @@ -106,6 +107,6 @@ func (pp *ListenerWrapper) WrapListener(l net.Listener) net.Listener { Listener: l, ReadHeaderTimeout: time.Duration(pp.Timeout), } - pl.Policy = pp.policy + pl.ConnPolicy = pp.policy return pl } diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 1c3b49447..cd0e5d949 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -28,6 +28,7 @@ import ( "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/internal" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" @@ -68,19 +69,20 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // lb_retry_match // // # active health checking -// health_uri -// health_port -// health_interval -// health_passes -// health_fails -// health_timeout -// health_status -// health_body +// health_uri +// health_port +// health_interval +// health_passes +// health_fails +// health_timeout +// health_status +// health_body +// health_method +// health_request_body // health_follow_redirects // health_headers { // [] // } -// health_method // // # passive health checking // fail_duration @@ -424,6 +426,18 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } h.HealthChecks.Active.Method = d.Val() + case "health_request_body": + if !d.NextArg() { + return d.ArgErr() + } + if h.HealthChecks == nil { + h.HealthChecks = new(HealthChecks) + } + if h.HealthChecks.Active == nil { + h.HealthChecks.Active = new(ActiveHealthChecks) + } + h.HealthChecks.Active.Body = d.Val() + case "health_interval": if !d.NextArg() { return d.ArgErr() @@ -688,7 +702,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { case "trusted_proxies": for d.NextArg() { if d.Val() == "private_ranges" { - h.TrustedProxies = append(h.TrustedProxies, caddyhttp.PrivateRangesCIDR()...) + h.TrustedProxies = append(h.TrustedProxies, internal.PrivateRangesCIDR()...) continue } h.TrustedProxies = append(h.TrustedProxies, d.Val()) diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index 3b5a6a3af..efa1dbf09 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -24,6 +24,7 @@ import ( "regexp" "runtime/debug" "strconv" + "strings" "time" "go.uber.org/zap" @@ -93,6 +94,9 @@ type ActiveHealthChecks struct { // The HTTP method to use for health checks (default "GET"). Method string `json:"method,omitempty"` + // The body to send with the health check request. + Body string `json:"body,omitempty"` + // Whether to follow HTTP redirects in response to active health checks (default off). FollowRedirects bool `json:"follow_redirects,omitempty"` @@ -396,6 +400,16 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ u.Path = h.HealthChecks.Active.Path } + // replacer used for both body and headers. Only globals (env vars, system info, etc.) are available + repl := caddy.NewReplacer() + + // if body is provided, create a reader for it, otherwise nil + var requestBody io.Reader + if h.HealthChecks.Active.Body != "" { + // set body, using replacer + requestBody = strings.NewReader(repl.ReplaceAll(h.HealthChecks.Active.Body, "")) + } + // attach dialing information to this request, as well as context values that // may be expected by handlers of this request ctx := h.ctx.Context @@ -403,15 +417,14 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{ dialInfoVarKey: dialInfo, }) - req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), nil) + req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), requestBody) if err != nil { return fmt.Errorf("making request: %v", err) } ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req) req = req.WithContext(ctx) - // set headers, using a replacer with only globals (env vars, system info, etc.) - repl := caddy.NewReplacer() + // set headers, using replacer repl.Set("http.reverse_proxy.active.target_upstream", networkAddr) for key, vals := range h.HealthChecks.Active.Headers { key = repl.ReplaceAll(key, "") diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index d42453684..9a82341d0 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -446,6 +446,9 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { // if H2C ("HTTP/2 over cleartext") is enabled and the upstream request is // HTTP without TLS, use the alternate H2C-capable transport instead if req.URL.Scheme == "http" && h.h2cTransport != nil { + // There is no dedicated DisableKeepAlives field in *http2.Transport. + // This is an alternative way to disable keep-alive. + req.Close = h.Transport.DisableKeepAlives return h.h2cTransport.RoundTrip(req) } diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 4f97edead..1883ac072 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -979,7 +979,7 @@ func (h *Handler) finalizeResponse( // we'll just log the error and abort the stream here and panic just as // the standard lib's proxy to propagate the stream error. // see issue https://github.com/caddyserver/caddy/issues/5951 - logger.Error("aborting with incomplete response", zap.Error(err)) + logger.Warn("aborting with incomplete response", zap.Error(err)) // no extra logging from stdlib panic(http.ErrAbortHandler) } diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 96a819b40..6caaabcda 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -602,6 +602,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error QUICConfig: &quic.Config{ Versions: []quic.Version{quic.Version1, quic.Version2}, }, + IdleTimeout: time.Duration(s.IdleTimeout), ConnContext: func(ctx context.Context, c quic.Connection) context.Context { return context.WithValue(ctx, quicConnCtxKey, c) }, diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go index 1bef890aa..84ca2e118 100644 --- a/modules/caddytls/certselection.go +++ b/modules/caddytls/certselection.go @@ -22,6 +22,8 @@ import ( "math/big" "github.com/caddyserver/certmagic" + + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) // CustomCertSelectionPolicy represents a policy for selecting the certificate @@ -122,6 +124,79 @@ nextChoice: return certmagic.DefaultCertificateSelector(hello, viable) } +// UnmarshalCaddyfile sets up the CustomCertSelectionPolicy from Caddyfile tokens. Syntax: +// +// cert_selection { +// all_tags +// any_tag +// public_key_algorithm +// serial_number +// subject_organization +// } +func (p *CustomCertSelectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + _, wrapper := d.Next(), d.Val() // consume wrapper name + + // No same-line options are supported + if d.CountRemainingArgs() > 0 { + return d.ArgErr() + } + + var hasPublicKeyAlgorithm bool + for nesting := d.Nesting(); d.NextBlock(nesting); { + optionName := d.Val() + switch optionName { + case "all_tags": + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + p.AllTags = append(p.AllTags, d.RemainingArgs()...) + case "any_tag": + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + p.AnyTag = append(p.AnyTag, d.RemainingArgs()...) + case "public_key_algorithm": + if hasPublicKeyAlgorithm { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + d.NextArg() + if err := p.PublicKeyAlgorithm.UnmarshalJSON([]byte(d.Val())); err != nil { + return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err) + } + hasPublicKeyAlgorithm = true + case "serial_number": + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + for d.NextArg() { + val, bi := d.Val(), bigInt{} + _, ok := bi.SetString(val, 10) + if !ok { + return d.Errf("parsing %s option '%s': invalid big.int value %s", wrapper, optionName, val) + } + p.SerialNumber = append(p.SerialNumber, bi) + } + case "subject_organization": + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + p.SubjectOrganization = append(p.SubjectOrganization, d.RemainingArgs()...) + default: + return d.ArgErr() + } + + // No nested blocks are supported + if d.NextBlock(nesting + 1) { + return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName) + } + } + + return nil +} + // bigInt is a big.Int type that interops with JSON encodings as a string. type bigInt struct{ big.Int } @@ -144,3 +219,6 @@ func (bi *bigInt) UnmarshalJSON(p []byte) error { } return nil } + +// Interface guard +var _ caddyfile.Unmarshaler = (*CustomCertSelectionPolicy)(nil) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index c9705ffdc..4ec0e673a 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -363,6 +363,136 @@ func (p ConnectionPolicy) SettingsEmpty() bool { p.InsecureSecretsLog == "" } +// UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax: +// +// connection_policy { +// alpn +// cert_selection { +// ... +// } +// ciphers +// client_auth { +// ... +// } +// curves +// default_sni +// match { +// ... +// } +// protocols [] +// # EXPERIMENTAL: +// drop +// fallback_sni +// insecure_secrets_log +// } +func (cp *ConnectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + _, wrapper := d.Next(), d.Val() + + // No same-line options are supported + if d.CountRemainingArgs() > 0 { + return d.ArgErr() + } + + var hasCertSelection, hasClientAuth, hasDefaultSNI, hasDrop, + hasFallbackSNI, hasInsecureSecretsLog, hasMatch, hasProtocols bool + for nesting := d.Nesting(); d.NextBlock(nesting); { + optionName := d.Val() + switch optionName { + case "alpn": + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + cp.ALPN = append(cp.ALPN, d.RemainingArgs()...) + case "cert_selection": + if hasCertSelection { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + p := &CustomCertSelectionPolicy{} + if err := p.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil { + return err + } + cp.CertSelection, hasCertSelection = p, true + case "client_auth": + if hasClientAuth { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + ca := &ClientAuthentication{} + if err := ca.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil { + return err + } + cp.ClientAuthentication, hasClientAuth = ca, true + case "ciphers": + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + cp.CipherSuites = append(cp.CipherSuites, d.RemainingArgs()...) + case "curves": + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + cp.Curves = append(cp.Curves, d.RemainingArgs()...) + case "default_sni": + if hasDefaultSNI { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + _, cp.DefaultSNI, hasDefaultSNI = d.NextArg(), d.Val(), true + case "drop": // EXPERIMENTAL + if hasDrop { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + cp.Drop, hasDrop = true, true + case "fallback_sni": // EXPERIMENTAL + if hasFallbackSNI { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + _, cp.FallbackSNI, hasFallbackSNI = d.NextArg(), d.Val(), true + case "insecure_secrets_log": // EXPERIMENTAL + if hasInsecureSecretsLog { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + if d.CountRemainingArgs() != 1 { + return d.ArgErr() + } + _, cp.InsecureSecretsLog, hasInsecureSecretsLog = d.NextArg(), d.Val(), true + case "match": + if hasMatch { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + matcherSet, err := ParseCaddyfileNestedMatcherSet(d) + if err != nil { + return err + } + cp.MatchersRaw, hasMatch = matcherSet, true + case "protocols": + if hasProtocols { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 2 { + return d.ArgErr() + } + _, cp.ProtocolMin, hasProtocols = d.NextArg(), d.Val(), true + if d.NextArg() { + cp.ProtocolMax = d.Val() + } + default: + return d.ArgErr() + } + + // No nested blocks are supported + if d.NextBlock(nesting + 1) { + return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName) + } + } + + return nil +} + // ClientAuthentication configures TLS client auth. type ClientAuthentication struct { // Certificate authority module which provides the certificate pool of trusted certificates @@ -819,4 +949,46 @@ func (d destructableWriter) Destruct() error { return d.Close() } var secretsLogPool = caddy.NewUsagePool() -var _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil) +// Interface guards +var ( + _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil) + _ caddyfile.Unmarshaler = (*ConnectionPolicy)(nil) +) + +// ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested +// matcher set, and returns its raw module map value. +func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) { + matcherMap := make(map[string]ConnectionMatcher) + + tokensByMatcherName := make(map[string][]caddyfile.Token) + for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { + matcherName := d.Val() + tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...) + } + + for matcherName, tokens := range tokensByMatcherName { + dd := caddyfile.NewDispenser(tokens) + dd.Next() // consume wrapper name + + unm, err := caddyfile.UnmarshalModule(dd, "tls.handshake_match."+matcherName) + if err != nil { + return nil, err + } + cm, ok := unm.(ConnectionMatcher) + if !ok { + return nil, fmt.Errorf("matcher module '%s' is not a connection matcher", matcherName) + } + matcherMap[matcherName] = cm + } + + matcherSet := make(caddy.ModuleMap) + for name, matcher := range matcherMap { + jsonBytes, err := json.Marshal(matcher) + if err != nil { + return nil, fmt.Errorf("marshaling %T matcher: %v", matcher, err) + } + matcherSet[name] = jsonBytes + } + + return matcherSet, nil +} diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index b01fb8334..f94622374 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -25,6 +25,8 @@ import ( "go.uber.org/zap" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/internal" ) func init() { @@ -48,14 +50,47 @@ func (MatchServerName) CaddyModule() caddy.ModuleInfo { // Match matches hello based on SNI. func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { + repl := caddy.NewReplacer() + // caddytls.TestServerNameMatcher calls this function without any context + if ctx := hello.Context(); ctx != nil { + // In some situations the existing context may have no replacer + if replAny := ctx.Value(caddy.ReplacerCtxKey); replAny != nil { + repl = replAny.(*caddy.Replacer) + } + } + for _, name := range m { - if certmagic.MatchWildcard(hello.ServerName, name) { + rs := repl.ReplaceAll(name, "") + if certmagic.MatchWildcard(hello.ServerName, rs) { return true } } return false } +// UnmarshalCaddyfile sets up the MatchServerName from Caddyfile tokens. Syntax: +// +// sni +func (m *MatchServerName) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + wrapper := d.Val() + + // At least one same-line option must be provided + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + + *m = append(*m, d.RemainingArgs()...) + + // No blocks are supported + if d.NextBlock(d.Nesting()) { + return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper) + } + } + + return nil +} + // MatchRemoteIP matches based on the remote IP of the // connection. Specific IPs or CIDR ranges can be specified. // @@ -83,16 +118,19 @@ func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo { // Provision parses m's IP ranges, either from IP or CIDR expressions. func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { + repl := caddy.NewReplacer() m.logger = ctx.Logger() for _, str := range m.Ranges { - cidrs, err := m.parseIPRange(str) + rs := repl.ReplaceAll(str, "") + cidrs, err := m.parseIPRange(rs) if err != nil { return err } m.cidrs = append(m.cidrs, cidrs...) } for _, str := range m.NotRanges { - cidrs, err := m.parseIPRange(str) + rs := repl.ReplaceAll(str, "") + cidrs, err := m.parseIPRange(rs) if err != nil { return err } @@ -145,6 +183,46 @@ func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool { return false } +// UnmarshalCaddyfile sets up the MatchRemoteIP from Caddyfile tokens. Syntax: +// +// remote_ip +// +// Note: IPs and CIDRs prefixed with ! symbol are treated as not_ranges +func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + wrapper := d.Val() + + // At least one same-line option must be provided + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + + for d.NextArg() { + val := d.Val() + var exclamation bool + if len(val) > 1 && val[0] == '!' { + exclamation, val = true, val[1:] + } + ranges := []string{val} + if val == "private_ranges" { + ranges = internal.PrivateRangesCIDR() + } + if exclamation { + m.NotRanges = append(m.NotRanges, ranges...) + } else { + m.Ranges = append(m.Ranges, ranges...) + } + } + + // No blocks are supported + if d.NextBlock(d.Nesting()) { + return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper) + } + } + + return nil +} + // MatchLocalIP matches based on the IP address of the interface // receiving the connection. Specific IPs or CIDR ranges can be specified. type MatchLocalIP struct { @@ -165,9 +243,11 @@ func (MatchLocalIP) CaddyModule() caddy.ModuleInfo { // Provision parses m's IP ranges, either from IP or CIDR expressions. func (m *MatchLocalIP) Provision(ctx caddy.Context) error { + repl := caddy.NewReplacer() m.logger = ctx.Logger() for _, str := range m.Ranges { - cidrs, err := m.parseIPRange(str) + rs := repl.ReplaceAll(str, "") + cidrs, err := m.parseIPRange(rs) if err != nil { return err } @@ -219,6 +299,36 @@ func (MatchLocalIP) matches(ip netip.Addr, ranges []netip.Prefix) bool { return false } +// UnmarshalCaddyfile sets up the MatchLocalIP from Caddyfile tokens. Syntax: +// +// local_ip +func (m *MatchLocalIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + wrapper := d.Val() + + // At least one same-line option must be provided + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + + for d.NextArg() { + val := d.Val() + if val == "private_ranges" { + m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...) + continue + } + m.Ranges = append(m.Ranges, val) + } + + // No blocks are supported + if d.NextBlock(d.Nesting()) { + return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper) + } + } + + return nil +} + // Interface guards var ( _ ConnectionMatcher = (*MatchServerName)(nil) @@ -226,4 +336,8 @@ var ( _ caddy.Provisioner = (*MatchLocalIP)(nil) _ ConnectionMatcher = (*MatchLocalIP)(nil) + + _ caddyfile.Unmarshaler = (*MatchLocalIP)(nil) + _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil) + _ caddyfile.Unmarshaler = (*MatchServerName)(nil) ) diff --git a/replacer.go b/replacer.go index e5d2913e9..65815c92a 100644 --- a/replacer.go +++ b/replacer.go @@ -15,6 +15,7 @@ package caddy import ( + "bytes" "fmt" "io" "net/http" @@ -354,6 +355,8 @@ func (f fileReplacementProvider) replace(key string) (any, bool) { zap.Error(err)) return nil, true } + body = bytes.TrimSuffix(body, []byte("\n")) + body = bytes.TrimSuffix(body, []byte("\r")) return string(body), true } diff --git a/replacer_test.go b/replacer_test.go index cf4d321b6..1c1a7048f 100644 --- a/replacer_test.go +++ b/replacer_test.go @@ -431,6 +431,14 @@ func TestReplacerNew(t *testing.T) { variable: "file.caddytest/integration/testdata/foo.txt", value: "foo", }, + { + variable: "file.caddytest/integration/testdata/foo_with_trailing_newline.txt", + value: "foo", + }, + { + variable: "file.caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt", + value: "foo" + getEOL(), + }, } { if val, ok := repl.providers[1].replace(tc.variable); ok { if val != tc.value { @@ -442,6 +450,13 @@ func TestReplacerNew(t *testing.T) { } } +func getEOL() string { + if os.PathSeparator == '\\' { + return "\r\n" // Windows EOL + } + return "\n" // Unix and modern macOS EOL +} + func TestReplacerNewWithoutFile(t *testing.T) { repl := NewReplacer().WithoutFile()