diff --git a/admin_test.go b/admin_test.go index e3d235b66..c637f92a9 100644 --- a/admin_test.go +++ b/admin_test.go @@ -19,6 +19,7 @@ import ( "crypto/x509" "encoding/json" "fmt" + "maps" "net/http" "net/http/httptest" "reflect" @@ -335,9 +336,7 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) { func testGetMetricValue(labels map[string]string) float64 { promLabels := prometheus.Labels{} - for k, v := range labels { - promLabels[k] = v - } + maps.Copy(promLabels, labels) metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels) if err != nil { @@ -377,9 +376,7 @@ func (m *mockModule) CaddyModule() ModuleInfo { func TestNewAdminHandlerRouterRegistration(t *testing.T) { originalModules := make(map[string]ModuleInfo) - for k, v := range modules { - originalModules[k] = v - } + maps.Copy(originalModules, modules) defer func() { modules = originalModules }() @@ -479,9 +476,7 @@ func TestAdminRouterProvisioning(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { originalModules := make(map[string]ModuleInfo) - for k, v := range modules { - originalModules[k] = v - } + maps.Copy(originalModules, modules) defer func() { modules = originalModules }() @@ -774,9 +769,7 @@ func (m *mockIssuerModule) CaddyModule() ModuleInfo { func TestManageIdentity(t *testing.T) { originalModules := make(map[string]ModuleInfo) - for k, v := range modules { - originalModules[k] = v - } + maps.Copy(originalModules, modules) defer func() { modules = originalModules }() diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index f0687a7e9..3fe27bfff 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -16,6 +16,7 @@ package httpcaddyfile import ( "encoding/json" + "maps" "net" "slices" "sort" @@ -365,9 +366,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) { // copy existing matcher definitions so we can augment // new ones that are defined only in this scope matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs)) - for key, val := range h.matcherDefs { - matcherDefs[key] = val - } + maps.Copy(matcherDefs, h.matcherDefs) // find and extract any embedded matcher definitions in this scope for i := 0; i < len(segments); i++ { @@ -483,12 +482,29 @@ func sortRoutes(routes []ConfigValue) { // we can only confidently compare path lengths if both // directives have a single path to match (issue #5037) if iPathLen > 0 && jPathLen > 0 { + // trim the trailing wildcard if there is one + iPathTrimmed := strings.TrimSuffix(iPM[0], "*") + jPathTrimmed := strings.TrimSuffix(jPM[0], "*") + // if both paths are the same except for a trailing wildcard, // sort by the shorter path first (which is more specific) - if strings.TrimSuffix(iPM[0], "*") == strings.TrimSuffix(jPM[0], "*") { + if iPathTrimmed == jPathTrimmed { return iPathLen < jPathLen } + // we use the trimmed length to compare the paths + // https://github.com/caddyserver/caddy/issues/7012#issuecomment-2870142195 + // credit to https://github.com/Hellio404 + // for sorts with many items, mixing matchers w/ and w/o wildcards will confuse the sort and result in incorrect orders + iPathLen = len(iPathTrimmed) + jPathLen = len(jPathTrimmed) + + // if both paths have the same length, sort lexically + // https://github.com/caddyserver/caddy/pull/7015#issuecomment-2871993588 + if iPathLen == jPathLen { + return iPathTrimmed < jPathTrimmed + } + // sort most-specific (longest) path first return iPathLen > jPathLen } diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 5127c0f90..1660e0f6f 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -24,6 +24,7 @@ import ( "io" "io/fs" "log" + "maps" "net" "net/http" "os" @@ -703,9 +704,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io if body != nil { req.Header.Set("Content-Type", "application/json") } - for k, v := range headers { - req.Header[k] = v - } + maps.Copy(req.Header, headers) // make an HTTP client that dials our network type, since admin // endpoints aren't always TCP, which is what the default transport diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 1072d1878..5c2ea7018 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -300,8 +300,10 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c info, err := fs.Stat(fileSystem, filename) if err != nil { err = fsrv.mapDirOpenError(fileSystem, err, filename) - if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) { + if errors.Is(err, fs.ErrNotExist) { return fsrv.notFound(w, r, next) + } else if errors.Is(err, fs.ErrInvalid) { + return caddyhttp.Error(http.StatusBadRequest, err) } else if errors.Is(err, fs.ErrPermission) { return caddyhttp.Error(http.StatusForbidden, err) } @@ -611,6 +613,11 @@ func (fsrv *FileServer) mapDirOpenError(fileSystem fs.FS, originalErr error, nam return originalErr } + var pathErr *fs.PathError + if errors.As(originalErr, &pathErr) { + return fs.ErrInvalid + } + parts := strings.Split(name, separator) for i := range parts { if parts[i] == "" { diff --git a/modules/caddyhttp/intercept/intercept.go b/modules/caddyhttp/intercept/intercept.go index 29889dcc0..cb23adf0a 100644 --- a/modules/caddyhttp/intercept/intercept.go +++ b/modules/caddyhttp/intercept/intercept.go @@ -118,6 +118,11 @@ func (irh interceptedResponseHandler) WriteHeader(statusCode int) { irh.ResponseRecorder.WriteHeader(statusCode) } +// EXPERIMENTAL: Subject to change or removal. +func (irh interceptedResponseHandler) Unwrap() http.ResponseWriter { + return irh.ResponseRecorder +} + // EXPERIMENTAL: Subject to change or removal. func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { buf := bufPool.Get().(*bytes.Buffer) diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go index 5db73a4a2..2325af9a7 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go @@ -17,6 +17,7 @@ package fastcgi import ( "encoding/json" "net/http" + "slices" "strconv" "strings" @@ -314,7 +315,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error // if the index is turned off, we skip the redirect and try_files if indexFile != "off" { - dirRedir := false + var dirRedir bool dirIndex := "{http.request.uri.path}/" + indexFile tryPolicy := "first_exist_fallback" @@ -328,13 +329,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error tryPolicy = "" } - for _, tf := range tryFiles { - if tf == dirIndex { - dirRedir = true - - break - } - } + dirRedir = slices.Contains(tryFiles, dirIndex) } if dirRedir { diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 23af0b3f9..24407df2b 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -528,13 +528,7 @@ func (h *HTTPTransport) shouldUseTLS(req *http.Request) bool { } port := req.URL.Port() - for i := range h.TLS.ExceptPorts { - if h.TLS.ExceptPorts[i] == port { - return false - } - } - - return true + return !slices.Contains(h.TLS.ExceptPorts, port) } // TLSEnabled returns true if TLS is enabled. diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index 1b93ede4b..12108ac03 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -22,6 +22,7 @@ import ( "net/http" "net/textproto" "os" + "slices" "strconv" "strings" "text/template" @@ -323,13 +324,7 @@ func cmdRespond(fl caddycmd.Flags) (int, error) { // figure out if status code was explicitly specified; this lets // us set a non-zero value as the default but is a little hacky - var statusCodeFlagSpecified bool - for _, fl := range os.Args { - if fl == "--status" { - statusCodeFlagSpecified = true - break - } - } + statusCodeFlagSpecified := slices.Contains(os.Args, "--status") // try to determine what kind of parameter the unnamed argument is if arg != "" { diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go index a561e3a1d..ac210d3b6 100644 --- a/modules/caddytls/certselection.go +++ b/modules/caddytls/certselection.go @@ -87,13 +87,7 @@ nextChoice: } if len(p.AnyTag) > 0 { - var found bool - for _, tag := range p.AnyTag { - if cert.HasTag(tag) { - found = true - break - } - } + found := slices.ContainsFunc(p.AnyTag, cert.HasTag) if !found { continue } diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 7c8436bc9..2f4dcf3c5 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -25,6 +25,7 @@ import ( "io" "os" "reflect" + "slices" "strings" "github.com/mholt/acmez/v3" @@ -369,13 +370,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { } // ensure ALPN includes the ACME TLS-ALPN protocol - var alpnFound bool - for _, a := range p.ALPN { - if a == acmez.ACMETLS1Protocol { - alpnFound = true - break - } - } + alpnFound := slices.Contains(p.ALPN, acmez.ACMETLS1Protocol) if !alpnFound && (cfg.NextProtos == nil || len(cfg.NextProtos) > 0) { cfg.NextProtos = append(cfg.NextProtos, acmez.ACMETLS1Protocol) } @@ -1004,10 +999,8 @@ func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x5 return fmt.Errorf("can't parse the given certificate: %s", err.Error()) } - for _, trustedLeafCert := range l.trustedLeafCerts { - if remoteLeafCert.Equal(trustedLeafCert) { - return nil - } + if slices.ContainsFunc(l.trustedLeafCerts, remoteLeafCert.Equal) { + return nil } return fmt.Errorf("client leaf certificate failed validation")