diff --git a/caddytest/integration/listener_test.go b/caddytest/integration/listener_test.go new file mode 100644 index 00000000000..30642b1aed9 --- /dev/null +++ b/caddytest/integration/listener_test.go @@ -0,0 +1,94 @@ +package integration + +import ( + "bytes" + "fmt" + "math/rand" + "net" + "net/http" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2/caddytest" +) + +func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.Tester { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %s", err) + } + + mux := http.NewServeMux() + mux.Handle("/", handlerFunc) + srv := &http.Server{ + Handler: mux, + } + go srv.Serve(l) + t.Cleanup(func() { + _ = srv.Close() + _ = l.Close() + }) + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + local_certs + servers :9443 { + listener_wrappers { + http_redirect + tls + } + } + } + localhost { + reverse_proxy %s + } + `, l.Addr().String()), "caddyfile") + return tester +} + +func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) { + const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte + // 1 more than an MB + body := make([]byte, uploadSize) + rand.New(rand.NewSource(0)).Read(body) + + tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) { + buf := new(bytes.Buffer) + _, err := buf.ReadFrom(request.Body) + if err != nil { + t.Fatalf("failed to read body: %s", err) + } + + if !bytes.Equal(buf.Bytes(), body) { + t.Fatalf("body not the same") + } + + writer.WriteHeader(http.StatusNoContent) + }) + resp, err := tester.Client.Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body)) + if err != nil { + t.Fatalf("failed to post: %s", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Fatalf("unexpected status: %d != %d", resp.StatusCode, http.StatusNoContent) + } +} + +func TestLargeHttpRequest(t *testing.T) { + tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) { + t.Fatal("not supposed to handle a request") + }) + + // We never read the body in any way, set an extra long header instead. + req, _ := http.NewRequest("POST", "http://localhost:9443", nil) + req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024)) + _, err := tester.Client.Do(req) + if err == nil { + t.Fatal("not supposed to succeed") + } +} diff --git a/go.mod b/go.mod index 75b93f21e98..4a2c40c0f0b 100644 --- a/go.mod +++ b/go.mod @@ -29,9 +29,9 @@ require ( github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 - go.opentelemetry.io/otel v1.19.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 - go.opentelemetry.io/otel/sdk v1.19.0 + go.opentelemetry.io/otel v1.21.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 + go.opentelemetry.io/otel/sdk v1.21.0 go.uber.org/zap v1.25.0 golang.org/x/crypto v0.14.0 golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 @@ -89,7 +89,7 @@ require ( github.com/felixge/httpsnoop v1.0.3 // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect @@ -132,16 +132,16 @@ require ( github.com/urfave/cli v1.22.14 // indirect go.etcd.io/bbolt v1.3.7 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/trace v1.19.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.step.sm/cli-utils v0.8.0 // indirect go.step.sm/crypto v0.35.1 go.step.sm/linkedca v0.20.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.11.0 // indirect - golang.org/x/sys v0.13.0 + golang.org/x/sys v0.14.0 golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.10.0 // indirect google.golang.org/grpc v1.59.0 // indirect diff --git a/go.sum b/go.sum index 5a178c0d634..177d1ae0a3a 100644 --- a/go.sum +++ b/go.sum @@ -153,8 +153,8 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -200,7 +200,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/go-tpm-tools v0.4.1 h1:gYU6iwRo0tY3V6NDnS6m+XYog+b3g6YFhHQl3sYaUL4= @@ -602,18 +602,18 @@ go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 h1:Zbpbmwav32Ea5jSotpmkWE go.opentelemetry.io/contrib/propagators/jaeger v1.17.0/go.mod h1:tcTUAlmO8nuInPDSBVfG+CP6Mzjy5+gNV4mPxMbL0IA= go.opentelemetry.io/contrib/propagators/ot v1.17.0 h1:ufo2Vsz8l76eI47jFjuVyjyB3Ae2DmfiCV/o6Vc8ii0= go.opentelemetry.io/contrib/propagators/ot v1.17.0/go.mod h1:SbKPj5XGp8K/sGm05XblaIABgMgw2jDczP8gGeuaVLk= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= -go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.step.sm/cli-utils v0.8.0 h1:b/Tc1/m3YuQq+u3ghTFP7Dz5zUekZj6GUmd5pCvkEXQ= @@ -626,7 +626,7 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -741,8 +741,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.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= diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 6dc2ad365cc..46846d4f8a7 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -378,11 +378,7 @@ func (app *App) Start() error { return context.WithValue(ctx, ConnCtxKey, c) }, } - h2server := &http2.Server{ - NewWriteScheduler: func() http2.WriteScheduler { - return http2.NewPriorityWriteScheduler(nil) - }, - } + h2server := new(http2.Server) // disable HTTP/2, which we enabled by default during provisioning if !srv.protocol("h2") { diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html index 1c8be7f1700..e0e12969e99 100644 --- a/modules/caddyhttp/fileserver/browse.html +++ b/modules/caddyhttp/fileserver/browse.html @@ -31,7 +31,7 @@ {{- end}} - {{- else if .HasExt ".mp4" ".mov" ".mpeg" ".mpg" ".avi" ".ogg" ".webm" ".mkv" ".vob" ".gifv" ".3gp"}} + {{- else if .HasExt ".mp4" ".mov" ".m4v" ".mpeg" ".mpg" ".avi" ".ogg" ".webm" ".mkv" ".vob" ".gifv" ".3gp"}} diff --git a/modules/caddyhttp/httpredirectlistener.go b/modules/caddyhttp/httpredirectlistener.go index 082dc7ce8bb..ce9ac030875 100644 --- a/modules/caddyhttp/httpredirectlistener.go +++ b/modules/caddyhttp/httpredirectlistener.go @@ -16,11 +16,11 @@ package caddyhttp import ( "bufio" + "bytes" "fmt" "io" "net" "net/http" - "sync" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -86,15 +86,17 @@ func (l *httpRedirectListener) Accept() (net.Conn, error) { } return &httpRedirectConn{ - Conn: c, - r: bufio.NewReader(io.LimitReader(c, maxHeaderBytes)), + Conn: c, + limit: maxHeaderBytes, + r: bufio.NewReader(c), }, nil } type httpRedirectConn struct { net.Conn - once sync.Once - r *bufio.Reader + once bool + limit int64 + r *bufio.Reader } // Read tries to peek at the first few bytes of the request, and if we get @@ -102,53 +104,58 @@ type httpRedirectConn struct { // like an HTTP request, then we perform a HTTP->HTTPS redirect on the same // port as the original connection. func (c *httpRedirectConn) Read(p []byte) (int, error) { - var errReturn error - c.once.Do(func() { - firstBytes, err := c.r.Peek(5) - if err != nil { - return - } - - // If the request doesn't look like HTTP, then it's probably - // TLS bytes and we don't need to do anything. - if !firstBytesLookLikeHTTP(firstBytes) { - return - } - - // Parse the HTTP request, so we can get the Host and URL to redirect to. - req, err := http.ReadRequest(c.r) - if err != nil { - return - } - - // Build the redirect response, using the same Host and URL, - // but replacing the scheme with https. - headers := make(http.Header) - headers.Add("Location", "https://"+req.Host+req.URL.String()) - resp := &http.Response{ - Proto: "HTTP/1.0", - Status: "308 Permanent Redirect", - StatusCode: 308, - ProtoMajor: 1, - ProtoMinor: 0, - Header: headers, - } - - err = resp.Write(c.Conn) - if err != nil { - errReturn = fmt.Errorf("couldn't write HTTP->HTTPS redirect") - return - } - - errReturn = fmt.Errorf("redirected HTTP request on HTTPS port") - c.Conn.Close() - }) - - if errReturn != nil { - return 0, errReturn + if c.once { + return c.r.Read(p) } + // no need to use sync.Once - net.Conn is not read from concurrently. + c.once = true - return c.r.Read(p) + firstBytes, err := c.r.Peek(5) + if err != nil { + return 0, err + } + + // If the request doesn't look like HTTP, then it's probably + // TLS bytes, and we don't need to do anything. + if !firstBytesLookLikeHTTP(firstBytes) { + return c.r.Read(p) + } + + // From now on, we can be almost certain the request is HTTP. + // The returned error will be non nil and caller are expected to + // close the connection. + + // Set the read limit, io.MultiReader is needed because + // when resetting, *bufio.Reader discards buffered data. + buffered, _ := c.r.Peek(c.r.Buffered()) + mr := io.MultiReader(bytes.NewReader(buffered), c.Conn) + c.r.Reset(io.LimitReader(mr, c.limit)) + + // Parse the HTTP request, so we can get the Host and URL to redirect to. + req, err := http.ReadRequest(c.r) + if err != nil { + return 0, fmt.Errorf("couldn't read HTTP request") + } + + // Build the redirect response, using the same Host and URL, + // but replacing the scheme with https. + headers := make(http.Header) + headers.Add("Location", "https://"+req.Host+req.URL.String()) + resp := &http.Response{ + Proto: "HTTP/1.0", + Status: "308 Permanent Redirect", + StatusCode: 308, + ProtoMajor: 1, + ProtoMinor: 0, + Header: headers, + } + + err = resp.Write(c.Conn) + if err != nil { + return 0, fmt.Errorf("couldn't write HTTP->HTTPS redirect") + } + + return 0, fmt.Errorf("redirected HTTP request on HTTPS port") } // firstBytesLookLikeHTTP reports whether a TLS record header diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 08be40d62d0..1a76aef4c5b 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -962,10 +962,12 @@ func (h *Handler) finalizeResponse( if err != nil { // we're streaming the response and we've already written headers, so // there's nothing an error handler can do to recover at this point; - // the standard lib's proxy panics at this point, but we'll just log - // the error and abort the stream here + // 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)) - return nil + // no extra logging from stdlib + panic(http.ErrAbortHandler) } if len(res.Trailer) > 0 { diff --git a/modules/caddyhttp/templates/caddyfile.go b/modules/caddyhttp/templates/caddyfile.go index 06ca3e26096..c3039aa890e 100644 --- a/modules/caddyhttp/templates/caddyfile.go +++ b/modules/caddyhttp/templates/caddyfile.go @@ -15,6 +15,9 @@ package templates import ( + "github.com/caddyserver/caddy/v2" + "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/modules/caddyhttp" ) @@ -49,6 +52,29 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) if !h.Args(&t.FileRoot) { return nil, h.ArgErr() } + case "extensions": + if h.NextArg() { + return nil, h.ArgErr() + } + if t.ExtensionsRaw != nil { + return nil, h.Err("extensions already specified") + } + for nesting := h.Nesting(); h.NextBlock(nesting); { + extensionModuleName := h.Val() + modID := "http.handlers.templates.functions." + extensionModuleName + unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) + if err != nil { + return nil, err + } + cf, ok := unm.(CustomFunctions) + if !ok { + return nil, h.Errf("module %s (%T) does not provide template functions", modID, unm) + } + if t.ExtensionsRaw == nil { + t.ExtensionsRaw = make(caddy.ModuleMap) + } + t.ExtensionsRaw[extensionModuleName] = caddyconfig.JSON(cf, nil) + } } } } diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 4da02b580ca..418f09e531c 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -23,6 +23,8 @@ import ( "strings" "text/template" + "go.uber.org/zap" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) @@ -319,7 +321,12 @@ type Templates struct { // the opening and closing delimiters. Default: `["{{", "}}"]` Delimiters []string `json:"delimiters,omitempty"` + // Extensions adds functions to the template's func map. These often + // act as components on web pages, for example. + ExtensionsRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=http.handlers.templates.functions"` + customFuncs []template.FuncMap + logger *zap.Logger } // Customfunctions is the interface for registering custom template functions. @@ -338,17 +345,14 @@ func (Templates) CaddyModule() caddy.ModuleInfo { // Provision provisions t. func (t *Templates) Provision(ctx caddy.Context) error { - fnModInfos := caddy.GetModules("http.handlers.templates.functions") - customFuncs := make([]template.FuncMap, 0, len(fnModInfos)) - for _, modInfo := range fnModInfos { - mod := modInfo.New() - fnMod, ok := mod.(CustomFunctions) - if !ok { - return fmt.Errorf("module %q does not satisfy the CustomFunctions interface", modInfo.ID) - } - customFuncs = append(customFuncs, fnMod.CustomTemplateFunctions()) + t.logger = ctx.Logger() + mods, err := ctx.LoadModule(t, "ExtensionsRaw") + if err != nil { + return fmt.Errorf("loading template extensions: %v", err) + } + for _, modIface := range mods.(map[string]any) { + t.customFuncs = append(t.customFuncs, modIface.(CustomFunctions).CustomTemplateFunctions()) } - t.customFuncs = customFuncs if t.MIMETypes == nil { t.MIMETypes = defaultMIMETypes diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index 8b3d6bfc486..a66a0c3054c 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -23,6 +23,7 @@ import ( "net/http" "os" "path" + "reflect" "strconv" "strings" "sync" @@ -37,6 +38,7 @@ import ( "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" gmhtml "github.com/yuin/goldmark/renderer/html" + "go.uber.org/zap" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" @@ -57,7 +59,7 @@ type TemplateContext struct { // NewTemplate returns a new template intended to be evaluated with this // context, as it is initialized with configuration from this context. func (c *TemplateContext) NewTemplate(tplName string) *template.Template { - c.tpl = template.New(tplName) + c.tpl = template.New(tplName).Option("missingkey=zero") // customize delimiters, if applicable if c.config != nil && len(c.config.Delimiters) == 2 { @@ -88,6 +90,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template { "fileExists": c.funcFileExists, "httpError": c.funcHTTPError, "humanize": c.funcHumanize, + "maybe": c.funcMaybe, }) return c.tpl } @@ -492,6 +495,51 @@ func (c TemplateContext) funcHumanize(formatType, data string) (string, error) { return "", fmt.Errorf("no know function was given") } +// funcMaybe invokes the plugged-in function named functionName if it is plugged in +// (is a module in the 'http.handlers.templates.functions' namespace). If it is not +// available, a log message is emitted. +// +// The first argument is the function name, and the rest of the arguments are +// passed on to the actual function. +// +// This function is useful for executing templates that use components that may be +// considered as optional in some cases (like during local development) where you do +// not want to require everyone to have a custom Caddy build to be able to execute +// your template. +// +// NOTE: This function is EXPERIMENTAL and subject to change or removal. +func (c TemplateContext) funcMaybe(functionName string, args ...any) (any, error) { + for _, funcMap := range c.CustomFuncs { + if fn, ok := funcMap[functionName]; ok { + val := reflect.ValueOf(fn) + if val.Kind() != reflect.Func { + continue + } + argVals := make([]reflect.Value, len(args)) + for i, arg := range args { + argVals[i] = reflect.ValueOf(arg) + } + returnVals := val.Call(argVals) + switch len(returnVals) { + case 0: + return "", nil + case 1: + return returnVals[0].Interface(), nil + case 2: + var err error + if !returnVals[1].IsNil() { + err = returnVals[1].Interface().(error) + } + return returnVals[0].Interface(), err + default: + return nil, fmt.Errorf("maybe %s: invalid number of return values: %d", functionName, len(returnVals)) + } + } + } + c.config.logger.Named("maybe").Warn("template function could not be found; ignoring invocation", zap.String("name", functionName)) + return "", nil +} + // WrappedHeader wraps niladic functions so that they // can be used in templates. (Template functions must // return a value.) diff --git a/modules/caddytls/fileloader.go b/modules/caddytls/fileloader.go index 430932b999a..8603bbe652b 100644 --- a/modules/caddytls/fileloader.go +++ b/modules/caddytls/fileloader.go @@ -29,6 +29,26 @@ func init() { // FileLoader loads certificates and their associated keys from disk. type FileLoader []CertKeyFilePair +// Provision implements caddy.Provisioner. +func (fl FileLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, pair := range fl { + for i, tag := range pair.Tags { + pair.Tags[i] = repl.ReplaceKnown(tag, "") + } + fl[k] = CertKeyFilePair{ + Certificate: repl.ReplaceKnown(pair.Certificate, ""), + Key: repl.ReplaceKnown(pair.Key, ""), + Format: repl.ReplaceKnown(pair.Format, ""), + Tags: pair.Tags, + } + } + return nil +} + // CaddyModule returns the Caddy module information. func (FileLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ @@ -87,4 +107,7 @@ func (fl FileLoader) LoadCertificates() ([]Certificate, error) { } // Interface guard -var _ CertificateLoader = (FileLoader)(nil) +var ( + _ CertificateLoader = (FileLoader)(nil) + _ caddy.Provisioner = (FileLoader)(nil) +) diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go index 33b31a54af0..89e978df631 100644 --- a/modules/caddytls/folderloader.go +++ b/modules/caddytls/folderloader.go @@ -43,6 +43,18 @@ func (FolderLoader) CaddyModule() caddy.ModuleInfo { } } +// Provision implements caddy.Provisioner. +func (fl FolderLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, path := range fl { + fl[k] = repl.ReplaceKnown(path, "") + } + return nil +} + // LoadCertificates loads all the certificates+keys in the directories // listed in fl from all files ending with .pem. This method of loading // certificates expects the certificate and key to be bundled into the @@ -146,4 +158,7 @@ func tlsCertFromCertAndKeyPEMBundle(bundle []byte) (tls.Certificate, error) { return cert, nil } -var _ CertificateLoader = (FolderLoader)(nil) +var ( + _ CertificateLoader = (FolderLoader)(nil) + _ caddy.Provisioner = (FolderLoader)(nil) +) diff --git a/modules/caddytls/pemloader.go b/modules/caddytls/pemloader.go index 61b08851c8a..9c5ec17c936 100644 --- a/modules/caddytls/pemloader.go +++ b/modules/caddytls/pemloader.go @@ -30,6 +30,25 @@ func init() { // of not needing to store them on disk at all. type PEMLoader []CertKeyPEMPair +// Provision implements caddy.Provisioner. +func (pl PEMLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, pair := range pl { + for i, tag := range pair.Tags { + pair.Tags[i] = repl.ReplaceKnown(tag, "") + } + pl[k] = CertKeyPEMPair{ + CertificatePEM: repl.ReplaceKnown(pair.CertificatePEM, ""), + KeyPEM: repl.ReplaceKnown(pair.KeyPEM, ""), + Tags: pair.Tags, + } + } + return nil +} + // CaddyModule returns the Caddy module information. func (PEMLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ @@ -69,4 +88,7 @@ func (pl PEMLoader) LoadCertificates() ([]Certificate, error) { } // Interface guard -var _ CertificateLoader = (PEMLoader)(nil) +var ( + _ CertificateLoader = (PEMLoader)(nil) + _ caddy.Provisioner = (PEMLoader)(nil) +) diff --git a/modules/caddytls/storageloader.go b/modules/caddytls/storageloader.go index ddaaa51560c..f9f0e7e680f 100644 --- a/modules/caddytls/storageloader.go +++ b/modules/caddytls/storageloader.go @@ -52,6 +52,22 @@ func (StorageLoader) CaddyModule() caddy.ModuleInfo { func (sl *StorageLoader) Provision(ctx caddy.Context) error { sl.storage = ctx.Storage() sl.ctx = ctx + + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, pair := range sl.Pairs { + for i, tag := range pair.Tags { + pair.Tags[i] = repl.ReplaceKnown(tag, "") + } + sl.Pairs[k] = CertKeyFilePair{ + Certificate: repl.ReplaceKnown(pair.Certificate, ""), + Key: repl.ReplaceKnown(pair.Key, ""), + Format: repl.ReplaceKnown(pair.Format, ""), + Tags: pair.Tags, + } + } return nil }