Compare commits

..

1 Commits

Author SHA1 Message Date
Francis Lavoie e38228b5f1 reverseproxy: Test that WebSockets + unix-sockets works 2026-04-17 11:45:16 -04:00
94 changed files with 675 additions and 4345 deletions
+1 -3
View File
@@ -8,7 +8,7 @@ The Caddy project would like to make sure that it stays on top of all relevant a
| Version | Supported |
| ----------- | ----------|
| 2.latest | ✔️ |
| < 2.latest | :x: |
| <= 2.latest | :x: |
## Acceptable Scope
@@ -25,8 +25,6 @@ Client-side exploits are out of scope. In other words, it is not a bug in Caddy
Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
Many reports are not security bugs and can be addressed by updating the documentation.
We accept security reports and patches, but do not assign CVEs, for code that has not been released with a non-prerelease tag.
-217
View File
@@ -1,217 +0,0 @@
# Caddy Project Guidelines
## Mission
**Every site on HTTPS.** Caddy is a security-first, modular, extensible server platform.
## Code Style
### Go Idioms
Follow [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments):
- **Error flow**: Early return, indent error handling—not else blocks
```go
if err != nil {
return err
}
// normal code
```
- **Naming**: initialisms (`URL`, `HTTP`, `ID`—not `Url`, `Http`, `Id`)
- **Receiver names**: 12 letters reflecting type (`c` for `Client`, `h` for `Handler`)
- **Error strings**: Lowercase, no trailing punctuation (`"something failed"` not `"Something failed."`)
- **Doc comments**: Full sentences starting with the name being documented
```go
// Handler serves HTTP requests for the file server.
type Handler struct { ... }
```
- **Empty slices**: `var t []string` (nil slice), not `t := []string{}` (non-nil zero-length)
- **Don't panic**: Use error returns for normal error handling
### Caddy Patterns
**Module registration**:
```go
func init() {
caddy.RegisterModule(MyModule{})
}
func (MyModule) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "namespace.category.name",
New: func() caddy.Module { return new(MyModule) },
}
}
```
**Module lifecycle**: `New()` → JSON unmarshal → `Provision()` → `Validate()` → use → `Cleanup()`
**Interface guards** — compile-time verification that modules implement required interfaces:
```go
var (
_ caddy.Provisioner = (*MyModule)(nil)
_ caddy.Validator = (*MyModule)(nil)
_ caddyfile.Unmarshaler = (*MyModule)(nil)
)
```
**Structured logging** — use the module-scoped logger from context:
```go
func (m *MyModule) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger()
m.logger.Debug("provisioning", zap.String("field", m.Field))
return nil
}
```
**Caddyfile support** — implement `UnmarshalCaddyfile(*caddyfile.Dispenser)` using the `Dispenser` API:
```go
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
//
// directive [arg1] [arg2] {
// subdir value
// }
func (m *MyModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
for d.NextArg() {
// handle inline arguments
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "subdir":
if !d.NextArg() {
return d.ArgErr()
}
m.Field = d.Val()
default:
return d.Errf("unrecognized subdirective: %s", d.Val())
}
}
return nil
}
```
**Admin API**: Implement `caddy.AdminRouter` for custom endpoints.
**Context**: Use `caddy.Context` for accessing other apps/modules and logging—don't store contexts in structs.
## Architecture
Caddy is built around a **module system** where everything is a module registered via `caddy.RegisterModule()`:
- **Apps** (`caddy.App`): Top-level modules like `http`, `tls`, `pki` that Caddy loads and runs
- **Modules** (`caddy.Module`): Extensible components with namespaced IDs (e.g., `http.handlers.file_server`)
- **Configuration**: Native JSON with adapters (Caddyfile → JSON via `caddyconfig/httpcaddyfile`)
| Directory | Purpose |
|-----------|---------|
| `modules/` | All standard modules (HTTP, TLS, PKI, etc.) |
| `modules/standard/imports.go` | Standard module registry |
| `caddyconfig/httpcaddyfile/` | Caddyfile → JSON adapter for HTTP |
| `caddytest/` | Test utilities and integration tests |
| `cmd/caddy/` | CLI entry point with module imports |
### Critical Packages
`caddyhttp` and `caddytls` require **extra scrutiny** in code review—these are security-critical.
## Quality Gates
**All required before PR is merge-ready:**
| Gate | Command | Notes |
|------|---------|-------|
| Tests pass | `go test -race -short ./...` | Race detection enabled |
| Lint clean | `golangci-lint run --timeout 10m` | No warnings in changed files |
| Builds | `go build ./...` | Must compile |
| Benchmarks | `go test -bench=. -benchmem` | Required for optimizations |
CI runs tests on **Linux, macOS, and Windows**—ensure cross-platform compatibility.
### Build & Test
```bash
# Build
cd cmd/caddy && go build
# Tests with race detection (matches CI)
go test -race -short ./...
# Integration tests
go test ./caddytest/integration/...
# Lint (matches CI)
golangci-lint run --timeout 10m
```
## Testing Conventions
**Table-driven tests** (preferred pattern):
```go
func TestFeature(t *testing.T) {
for i, tc := range []struct {
input string
expected string
wantErr bool
}{
{input: "valid", expected: "result", wantErr: false},
{input: "invalid", expected: "", wantErr: true},
} {
actual, err := Function(tc.input)
if tc.wantErr && err == nil {
t.Errorf("Test %d: expected error but got none", i)
}
if !tc.wantErr && err != nil {
t.Errorf("Test %d: unexpected error: %v", i, err)
}
if actual != tc.expected {
t.Errorf("Test %d: expected %q, got %q", i, tc.expected, actual)
}
}
}
```
**Integration tests** use `caddytest.Tester`:
```go
func TestHTTPFeature(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
http_port 9080
}
localhost:9080 {
respond "hello"
}`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "hello")
}
```
Use non-standard ports (9080, 9443, 2999) to avoid conflicts with running servers.
## AI Contribution Policy
Per [CONTRIBUTING.md](.github/CONTRIBUTING.md), AI-assisted code **MUST** be:
1. **Disclosed** — Tell reviewers when code was AI-generated or AI-assisted, mentioning which agent/model is used
2. **Fully comprehended** — You must be able to explain every line
3. **Tested** — Automated tests when feasible, thorough manual tests otherwise
4. **Licensed** — Verify AI output doesn't include plagiarized or incompatibly-licensed code
5. **Contributor License Agreement (CLA)** — The CLA must be signed by the human user
**Do NOT submit code you cannot fully explain.** Contributors are responsible for their submissions.
## Dependencies
- **Avoid new dependencies** — Justify any additions; tiny deps can be inlined
- **No exported dependency types** — Caddy must not export types defined by external packages
- Use Go modules; check with `go mod tidy`
## Further Reading
- [CONTRIBUTING.md](.github/CONTRIBUTING.md) — Full PR process and expectations
- [Extending Caddy](https://caddyserver.com/docs/extending-caddy) — Module development guide
- [JSON Config](https://caddyserver.com/docs/json/) — Native configuration reference
- [Caddyfile](https://caddyserver.com/docs/caddyfile/concepts) — Caddyfile syntax guide
+44 -52
View File
@@ -45,8 +45,6 @@ import (
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2/internal"
)
// testCertMagicStorageOverride is a package-level test hook. Tests may set
@@ -120,6 +118,10 @@ type AdminConfig struct {
//
// EXPERIMENTAL: This feature is subject to change.
Remote *RemoteAdmin `json:"remote,omitempty"`
// Holds onto the routers so that we can later provision them
// if they require provisioning.
routers []AdminRouter
}
// ConfigSettings configures the management of configuration.
@@ -208,8 +210,8 @@ type AdminAccess struct {
// AdminPermissions specifies what kinds of requests are allowed
// to be made to the admin endpoint.
type AdminPermissions struct {
// The API paths allowed. A request path must either equal an
// allowed path or be a subpath with a path-segment boundary.
// The API paths allowed. Paths are simple prefix matches.
// Any subpath of the specified paths will be allowed.
Paths []string `json:"paths,omitempty"`
// The HTTP methods allowed for the given paths.
@@ -218,7 +220,7 @@ type AdminPermissions struct {
// newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr.
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) (adminHandler, error) {
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler {
muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively
@@ -275,21 +277,34 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx
// register third-party module endpoints
for _, m := range GetModules("admin.api") {
router := m.New().(AdminRouter)
// provision the router before registering its routes, so
// handlers have access to all provisioned state
if provisioner, ok := router.(Provisioner); ok {
if err := provisioner.Provision(ctx); err != nil {
return adminHandler{}, fmt.Errorf("provisioning admin router module %s: %v", m.ID, err)
}
}
for _, route := range router.Routes() {
addRoute(route.Pattern, handlerLabel, route.Handler)
}
admin.routers = append(admin.routers, router)
}
return muxWrap, nil
return muxWrap
}
// provisionAdminRouters provisions all the router modules
// in the admin.api namespace that need provisioning.
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
for _, router := range admin.routers {
provisioner, ok := router.(Provisioner)
if !ok {
continue
}
err := provisioner.Provision(ctx)
if err != nil {
return err
}
}
// We no longer need the routers once provisioned, allow for GC
admin.routers = nil
return nil
}
// allowedOrigins returns a list of origins that are allowed.
@@ -413,7 +428,11 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
return err
}
handler, err := cfg.Admin.newAdminHandler(addr, false, ctx)
handler := cfg.Admin.newAdminHandler(addr, false, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return err
}
@@ -537,7 +556,11 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
// make the HTTP handler but disable Host/Origin enforcement
// because we are using TLS authentication instead
handler, err := cfg.Admin.newAdminHandler(addr, true, ctx)
handler := cfg.Admin.newAdminHandler(addr, true, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return err
}
@@ -693,7 +716,7 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
// verify path
pathFound := accessPerm.Paths == nil
for _, allowedPath := range accessPerm.Paths {
if adminPathAllowed(r.URL.Path, allowedPath) {
if strings.HasPrefix(r.URL.Path, allowedPath) {
pathFound = true
break
}
@@ -722,19 +745,6 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
}
}
func adminPathAllowed(reqPath, allowedPath string) bool {
if allowedPath == "" || allowedPath == "/" {
return strings.HasPrefix(reqPath, allowedPath)
}
if reqPath == allowedPath {
return true
}
if strings.HasSuffix(allowedPath, "/") {
return strings.HasPrefix(reqPath, allowedPath)
}
return strings.HasPrefix(reqPath, allowedPath+"/")
}
func stopAdminServer(srv *http.Server) error {
if srv == nil {
return fmt.Errorf("no admin server")
@@ -790,7 +800,7 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
zap.String("uri", r.RequestURI),
zap.String("remote_ip", ip),
zap.String("remote_port", port),
zap.Object("headers", internal.LoggableHTTPHeader{Header: r.Header}),
zap.Reflect("headers", r.Header),
)
if r.TLS != nil {
log = log.With(
@@ -1051,9 +1061,6 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
buf.Reset()
defer bufPool.Put(buf)
const maxConfigSize = 100 * 1024 * 1024 // 100 MB
r.Body = http.MaxBytesReader(w, r.Body, maxConfigSize)
_, err := io.Copy(buf, r.Body)
if err != nil {
return APIError{
@@ -1136,20 +1143,6 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
return nil
}
func parseCanonicalArrayIndex(idx string) (int, error) {
if idx == "" {
return 0, fmt.Errorf("empty index")
}
i, err := strconv.Atoi(idx)
if err != nil {
return 0, err
}
if strconv.Itoa(i) != idx {
return 0, fmt.Errorf("non-canonical array index")
}
return i, nil
}
// unsyncedConfigAccess traverses into the current config and performs
// the operation at path according to method, using body and out as
// needed. This is a low-level, unsynchronized function; most callers
@@ -1211,12 +1204,11 @@ traverseLoop:
var idx int
if method != http.MethodPost {
idxStr := parts[len(parts)-1]
idx, err = parseCanonicalArrayIndex(idxStr)
idx, err = strconv.Atoi(idxStr)
if err != nil {
return fmt.Errorf("[%s] invalid array index '%s': %v",
path, idxStr, err)
}
if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
}
@@ -1316,7 +1308,7 @@ traverseLoop:
}
case []any:
partInt, err := parseCanonicalArrayIndex(part)
partInt, err := strconv.Atoi(part)
if err != nil {
return fmt.Errorf("[/%s] invalid array index '%s': %v",
strings.Join(parts[:i+1], "/"), part, err)
+15 -204
View File
@@ -15,13 +15,9 @@
package caddy
import (
"bytes"
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"maps"
"net/http"
@@ -35,8 +31,6 @@ import (
"github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
var testCfg = []byte(`{
@@ -57,13 +51,6 @@ var testCfg = []byte(`{
}
`)
type testAdminPublicKey string
func (k testAdminPublicKey) Equal(x crypto.PublicKey) bool {
other, ok := x.(testAdminPublicKey)
return ok && k == other
}
func TestUnsyncedConfigAccess(t *testing.T) {
// each test is performed in sequence, so
// each change builds on the previous ones;
@@ -255,51 +242,6 @@ func TestAdminHandlerErrorHandling(t *testing.T) {
}
}
func TestAdminHandlerServeHTTPRedactsSensitiveHeadersInLogs(t *testing.T) {
core, logs := observer.New(zap.InfoLevel)
defaultLoggerMu.Lock()
origLogger := defaultLogger.logger
defaultLogger.logger = zap.New(core)
defaultLoggerMu.Unlock()
t.Cleanup(func() {
defaultLoggerMu.Lock()
defaultLogger.logger = origLogger
defaultLoggerMu.Unlock()
})
handler := adminHandler{
mux: http.NewServeMux(),
}
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer secret")
req.Header.Set("Cookie", "session=secret")
req.Header.Set("X-Test", "ok")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if logs.Len() == 0 {
t.Fatal("expected request log entry")
}
ctx := logs.All()[0].ContextMap()
headers, ok := ctx["headers"].(map[string]any)
if !ok {
t.Fatalf("expected headers field in log context, got %T", ctx["headers"])
}
if got := headers["Authorization"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
t.Fatalf("expected redacted Authorization header, got %#v", got)
}
if got := headers["Cookie"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
t.Fatalf("expected redacted Cookie header, got %#v", got)
}
if got := headers["X-Test"]; !reflect.DeepEqual(got, []any{"ok"}) {
t.Fatalf("expected X-Test header to remain visible, got %#v", got)
}
}
func initAdminMetrics() {
if adminMetrics.requestErrors != nil {
prometheus.Unregister(adminMetrics.requestErrors)
@@ -340,10 +282,7 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
if err != nil {
t.Fatalf("Failed to parse address: %v", err)
}
handler, err := cfg.Admin.newAdminHandler(addr, false, Context{})
if err != nil {
t.Fatalf("Failed to create admin handler: %v", err)
}
handler := cfg.Admin.newAdminHandler(addr, false, Context{})
tests := []struct {
name string
@@ -464,10 +403,7 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
admin := &AdminConfig{
EnforceOrigin: false,
}
handler, err := admin.newAdminHandler(addr, false, Context{})
if err != nil {
t.Fatalf("Failed to create admin handler: %v", err)
}
handler := admin.newAdminHandler(addr, false, Context{})
req := httptest.NewRequest("GET", "/mock", nil)
req.Host = "localhost:2019"
@@ -479,6 +415,10 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
t.Logf("Response body: %s", rr.Body.String())
}
if len(admin.routers) != 1 {
t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
}
}
type mockProvisionableRouter struct {
@@ -516,16 +456,19 @@ func TestAdminRouterProvisioning(t *testing.T) {
name string
provisionErr error
wantErr bool
routersAfter int // expected number of routers after provisioning
}{
{
name: "successful provisioning",
provisionErr: nil,
wantErr: false,
routersAfter: 0,
},
{
name: "provisioning error",
provisionErr: fmt.Errorf("provision failed"),
wantErr: true,
routersAfter: 1,
},
}
@@ -561,7 +504,8 @@ func TestAdminRouterProvisioning(t *testing.T) {
t.Fatalf("Failed to parse address: %v", err)
}
_, err = admin.newAdminHandler(addr, false, Context{})
_ = admin.newAdminHandler(addr, false, Context{})
err = admin.provisionAdminRouters(Context{})
if test.wantErr {
if err == nil {
@@ -572,6 +516,10 @@ func TestAdminRouterProvisioning(t *testing.T) {
t.Errorf("Expected no error but got: %v", err)
}
}
if len(admin.routers) != test.routersAfter {
t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
}
})
}
}
@@ -656,99 +604,6 @@ func TestAllowedOriginsUnixSocket(t *testing.T) {
}
}
func TestRemoteAdminAccessControlPathSegmentMatching(t *testing.T) {
const authorizedKey testAdminPublicKey = "authorized"
peerCert := &x509.Certificate{PublicKey: authorizedKey}
tests := []struct {
name string
allowedPath string
requestPath string
wantErr bool
}{
{
name: "exact path",
allowedPath: "/pki/ca/prod",
requestPath: "/pki/ca/prod",
wantErr: false,
},
{
name: "subpath",
allowedPath: "/pki/ca/prod",
requestPath: "/pki/ca/prod/certificates",
wantErr: false,
},
{
name: "trailing slash subpath",
allowedPath: "/pki/ca/prod/",
requestPath: "/pki/ca/prod/certificates",
wantErr: false,
},
{
name: "sibling with shared prefix",
allowedPath: "/pki/ca/prod",
requestPath: "/pki/ca/prod-backup",
wantErr: true,
},
{
name: "same segment plus digit",
allowedPath: "/pki/ca/prod",
requestPath: "/pki/ca/prod1",
wantErr: true,
},
{
name: "root path",
allowedPath: "/",
requestPath: "/pki/ca/prod",
wantErr: false,
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
remote := RemoteAdmin{
AccessControl: []*AdminAccess{
{
Permissions: []AdminPermissions{
{
Methods: []string{http.MethodGet},
Paths: []string{test.allowedPath},
},
},
publicKeys: []crypto.PublicKey{authorizedKey},
},
},
}
req := httptest.NewRequest(http.MethodGet, "https://localhost:2021"+test.requestPath, nil)
req.TLS = &tls.ConnectionState{
VerifiedChains: [][]*x509.Certificate{{peerCert}},
}
err := remote.enforceAccessControls(req)
if test.wantErr {
if err == nil {
t.Errorf("test %d (%s): allowed path %q, request path %q: expected forbidden error, got nil", i, test.name, test.allowedPath, test.requestPath)
return
}
var apiErr APIError
if !errors.As(err, &apiErr) {
t.Errorf("test %d (%s): allowed path %q, request path %q: expected APIError with HTTP status %d, got %T: %v", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, err, err)
return
}
if apiErr.HTTPStatus != http.StatusForbidden {
t.Errorf("test %d (%s): allowed path %q, request path %q: expected HTTP status %d, got %d", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, apiErr.HTTPStatus)
}
return
}
if err != nil {
t.Errorf("test %d (%s): allowed path %q, request path %q: expected no error, got %v", i, test.name, test.allowedPath, test.requestPath, err)
}
})
}
}
func TestReplaceRemoteAdminServer(t *testing.T) {
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
@@ -1101,47 +956,3 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
})
}
}
func TestUnsyncedConfigAccessCanonicalArrayIndices(t *testing.T) {
rawCfg = map[string]any{
rawConfigKey: map[string]any{
"list": []any{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"},
},
}
tests := []struct {
name string
path string
wantOutput string
wantErr bool
}{
{name: "allow zero", path: "/" + rawConfigKey + "/list/0", wantOutput: "\"zero\"\n"},
{name: "allow one", path: "/" + rawConfigKey + "/list/1", wantOutput: "\"one\"\n"},
{name: "allow ten", path: "/" + rawConfigKey + "/list/10", wantOutput: "\"ten\"\n"},
{name: "reject leading zero", path: "/" + rawConfigKey + "/list/01", wantErr: true},
{name: "reject multiple leading zeros", path: "/" + rawConfigKey + "/list/002", wantErr: true},
{name: "reject plus sign", path: "/" + rawConfigKey + "/list/+1", wantErr: true},
{name: "reject negative zero", path: "/" + rawConfigKey + "/list/-0", wantErr: true},
}
for i, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var gotOutput bytes.Buffer
err := unsyncedConfigAccess(http.MethodGet, tc.path, nil, &gotOutput)
if tc.wantErr {
if err == nil {
t.Errorf("test %d (%s): input path %q: expected error, got nil with output %q", i, tc.name, tc.path, gotOutput.String())
}
return
}
if err != nil {
t.Errorf("test %d (%s): input path %q: expected no error with output %q, got error %v with output %q", i, tc.name, tc.path, tc.wantOutput, err, gotOutput.String())
}
if gotOutput.String() != tc.wantOutput {
t.Errorf("test %d (%s): input path %q: expected output %q, got %q", i, tc.name, tc.path, tc.wantOutput, gotOutput.String())
}
})
}
}
+10 -3
View File
@@ -440,6 +440,13 @@ func run(newCfg *Config, start bool) (Context, error) {
}
}()
// Provision any admin routers which may need to access
// some of the other apps at runtime
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return ctx, err
}
// Start
err = func() error {
started := make([]string, 0, len(ctx.cfg.apps))
@@ -759,7 +766,7 @@ func Validate(cfg *Config) error {
// code is emitted.
func exitProcess(ctx context.Context, logger *zap.Logger) {
// let the rest of the program know we're quitting; only do it once
if !exiting.CompareAndSwap(false, true) {
if !atomic.CompareAndSwapInt32(exiting, 0, 1) {
return
}
@@ -838,11 +845,11 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
}()
}
var exiting atomic.Bool
var exiting = new(int32) // accessed atomically
// Exiting returns true if the process is exiting.
// EXPERIMENTAL API: subject to change or removal.
func Exiting() bool { return exiting.Load() }
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
// OnExit registers a callback to invoke during process exit.
// This registration is PROCESS-GLOBAL, meaning that each
+2 -2
View File
@@ -155,7 +155,7 @@ func (l *lexer) next() (bool, error) {
// want to keep.
if ch == '\n' {
if len(val) == 2 {
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alphanumeric characters, dashes and underscores; got empty string", l.line)
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line)
}
// check if there's too many <
@@ -165,7 +165,7 @@ func (l *lexer) next() (bool, error) {
heredocMarker = string(val[2:])
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
return false, fmt.Errorf("heredoc marker on line #%d must contain only alphanumeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
}
inHeredoc = true
+1 -1
View File
@@ -424,7 +424,7 @@ EOF
{
input: []byte("not-a-heredoc <<\n"),
expectErr: true,
errorMessage: "missing opening heredoc marker on line #1; must contain only alphanumeric characters, dashes and underscores; got empty string",
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string",
},
{
input: []byte(`heredoc <<<EOF
+1 -1
View File
@@ -683,7 +683,7 @@ func (p *parser) directive() error {
// openCurlyBrace expects the current token to be an
// opening curly brace. This acts like an assertion
// because it returns an error if the token is not
// an opening curly brace. It does NOT advance the token.
// a opening curly brace. It does NOT advance the token.
func (p *parser) openCurlyBrace() error {
if p.Val() != "{" {
if p.valLooksLikeGlobalOptionsAfterImportedSnippets() {
+20 -5
View File
@@ -550,11 +550,26 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
}
case acmeIssuer != nil:
// implicit ACME issuers (from various subdirectives) should inherit from
// any globally-configured ACME issuer templates, then apply the local
// shortcut settings as overrides.
defaultIssuers := implicitACMEIssuers(h, acmeIssuer)
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
// if an ACME CA endpoint was set, the user expects to use that specific one,
// not any others that may be defaults, so replace all defaults with that ACME CA
if acmeIssuer.CA != "" {
defaultIssuers = []certmagic.Issuer{acmeIssuer}
}
for _, issuer := range defaultIssuers {
// apply settings from the implicitly-configured ACMEIssuer to any
// default ACMEIssuers, but preserve each default issuer's CA endpoint,
// because, for example, if you configure the DNS challenge, it should
// apply to any of the default ACMEIssuers, but you don't want to trample
// out their unique CA endpoints
if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil {
acmeCopy := *acmeIssuer
acmeCopy.CA = iss.CA
issuer = &acmeCopy
}
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: issuer,
@@ -1053,7 +1068,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
if !d.NextArg() {
return nil, d.ArgErr()
}
interval, err := caddy.ParseDuration(d.Val())
interval, err := time.ParseDuration(d.Val() + "ns")
if err != nil {
return nil, d.Errf("failed to parse interval: %v", err)
}
+2 -2
View File
@@ -66,14 +66,14 @@ func TestLogDirectiveSyntax(t *testing.T) {
input: `:8080 {
log {
sampling {
interval 2s
interval 2
first 3
thereafter 4
}
}
}
`,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2000000000,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false,
},
} {
-2
View File
@@ -484,8 +484,6 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
metrics.PerHost = true
case "observe_catchall_hosts":
metrics.ObserveCatchallHosts = true
case "otlp":
metrics.OTLP = true
default:
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
}
-125
View File
@@ -3,9 +3,7 @@ package httpcaddyfile
import (
"encoding/json"
"testing"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls"
_ "github.com/caddyserver/caddy/v2/modules/logging"
@@ -168,126 +166,3 @@ func TestGlobalResolversOption(t *testing.T) {
})
}
}
func TestGlobalCertIssuerAppliesToImplicitACMEIssuer(t *testing.T) {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
input := `{
cert_issuer acme {
disable_tlsalpn_challenge
}
}
report.company.intern {
tls {
ca https://deglacme01.company.intern/acme/acme/directory
ca_root /etc/certs/company_root2.crt
}
respond "ok"
}`
out, _, err := adapter.Adapt([]byte(input), nil)
if err != nil {
t.Fatalf("adapting caddyfile: %v", err)
}
var config struct {
Apps struct {
TLS *caddytls.TLS `json:"tls"`
} `json:"apps"`
}
if err := json.Unmarshal(out, &config); err != nil {
t.Fatalf("unmarshaling adapted config: %v", err)
}
if config.Apps.TLS == nil || config.Apps.TLS.Automation == nil {
t.Fatal("expected tls automation config")
}
var subjectPolicy *caddytls.AutomationPolicy
for _, ap := range config.Apps.TLS.Automation.Policies {
if len(ap.SubjectsRaw) == 1 && ap.SubjectsRaw[0] == "report.company.intern" {
subjectPolicy = ap
break
}
}
if subjectPolicy == nil {
t.Fatal("expected subject-specific automation policy")
}
if len(subjectPolicy.IssuersRaw) != 1 {
t.Fatalf("expected one issuer for subject-specific policy, got %d", len(subjectPolicy.IssuersRaw))
}
var issuer caddytls.ACMEIssuer
if err := json.Unmarshal(subjectPolicy.IssuersRaw[0], &issuer); err != nil {
t.Fatalf("unmarshaling issuer: %v", err)
}
if issuer.CA != "https://deglacme01.company.intern/acme/acme/directory" {
t.Fatalf("expected custom ACME CA, got %q", issuer.CA)
}
if len(issuer.TrustedRootsPEMFiles) != 1 || issuer.TrustedRootsPEMFiles[0] != "/etc/certs/company_root2.crt" {
t.Fatalf("expected trusted roots to include site CA root, got %v", issuer.TrustedRootsPEMFiles)
}
if issuer.Challenges == nil || issuer.Challenges.TLSALPN == nil || !issuer.Challenges.TLSALPN.Disabled {
t.Fatalf("expected tls-alpn challenge to be disabled, got %#v", issuer.Challenges)
}
}
func TestMergeACMEIssuers(t *testing.T) {
base := &caddytls.ACMEIssuer{
Email: "ops@example.com",
Challenges: &caddytls.ChallengesConfig{
HTTP: &caddytls.HTTPChallengeConfig{
AlternatePort: 8080,
},
TLSALPN: &caddytls.TLSALPNChallengeConfig{
Disabled: true,
AlternatePort: 8443,
},
DNS: &caddytls.DNSChallengeConfig{
Resolvers: []string{"1.1.1.1"},
OverrideDomain: "_acme-challenge.example.net",
},
},
TrustedRootsPEMFiles: []string{"global.pem"},
}
overrides := &caddytls.ACMEIssuer{
CA: "https://deglacme01.company.intern/acme/acme/directory",
Challenges: &caddytls.ChallengesConfig{
HTTP: &caddytls.HTTPChallengeConfig{
Disabled: true,
},
DNS: &caddytls.DNSChallengeConfig{
PropagationTimeout: caddy.Duration(time.Minute),
},
},
TrustedRootsPEMFiles: []string{"site.pem"},
}
merged := mergeACMEIssuers(base, overrides)
if merged.CA != overrides.CA {
t.Fatalf("expected merged CA %q, got %q", overrides.CA, merged.CA)
}
if merged.Email != base.Email {
t.Fatalf("expected merged email %q, got %q", base.Email, merged.Email)
}
if len(merged.TrustedRootsPEMFiles) != 2 || merged.TrustedRootsPEMFiles[0] != "global.pem" || merged.TrustedRootsPEMFiles[1] != "site.pem" {
t.Fatalf("expected merged roots [global.pem site.pem], got %v", merged.TrustedRootsPEMFiles)
}
if merged.Challenges == nil || merged.Challenges.HTTP == nil || !merged.Challenges.HTTP.Disabled || merged.Challenges.HTTP.AlternatePort != 8080 {
t.Fatalf("expected merged HTTP challenge config to preserve alternate port and apply disable flag, got %#v", merged.Challenges)
}
if merged.Challenges.TLSALPN == nil || !merged.Challenges.TLSALPN.Disabled || merged.Challenges.TLSALPN.AlternatePort != 8443 {
t.Fatalf("expected merged TLS-ALPN challenge config to preserve global settings, got %#v", merged.Challenges)
}
if merged.Challenges.DNS == nil || merged.Challenges.DNS.PropagationTimeout != caddy.Duration(time.Minute) || len(merged.Challenges.DNS.Resolvers) != 1 || merged.Challenges.DNS.Resolvers[0] != "1.1.1.1" || merged.Challenges.DNS.OverrideDomain != "_acme-challenge.example.net" {
t.Fatalf("expected merged DNS challenge config to preserve global values and apply overrides, got %#v", merged.Challenges)
}
if base.CA != "" {
t.Fatalf("expected base issuer to remain unchanged, got CA %q", base.CA)
}
if len(base.TrustedRootsPEMFiles) != 1 || base.TrustedRootsPEMFiles[0] != "global.pem" {
t.Fatalf("expected base roots to remain unchanged, got %v", base.TrustedRootsPEMFiles)
}
}
+1 -336
View File
@@ -612,289 +612,6 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
return nil
}
// implicitACMEIssuers returns the issuers to use for ACME-related tls
// shortcuts such as ca, ca_root, and dns. If any global cert_issuer options
// configure ACME issuers, those become the templates for the local shortcut
// configuration; otherwise, default ACME issuers are used.
func implicitACMEIssuers(h Helper, acmeIssuer *caddytls.ACMEIssuer) []certmagic.Issuer {
globalIssuers, _ := h.Option("cert_issuer").([]certmagic.Issuer)
var implicitIssuers []certmagic.Issuer
for _, issuer := range globalIssuers {
acmeWrapper, ok := issuer.(acmeCapable)
if !ok {
continue
}
baseIssuer := acmeWrapper.GetACMEIssuer()
if baseIssuer == nil {
continue
}
implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
}
if len(implicitIssuers) > 0 {
return implicitIssuers
}
// If an ACME CA endpoint was set locally, the user expects to use only that
// CA rather than the usual default fallback issuers.
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
if acmeIssuer.CA != "" {
defaultIssuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
}
implicitIssuers = make([]certmagic.Issuer, 0, len(defaultIssuers))
for _, issuer := range defaultIssuers {
acmeWrapper, ok := issuer.(acmeCapable)
if !ok {
implicitIssuers = append(implicitIssuers, issuer)
continue
}
baseIssuer := acmeWrapper.GetACMEIssuer()
if baseIssuer == nil {
implicitIssuers = append(implicitIssuers, issuer)
continue
}
implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
}
return implicitIssuers
}
func mergeACMEIssuers(base, overrides *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
if base == nil {
return cloneACMEIssuer(overrides)
}
merged := cloneACMEIssuer(base)
if overrides == nil {
return merged
}
if overrides.CA != "" {
merged.CA = overrides.CA
}
if overrides.TestCA != "" {
merged.TestCA = overrides.TestCA
}
if overrides.Email != "" {
merged.Email = overrides.Email
}
if overrides.Profile != "" {
merged.Profile = overrides.Profile
}
if overrides.AccountKey != "" {
merged.AccountKey = overrides.AccountKey
}
if overrides.ExternalAccount != nil {
merged.ExternalAccount = cloneACMEEAB(overrides.ExternalAccount)
}
if overrides.ACMETimeout != 0 {
merged.ACMETimeout = overrides.ACMETimeout
}
if len(overrides.TrustedRootsPEMFiles) > 0 {
merged.TrustedRootsPEMFiles = appendUniqueStrings(merged.TrustedRootsPEMFiles, overrides.TrustedRootsPEMFiles...)
}
if overrides.PreferredChains != nil {
merged.PreferredChains = cloneChainPreference(overrides.PreferredChains)
}
if overrides.CertificateLifetime != 0 {
merged.CertificateLifetime = overrides.CertificateLifetime
}
if len(overrides.NetworkProxyRaw) > 0 {
merged.NetworkProxyRaw = slices.Clone(overrides.NetworkProxyRaw)
}
merged.Challenges = mergeChallengesConfig(merged.Challenges, overrides.Challenges)
return merged
}
func mergeChallengesConfig(base, overrides *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
if base == nil {
return cloneChallengesConfig(overrides)
}
merged := cloneChallengesConfig(base)
if overrides == nil {
return merged
}
merged.HTTP = mergeHTTPChallengeConfig(merged.HTTP, overrides.HTTP)
merged.TLSALPN = mergeTLSALPNChallengeConfig(merged.TLSALPN, overrides.TLSALPN)
merged.DNS = mergeDNSChallengeConfig(merged.DNS, overrides.DNS)
if overrides.BindHost != "" {
merged.BindHost = overrides.BindHost
}
if overrides.Distributed != nil {
value := *overrides.Distributed
merged.Distributed = &value
}
return merged
}
func mergeHTTPChallengeConfig(base, overrides *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
if base == nil {
return cloneHTTPChallengeConfig(overrides)
}
merged := cloneHTTPChallengeConfig(base)
if overrides == nil {
return merged
}
if overrides.Disabled {
merged.Disabled = true
}
if overrides.AlternatePort != 0 {
merged.AlternatePort = overrides.AlternatePort
}
return merged
}
func mergeTLSALPNChallengeConfig(base, overrides *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
if base == nil {
return cloneTLSALPNChallengeConfig(overrides)
}
merged := cloneTLSALPNChallengeConfig(base)
if overrides == nil {
return merged
}
if overrides.Disabled {
merged.Disabled = true
}
if overrides.AlternatePort != 0 {
merged.AlternatePort = overrides.AlternatePort
}
return merged
}
func mergeDNSChallengeConfig(base, overrides *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
if base == nil {
return cloneDNSChallengeConfig(overrides)
}
merged := cloneDNSChallengeConfig(base)
if overrides == nil {
return merged
}
if len(overrides.ProviderRaw) > 0 {
merged.ProviderRaw = slices.Clone(overrides.ProviderRaw)
}
if overrides.PropagationDelay != 0 {
merged.PropagationDelay = overrides.PropagationDelay
}
if overrides.PropagationTimeout != 0 {
merged.PropagationTimeout = overrides.PropagationTimeout
}
if overrides.Resolvers != nil {
merged.Resolvers = slices.Clone(overrides.Resolvers)
}
if overrides.OverrideDomain != "" {
merged.OverrideDomain = overrides.OverrideDomain
}
if overrides.TTL != 0 {
merged.TTL = overrides.TTL
}
return merged
}
func cloneACMEIssuer(iss *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
if iss == nil {
return nil
}
cloned := *iss
cloned.Challenges = cloneChallengesConfig(iss.Challenges)
cloned.ExternalAccount = cloneACMEEAB(iss.ExternalAccount)
cloned.TrustedRootsPEMFiles = slices.Clone(iss.TrustedRootsPEMFiles)
cloned.PreferredChains = cloneChainPreference(iss.PreferredChains)
cloned.NetworkProxyRaw = slices.Clone(iss.NetworkProxyRaw)
return &cloned
}
func cloneChallengesConfig(cfg *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
if cfg == nil {
return nil
}
cloned := *cfg
cloned.HTTP = cloneHTTPChallengeConfig(cfg.HTTP)
cloned.TLSALPN = cloneTLSALPNChallengeConfig(cfg.TLSALPN)
cloned.DNS = cloneDNSChallengeConfig(cfg.DNS)
if cfg.Distributed != nil {
value := *cfg.Distributed
cloned.Distributed = &value
}
return &cloned
}
func cloneHTTPChallengeConfig(cfg *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
if cfg == nil {
return nil
}
cloned := *cfg
return &cloned
}
func cloneTLSALPNChallengeConfig(cfg *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
if cfg == nil {
return nil
}
cloned := *cfg
return &cloned
}
func cloneDNSChallengeConfig(cfg *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
if cfg == nil {
return nil
}
cloned := *cfg
cloned.ProviderRaw = slices.Clone(cfg.ProviderRaw)
cloned.Resolvers = slices.Clone(cfg.Resolvers)
return &cloned
}
func cloneACMEEAB(eab *acme.EAB) *acme.EAB {
if eab == nil {
return nil
}
cloned := *eab
return &cloned
}
func cloneChainPreference(pref *caddytls.ChainPreference) *caddytls.ChainPreference {
if pref == nil {
return nil
}
cloned := *pref
cloned.RootCommonName = slices.Clone(pref.RootCommonName)
cloned.AnyCommonName = slices.Clone(pref.AnyCommonName)
if pref.Smallest != nil {
value := *pref.Smallest
cloned.Smallest = &value
}
return &cloned
}
func appendUniqueStrings(existing []string, additions ...string) []string {
for _, value := range additions {
if !slices.Contains(existing, value) {
existing = append(existing, value)
}
}
return existing
}
// newBaseAutomationPolicy returns a new TLS automation policy that gets
// its values from the global options map. It should be used as the base
// for any other automation policies. A nil policy (and no error) will be
@@ -1036,7 +753,7 @@ outer:
// otherwise the one without any subjects (a catch-all) would be
// eaten up by the one with subjects; and if both have subjects, we
// need to combine their lists
if automationPoliciesHaveSameIssuers(aps[i], aps[j]) &&
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
aps[i].MustStaple == aps[j].MustStaple &&
@@ -1128,58 +845,6 @@ func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) b
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
}
func automationPoliciesHaveSameIssuers(a, b *caddytls.AutomationPolicy) bool {
if reflect.DeepEqual(a.IssuersRaw, b.IssuersRaw) {
return automationPoliciesHaveCompatibleImplicitIssuers(a, b)
}
return automationPolicyUsesDefaultInternalIssuer(a) && automationPolicyUsesDefaultInternalIssuer(b)
}
func automationPolicyUsesDefaultInternalIssuer(ap *caddytls.AutomationPolicy) bool {
if len(ap.IssuersRaw) == 0 && len(ap.Issuers) == 0 {
return automationPolicyImplicitIssuerClass(ap) == "internal"
}
return len(ap.IssuersRaw) == 1 &&
len(ap.Issuers) == 0 &&
string(bytes.TrimSpace(ap.IssuersRaw[0])) == `{"module":"internal"}`
}
// automationPoliciesHaveCompatibleImplicitIssuers returns whether two policies
// without explicit issuers can be consolidated without changing default issuer
// selection for their subjects.
func automationPoliciesHaveCompatibleImplicitIssuers(a, b *caddytls.AutomationPolicy) bool {
if len(a.IssuersRaw) > 0 || len(a.Issuers) > 0 ||
len(b.IssuersRaw) > 0 || len(b.Issuers) > 0 {
return true
}
aClass := automationPolicyImplicitIssuerClass(a)
bClass := automationPolicyImplicitIssuerClass(b)
return aClass == "catch-all" || bClass == "catch-all" || aClass == bClass
}
func automationPolicyImplicitIssuerClass(ap *caddytls.AutomationPolicy) string {
if len(ap.SubjectsRaw) == 0 {
return "catch-all"
}
hasPublic := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
return subjectQualifiesForPublicCert(ap, subj)
})
hasInternal := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
return !subjectQualifiesForPublicCert(ap, subj)
})
switch {
case hasPublic && hasInternal:
return "mixed"
case hasPublic:
return "public"
default:
return "internal"
}
}
// automationPolicyHasAllPublicNames returns true if all the names on the policy
// do NOT qualify for public certs OR are tailscale domains.
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
-18
View File
@@ -3,7 +3,6 @@ package httpcaddyfile
import (
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
@@ -55,20 +54,3 @@ func TestAutomationPolicyIsSubset(t *testing.T) {
}
}
}
func TestAutomationPoliciesAllowSameHostOnDifferentPorts(t *testing.T) {
input := `https://example.com:5000 localhost:5000 {
respond "one"
}
https://example.net localhost:8080 {
respond "two"
}
`
adapter := caddyfile.Adapter{ServerType: ServerType{}}
_, _, err := adapter.Adapt([]byte(input), nil)
if err != nil {
t.Fatalf("adapting Caddyfile: %v", err)
}
}
+6 -6
View File
@@ -518,7 +518,7 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
return resp
}
// AssertResponse requests a URI and asserts the status code and body.
// AssertResponse request a URI and assert the status code and the body contains a string
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper()
@@ -541,7 +541,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
// Verb specific test functions
// AssertGetResponse requests a URI with GET and expects a status code and body text.
// AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper()
@@ -553,7 +553,7 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertDeleteResponse requests a URI with DELETE and expects a status code and body text.
// AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper()
@@ -565,7 +565,7 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPostResponseBody requests a URI with POST and asserts the response code and body.
// AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper()
@@ -580,7 +580,7 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPutResponseBody requests a URI with PUT and asserts the response code and body.
// AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper()
@@ -595,7 +595,7 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPatchResponseBody requests a URI with PATCH and asserts the response code and body.
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper()
-22
View File
@@ -55,28 +55,6 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsPreferHTTPSPortOverAlternatePort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
}
localhost {
respond "Canonical"
}
localhost:10443 {
respond "Alternate"
}
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
@@ -1,7 +1,7 @@
{
log {
sampling {
interval 5m
interval 300
first 50
thereafter 40
}
@@ -13,7 +13,7 @@
"logs": {
"default": {
"sampling": {
"interval": 300000000000,
"interval": 300,
"first": 50,
"thereafter": 40
}
@@ -6,4 +6,4 @@ handle {
END!
}
----------
heredoc marker on line #4 must contain only alphanumeric characters, dashes and underscores; got 'END!'
heredoc marker on line #4 must contain only alpha-numeric characters, dashes and underscores; got 'END!'
@@ -1,7 +1,7 @@
:80 {
log {
sampling {
interval 5m
interval 300
first 50
thereafter 40
}
@@ -18,7 +18,7 @@
},
"log0": {
"sampling": {
"interval": 300000000000,
"interval": 300,
"first": 50,
"thereafter": 40
},
@@ -1,35 +0,0 @@
{
metrics {
otlp
}
}
:80 {
respond "Hello"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"body": "Hello",
"handler": "static_response"
}
]
}
]
}
},
"metrics": {
"otlp": true
}
}
}
}
@@ -1,58 +0,0 @@
:8884
reverse_proxy 127.0.0.1:65535 {
lb_retries 3
lb_retry_match expression `{rp.status_code} in [502, 503]`
lb_retry_match expression `{rp.is_transport_error} || {rp.status_code} == 502`
lb_retry_match expression `method('POST') && {rp.status_code} == 503`
lb_retry_match `{rp.status_code} == 504`
lb_retry_match `{rp.is_transport_error} && method('PUT')`
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"retries": 3,
"retry_match": [
{
"expression": "{http.reverse_proxy.status_code} in [502, 503]"
},
{
"expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} == 502"
},
{
"expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} == 503"
},
{
"expression": "{http.reverse_proxy.status_code} == 504"
},
{
"expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('PUT')"
}
]
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
@@ -1,147 +0,0 @@
:8884
reverse_proxy 127.0.0.1:65535 {
lb_retries 5
# request matchers (backward-compatible, non-expression)
lb_retry_match {
method POST PUT
}
lb_retry_match {
path /foo*
}
lb_retry_match {
header X-Idempotency-Key *
}
# response status code via expression
lb_retry_match {
expression `{rp.status_code} in [502, 503, 504]`
}
# response header via expression
lb_retry_match {
expression `{rp.header.X-Retry} == "true"`
}
# CEL request functions combined with response placeholders
lb_retry_match {
expression `method('POST') && {rp.status_code} >= 500`
}
lb_retry_match {
expression `path('/api*') && {rp.status_code} in [502, 503]`
}
lb_retry_match {
expression `host('example.com') && {rp.status_code} == 503`
}
lb_retry_match {
expression `query({'retry': 'true'}) && {rp.status_code} >= 500`
}
lb_retry_match {
expression `header({'X-Idempotency-Key': '*'}) && {rp.status_code} in [502, 503]`
}
lb_retry_match {
expression `protocol('https') && {rp.status_code} == 502`
}
lb_retry_match {
expression `path_regexp('^/api/v[0-9]+/') && {rp.status_code} >= 500`
}
lb_retry_match {
expression `header_regexp('Content-Type', '^application/json') && {rp.status_code} == 502`
}
# transport error handling via placeholder
lb_retry_match {
expression `{rp.is_transport_error} || {rp.status_code} in [502, 503]`
}
lb_retry_match {
expression `{rp.is_transport_error} && method('POST')`
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"retries": 5,
"retry_match": [
{
"method": [
"POST",
"PUT"
]
},
{
"path": [
"/foo*"
]
},
{
"header": {
"X-Idempotency-Key": [
"*"
]
}
},
{
"expression": "{http.reverse_proxy.status_code} in [502, 503, 504]"
},
{
"expression": "{http.reverse_proxy.header.X-Retry} == \"true\""
},
{
"expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
},
{
"expression": "path('/api*') \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]"
},
{
"expression": "host('example.com') \u0026\u0026 {http.reverse_proxy.status_code} == 503"
},
{
"expression": "query({'retry': 'true'}) \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
},
{
"expression": "header({'X-Idempotency-Key': '*'}) \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]"
},
{
"expression": "protocol('https') \u0026\u0026 {http.reverse_proxy.status_code} == 502"
},
{
"expression": "path_regexp('^/api/v[0-9]+/') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
},
{
"expression": "header_regexp('Content-Type', '^application/json') \u0026\u0026 {http.reverse_proxy.status_code} == 502"
},
{
"expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} in [502, 503]"
},
{
"expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('POST')"
}
]
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
+103 -203
View File
@@ -1,13 +1,17 @@
package integration
import (
"bufio"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/textproto"
"os"
"runtime"
"strings"
"sync/atomic"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
@@ -564,232 +568,128 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
// TestReverseProxyRetryMatchStatusCode verifies that lb_retry_match with a
// CEL expression matching on {rp.status_code} causes the request to be
// retried on the next upstream when the first upstream returns a matching
// status code
func TestReverseProxyRetryMatchStatusCode(t *testing.T) {
// Bad upstream: returns 502
badSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
}),
func TestReverseProxyWebSocketUpgradeUnixSocket(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
badLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go badSrv.Serve(badLn)
t.Cleanup(func() { badSrv.Close(); badLn.Close() })
// Good upstream: returns 200
goodSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}),
}
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
f, err := os.CreateTemp("", "*.sock")
if err != nil {
t.Fatalf("failed to listen: %v", err)
t.Fatalf("failed to create temporary socket file: %v", err)
}
go goodSrv.Serve(goodLn)
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
_ = os.Remove(f.Name())
socketName := f.Name()
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy %s %s {
lb_policy round_robin
lb_retries 1
lb_retry_match {
expression `+"`{rp.status_code} in [502, 503]`"+`
backend := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/ws" {
http.NotFound(w, req)
return
}
}
}
`, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
}
// TestReverseProxyRetryMatchHeader verifies that lb_retry_match with a CEL
// expression matching on {rp.header.*} causes the request to be retried when
// the upstream sets a matching response header
func TestReverseProxyRetryMatchHeader(t *testing.T) {
var badHits atomic.Int32
// Bad upstream: returns 200 but signals retry via header
badSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
badHits.Add(1)
w.Header().Set("X-Upstream-Retry", "true")
w.Write([]byte("bad"))
}),
}
badLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go badSrv.Serve(badLn)
t.Cleanup(func() { badSrv.Close(); badLn.Close() })
// Good upstream: returns 200 without retry header
goodSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("good"))
}),
}
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go goodSrv.Serve(goodLn)
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy %s %s {
lb_policy round_robin
lb_retries 1
lb_retry_match {
expression `+"`{rp.header.X-Upstream-Retry} == \"true\"`"+`
if !strings.EqualFold(req.Header.Get("Upgrade"), "websocket") ||
!strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") {
http.Error(w, "missing websocket upgrade headers", http.StatusBadRequest)
return
}
}
}
`, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "good")
if badHits.Load() != 1 {
t.Errorf("bad upstream hits: got %d, want 1", badHits.Load())
}
}
// TestReverseProxyRetryMatchCombined verifies that a CEL expression combining
// request path matching with response status code matching works correctly -
// only retrying when both conditions are met
func TestReverseProxyRetryMatchCombined(t *testing.T) {
// Upstream: returns 502 for all requests
srv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
}),
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go srv.Serve(ln)
t.Cleanup(func() { srv.Close(); ln.Close() })
// Good upstream
goodSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}),
}
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go goodSrv.Serve(goodLn)
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy %s %s {
lb_policy round_robin
lb_retries 1
lb_retry_match {
expression `+"`path('/retry*') && {rp.status_code} in [502, 503]`"+`
wsKey := req.Header.Get("Sec-WebSocket-Key")
if wsKey == "" {
http.Error(w, "missing Sec-WebSocket-Key", http.StatusBadRequest)
return
}
}
}
`, goodLn.Addr().String(), ln.Addr().String()), "caddyfile")
// /retry path matches the expression - should retry to good upstream
tester.AssertGetResponse("http://localhost:9080/retry", 200, "ok")
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "hijacker not supported", http.StatusInternalServerError)
return
}
// /other path does NOT match - should return the 502
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/other", nil)
tester.AssertResponse(req, 502, "")
}
// TestReverseProxyRetryMatchIsTransportError verifies that the
// {rp.is_transport_error} == true CEL function correctly identifies transport errors
// and allows retrying them alongside response-based matching
func TestReverseProxyRetryMatchIsTransportError(t *testing.T) {
// Good upstream: returns 200
goodSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}),
}
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go goodSrv.Serve(goodLn)
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
// Broken upstream: accepts connections but closes immediately
brokenLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
t.Cleanup(func() { brokenLn.Close() })
go func() {
for {
conn, err := brokenLn.Accept()
conn, brw, err := hj.Hijack()
if err != nil {
return
}
conn.Close()
}
}()
defer conn.Close()
_, _ = brw.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
_, _ = brw.WriteString("Upgrade: websocket\r\n")
_, _ = brw.WriteString("Connection: Upgrade\r\n")
_, _ = brw.WriteString("Sec-WebSocket-Accept: " + computeWebSocketAccept(wsKey) + "\r\n")
_, _ = brw.WriteString("\r\n")
_ = brw.Flush()
}),
}
unixListener, err := net.Listen("unix", socketName)
if err != nil {
t.Fatalf("failed to listen on unix socket: %v", err)
}
go backend.Serve(unixListener)
t.Cleanup(func() {
_ = backend.Close()
_ = unixListener.Close()
_ = os.Remove(socketName)
})
runtime.Gosched()
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy %s %s {
lb_policy round_robin
lb_retries 1
lb_retry_match {
expression `+"`{rp.is_transport_error} || {rp.status_code} in [502, 503]`"+`
}
}
reverse_proxy unix/%s
}
`, goodLn.Addr().String(), brokenLn.Addr().String()), "caddyfile")
`, socketName), "caddyfile")
// Transport error on broken upstream should be retried to good upstream
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
conn, err := net.Dial("tcp", "127.0.0.1:9080")
if err != nil {
t.Fatalf("failed to dial caddy listener: %v", err)
}
defer conn.Close()
wsKey := "dGhlIHNhbXBsZSBub25jZQ=="
request := strings.Join([]string{
"GET /ws HTTP/1.1",
"Host: localhost:9080",
"Connection: Upgrade",
"Upgrade: websocket",
"Sec-WebSocket-Version: 13",
"Sec-WebSocket-Key: " + wsKey,
"",
"",
}, "\r\n")
if _, err := io.WriteString(conn, request); err != nil {
t.Fatalf("failed to send websocket handshake request: %v", err)
}
tpr := textproto.NewReader(bufio.NewReader(conn))
statusLine, err := tpr.ReadLine()
if err != nil {
t.Fatalf("failed reading handshake status line: %v", err)
}
if !strings.Contains(statusLine, "101") || !strings.Contains(strings.ToLower(statusLine), "switching protocols") {
t.Fatalf("unexpected status line: %q", statusLine)
}
headers, err := tpr.ReadMIMEHeader()
if err != nil {
t.Fatalf("failed reading handshake headers: %v", err)
}
if !strings.EqualFold(headers.Get("Upgrade"), "websocket") {
t.Fatalf("unexpected Upgrade header: %q", headers.Get("Upgrade"))
}
if !strings.Contains(strings.ToLower(headers.Get("Connection")), "upgrade") {
t.Fatalf("unexpected Connection header: %q", headers.Get("Connection"))
}
}
func computeWebSocketAccept(wsKey string) string {
h := sha1.Sum([]byte(wsKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
return base64.StdEncoding.EncodeToString(h[:])
}
+1 -1
View File
@@ -159,7 +159,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
}
// We only accept HTTP/2!
if r.ProtoMajor != 2 {
t.Error("Not an HTTP/2 request, rejected!")
t.Error("Not a HTTP/2 request, rejected!")
w.WriteHeader(http.StatusInternalServerError)
return
}
+1 -17
View File
@@ -58,7 +58,7 @@ func cmdStart(fl Flags) (int, error) {
// open a listener to which the child process will connect when
// it is ready to confirm that it has successfully started
ln, err := listenTCPForPingback(net.Listen)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("opening listener for success confirmation: %v", err)
@@ -169,22 +169,6 @@ func cmdStart(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil
}
type tcpListenFunc func(network, address string) (net.Listener, error)
func listenTCPForPingback(listen tcpListenFunc) (net.Listener, error) {
ln, ipv4Err := listen("tcp4", "127.0.0.1:0")
if ipv4Err == nil {
return ln, nil
}
ln, ipv6Err := listen("tcp6", "[::1]:0")
if ipv6Err == nil {
return ln, nil
}
return nil, fmt.Errorf("listen on 127.0.0.1:0: %v; listen on [::1]:0: %v", ipv4Err, ipv6Err)
}
func cmdRun(fl Flags) (int, error) {
caddy.TrapSignals()
+1 -1
View File
@@ -566,7 +566,7 @@ argument of --directory. If the directory does not exist, it will be created.
// following format:
//
// - lowercase
// - ASCII lowercase letters, digits and hyphens only
// - alphanumeric and hyphen characters only
// - cannot start or end with a hyphen
// - hyphen cannot be adjacent to another hyphen
//
-76
View File
@@ -1,8 +1,6 @@
package caddycmd
import (
"errors"
"net"
"reflect"
"strings"
"testing"
@@ -171,80 +169,6 @@ here"
}
}
func TestListenTCPForPingbackUsesIPv4Loopback(t *testing.T) {
var calls []string
expected := &stubListener{addr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234}}
actual, err := listenTCPForPingback(func(network, address string) (net.Listener, error) {
calls = append(calls, network+" "+address)
return expected, nil
})
if err != nil {
t.Fatalf("listenTCPForPingback returned error: %v", err)
}
if actual != expected {
t.Fatalf("expected listener %p, got %p", expected, actual)
}
expectCalls := []string{"tcp4 127.0.0.1:0"}
if !reflect.DeepEqual(calls, expectCalls) {
t.Fatalf("expected calls %v, got %v", expectCalls, calls)
}
}
func TestListenTCPForPingbackFallsBackToIPv6Loopback(t *testing.T) {
var calls []string
expected := &stubListener{addr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 1234}}
actual, err := listenTCPForPingback(func(network, address string) (net.Listener, error) {
calls = append(calls, network+" "+address)
if len(calls) == 1 {
return nil, errors.New("ipv4 unavailable")
}
return expected, nil
})
if err != nil {
t.Fatalf("listenTCPForPingback returned error: %v", err)
}
if actual != expected {
t.Fatalf("expected listener %p, got %p", expected, actual)
}
expectCalls := []string{"tcp4 127.0.0.1:0", "tcp6 [::1]:0"}
if !reflect.DeepEqual(calls, expectCalls) {
t.Fatalf("expected calls %v, got %v", expectCalls, calls)
}
}
func TestListenTCPForPingbackReportsBothFailures(t *testing.T) {
_, err := listenTCPForPingback(func(network, address string) (net.Listener, error) {
return nil, errors.New(network + " failed")
})
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "tcp4 failed") ||
!strings.Contains(err.Error(), "tcp6 failed") {
t.Fatalf("expected both listener errors, got: %v", err)
}
}
type stubListener struct {
addr net.Addr
}
func (sl *stubListener) Accept() (net.Conn, error) {
return nil, net.ErrClosed
}
func (sl *stubListener) Close() error {
return nil
}
func (sl *stubListener) Addr() net.Addr {
return sl.addr
}
func Test_isCaddyfile(t *testing.T) {
type args struct {
configFile string
+1 -1
View File
@@ -234,7 +234,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
// not sure why), and since New() should return a pointer
// value, we need to dereference it first
iface := any(modInfo.New())
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Pointer {
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
}
modPkgPath := reflect.TypeOf(iface).PkgPath()
+1 -1
View File
@@ -378,7 +378,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
// value must be a pointer for unmarshaling into concrete type, even if
// the module's concrete type is a slice or map; New() *should* return
// a pointer, otherwise unmarshaling errors or panics will occur
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Pointer {
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr {
log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+
" so we are using reflection to make a pointer instead; please fix this by"+
" using new(Type) or &Type notation in your module's New() function.", id)
+29 -29
View File
@@ -1,26 +1,26 @@
module github.com/caddyserver/caddy/v2
go 1.25.1
go 1.25.0
require (
github.com/BurntSushi/toml v1.6.0
github.com/DeRuina/timberjack v1.4.2
github.com/DeRuina/timberjack v1.4.1
github.com/KimMachineGun/automemlimit v0.7.5
github.com/Masterminds/sprig/v3 v3.3.0
github.com/alecthomas/chroma/v2 v2.24.1
github.com/alecthomas/chroma/v2 v2.23.1
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.25.3
github.com/caddyserver/certmagic v0.25.2
github.com/caddyserver/zerossl v0.1.5
github.com/cloudflare/circl v1.6.3
github.com/dustin/go-humanize v1.0.1
github.com/go-chi/chi/v5 v5.2.5
github.com/google/cel-go v0.28.1
github.com/google/cel-go v0.28.0
github.com/google/uuid v1.6.0
github.com/klauspost/compress v1.18.6
github.com/klauspost/compress v1.18.5
github.com/klauspost/cpuid/v2 v2.3.0
github.com/mholt/acmez/v3 v3.1.6
github.com/prometheus/client_golang v1.23.2
github.com/quic-go/quic-go v0.59.1
github.com/quic-go/quic-go v0.59.0
github.com/smallstep/certificates v0.30.2
github.com/smallstep/nosql v0.8.0
github.com/smallstep/truststore v0.13.0
@@ -30,29 +30,27 @@ require (
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747
github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go.opentelemetry.io/contrib/propagators/autoprop v0.68.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/sdk/metric v1.43.0
go.step.sm/crypto v0.81.0
go.step.sm/crypto v0.77.2
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.28.0
go.uber.org/zap v1.27.1
go.uber.org/zap/exp v0.3.0
golang.org/x/crypto v0.52.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
golang.org/x/net v0.55.0
golang.org/x/crypto v0.50.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
golang.org/x/term v0.43.0
golang.org/x/term v0.42.0
golang.org/x/time v0.15.0
gopkg.in/yaml.v3 v3.0.1
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
@@ -63,16 +61,16 @@ require (
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.19.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/jackc/pgx/v5 v5.9.2 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -89,6 +87,7 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.43.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 // indirect
@@ -105,13 +104,14 @@ require (
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect
go.opentelemetry.io/otel/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
google.golang.org/api v0.277.0 // indirect
google.golang.org/api v0.272.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
)
@@ -129,7 +129,7 @@ require (
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -149,7 +149,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.12.0
github.com/pires/go-proxyproto v0.11.0
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.67.5 // indirect
@@ -168,11 +168,11 @@ require (
go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.45.0
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/grpc v1.81.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
golang.org/x/tools v0.43.0 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
howett.net/plist v1.0.0 // indirect
)
+92 -92
View File
@@ -2,18 +2,18 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE=
cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U=
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=
cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
@@ -28,8 +28,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
github.com/DeRuina/timberjack v1.4.2/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
github.com/DeRuina/timberjack v1.4.1 h1:JftM5HN/ITKehAXjtdbGqN5XZIS1biHm7VSjU0Qbtqg=
github.com/DeRuina/timberjack v1.4.1/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -43,8 +43,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -53,40 +53,40 @@ github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmO
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 h1:zuSf4olLKZW8cF/W9Y5wvGT+/0raY/3kVp49KsGs0QY=
github.com/aws/aws-sdk-go-v2/service/kms v1.51.1/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
@@ -133,8 +133,8 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
@@ -149,8 +149,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -168,8 +168,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
@@ -187,10 +187,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
@@ -205,14 +205,14 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -259,8 +259,8 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhM
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.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
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=
@@ -280,8 +280,8 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
@@ -377,8 +377,8 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o=
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 h1:0D3GFvELGIwQGfC6agLsbrEYSGWZTRTxIXxcQUqrOuk=
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0/go.mod h1:DM2NV7Zb8CcGeVPt6glouY0FAiwZQ/iqgcWExhgWeN8=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 h1:wLGFvNBPqQhzBn0QRBZjrriH8lZ9gqtTz8ufHEjLg7k=
@@ -431,8 +431,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.step.sm/crypto v0.81.0 h1:e+ouzpNt3Xm4dp7HGXhgYB5y4iFik3vh3phHKWmvugU=
go.step.sm/crypto v0.81.0/go.mod h1:fsTizqQeASjTXnbv9O00XtRlIuXRkCdoRiJNyXGQujc=
go.step.sm/crypto v0.77.2 h1:qFjjei+RHc5kP5R7NW9OUWT7SqWIuAOvOkXqg4fNWj8=
go.step.sm/crypto v0.77.2/go.mod h1:W0YJb9onM5l78qgkXIJ2Up6grnwW8EtpCKIza/NCg0o=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -441,8 +441,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
@@ -456,10 +456,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 h1:sQVhWLXbNsa8CTzHOX3IHc7C4Q2JyxI5AweuMQZ/5H0=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -467,8 +467,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -477,8 +477,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -506,8 +506,8 @@ golang.org/x/sys v0.12.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/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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=
@@ -517,8 +517,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -528,8 +528,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -538,21 +538,21 @@ 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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk=
google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
-54
View File
@@ -1,54 +0,0 @@
package internal
import (
"net/http"
"strings"
"go.uber.org/zap/zapcore"
)
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
// Headers with potentially sensitive information (Cookie, Set-Cookie,
// Authorization, and Proxy-Authorization) are logged with empty values.
type LoggableHTTPHeader struct {
http.Header
ShouldLogCredentials bool
}
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
if h.Header == nil {
return nil
}
for key, val := range h.Header {
if !h.ShouldLogCredentials {
switch strings.ToLower(key) {
case "cookie", "set-cookie", "authorization", "proxy-authorization":
val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool.
}
}
enc.AddArray(key, LoggableStringArray(val))
}
return nil
}
// LoggableStringArray makes a slice of strings marshalable for logging.
type LoggableStringArray []string
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
if sa == nil {
return nil
}
for _, s := range sa {
enc.AppendString(s)
}
return nil
}
// Interface guards
var (
_ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil)
_ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil)
)
+14 -10
View File
@@ -30,6 +30,10 @@ import (
"go.uber.org/zap"
)
func reuseUnixSocket(_, _ string) (any, error) {
return nil, nil
}
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
var socketFile *os.File
@@ -116,8 +120,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
// re-wrapped in a new fakeCloseListener each time the listener
// is reused. This type is atomic and values must not be copied.
type fakeCloseListener struct {
closed atomic.Bool
*sharedListener // embedded, so we also become a net.Listener
closed int32 // accessed atomically; belongs to this struct only
*sharedListener // embedded, so we also become a net.Listener
keepAliveConfig net.KeepAliveConfig
}
@@ -127,7 +131,7 @@ type canSetKeepAliveConfig interface {
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// if the listener is already "closed", return error
if fcl.closed.Load() {
if atomic.LoadInt32(&fcl.closed) == 1 {
return nil, fakeClosedErr(fcl)
}
@@ -151,7 +155,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// that we set when Close() was called, and return a non-temporary and
// non-timeout error value to the caller, masking the "true" error, so
// that server loops / goroutines won't retry, linger, and leak
if fcl.closed.Load() {
if atomic.LoadInt32(&fcl.closed) == 1 {
// we dereference the sharedListener explicitly even though it's embedded
// so that it's clear in the code that side-effects are shared with other
// users of this listener, not just our own reference to it; we also don't
@@ -171,7 +175,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// underlying listener. The underlying listener is only closed
// if the caller is the last known user of the socket.
func (fcl *fakeCloseListener) Close() error {
if fcl.closed.CompareAndSwap(false, true) {
if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) {
// There are two ways I know of to get an Accept()
// function to return to the server loop that called
// it: close the listener, or set a deadline in the
@@ -234,13 +238,13 @@ func (sl *sharedListener) Destruct() error {
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
// or more specifically, *net.UDPConn
type fakeClosePacketConn struct {
closed atomic.Bool
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
closed int32 // accessed atomically; belongs to this struct only
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
}
func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
// if the listener is already "closed", return error
if fcpc.closed.Load() {
if atomic.LoadInt32(&fcpc.closed) == 1 {
return 0, nil, &net.OpError{
Op: "readfrom",
Net: fcpc.LocalAddr().Network(),
@@ -254,7 +258,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
if err != nil {
// this server was stopped, so clear the deadline and let
// any new server continue reading; but we will exit
if fcpc.closed.Load() {
if atomic.LoadInt32(&fcpc.closed) == 1 {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
return n, addr, err
@@ -269,7 +273,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
func (fcpc *fakeClosePacketConn) Close() error {
if fcpc.closed.CompareAndSwap(false, true) {
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
}
-21
View File
@@ -1,21 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build (!unix || solaris) && !windows
package caddy
func reuseUnixSocket(_, _ string) (any, error) {
return nil, nil
}
-89
View File
@@ -1,89 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package caddy
import (
"errors"
"fmt"
"io/fs"
"net"
"os"
"strings"
"syscall"
"time"
)
var errUnixSocketAlreadyInUse = errors.New("unix socket is already in use by another process")
func reuseUnixSocket(network, addr string) (any, error) {
if !IsUnixNetwork(network) {
return nil, nil
}
// Note: This is here mainly for proper compatibility, because Unix sockets with abstract names are in an interesting limbo state on Windows:
// Go already translates `@` characters to `\0` for Windows: https://github.com/golang/go/blob/65d5c5f6dd8aa7b221cff6ec3f5101ea2e5f3efa/src/syscall/syscall_windows.go#L910
// ...but there still is an open issue about the fact that this is not properly supported: https://github.com/microsoft/WSL/issues/4240#issuecomment-620805115
// The main issue is that the original announcement proclaimed support for this feature, but it was (apparently) never implemented: https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/
isAbstractUnixSocket := strings.HasPrefix(addr, "@")
if isAbstractUnixSocket {
// Abstract Unix sockets do not require us to remove stale socket files.
return nil, nil
}
// On Windows, we're using the `fakeCloseListener` wrappers around a single, ever-living listener.
// So, if there's an active listener entry in the pool, we're the current owner of the Unix socket file.
_, socketBelongsToCurrentProcess := listenerPool.References(listenerKey(network, addr))
if socketBelongsToCurrentProcess {
// Reuse/cleanup is entirely handled by the refcounting mechanism in `listenerPool`.
return nil, nil
}
// If the socket file does not exist or has no backing server process, this will fail instantly.
connection, err := net.DialTimeout("unix", addr, 10*time.Millisecond)
if err == nil {
connection.Close()
return nil, fmt.Errorf("cannot reuse socket %v: %w", addr, errUnixSocketAlreadyInUse)
}
// Windows returns this error code both if the socket file does not exist and if it isn't backed by a server process anymore.
// See: https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2#wsaeconnrefused
const WSAECONNREFUSED syscall.Errno = 10061
var errno syscall.Errno
hasNoListeningServerProcess := errors.As(err, &errno) && errno == WSAECONNREFUSED
if !hasNoListeningServerProcess {
return nil, fmt.Errorf("cannot reuse socket %v: %w", addr, errUnixSocketAlreadyInUse)
}
// If the socket file exists, it hasn't been created by our process, and it seemingly
// isn't backed by a server process anymore. Try to delete it so we can bind to it later.
err = os.Remove(addr)
if err == nil {
return nil, nil
} else if errors.Is(err, fs.ErrNotExist) {
// Either the file didn't exist in the first place, or it was deleted before we were able to.
return nil, nil
} else {
// We failed to delete the file. Likely, it belongs to another (active) process.
return nil, err
}
}
+10 -12
View File
@@ -63,7 +63,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
if err != nil {
return nil, err
}
unixSocket.count.Add(1)
atomic.AddInt32(unixSocket.count, 1)
unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
case *unixConn:
@@ -71,7 +71,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
if err != nil {
return nil, err
}
unixSocket.count.Add(1)
atomic.AddInt32(unixSocket.count, 1)
unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
}
@@ -165,9 +165,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
if !fd {
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so...
if unix, ok := ln.(*net.UnixConn); ok {
cnt := new(atomic.Int32)
cnt.Store(1)
ln = &unixConn{unix, lnKey, cnt}
one := int32(1)
ln = &unixConn{unix, lnKey, &one}
unixSockets[lnKey] = ln.(*unixConn)
}
}
@@ -182,9 +181,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
// (we do our own "unlink on close" -- not required, but more tidy)
if unix, ok := ln.(*net.UnixListener); ok {
unix.SetUnlinkOnClose(false)
cnt := new(atomic.Int32)
cnt.Store(1)
ln = &unixListener{unix, lnKey, cnt}
one := int32(1)
ln = &unixListener{unix, lnKey, &one}
unixSockets[lnKey] = ln.(*unixListener)
}
}
@@ -218,11 +216,11 @@ func reusePort(network, address string, conn syscall.RawConn) error {
type unixListener struct {
*net.UnixListener
mapKey string
count *atomic.Int32
count *int32 // accessed atomically
}
func (uln *unixListener) Close() error {
newCount := uln.count.Add(-1)
newCount := atomic.AddInt32(uln.count, -1)
if newCount == 0 {
file, err := uln.File()
var name string
@@ -244,11 +242,11 @@ func (uln *unixListener) Close() error {
type unixConn struct {
*net.UnixConn
mapKey string
count *atomic.Int32
count *int32 // accessed atomically
}
func (uc *unixConn) Close() error {
newCount := uc.count.Add(-1)
newCount := atomic.AddInt32(uc.count, -1)
if newCount == 0 {
file, err := uc.File()
var name string
+6 -19
View File
@@ -462,10 +462,7 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
sqs := newSharedQUICState(tlsConf)
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
//nolint:gosec
quicTlsConfig := &tls.Config{
GetConfigForClient: sqs.getConfigForClient,
GetEncryptedClientHelloKeys: sqs.getEncryptedClientHelloKeys,
}
quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient}
// Require clients to verify their source address when we're handling more than 1000 handshakes per second.
// TODO: make tunable?
limiter := rate.NewLimiter(1000, 1000)
@@ -543,16 +540,6 @@ func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Co
return sqs.activeTlsConf.GetConfigForClient(ch)
}
// getEncryptedClientHelloKeys is used as tls.Config's GetEncryptedClientHelloKeys field.
func (sqs *sharedQUICState) getEncryptedClientHelloKeys(ch *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
sqs.rmu.RLock()
defer sqs.rmu.RUnlock()
if sqs.activeTlsConf.GetEncryptedClientHelloKeys == nil {
return nil, nil
}
return sqs.activeTlsConf.GetEncryptedClientHelloKeys(ch)
}
// addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc
// so that when cancelled, the active tls.Config will change
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) {
@@ -624,8 +611,8 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉")
type fakeCloseQuicListener struct {
closed atomic.Int32
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
closed int32 // accessed atomically; belongs to this struct only
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
context context.Context
contextCancel context.CancelCauseFunc
}
@@ -642,16 +629,16 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
}
// if the listener is "closed", return a fake closed error instead
if fcql.closed.Load() == 1 && errors.Is(err, context.Canceled) {
if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) {
return nil, fakeClosedErr(fcql)
}
return nil, err
}
func (fcql *fakeCloseQuicListener) Close() error {
if fcql.closed.CompareAndSwap(0, 1) {
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
fcql.contextCancel(errFakeClosed)
} else if fcql.closed.CompareAndSwap(1, 2) {
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
}
return nil
-58
View File
@@ -15,7 +15,6 @@
package caddy
import (
"crypto/tls"
"reflect"
"testing"
@@ -176,63 +175,6 @@ func TestJoinNetworkAddress(t *testing.T) {
}
}
func TestSharedQUICStateGetEncryptedClientHelloKeys(t *testing.T) {
hello := &tls.ClientHelloInfo{ServerName: "example.com"}
initialKeys := []tls.EncryptedClientHelloKey{{Config: []byte("initial"), PrivateKey: []byte("initial-key")}}
updatedKeys := []tls.EncryptedClientHelloKey{{Config: []byte("updated"), PrivateKey: []byte("updated-key")}}
initialConfig := &tls.Config{
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
return nil, nil
},
GetEncryptedClientHelloKeys: func(*tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
return initialKeys, nil
},
}
sqs := newSharedQUICState(initialConfig)
keys, err := sqs.getEncryptedClientHelloKeys(hello)
if err != nil {
t.Fatalf("getting initial ECH keys: %v", err)
}
if !reflect.DeepEqual(keys, initialKeys) {
t.Fatalf("unexpected initial ECH keys: got %#v, want %#v", keys, initialKeys)
}
updatedConfig := &tls.Config{
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
return nil, nil
},
GetEncryptedClientHelloKeys: func(*tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
return updatedKeys, nil
},
}
_, cancel := sqs.addState(updatedConfig)
sqs.rmu.Lock()
sqs.activeTlsConf = updatedConfig
sqs.rmu.Unlock()
keys, err = sqs.getEncryptedClientHelloKeys(hello)
if err != nil {
t.Fatalf("getting updated ECH keys: %v", err)
}
if !reflect.DeepEqual(keys, updatedKeys) {
t.Fatalf("unexpected updated ECH keys: got %#v, want %#v", keys, updatedKeys)
}
cancel(nil)
keys, err = sqs.getEncryptedClientHelloKeys(hello)
if err != nil {
t.Fatalf("getting restored ECH keys: %v", err)
}
if !reflect.DeepEqual(keys, initialKeys) {
t.Fatalf("unexpected restored ECH keys: got %#v, want %#v", keys, initialKeys)
}
}
func TestParseNetworkAddress(t *testing.T) {
for i, tc := range []struct {
input string
+37 -17
View File
@@ -20,6 +20,7 @@ import (
"crypto/tls"
"errors"
"fmt"
"maps"
"net"
"net/http"
"strconv"
@@ -68,7 +69,6 @@ func init() {
// `{http.request.orig_uri.path.dir}` | The request's original directory
// `{http.request.orig_uri.path.file}` | The request's original filename
// `{http.request.orig_uri.query}` | The request's original query string (without `?`)
// `{http.request.orig_uri.prefixed_query}` | The request's original query string with a `?` prefix, if non-empty
// `{http.request.port}` | The port part of the request's Host header
// `{http.request.proto}` | The protocol of the request
// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
@@ -98,15 +98,11 @@ func init() {
// `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional)
// `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional)
// `{http.request.uri}` | The full request URI
// `{http.request.uri_escaped}` | The full request URI with query-style URL encoding applied (using url.QueryEscape)
// `{http.request.uri.path}` | The path component of the request URI
// `{http.request.uri.path_escaped}` | The path component of the request URI with query-style URL encoding applied (using url.QueryEscape)
// `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left)
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename
// `{http.request.uri.path.file}` | The filename of the path, excluding directory
// `{http.request.uri.query}` | The query string (without `?`)
// `{http.request.uri.query_escaped}` | The query string with query-style URL encoding applied (using url.QueryEscape)
// `{http.request.uri.prefixed_query}` | The query string with a `?` prefix, if non-empty
// `{http.request.uri.query.*}` | Individual query string value
// `{http.response.header.*}` | Specific response header field
// `{http.vars.*}` | Custom variables in the HTTP handler chain
@@ -207,9 +203,6 @@ func (app *App) Provision(ctx caddy.Context) error {
app.Metrics.httpMetrics = &httpMetrics{}
// Scan config for allowed hosts to prevent cardinality explosion
app.Metrics.scanConfigForHosts(app)
if err := app.Metrics.provisionOTLP(ctx); err != nil {
return err
}
}
// prepare each server
oldContext := ctx.Context
@@ -221,6 +214,8 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.ctx = ctx
srv.logger = app.logger.Named("log")
srv.errorLogger = app.logger.Named("log.error")
srv.shutdownAtMu = new(sync.RWMutex)
if srv.Metrics != nil {
srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead")
app.Metrics = cmp.Or(app.Metrics, &Metrics{
@@ -240,7 +235,12 @@ func (app *App) Provision(ctx caddy.Context) error {
// if no protocols configured explicitly, enable all except h2c
if len(srv.Protocols) == 0 {
srv.Protocols = srv.protocolsWithDefaults()
srv.Protocols = []string{"h1", "h2", "h3"}
}
srvProtocolsUnique := map[string]struct{}{}
for _, srvProtocol := range srv.Protocols {
srvProtocolsUnique[srvProtocol] = struct{}{}
}
if srv.ListenProtocols != nil {
@@ -251,7 +251,31 @@ func (app *App) Provision(ctx caddy.Context) error {
for i, lnProtocols := range srv.ListenProtocols {
if lnProtocols != nil {
srv.ListenProtocols[i] = srv.listenerProtocolsWithDefaults(lnProtocols)
// populate empty listen protocols with server protocols
lnProtocolsDefault := false
var lnProtocolsInclude []string
srvProtocolsInclude := maps.Clone(srvProtocolsUnique)
// keep existing listener protocols unless they are empty
for _, lnProtocol := range lnProtocols {
if lnProtocol == "" {
lnProtocolsDefault = true
} else {
lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
delete(srvProtocolsInclude, lnProtocol)
}
}
// append server protocols to listener protocols if any listener protocols were empty
if lnProtocolsDefault {
for _, srvProtocol := range srv.Protocols {
if _, ok := srvProtocolsInclude[srvProtocol]; ok {
lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
}
}
}
srv.ListenProtocols[i] = lnProtocolsInclude
}
}
}
@@ -665,7 +689,9 @@ func (app *App) Stop() error {
for _, addr := range na.Expand() {
if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 {
app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String()))
server.shutdownAt.Store(&scheduledTime)
server.shutdownAtMu.Lock()
server.shutdownAt = scheduledTime
server.shutdownAtMu.Unlock()
delay = true
} else {
app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String()))
@@ -790,12 +816,6 @@ func (app *App) Stop() error {
}
}
// flush and shut down the OTLP metrics exporter (if configured) so any
// last data point reaches the collector before the process exits
if err := app.Metrics.shutdown(ctx); err != nil {
app.logger.Error("shutting down OTLP metrics", zap.Error(err))
}
app.stopped = true
return nil
}
+8 -46
View File
@@ -173,7 +173,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
for d := range serverDomainSet {
echDomains = append(echDomains, d)
}
app.tlsApp.RegisterServerNames(echDomains, httpsRRALPNs(srv))
app.tlsApp.RegisterServerNames(echDomains)
// nothing more to do here if there are no domains that qualify for
// automatic HTTPS and there are no explicit TLS connection policies:
@@ -258,13 +258,18 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// an empty string to indicate a catch-all, which we have to
// treat special later
if len(serverDomainSet) == 0 {
app.recordAutoHTTPSRedirectAddress(redirDomains, "", addr)
redirDomains[""] = append(redirDomains[""], addr)
continue
}
// ...and associate it with each domain in this server
for d := range serverDomainSet {
app.recordAutoHTTPSRedirectAddress(redirDomains, d, addr)
// if this domain is used on more than one HTTPS-enabled
// port, we'll have to choose one, so prefer the HTTPS port
if _, ok := redirDomains[d]; !ok ||
addr.StartPort == uint(app.httpsPort()) {
redirDomains[d] = append(redirDomains[d], addr)
}
}
}
}
@@ -512,35 +517,6 @@ redirServersLoop:
return nil
}
// recordAutoHTTPSRedirectAddress stores redirect destinations for one domain
// using a single winning port while keeping all bind addresses on that port.
//
// This is needed to avoid two opposite regressions in auto-HTTPS redirects:
// preserve all listener addresses when a site binds multiple addresses on the
// same HTTPS port, but do not mix in alternate HTTPS ports when the canonical
// app HTTPS port is also available.
func (app *App) recordAutoHTTPSRedirectAddress(redirDomains map[string][]caddy.NetworkAddress, domain string, addr caddy.NetworkAddress) {
existing := redirDomains[domain]
if len(existing) == 0 {
redirDomains[domain] = []caddy.NetworkAddress{addr}
return
}
existingPort := existing[0].StartPort
if addr.StartPort != existingPort {
if addr.StartPort == uint(app.httpsPort()) && existingPort != uint(app.httpsPort()) {
redirDomains[domain] = []caddy.NetworkAddress{addr}
}
return
}
if slices.Contains(existing, addr) {
return
}
redirDomains[domain] = append(existing, addr)
}
func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
redirTo := "https://{http.request.host}"
@@ -574,20 +550,6 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
}
}
func httpsRRALPNs(srv *Server) []string {
alpn := make(map[string]struct{}, 3)
if srv.protocol("h3") {
alpn["h3"] = struct{}{}
}
if srv.protocol("h2") {
alpn["h2"] = struct{}{}
}
if srv.protocol("h1") {
alpn["http/1.1"] = struct{}{}
}
return caddytls.OrderedHTTPSRRALPN(alpn)
}
// createAutomationPolicies ensures that automated certificates for this
// app are managed properly. This adds up to two automation policies:
// one for the public names, and one for the internal names. If a catch-all
-47
View File
@@ -1,47 +0,0 @@
package caddyhttp
import (
"reflect"
"testing"
)
func TestHTTPSRRALPNsDefaultProtocols(t *testing.T) {
srv := &Server{}
got := httpsRRALPNs(srv)
want := []string{"h3", "h2", "http/1.1"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
}
}
func TestHTTPSRRALPNsListenProtocolOverrides(t *testing.T) {
srv := &Server{
Protocols: []string{"h1", "h2"},
ListenProtocols: [][]string{
{"h1"},
nil,
{},
{"h3", ""},
},
}
got := httpsRRALPNs(srv)
want := []string{"h3", "h2", "http/1.1"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
}
}
func TestHTTPSRRALPNsIgnoresH2COnly(t *testing.T) {
srv := &Server{
Protocols: []string{"h2c"},
}
got := httpsRRALPNs(srv)
if len(got) != 0 {
t.Fatalf("unexpected ALPN values: got %v want none", got)
}
}
+4 -27
View File
@@ -37,12 +37,6 @@ func init() {
// `{http.auth.user.*}` placeholders may be set for any authentication
// modules that provide user metadata.
//
// If authentication is rejected but a provider returns user information,
// the placeholder `{http.auth.candidate.id}` will be set to the candidate
// username, and also `{http.auth.candidate.*}` placeholders may be set
// for candidate user metadata. Candidate placeholders do not represent a
// successfully authenticated principal.
//
// In case of an error, the placeholder `{http.auth.<provider>.error}`
// will be set to the error message returned by the authentication
// provider.
@@ -84,8 +78,6 @@ func (a *Authentication) Provision(ctx caddy.Context) error {
func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var user User
var candidate User
var hasCandidate bool
var authed bool
var err error
for provName, prov := range a.Providers {
@@ -102,34 +94,19 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
if authed {
break
}
if userHasInfo(user) {
candidate = user
hasCandidate = true
}
}
if !authed {
if hasCandidate {
setAuthUserPlaceholders(repl, "http.auth.candidate", candidate)
}
return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated"))
}
setAuthUserPlaceholders(repl, "http.auth.user", user)
repl.Set("http.auth.user.id", user.ID)
for k, v := range user.Metadata {
repl.Set("http.auth.user."+k, v)
}
return next.ServeHTTP(w, r)
}
func userHasInfo(user User) bool {
return user.ID != "" || len(user.Metadata) > 0
}
func setAuthUserPlaceholders(repl *caddy.Replacer, namespace string, user User) {
repl.Set(namespace+".id", user.ID)
for k, v := range user.Metadata {
repl.Set(namespace+"."+k, v)
}
}
// Authenticator is a type which can authenticate a request.
// If a request was not authenticated, it returns false. An
// error is only returned if authenticating the request fails
@@ -1,197 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyauth
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestAuthenticationRejectedUserSetsCandidatePlaceholders(t *testing.T) {
auth := Authentication{
Providers: map[string]Authenticator{
"test": staticAuthenticator{
user: User{
ID: "alice",
Metadata: map[string]string{
"role": "admin",
},
},
},
},
logger: zap.NewNop(),
}
req, repl := newRequestWithReplacer()
nextCalled := false
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
nextCalled = true
return nil
}))
if err == nil {
t.Fatal("expected authentication error")
}
var handlerErr caddyhttp.HandlerError
if !errors.As(err, &handlerErr) {
t.Fatalf("expected HandlerError, got %T", err)
}
if handlerErr.StatusCode != http.StatusUnauthorized {
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, handlerErr.StatusCode)
}
if nextCalled {
t.Fatal("next handler was called for rejected authentication")
}
assertPlaceholder(t, repl, "http.auth.candidate.id", "alice")
assertPlaceholder(t, repl, "http.auth.candidate.role", "admin")
assertPlaceholderAbsent(t, repl, "http.auth.user.id")
assertPlaceholderAbsent(t, repl, "http.auth.user.role")
}
func TestAuthenticationSuccessfulUserSetsUserPlaceholdersOnly(t *testing.T) {
auth := Authentication{
Providers: map[string]Authenticator{
"test": staticAuthenticator{
user: User{
ID: "alice",
Metadata: map[string]string{
"role": "admin",
},
},
authed: true,
},
},
logger: zap.NewNop(),
}
req, repl := newRequestWithReplacer()
nextCalled := false
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
nextCalled = true
return nil
}))
if err != nil {
t.Fatalf("expected no authentication error, got %v", err)
}
if !nextCalled {
t.Fatal("next handler was not called for successful authentication")
}
assertPlaceholder(t, repl, "http.auth.user.id", "alice")
assertPlaceholder(t, repl, "http.auth.user.role", "admin")
assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
assertPlaceholderAbsent(t, repl, "http.auth.candidate.role")
}
func TestAuthenticationSuccessfulProviderDoesNotExposeEarlierCandidate(t *testing.T) {
auth := Authentication{
Providers: map[string]Authenticator{
"first": staticAuthenticator{
user: User{
ID: "rejected",
Metadata: map[string]string{
"role": "guest",
},
},
},
"second": staticAuthenticator{
user: User{
ID: "accepted",
Metadata: map[string]string{
"role": "admin",
},
},
authed: true,
},
},
logger: zap.NewNop(),
}
req, repl := newRequestWithReplacer()
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
return nil
}))
if err != nil {
t.Fatalf("expected no authentication error, got %v", err)
}
assertPlaceholder(t, repl, "http.auth.user.id", "accepted")
assertPlaceholder(t, repl, "http.auth.user.role", "admin")
assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
assertPlaceholderAbsent(t, repl, "http.auth.candidate.role")
}
func TestAuthenticationRejectedEmptyUserDoesNotSetCandidatePlaceholders(t *testing.T) {
auth := Authentication{
Providers: map[string]Authenticator{
"test": staticAuthenticator{},
},
logger: zap.NewNop(),
}
req, repl := newRequestWithReplacer()
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
t.Fatal("next handler was called for rejected authentication")
return nil
}))
if err == nil {
t.Fatal("expected authentication error")
}
assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
}
func newRequestWithReplacer() (*http.Request, *caddy.Replacer) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
return req.WithContext(ctx), repl
}
func assertPlaceholder(t *testing.T, repl *caddy.Replacer, key, expected string) {
t.Helper()
actual, ok := repl.GetString(key)
if !ok {
t.Fatalf("expected placeholder %q to be set", key)
}
if actual != expected {
t.Fatalf("expected placeholder %q to be %q, got %q", key, expected, actual)
}
}
func assertPlaceholderAbsent(t *testing.T, repl *caddy.Replacer, key string) {
t.Helper()
if actual, ok := repl.GetString(key); ok {
t.Fatalf("expected placeholder %q to be absent, got %q", key, actual)
}
}
type staticAuthenticator struct {
user User
authed bool
err error
}
func (a staticAuthenticator) Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) {
return a.user, a.authed, a.err
}
+3 -3
View File
@@ -108,7 +108,7 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &m.Expr)
}
// otherwise, it's a full object, so unmarshal it,
// using a temp map to avoid infinite recursion
// using an temp map to avoid infinite recursion
var tmpJson map[string]any
err := json.Unmarshal(data, &tmpJson)
*m = MatchExpression{
@@ -118,7 +118,7 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
return err
}
// Provision sets up m.
// Provision sets ups m.
func (m *MatchExpression) Provision(ctx caddy.Context) error {
m.log = ctx.Logger()
@@ -319,7 +319,7 @@ func (cr celHTTPRequest) Value() any { return cr }
var pkixNameCELType = cel.ObjectType("pkix.Name", traits.ReceiverType)
// celPkixName wraps a pkix.Name with
// celPkixName wraps an pkix.Name with
// methods to satisfy the ref.Val interface.
type celPkixName struct{ *pkix.Name }
+1 -1
View File
@@ -79,7 +79,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
wantResult: true,
},
{
name: "header matches a placeholder replaced during the header matcher (MatchHeader)",
name: "header matches an placeholder replaced during the header matcher (MatchHeader)",
expression: &MatchExpression{
Expr: `header({'Field': '\{http.request.uri.path}'})`,
},
+2 -2
View File
@@ -162,7 +162,7 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
// to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding
// by appending a hyphen and the encoder name; the problem is, the client will
// send back that Etag in an If-None-Match header, but upstream handlers that set
// send back that Etag in a If-None-Match header, but upstream handlers that set
// the Etag in the first place don't know that we appended to their Etag! so here
// we have to strip our addition so the upstream handlers can still honor client
// caches without knowing about our changes...
@@ -369,7 +369,7 @@ const sniffLen = 512
// ReadFrom will try to use sendfile to copy from the reader to the response writer.
// It's only used if the response writer implements io.ReaderFrom and the data can't be compressed.
// It's based on the standard library HTTP/1.1 response writer implementation.
// It's based on stdlin http1.1 response writer implementation.
// https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/net/http/server.go#L586
func (rw *responseWriter) ReadFrom(r io.Reader) (int64, error) {
rf, ok := rw.ResponseWriter.(io.ReaderFrom)
+2 -14
View File
@@ -281,13 +281,7 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
sortParam = sortCookie.Value
}
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS
Name: "sort",
Value: sortParam,
Secure: r.TLS != nil,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil})
}
// then figure out the order
@@ -298,13 +292,7 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
orderParam = orderCookie.Value
}
case sortOrderAsc, sortOrderDesc:
http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS
Name: "order",
Value: orderParam,
Secure: r.TLS != nil,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil})
}
// finally, apply the sorting and limiting
@@ -28,7 +28,6 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/internal/filesystems"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
)
type testCase struct {
@@ -189,105 +188,6 @@ func fileMatcherTest(t *testing.T, i int, tc testCase) {
}
}
func TestTryFilesRewriteEscapesMatchedPath(t *testing.T) {
root := t.TempDir()
tests := []struct {
name string
requestTarget string
filename string
extraFiles []string
wantPath string
wantRequestURI string
skipWindows bool
}{
{
name: "question mark in path",
requestTarget: "/%3F.html",
filename: "?.html",
wantPath: "/?.html",
wantRequestURI: "/%3F.html",
skipWindows: true,
},
{
name: "percent in path",
requestTarget: "/%25.html",
filename: "%.html",
wantPath: "/%.html",
wantRequestURI: "/%25.html",
},
{
name: "encoded question mark remains percent-encoded",
requestTarget: "/%253F.html",
filename: "%3F.html",
wantPath: "/%3F.html",
wantRequestURI: "/%253F.html",
},
{
name: "question mark in nested path",
requestTarget: "/nested/%3F.html",
filename: filepath.Join("nested", "?.html"),
wantPath: "/nested/?.html",
wantRequestURI: "/nested/%3F.html",
skipWindows: true,
},
{
name: "encoded slash in filename does not conflict with nesting",
requestTarget: "/nested%252Ffile.html",
filename: "nested%2Ffile.html",
extraFiles: []string{filepath.Join("nested", "file.html")},
wantPath: "/nested%2Ffile.html",
wantRequestURI: "/nested%252Ffile.html",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.skipWindows && runtime.GOOS == "windows" {
t.Skip("Windows file names cannot contain question marks")
}
for _, name := range append([]string{tc.filename}, tc.extraFiles...) {
filename := filepath.Join(root, name)
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
t.Fatalf("creating test file parent directory: %v", err)
}
if err := os.WriteFile(filename, []byte(name), 0o600); err != nil {
t.Fatalf("writing test file: %v", err)
}
}
m := &MatchFile{
fsmap: &filesystems.FileSystemMap{},
Root: root,
TryFiles: []string{"{http.request.uri.path}"},
}
req := httptest.NewRequest(http.MethodGet, "http://example.com"+tc.requestTarget, nil)
repl := caddyhttp.NewTestReplacer(req)
matched, err := m.MatchWithError(req)
if err != nil {
t.Fatalf("matching file: %v", err)
}
if !matched {
t.Fatalf("expected request %s to match %s", tc.requestTarget, tc.filename)
}
rewrite.Rewrite{URI: "{http.matchers.file.relative}"}.Rewrite(req, repl)
if req.URL.Path != tc.wantPath {
t.Errorf("rewritten path = %q, want %q", req.URL.Path, tc.wantPath)
}
if req.RequestURI != tc.wantRequestURI {
t.Errorf("rewritten request URI = %q, want %q", req.RequestURI, tc.wantRequestURI)
}
if req.URL.RawQuery != "" {
t.Errorf("rewritten raw query = %q, want empty", req.URL.RawQuery)
}
})
}
}
func TestPHPFileMatcher(t *testing.T) {
for i, tc := range []struct {
path string
+5 -24
View File
@@ -29,7 +29,6 @@ import (
"runtime"
"strconv"
"strings"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@@ -124,7 +123,7 @@ type FileServer struct {
// put "hidden" in the list. To hide only ./hidden, put "./hidden" in the list.
//
// When possible, all paths are resolved to their absolute form before
// comparisons are made. For maximum clarity and explicitness, use complete,
// comparisons are made. For maximum clarity and explictness, use complete,
// absolute paths; or, for greater portability, use relative paths instead.
//
// Note that hide comparisons are case-sensitive. On case-insensitive
@@ -580,17 +579,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
// that errors generated by ServeContent are written immediately
// to the response, so we cannot handle them (but errors there
// are rare)
//
// There are a few file modification times that aren't useful
// to send in Last-Modified headers, but the golang http library only
// omits Last-Modified headers for the Unix epoch time. So, force
// the modification time to the epoch time if it's not useful.
zeroTime := time.Time{}
modTime := info.ModTime()
if !usefulModTime(modTime) {
modTime = zeroTime
}
http.ServeContent(w, r, info.Name(), modTime, file.(io.ReadSeeker))
http.ServeContent(w, r, info.Name(), info.ModTime(), file.(io.ReadSeeker))
return nil
}
@@ -737,14 +726,6 @@ func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next ca
return caddyhttp.Error(http.StatusNotFound, nil)
}
// Indicates whether a file's modification time is useful for validator
// generation purposes (i.e. inclusion in ETag and Last-Modified headers).
// See issues #5548 and #7730.
func usefulModTime(modTime time.Time) bool {
mtimeunix := modTime.Unix()
return mtimeunix != 0 && mtimeunix != 1
}
// calculateEtag computes an entity tag using a strong validator
// without consuming the contents of the file. It requires the
// file info contain the correct size and modification time.
@@ -762,8 +743,8 @@ func usefulModTime(modTime time.Time) bool {
// which we consider precise enough to qualify as a strong validator.
func calculateEtag(d os.FileInfo) string {
mtime := d.ModTime()
if !usefulModTime(mtime) {
return ""
if mtimeUnix := mtime.Unix(); mtimeUnix == 0 || mtimeUnix == 1 {
return "" // not useful anyway; see issue #5548
}
var sb strings.Builder
sb.WriteRune('"')
@@ -804,7 +785,7 @@ func redirect(w http.ResponseWriter, r *http.Request, toPath string) error {
if r.URL.RawQuery != "" {
toPath += "?" + r.URL.RawQuery
}
http.Redirect(w, r, toPath, http.StatusPermanentRedirect) //nolint:gosec // toPath is a same-origin path and leading // is stripped above
http.Redirect(w, r, toPath, http.StatusPermanentRedirect)
return nil
}
@@ -15,17 +15,10 @@
package fileserver
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2"
)
func TestFileHidden(t *testing.T) {
@@ -135,52 +128,3 @@ func TestFileHidden(t *testing.T) {
}
}
}
// Check to make sure that we don't serve ETag and Last-Modified headers
// for files with invalid modification times
func TestModTimeHeaders(t *testing.T) {
check_validator_headers(time.Now(), true, t)
check_validator_headers(time.Unix(0, 0), false, t)
check_validator_headers(time.Unix(1, 0), false, t)
check_validator_headers(time.Unix(2, 0), true, t)
}
func check_validator_headers(modTime time.Time, expect_headers bool, t *testing.T) {
f := false
fsrv := FileServer{
Root: "./testdata",
CanonicalURIs: &f,
}
w := httptest.NewRecorder()
r, err := http.NewRequest("GET", "/modtime.txt", nil)
if err != nil {
t.Fatal(err)
}
repl := caddy.NewReplacer()
ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
r = r.WithContext(ctx)
ctx2, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) // module will be nil by default
fsrv.Provision(ctx2)
path := "testdata/modtime.txt"
os.Chtimes(path, modTime, modTime)
fsrv.ServeHTTP(w, r, nil)
if expect_headers {
if w.Header().Get("ETag") == "" {
t.Errorf("Didn't get ETag header for file with valid mod time %s", modTime)
}
if w.Header().Get("Last-Modified") == "" {
t.Errorf("Didn't get Last-Modified header for file with valid mod time %s", modTime)
}
} else {
if w.Header().Get("ETag") != "" {
t.Errorf("Got ETag header for file with invalid mod time %s", modTime)
}
if w.Header().Get("Last-Modified") != "" {
t.Errorf("Got Last-Modified header for file with invalid mod time %s", modTime)
}
}
}
View File
+1 -1
View File
@@ -15,7 +15,7 @@ type connectionStater interface {
// http2Listener wraps the listener to solve the following problems:
// 1. prevent genuine h2c connections from succeeding if h2c is not enabled
// and the connection doesn't implement connectionStater or the resulting NegotiatedProtocol
// and the connection doesn't implment connectionStater or the resulting NegotiatedProtocol
// isn't http2.
// This does allow a connection to pass as tls enabled even if it's not, listener wrappers
// can do this.
+1 -1
View File
@@ -101,7 +101,7 @@ type httpRedirectConn struct {
// Read tries to peek at the first few bytes of the request, and if we get
// an error reading the headers, and that error was due to the bytes looking
// like an HTTP request, then we perform an HTTP->HTTPS redirect on the same
// 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) {
if c.once {
+41 -6
View File
@@ -18,10 +18,9 @@ import (
"crypto/tls"
"net"
"net/http"
"strings"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2/internal"
)
// LoggableHTTPRequest makes an HTTP request loggable with zap.Object().
@@ -48,12 +47,12 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("method", r.Method)
enc.AddString("host", r.Host)
enc.AddString("uri", r.RequestURI)
enc.AddObject("headers", internal.LoggableHTTPHeader{
enc.AddObject("headers", LoggableHTTPHeader{
Header: r.Header,
ShouldLogCredentials: r.ShouldLogCredentials,
})
if r.TransferEncoding != nil {
enc.AddArray("transfer_encoding", internal.LoggableStringArray(r.TransferEncoding))
enc.AddArray("transfer_encoding", LoggableStringArray(r.TransferEncoding))
}
if r.TLS != nil {
enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
@@ -62,10 +61,44 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
}
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
type LoggableHTTPHeader = internal.LoggableHTTPHeader
// Headers with potentially sensitive information (Cookie, Set-Cookie,
// Authorization, and Proxy-Authorization) are logged with empty values.
type LoggableHTTPHeader struct {
http.Header
ShouldLogCredentials bool
}
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
if h.Header == nil {
return nil
}
for key, val := range h.Header {
if !h.ShouldLogCredentials {
switch strings.ToLower(key) {
case "cookie", "set-cookie", "authorization", "proxy-authorization":
val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool.
}
}
enc.AddArray(key, LoggableStringArray(val))
}
return nil
}
// LoggableStringArray makes a slice of strings marshalable for logging.
type LoggableStringArray = internal.LoggableStringArray
type LoggableStringArray []string
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
if sa == nil {
return nil
}
for _, s := range sa {
enc.AppendString(s)
}
return nil
}
// LoggableTLSConnState makes a TLS connection state loggable with zap.Object().
type LoggableTLSConnState tls.ConnectionState
@@ -88,5 +121,7 @@ func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error
// Interface guards
var (
_ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil)
_ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil)
_ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil)
_ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil)
)
+6 -27
View File
@@ -435,12 +435,12 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
// can be used instead.
reqPath := strings.ToLower(r.URL.Path)
// See #2917; Windows ignores trailing dots and spaces
// when accessing files (sigh), potentially causing a
// security risk (cry) if PHP files end up being served
// as static files, exposing the source code, instead of
// being matched by *.php to be treated as PHP scripts.
if runtime.GOOS == "windows" { // issue #5613
// Windows treats backslashes as path separators and
// ignores trailing dots and spaces when accessing files
// (sigh), potentially causing a security risk (cry) if
// protected files are not matched as intended.
reqPath = strings.ReplaceAll(reqPath, `\`, "/")
reqPath = strings.TrimRight(reqPath, ". ")
}
@@ -478,12 +478,7 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
// the intent is to compare that part of the path in raw/escaped
// space; i.e. "%40"=="%40", not "@", and "%2F"=="%2F", not "/"
if strings.Contains(matchPattern, "%") {
escapedPath := r.URL.EscapedPath()
if runtime.GOOS == "windows" {
escapedPath = windowsEscapedPathSeparatorRepl.Replace(escapedPath)
matchPattern = windowsEscapedPathSeparatorRepl.Replace(matchPattern)
}
reqPathForPattern := CleanPath(escapedPath, mergeSlashes)
reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes)
if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) {
return true, nil
}
@@ -648,14 +643,6 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b
return matches
}
// windowsEscapedPathSeparatorRepl normalizes Windows backslash separators
// while preserving escaped-path matching semantics.
var windowsEscapedPathSeparatorRepl = strings.NewReplacer(
`\`, "%2f",
"%5c", "%2f",
"%5C", "%2f",
)
// CELLibrary produces options that expose this matcher for use in CEL
// expression matchers.
//
@@ -1575,14 +1562,6 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er
// instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
// if the token is quoted (backtick), treat it as a shorthand
// for an expression matcher, same as @named matcher parsing
if d.Token().Quoted() {
expressionToken := d.Token().Clone()
expressionToken.Text = "expression"
tokensByMatcherName["expression"] = append(tokensByMatcherName["expression"], expressionToken, d.Token())
continue
}
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
}
+10 -53
View File
@@ -461,61 +461,18 @@ func TestPathMatcherWindows(t *testing.T) {
return
}
req := &http.Request{URL: &url.URL{Path: "/index.php . . .."}}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
for _, tc := range []struct {
name string
path string
requestTarget string
match MatchPath
}{
{
name: "trailing dots and spaces",
path: "/index.php . . ..",
match: MatchPath{"*.php"},
},
{
name: "encoded backslash path separator",
requestTarget: `/private%5csecret.txt`,
match: MatchPath{"/private/*"},
},
{
name: "encoded backslash path separator with escaped wildcard",
requestTarget: `/private%5csecret.txt`,
match: MatchPath{"/private/%*"},
},
{
name: "uppercase encoded backslash path separator with escaped wildcard",
requestTarget: `/private%5Csecret.txt`,
match: MatchPath{"/private/%*"},
},
{
name: "encoded backslash in escaped pattern",
requestTarget: `/private%5csecret.txt`,
match: MatchPath{"/private%5c%*"},
},
} {
t.Run(tc.name, func(t *testing.T) {
u := &url.URL{Path: tc.path}
if tc.requestTarget != "" {
var err error
u, err = url.ParseRequestURI(tc.requestTarget)
if err != nil {
t.Fatalf("Parsing request target: %v", err)
}
}
req := &http.Request{URL: u}
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
matched, err := tc.match.MatchWithError(req)
if err != nil {
t.Errorf("Expected no error, but got: %v", err)
}
if !matched {
t.Errorf("Expected %q to match %v", req.URL.Path, tc.match)
}
})
match := MatchPath{"*.php"}
matched, err := match.MatchWithError(req)
if err != nil {
t.Errorf("Expected no error, but got: %v", err)
}
if !matched {
t.Errorf("Expected to match; should ignore trailing dots and spaces")
}
}
+4 -84
View File
@@ -3,7 +3,6 @@ package caddyhttp
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"sync"
@@ -11,14 +10,9 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
otelprom "go.opentelemetry.io/contrib/bridges/prometheus"
"go.opentelemetry.io/contrib/exporters/autoexport"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"github.com/caddyserver/caddy/v2"
caddymetrics "github.com/caddyserver/caddy/v2/internal/metrics"
"github.com/caddyserver/caddy/v2/internal/metrics"
)
// Metrics configures metrics observations.
@@ -73,20 +67,10 @@ type Metrics struct {
// for production environments exposed to the internet).
ObserveCatchallHosts bool `json:"observe_catchall_hosts,omitempty"`
// Enable pushing metrics via OTLP in addition to the existing Prometheus
// scrape endpoints. When set, a PeriodicReader is attached to the shared
// Prometheus registry (via a Prometheus -> OpenTelemetry bridge), and the
// exporter is autoconfigured from the standard OTEL_* environment
// variables (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL,
// OTEL_METRICS_EXPORTER, ...). Set OTEL_METRICS_EXPORTER=none or simply
// keep this field false to disable OTLP export.
OTLP bool `json:"otlp,omitempty"`
init sync.Once
httpMetrics *httpMetrics
allowedHosts map[string]struct{}
hasHTTPSServer bool
meterProvider *sdkmetric.MeterProvider
}
type httpMetrics struct {
@@ -163,70 +147,6 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
}, httpLabels)
}
// provisionOTLP wires a MeterProvider that periodically reads the process-wide
// Prometheus registry and pushes the result via OTLP. The exporter and reader
// are autoconfigured from the standard OTEL_* environment variables, matching
// the ergonomics of the existing `tracing` directive. It is a no-op when
// m.OTLP is false, and honors OTEL_METRICS_EXPORTER=none (autoexport
// short-circuits to a no-op reader in that case).
func (m *Metrics) provisionOTLP(ctx caddy.Context) error {
if !m.OTLP {
return nil
}
// Register a Prometheus -> OpenTelemetry bridge against the process-wide
// Prometheus registry as the *default* source the NewMetricReader below
// will read from.
//
// NB: despite the "With*" naming, autoexport.WithFallbackMetricProducer is
// a package-level setter (it returns nothing) — it mutates autoexport's
// internal producer registry and takes effect on the very next call to
// NewMetricReader. It is NOT a MetricOption and must not be passed as one.
// Users can still override the source by setting OTEL_METRICS_PRODUCERS.
reg := ctx.GetMetricsRegistry()
autoexport.WithFallbackMetricProducer(func(context.Context) (sdkmetric.Producer, error) {
return otelprom.NewMetricProducer(otelprom.WithGatherer(reg)), nil
})
reader, err := autoexport.NewMetricReader(ctx)
if err != nil {
return fmt.Errorf("creating OTLP metric reader: %w", err)
}
version, _ := caddy.Version()
res, err := resource.Merge(resource.Default(), resource.NewSchemaless(
semconv.WebEngineName(ServerHeader),
semconv.WebEngineVersion(version),
))
if err != nil {
return fmt.Errorf("building OTLP metrics resource: %w", err)
}
m.meterProvider = sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(reader),
)
return nil
}
// shutdown flushes and tears down the OTLP MeterProvider if one was provisioned.
// Both ForceFlush and Shutdown are always attempted so that a flush failure
// does not prevent the reader goroutines from being stopped; errors from both
// are returned joined.
func (m *Metrics) shutdown(ctx context.Context) error {
if m == nil || m.meterProvider == nil {
return nil
}
// ForceFlush gives the final collection a chance to reach the collector
// before the reader goroutine is stopped by Shutdown.
return errors.Join(
m.meterProvider.ForceFlush(ctx),
m.meterProvider.Shutdown(ctx),
)
}
// scanConfigForHosts scans the HTTP app configuration to build a set of allowed hosts
// for metrics collection, similar to how auto-HTTPS scans for domain names.
func (m *Metrics) scanConfigForHosts(app *App) {
@@ -314,7 +234,7 @@ func newMetricsInstrumentedRoute(ctx caddy.Context, handler string, next Handler
func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
server := serverNameFromContext(r.Context())
labels := prometheus.Labels{"server": server, "handler": h.handler}
method := caddymetrics.SanitizeMethod(r.Method)
method := metrics.SanitizeMethod(r.Method)
// the "code" value is set later, but initialized here to eliminate the possibility
// of a panic
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
@@ -344,7 +264,7 @@ func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Requ
// being called when the headers are written.
// Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader.
writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool {
statusLabels["code"] = caddymetrics.SanitizeCode(status)
statusLabels["code"] = metrics.SanitizeCode(status)
ttfb := time.Since(start).Seconds()
h.metrics.httpMetrics.responseDuration.With(statusLabels).Observe(ttfb)
return false
@@ -360,7 +280,7 @@ func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Requ
if statusLabels["code"] == "" {
// we still sanitize it, even though it's likely to be 0. A 200 is
// returned on fallthrough so we want to reflect that.
statusLabels["code"] = caddymetrics.SanitizeCode(status)
statusLabels["code"] = metrics.SanitizeCode(status)
}
h.metrics.httpMetrics.requestDuration.With(statusLabels).Observe(dur)
-50
View File
@@ -523,56 +523,6 @@ func TestMetricsInstrumentedRoute(t *testing.T) {
}
}
func TestMetricsProvisionOTLPDisabled(t *testing.T) {
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
m := &Metrics{OTLP: false}
if err := m.provisionOTLP(ctx); err != nil {
t.Fatalf("provisionOTLP returned unexpected error: %v", err)
}
if m.meterProvider != nil {
t.Fatalf("meterProvider should remain nil when OTLP is disabled")
}
// shutdown must be safe on a never-provisioned Metrics.
if err := m.shutdown(context.Background()); err != nil {
t.Fatalf("shutdown returned unexpected error: %v", err)
}
}
func TestMetricsProvisionOTLPNoopExporter(t *testing.T) {
// OTEL_METRICS_EXPORTER=none makes autoexport return its built-in
// no-op reader, which avoids any network I/O while still exercising
// the full provisionOTLP -> shutdown lifecycle.
t.Setenv("OTEL_METRICS_EXPORTER", "none")
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
m := &Metrics{OTLP: true}
if err := m.provisionOTLP(ctx); err != nil {
t.Fatalf("provisionOTLP returned unexpected error: %v", err)
}
if m.meterProvider == nil {
t.Fatalf("provisionOTLP did not create a MeterProvider")
}
if err := m.shutdown(context.Background()); err != nil {
t.Fatalf("shutdown returned unexpected error: %v", err)
}
}
// shutdown on a nil receiver is a convenience so App.Stop can call it
// without guarding against app.Metrics being unset.
func TestMetricsShutdownNilReceiver(t *testing.T) {
var m *Metrics
if err := m.shutdown(context.Background()); err != nil {
t.Fatalf("shutdown on nil Metrics returned unexpected error: %v", err)
}
}
func BenchmarkMetricsInstrumentedRoute(b *testing.B) {
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
m := &Metrics{
+7 -4
View File
@@ -387,14 +387,17 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
switch key {
case "http.shutting_down":
server := req.Context().Value(ServerCtxKey).(*Server)
return server.shutdownAt.Load() != nil, true
server.shutdownAtMu.RLock()
defer server.shutdownAtMu.RUnlock()
return !server.shutdownAt.IsZero(), true
case "http.time_until_shutdown":
server := req.Context().Value(ServerCtxKey).(*Server)
t := server.shutdownAt.Load()
if t == nil {
server.shutdownAtMu.RLock()
defer server.shutdownAtMu.RUnlock()
if server.shutdownAt.IsZero() {
return nil, true
}
return time.Until(*t), true
return time.Until(server.shutdownAt), true
}
return nil, false
+1 -1
View File
@@ -67,7 +67,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// lb_retries <retries>
// lb_try_duration <duration>
// lb_try_interval <interval>
// lb_retry_match <matcher>
// lb_retry_match <request-matcher>
//
// # active health checking
// health_uri <uri>
@@ -135,8 +135,8 @@ type client struct {
logger *zap.Logger
}
// Do makes the request and returns an io.Reader that translates the data read
// from the FastCGI responder out of FastCGI packets before returning it.
// Do made the request and returns a io.Reader that translates the data read
// from fcgi responder out of fcgi packet before returning it.
func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
// check for CONTENT_LENGTH, since the lack of it or wrong value will cause the backend to hang
if clStr, ok := p["CONTENT_LENGTH"]; !ok {
@@ -179,7 +179,7 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error)
return r, err
}
// clientCloser is an io.ReadCloser. It wraps an io.Reader with a Closer
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
// that closes the client connection.
type clientCloser struct {
rwc net.Conn
@@ -208,8 +208,8 @@ func (f clientCloser) Close() error {
return f.rwc.Close()
}
// Request returns an HTTP response with header and body
// from the FastCGI responder.
// Request returns a HTTP Response with Header and Body
// from fcgi responder
func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
r, err := c.Do(p, req)
if err != nil {
@@ -28,6 +28,8 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/text/language"
"golang.org/x/text/search"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -416,19 +418,14 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
return env, nil
}
var splitSearchNonASCII = search.New(language.Und, search.IgnoreCase)
// splitPos returns the index where path should
// be split based on t.SplitPath.
//
// example: if splitPath is [".php"]
// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path")
//
// Matching is strictly ASCII case-insensitive. Bytes >= utf8.RuneSelf in path
// never match any split entry: split strings are validated ASCII-only and
// lower-cased in Provision(), so any Unicode equivalence (e.g. fullwidth or
// mathematical letters folding to ASCII) would let an attacker upload a file
// whose name contains such code points and have it served as PHP. See
// FrankenPHP advisories GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8.
//
// Adapted from FrankenPHP's code (copyright 2026 Kévin Dunglas, MIT license)
func (t Transport) splitPos(path string) int {
// TODO: from v1...
@@ -441,18 +438,31 @@ func (t Transport) splitPos(path string) int {
pathLen := len(path)
// We are sure that split strings are all ASCII-only and lower-case because of validation and normalization in Provision().
for _, split := range t.SplitPath {
splitLen := len(split)
if splitLen == 0 || splitLen > pathLen {
continue
}
for i := 0; i <= pathLen-splitLen; i++ {
for i := range pathLen {
if path[i] >= utf8.RuneSelf {
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
return end
}
break
}
if i+splitLen > pathLen {
continue
}
match := true
for j := range splitLen {
c := path[i+j]
if c >= utf8.RuneSelf {
match = false
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
return end
}
break
}
@@ -191,65 +191,6 @@ func TestSplitPos(t *testing.T) {
splitPath: []string{".php"},
wantPos: 9,
},
// Regression tests adapted from FrankenPHP advisories
// GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8: search.IgnoreCase
// matched Unicode equivalents of ASCII letters as ".php", and an
// inner non-ASCII byte path could leave the match flag stale.
{
name: "non-ascii byte after dot must not match",
path: "/PoC-match-unset.¡.txt",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "non-ascii byte mid-extension must not match",
path: "/script.p\xc2\xa1p",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "small full stop ﹒ in extension must not match",
path: "/shell﹒php",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "fullwidth full stop in extension must not match",
path: "/shellphp",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "fullwidth p in extension must not match",
path: "/shell.hp",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "circled php must not match",
path: "/shell.ⓟⓗⓟ",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "mathematical sans-serif bold php must not match",
path: "/shell.\U0001D5FD\U0001D5F5\U0001D5FD",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "mathematical script php must not match",
path: "/shell.\U0001D4C5\U0001D4BD\U0001D4C5",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "circled php with later real php still picks the real one",
path: "/shell.ⓟⓗⓟ.anything-after-payload.php",
splitPath: []string{".php"},
// "/shell." (7) + "ⓟⓗⓟ" (3*3 bytes) + ".anything-after-payload.php" (27) = 43
wantPos: 43,
},
}
for _, tt := range tests {
@@ -303,31 +244,3 @@ func TestSplitPosUnicodeSecurityRegression(t *testing.T) {
assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php")
}
}
// TestSplitPosSecurityRegressionUnicodeBypass guards against the FrankenPHP
// advisories GHSA-3g8v-8r37-cgjm (uninitialized match flag on inner non-ASCII
// byte) and GHSA-v4h7-cj44-8fc8 (Unicode equivalence via search.IgnoreCase
// folding fullwidth/mathematical/circled letters onto ASCII). Every payload
// below produced a false positive in the vulnerable implementation; none
// must match here.
func TestSplitPosSecurityRegressionUnicodeBypass(t *testing.T) {
t.Parallel()
tr := Transport{SplitPath: []string{".php"}}
payloads := []string{
"/PoC-match-unset.¡.txt", // GHSA-3g8v: stale match=true on IndexString fallback
"/shell﹒php", // U+FE52 small full stop
"/shellphp", // U+FF0E fullwidth full stop
"/shell.hp", // U+FF50 fullwidth p
"/shell.pp", // U+FF48 fullwidth h
"/shell.ph", // U+FF50 fullwidth p (trailing)
"/shell.\U0001D5C1\U0001D5B5\U0001D5C1", // mathematical sans-serif p/h
"/shell.\U0001D5FD\U0001D5F5\U0001D5FD", // mathematical sans-serif bold p/h
"/shell.\U0001D4C5\U0001D4BD\U0001D4C5", // mathematical script p/h
"/shell.ⓟⓗⓟ", // circled latin small
}
for _, p := range payloads {
assert.Equalf(t, -1, tr.splitPos(p), "payload %q must not be detected as .php", p)
}
}
@@ -522,7 +522,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
body = io.LimitReader(body, h.HealthChecks.Active.MaxSize)
}
defer func() {
// drain any remaining body so connection could be reused
// drain any remaining body so connection could be re-used
_, _ = io.Copy(io.Discard, body)
resp.Body.Close()
}()
+2 -1
View File
@@ -174,7 +174,7 @@ func (u *Upstream) fillDynamicHost() {
// Host is the basic, in-memory representation of the state of a remote host.
// Its fields are accessed atomically and Host values must not be copied.
type Host struct {
numRequests atomic.Int64
numRequests atomic.Int64 // atomic.Int64 is automatically aligned for us (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG)
fails atomic.Int64
activePasses atomic.Int64
activeFails atomic.Int64
@@ -250,6 +250,7 @@ func (h *Host) resetHealth() {
// (This returns the status only from the "active" health checks.)
func (u *Upstream) healthy() bool {
return u.unhealthy.Load() == 0
// return atomic.LoadInt32(&u.unhealthy) == 0
}
// SetHealthy sets the upstream has healthy or unhealthy
@@ -4,14 +4,11 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/url"
"reflect"
"testing"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestHTTPTransportUnmarshalCaddyFileWithCaPools(t *testing.T) {
@@ -197,85 +194,3 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
})
}
}
// TestHTTPTransport_DialContext_DialInfoOverride is a regression test for
// issue #6447: a `tcp4/`-prefixed upstream silently fell back to plain `tcp`
// because dialContext only honored DialInfo for unix networks. PR #7300 widened
// the condition so DialInfo is honored when no upstream HTTP proxy is in use,
// and skipped (for non-unix networks) when one is. Both halves are pinned here.
func TestHTTPTransport_DialContext_DialInfoOverride(t *testing.T) {
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
ln, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
t.Cleanup(func() { ln.Close() })
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
c.Close()
}
}()
ht := &HTTPTransport{}
rt, err := ht.NewTransport(ctx)
if err != nil {
t.Fatalf("NewTransport: %v", err)
}
proxyURL, err := url.Parse("http://proxy.example:8080")
if err != nil {
t.Fatalf("parse proxy URL: %v", err)
}
tests := []struct {
name string
proxy bool
dialInfo string
defaultAddr string
}{
{
// no proxy: DialInfo should be applied, so the dial lands on
// the live listener despite the bogus default address.
name: "honors DialInfo when no proxy",
proxy: false,
dialInfo: ln.Addr().String(),
defaultAddr: "127.0.0.1:1",
},
{
// proxy active: DialInfo must NOT be applied for non-unix
// networks; the default address (the live listener) is used.
name: "skips DialInfo when proxy active",
proxy: true,
dialInfo: "127.0.0.1:1",
defaultAddr: ln.Addr().String(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dialCtx := context.WithValue(context.Background(), caddyhttp.VarsCtxKey, make(map[string]any))
caddyhttp.SetVar(dialCtx, dialInfoVarKey, DialInfo{
Network: "tcp4",
Address: tt.dialInfo,
})
if tt.proxy {
caddyhttp.SetVar(dialCtx, proxyVarKey, proxyURL)
}
conn, err := rt.DialContext(dialCtx, "tcp", tt.defaultAddr)
if err != nil {
t.Fatalf("DialContext: %v", err)
}
t.Cleanup(func() { conn.Close() })
if got := conn.RemoteAddr().String(); got != ln.Addr().String() {
t.Fatalf("conn.RemoteAddr() = %s, want %s", got, ln.Addr().String())
}
})
}
}
@@ -1,7 +1,6 @@
package reverseproxy
import (
"context"
"errors"
"io"
"net"
@@ -9,13 +8,11 @@ import (
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
@@ -258,530 +255,3 @@ func TestDialErrorBodyRetry(t *testing.T) {
})
}
}
// newExpressionMatcher provisions a MatchExpression for use in tests
func newExpressionMatcher(t *testing.T, expr string) *caddyhttp.MatchExpression {
t.Helper()
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
t.Cleanup(cancel)
m := &caddyhttp.MatchExpression{Expr: expr}
if err := m.Provision(ctx); err != nil {
t.Fatalf("failed to provision expression %q: %v", expr, err)
}
return m
}
// minimalHandlerWithRetryMatch is like minimalHandler but also configures
// RetryMatch so that response-based retry can be tested
func minimalHandlerWithRetryMatch(retries int, retryMatch caddyhttp.MatcherSets, upstreams ...*Upstream) *Handler {
h := minimalHandler(retries, upstreams...)
h.LoadBalancing.RetryMatch = retryMatch
return h
}
// TestResponseRetryStatusCode verifies that when an upstream returns a status
// code matching a retry_match expression, the request is retried on the next
// upstream
func TestResponseRetryStatusCode(t *testing.T) {
// Bad upstream: returns 502
badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
}))
t.Cleanup(badServer.Close)
// Good upstream: returns 200
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}))
t.Cleanup(goodServer.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, "{http.reverse_proxy.status_code} in [502, 503]"),
},
}
// RoundRobin picks index 1 first, then 0
upstreams := []*Upstream{
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
{Host: new(Host), Dial: badServer.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
gotStatus := rec.Code
if err != nil {
if herr, ok := err.(caddyhttp.HandlerError); ok {
gotStatus = herr.StatusCode
}
}
if gotStatus != http.StatusOK {
t.Errorf("status: got %d, want %d (err=%v)", gotStatus, http.StatusOK, err)
}
}
// TestResponseRetryHeader verifies that response header matching triggers
// retries via a CEL expression checking {rp.header.*}
func TestResponseRetryHeader(t *testing.T) {
// Bad upstream: returns 200 but with X-Upstream-Retry header
badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Upstream-Retry", "true")
w.WriteHeader(http.StatusOK)
w.Write([]byte("bad"))
}))
t.Cleanup(badServer.Close)
// Good upstream: returns 200 without retry header
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("good"))
}))
t.Cleanup(goodServer.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, `{http.reverse_proxy.header.X-Upstream-Retry} == "true"`),
},
}
// RoundRobin picks index 1 first, then 0
upstreams := []*Upstream{
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
{Host: new(Host), Dial: badServer.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if rec.Code != http.StatusOK {
t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
}
if rec.Body.String() != "good" {
t.Errorf("body: got %q, want %q (retried to wrong upstream)", rec.Body.String(), "good")
}
}
// TestResponseRetryNoMatchNoRetry verifies that when no retry_match entries
// match the response, the original response is returned without retrying
func TestResponseRetryNoMatchNoRetry(t *testing.T) {
var hits atomic.Int32
// Server that returns 500 - but retry_match only matches 502/503
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Add(1)
w.WriteHeader(http.StatusInternalServerError)
}))
t.Cleanup(server.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, "{http.reverse_proxy.status_code} in [502, 503]"),
},
}
upstreams := []*Upstream{
{Host: new(Host), Dial: server.Listener.Addr().String()},
{Host: new(Host), Dial: server.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
_ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
// Only one hit - no retry since 500 doesn't match [502, 503]
if hits.Load() != 1 {
t.Errorf("upstream hits: got %d, want 1 (should not have retried)", hits.Load())
}
}
// TestResponseRetryExhaustedPreservesStatusCode verifies that when retries
// are exhausted, the actual upstream status code (e.g. 503) is reported
// to the client, not a generic 502
func TestResponseRetryExhaustedPreservesStatusCode(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable) // 503
}))
t.Cleanup(server.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, "{http.reverse_proxy.status_code} == 503"),
},
}
upstreams := []*Upstream{
{Host: new(Host), Dial: server.Listener.Addr().String()},
{Host: new(Host), Dial: server.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
gotStatus := rec.Code
if err != nil {
if herr, ok := err.(caddyhttp.HandlerError); ok {
gotStatus = herr.StatusCode
}
}
// Must return 503 (actual upstream status), not 502 (generic proxy error)
if gotStatus != http.StatusServiceUnavailable {
t.Errorf("status: got %d, want %d (status code not preserved)", gotStatus, http.StatusServiceUnavailable)
}
}
// TestResponseRetryHeaderCleanup verifies that stale response header
// placeholders from a previous upstream attempt are cleaned up before the
// next retry evaluation. Without cleanup, a header like X-Retry: true from
// upstream A would leak into the retry match for upstream B even if B does
// not set that header
func TestResponseRetryHeaderCleanup(t *testing.T) {
// First upstream: returns 200 with X-Retry header (triggers retry)
firstServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Retry", "true")
w.WriteHeader(http.StatusOK)
w.Write([]byte("first"))
}))
t.Cleanup(firstServer.Close)
// Second upstream: returns 200 WITHOUT X-Retry header (should NOT retry)
secondServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("second"))
}))
t.Cleanup(secondServer.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, `{http.reverse_proxy.header.X-Retry} == "true"`),
},
}
// RoundRobin picks index 1 first, then 0
upstreams := []*Upstream{
{Host: new(Host), Dial: secondServer.Listener.Addr().String()},
{Host: new(Host), Dial: firstServer.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Should get "second" - the first upstream's X-Retry header must not
// leak into the second upstream's retry evaluation
if rec.Body.String() != "second" {
t.Errorf("body: got %q, want %q (stale header leaked between retries)", rec.Body.String(), "second")
}
}
// TestRequestOnlyMatcherDoesNotRetryResponses verifies that a pure request
// matcher like method PUT in lb_retry_match does NOT trigger response-based
// retries. Only expression matchers (which can reference response data)
// should trigger response retries
func TestRequestOnlyMatcherDoesNotRetryResponses(t *testing.T) {
var hits atomic.Int32
// Server returns 200 OK for all requests
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Add(1)
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}))
t.Cleanup(server.Close)
// method PUT matcher - should NOT trigger response retries
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
caddyhttp.MatchMethod{"PUT"},
},
}
upstreams := []*Upstream{
{Host: new(Host), Dial: server.Listener.Addr().String()},
{Host: new(Host), Dial: server.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodPut, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Should hit only once - no retry for 200 OK even though method matches
if hits.Load() != 1 {
t.Errorf("upstream hits: got %d, want 1 (should not retry successful responses)", hits.Load())
}
if rec.Code != http.StatusOK {
t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
}
}
// brokenUpstreamAddr returns the address of a TCP listener that accepts
// connections but immediately closes them, causing a transport error (not
// a dial error). This simulates an upstream that is reachable but broken
func brokenUpstreamAddr(t *testing.T) string {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
t.Cleanup(func() { ln.Close() })
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
conn.Close()
}
}()
return ln.Addr().String()
}
// TestTransportErrorPlaceholder verifies that the is_transport_error
// placeholder is set to true during transport error evaluation in tryAgain()
// and that expression matchers using {rp.is_transport_error} can match it
func TestTransportErrorPlaceholder(t *testing.T) {
broken := brokenUpstreamAddr(t)
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}))
t.Cleanup(goodServer.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, "{http.reverse_proxy.is_transport_error} == true"),
},
}
// RoundRobin picks index 1 first (broken), then 0 (good)
upstreams := []*Upstream{
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
{Host: new(Host), Dial: broken},
}
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodPost, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
gotStatus := rec.Code
if err != nil {
if herr, ok := err.(caddyhttp.HandlerError); ok {
gotStatus = herr.StatusCode
}
}
// POST transport error should be retried because is_transport_error matched
if gotStatus != http.StatusOK {
t.Errorf("status: got %d, want %d (transport error should have been retried)", gotStatus, http.StatusOK)
}
}
// TestTransportErrorPlaceholderNotSetForResponses verifies that the
// is_transport_error placeholder is NOT set when evaluating response
// matchers, so {rp.is_transport_error} is false for response retries
func TestTransportErrorPlaceholderNotSetForResponses(t *testing.T) {
var hits atomic.Int32
// Server returns 502 - but the matcher only checks is_transport_error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Add(1)
w.WriteHeader(http.StatusBadGateway)
}))
t.Cleanup(server.Close)
// Only matches transport errors, not response errors
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, "{http.reverse_proxy.is_transport_error} == true"),
},
}
upstreams := []*Upstream{
{Host: new(Host), Dial: server.Listener.Addr().String()},
{Host: new(Host), Dial: server.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
_ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
// Should hit only once - is_transport_error is false during response
// evaluation so the 502 is NOT retried
if hits.Load() != 1 {
t.Errorf("upstream hits: got %d, want 1 (is_transport_error should be false for responses)", hits.Load())
}
}
// TestRetryMatchAllowsExpressionMixedWithOtherMatchers verifies that
// lb_retry_match accepts a block mixing expression with other matchers
func TestRetryMatchAllowsExpressionMixedWithOtherMatchers(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "expression alone",
input: `reverse_proxy localhost:9080 {
lb_retry_match {
expression ` + "`{rp.status_code} in [502, 503]`" + `
}
}`,
},
{
name: "method alone",
input: `reverse_proxy localhost:9080 {
lb_retry_match {
method PUT
}
}`,
},
{
name: "expression mixed with method",
input: `reverse_proxy localhost:9080 {
lb_retry_match {
method POST
expression ` + "`{rp.status_code} in [502, 503]`" + `
}
}`,
},
{
name: "expression mixed with path",
input: `reverse_proxy localhost:9080 {
lb_retry_match {
path /api*
expression ` + "`{rp.status_code} == 502`" + `
}
}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := &Handler{}
d := caddyfile.NewTestDispenser(tc.input)
err := h.UnmarshalCaddyfile(d)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
// TestSubrouteErrorFallbackWithBody is similar to TestDialErrorBodyRetry but
// mimics Subroute's Error handler rather than testing retries specifically
func TestSubrouteErrorFallbackWithBody(t *testing.T) {
// Good upstream: echoes the request body with 200 OK.
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read body: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write(body)
if err != nil {
t.Errorf("error writing in good server: %v", err)
}
}))
t.Cleanup(goodServer.Close)
// Handler which will dial error
badProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: deadUpstreamAddr(t)})
bodyReader := newCloseOnCloseReader("hello world")
req := httptest.NewRequest("POST", "http://localhost/", bodyReader)
// httptest.NewRequest wraps the reader in NopCloser; replace
// it with our close-aware reader so Close() is propagated.
req.Body = bodyReader
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := badProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
if err == nil {
t.Fatalf("Expected error from badProxy.ServeHTTP")
}
// Simulate the Subroute's Error handler by calling another handler with the
// same request and recorder
goodProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: goodServer.Listener.Addr().String()})
err = goodProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
if err != nil {
t.Fatalf("Expected no error from goodProxy.ServeHTTP, got: %v", err)
}
if rec.Code != http.StatusOK {
t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
}
expectedBody := "hello world"
if rec.Body.String() != expectedBody {
t.Errorf("body: got %q, want %q", rec.Body.String(), expectedBody)
}
}
+29 -168
View File
@@ -449,39 +449,6 @@ func (h *Handler) Cleanup() error {
return err
}
// bodyNopCloserIfNotRead wraps a request body to prevent closing if not read, i.e., when
// dialing to upstream fails.
// It will close the body as normal if the body is read.
type bodyNopCloserIfNotRead struct {
io.ReadCloser
read int // tracks the number of bytes read, -1 when first Read returns 0, io.EOF
}
func (b *bodyNopCloserIfNotRead) Read(p []byte) (int, error) {
if b.read == -1 {
return 0, io.EOF
}
n, err := b.ReadCloser.Read(p)
// first Read returns 0, io.EOF
if b.read == 0 && n == 0 && err == io.EOF {
b.read = -1
} else {
b.read += n
}
return n, err
}
func (b *bodyNopCloserIfNotRead) Close() error {
// don't close the body
if b.read == 0 {
return nil
}
// close as usual, when -1, any read will return EOF as the original read will do
// in other cases, the read will fail as body is closed because we do not want partial bodies to be sent to the upstream
// users can buffer the entire request body to allow the request to be resent
return b.ReadCloser.Close()
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@@ -521,19 +488,20 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
reqHost := clonedReq.Host
reqHeader := clonedReq.Header
// If the request contained a body, wrap it in io.NopCloser
// to prevent Go's transport from closing it on dial errors.
// cloneRequest does a shallow copy, so clonedReq.Body and
// When retries are configured and there is a body, wrap it in
// io.NopCloser to prevent Go's transport from closing it on dial
// errors. cloneRequest does a shallow copy, so clonedReq.Body and
// r.Body share the same io.ReadCloser — a dial-failure Close()
// would kill the original body for all subsequent retry
// attempts or subsequent handlers. The real body is closed by
// the HTTP server when the handler returns.
// would kill the original body for all subsequent retry attempts.
// The real body is closed by the HTTP server when the handler
// returns.
//
// If the body was already fully buffered (via request_buffers),
// we also extract the buffer so the retry loop can replay it
// from the beginning on each attempt. (see #6259, #7546, #7713)
// from the beginning on each attempt. (see #6259, #7546)
var bufferedReqBody *bytes.Buffer
if clonedReq.Body != nil {
if clonedReq.Body != nil && h.LoadBalancing != nil &&
(h.LoadBalancing.Retries > 0 || h.LoadBalancing.TryDuration > 0) {
if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
bufferedReqBody = reqBodyBuf.buf
reqBodyBuf.buf = nil
@@ -543,7 +511,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
bufPool.Put(bufferedReqBody)
}()
} else {
clonedReq.Body = &bodyNopCloserIfNotRead{ReadCloser: clonedReq.Body}
clonedReq.Body = io.NopCloser(clonedReq.Body)
}
}
@@ -606,17 +574,6 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
// get the updated list of upstreams
upstreams := h.Upstreams
if h.DynamicUpstreams != nil {
if retries > 0 {
// after a failure (and thus during a retry), give dynamic upstream modules an opportunity
// to purge their relevant cache entries so we don't keep retrying bad upstreams
if cachingDynamicUpstreams, ok := h.DynamicUpstreams.(CachingUpstreamSource); ok {
if err := cachingDynamicUpstreams.ResetCache(r); err != nil {
if c := h.logger.Check(zapcore.ErrorLevel, "failed clearing dynamic upstream source's cache"); c != nil {
c.Write(zap.Error(err))
}
}
}
}
dUpstreams, err := h.DynamicUpstreams.GetUpstreams(r)
if err != nil {
if c := h.logger.Check(zapcore.ErrorLevel, "failed getting dynamic upstreams; falling back to static upstreams"); c != nil {
@@ -713,12 +670,8 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
return true, succ.error
}
// remember this failure (if enabled); response-based retries
// are not counted as failures since the upstream did respond
// successfully - only the response content triggered a retry
if _, isRetryableResponse := proxyErr.(retryableResponseError); !isRetryableResponse {
h.countFailure(upstream)
}
// remember this failure (if enabled)
h.countFailure(upstream)
// if we've tried long enough, break
if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
@@ -1102,45 +1055,6 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
res.Body, _ = h.bufferedBody(res.Body, h.ResponseBuffers)
}
// set response placeholders so they can be used in retry match
// expressions and handle_response routes; clear stale header
// placeholders from a previous attempt first so they don't
// leak into the next retry evaluation
repl.DeleteByPrefix("http.reverse_proxy.header.")
for field, value := range res.Header {
repl.Set("http.reverse_proxy.header."+field, strings.Join(value, ","))
}
repl.Set("http.reverse_proxy.status_code", res.StatusCode)
repl.Set("http.reverse_proxy.status_text", res.Status)
// check if the response matches a retry match entry; if so,
// close the body and return a retryable error so the request
// is retried with the next upstream. Only evaluate matcher sets
// that contain at least one expression matcher, since those are
// the ones that can reference response data ({rp.status_code},
// {rp.header.*}). Pure request-only matchers (method, path, etc.)
// are skipped to avoid retrying every response that matches a
// request condition
if h.LoadBalancing != nil && len(h.LoadBalancing.RetryMatch) > 0 {
for _, matcherSet := range h.LoadBalancing.RetryMatch {
if !matcherSetHasExpressionMatcher(matcherSet) {
continue
}
match, err := matcherSet.MatchWithError(req)
if err != nil {
h.logger.Error("error matching request for retry", zap.Error(err))
break
}
if match {
res.Body.Close()
return retryableResponseError{
error: fmt.Errorf("upstream response matched retry_match (status %d)", res.StatusCode),
statusCode: res.StatusCode,
}
}
}
}
// see if any response handler is configured for this response from the backend
for i, rh := range h.HandleResponse {
if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) {
@@ -1160,6 +1074,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
break
}
// set up the replacer so that parts of the original response can be
// used for routing decisions
for field, value := range res.Header {
repl.Set("http.reverse_proxy.header."+field, strings.Join(value, ","))
}
repl.Set("http.reverse_proxy.status_code", res.StatusCode)
repl.Set("http.reverse_proxy.status_text", res.Status)
if c := logger.Check(zapcore.DebugLevel, "handling response"); c != nil {
c.Write(zap.Int("handler", i))
}
@@ -1344,29 +1266,18 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int
// specifically a dialer error, we need to be careful
if proxyErr != nil {
_, isDialError := proxyErr.(DialError)
_, isRetryableResponse := proxyErr.(retryableResponseError)
herr, isHandlerError := proxyErr.(caddyhttp.HandlerError)
// if the error occurred after a connection was established,
// we have to assume the upstream received the request, and
// retries need to be carefully decided, because some requests
// are not idempotent; retryableResponseError is excluded here
// because its retry decision was already made in reverseProxy()
// when the response matchers were evaluated
if !isDialError && !isRetryableResponse && (!isHandlerError || !errors.Is(herr, errNoUpstream)) {
// are not idempotent
if !isDialError && (!isHandlerError || !errors.Is(herr, errNoUpstream)) {
if lb.RetryMatch == nil && req.Method != "GET" {
// by default, don't retry requests if they aren't GET
return false
}
// set transport error flag so CEL expressions can use
// {rp.is_transport_error} to decide whether to retry
repl, _ := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
if repl != nil {
repl.Set("http.reverse_proxy.is_transport_error", true)
defer repl.Delete("http.reverse_proxy.is_transport_error")
}
match, err := lb.RetryMatch.AnyMatchWithError(req)
if err != nil {
logger.Error("error matching request for retry", zap.Error(err))
@@ -1596,12 +1507,6 @@ func removeConnectionHeaders(h http.Header) {
// statusError returns an error value that has a status code.
func statusError(err error) error {
// if a response-based retry was exhausted, use the actual upstream
// status code instead of a generic 502
if rre, ok := err.(retryableResponseError); ok {
return caddyhttp.Error(rre.statusCode, err)
}
// errors proxying usually mean there is a problem with the upstream(s)
statusCode := http.StatusBadGateway
@@ -1653,15 +1558,13 @@ type LoadBalancing struct {
// to spin if all backends are down and latency is very low.
TryInterval caddy.Duration `json:"try_interval,omitempty"`
// A list of matcher sets that controls retry behavior. Matcher sets
// without expression matchers (e.g. method, path) restrict which
// requests are retried on transport errors - if unspecified, only
// GET requests will be retried. Matcher sets with CEL expression
// matchers are evaluated against upstream responses and can
// reference {rp.status_code}, {rp.header.*}, and
// {rp.is_transport_error}. Dial errors are always retried
// regardless of this setting. Retries use the next available
// upstream per the load balancing policy
// A list of matcher sets that restricts with which requests retries are
// allowed. A request must match any of the given matcher sets in order
// to be retried if the connection to the upstream succeeded but the
// subsequent round-trip failed. If the connection to the upstream failed,
// a retry is always allowed. If unspecified, only GET requests will be
// allowed to be retried. Note that a retry is done with the next available
// host according to the load balancing policy.
RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty" caddy:"namespace=http.matchers"`
SelectionPolicy Selector `json:"-"`
@@ -1683,28 +1586,10 @@ type Selector interface {
// may be called during each retry, multiple times per request, and as
// such, needs to be instantaneous. The returned slice will not be
// modified.
//
// For upstream sources that cache results, implement the
// [CachingUpstreamSource] interface for optimal performance.
type UpstreamSource interface {
GetUpstreams(*http.Request) ([]*Upstream, error)
}
// CachingUpstreamSource is an upstream source that caches its upstreams.
// The relevant cache entry can be cleared/reset for a given request during
// retries if a request fails. This can help ensure that failing backends
// are not retried.
//
// EXPERIMENTAL: Subject to change.
type CachingUpstreamSource interface {
UpstreamSource
// ResetCache clears any cache entry related to the given request.
// The next time GetUpstreams is called, it should have new upstream
// information for the given request.
ResetCache(*http.Request) error
}
// Hop-by-hop headers. These are removed when sent to the backend.
// As of RFC 7230, hop-by-hop headers are required to appear in the
// Connection header field. These are the headers defined by the
@@ -1777,34 +1662,10 @@ type RequestHeaderOpsTransport interface {
RequestHeaderOps() *headers.HeaderOps
}
// matcherSetHasExpressionMatcher reports whether a matcher set contains
// at least one expression matcher. Expression matchers can reference
// response data via placeholders like {rp.status_code}. Matcher sets
// without expression matchers only test request properties and should
// not be evaluated for response-based retry decisions
func matcherSetHasExpressionMatcher(matcherSet caddyhttp.MatcherSet) bool {
for _, m := range matcherSet {
if _, ok := m.(*caddyhttp.MatchExpression); ok {
return true
}
}
return false
}
// roundtripSucceededError is an error type that is returned if the
// roundtrip succeeded, but an error occurred after-the-fact.
type roundtripSucceededError struct{ error }
// retryableResponseError is returned when the upstream response matched
// a retry_match entry, indicating the request should be retried with the
// next upstream. It preserves the original status code so that if retries
// are exhausted, the actual upstream status is reported instead of a
// generic 502
type retryableResponseError struct {
error
statusCode int
}
// bodyReadCloser is a reader that, upon closing, will return
// its buffer to the pool and close the underlying body reader.
type bodyReadCloser struct {
@@ -664,12 +664,10 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
return upstream
}
cookie := &http.Cookie{
Name: s.Name,
Value: sha,
Path: "/",
Secure: false,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Name: s.Name,
Value: sha,
Path: "/",
Secure: false,
}
isProxyHttps := false
if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted {
@@ -568,7 +568,7 @@ func TestQueryHashPolicy(t *testing.T) {
pool[1].setHealthy(false)
h = queryPolicy.Select(pool, request, nil)
if h != nil {
t.Error("Expected query policy host to be nil.")
t.Error("Expected query policy policy host to be nil.")
}
request = httptest.NewRequest(http.MethodGet, "/?foo=aa11&foo=bb22", nil)
@@ -630,7 +630,7 @@ func TestURIHashPolicy(t *testing.T) {
pool[1].setHealthy(false)
h = uriPolicy.Select(pool, request, nil)
if h != nil {
t.Error("Expected uri policy host to be nil.")
t.Error("Expected uri policy policy host to be nil.")
}
}
+4 -17
View File
@@ -119,18 +119,6 @@ func (su *SRVUpstreams) Provision(ctx caddy.Context) error {
return nil
}
func (su *SRVUpstreams) ResetCache(r *http.Request) error {
srvsMu.Lock()
if r == nil {
srvs = make(map[string]srvLookup)
} else {
suAddr, _, _, _ := su.expandedAddr(r)
delete(srvs, suAddr)
}
srvsMu.Unlock()
return nil
}
func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
suAddr, service, proto, name := su.expandedAddr(r)
@@ -566,9 +554,8 @@ var (
// Interface guards
var (
_ caddy.Provisioner = (*SRVUpstreams)(nil)
_ UpstreamSource = (*SRVUpstreams)(nil)
_ CachingUpstreamSource = (*SRVUpstreams)(nil)
_ caddy.Provisioner = (*AUpstreams)(nil)
_ UpstreamSource = (*AUpstreams)(nil)
_ caddy.Provisioner = (*SRVUpstreams)(nil)
_ UpstreamSource = (*SRVUpstreams)(nil)
_ caddy.Provisioner = (*AUpstreams)(nil)
_ UpstreamSource = (*AUpstreams)(nil)
)
+6 -26
View File
@@ -211,7 +211,12 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
var newPath, newQuery, newFrag string
if path != "" {
path = escapePathPlaceholders(path, r, repl)
// replace the `path` placeholder to escaped path
pathPlaceholder := "{http.request.uri.path}"
if strings.Contains(path, pathPlaceholder) {
path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
}
newPath = repl.ReplaceAll(path, "")
}
@@ -295,31 +300,6 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
return r.Method != oldMethod || r.RequestURI != oldURI
}
func escapePathPlaceholders(path string, r *http.Request, repl *caddy.Replacer) string {
// Replace path-valued placeholders in escaped form before the URI is parsed,
// otherwise literal '?' and '%' bytes from the path can be interpreted as URI
// delimiters or percent-escape sequences during the rewrite.
pathPlaceholder := "{http.request.uri.path}"
if strings.Contains(path, pathPlaceholder) {
path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
}
fileMatchRelativePlaceholder := "{http.matchers.file.relative}"
if strings.Contains(path, fileMatchRelativePlaceholder) {
if val, ok := repl.Get("http.matchers.file.relative"); ok {
if relativePath, ok := val.(string); ok {
path = strings.ReplaceAll(path, fileMatchRelativePlaceholder, escapePathPreservingSlashes(relativePath))
}
}
}
return path
}
func escapePathPreservingSlashes(path string) string {
return strings.ReplaceAll(url.PathEscape(path), "%2F", "/")
}
// buildQueryString takes an input query string and
// performs replacements on each component, returning
// the resulting query string. This function appends
+4 -5
View File
@@ -18,7 +18,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"slices"
"strings"
"github.com/caddyserver/caddy/v2"
@@ -242,8 +241,8 @@ func (routes RouteList) Compile(next Handler) Handler {
mid = append(mid, wrapRoute(route))
}
stack := next
for _, middleware := range slices.Backward(mid) {
stack = middleware(stack)
for i := len(mid) - 1; i >= 0; i-- {
stack = mid[i](stack)
}
return stack
}
@@ -306,8 +305,8 @@ func wrapRoute(route Route) Middleware {
}
// compile this route's handler stack
for _, middleware := range slices.Backward(route.middleware) {
nextCopy = middleware(nextCopy)
for i := len(route.middleware) - 1; i >= 0; i-- {
nextCopy = route.middleware[i](nextCopy)
}
// Apply metrics instrumentation once for the entire route,
+14 -51
View File
@@ -28,7 +28,7 @@ import (
"runtime"
"slices"
"strings"
"sync/atomic"
"sync"
"time"
"github.com/caddyserver/certmagic"
@@ -291,7 +291,8 @@ type Server struct {
trustedProxies IPRangeSource
shutdownAt atomic.Pointer[time.Time]
shutdownAt time.Time
shutdownAtMu *sync.RWMutex
// registered callback functions
connStateFuncs []func(net.Conn, http.ConnState)
@@ -300,8 +301,6 @@ type Server struct {
onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023)
}
var defaultProtocols = []string{"h1", "h2", "h3"}
var (
ServerHeader = "Caddy"
serverHeader = []string{ServerHeader}
@@ -901,56 +900,20 @@ func (s *Server) logRequest(
// protocol returns true if the protocol proto is configured/enabled.
func (s *Server) protocol(proto string) bool {
if s.ListenProtocols == nil {
return slices.Contains(s.protocolsWithDefaults(), proto)
}
for _, lnProtocols := range s.ListenProtocols {
if slices.Contains(s.listenerProtocolsWithDefaults(lnProtocols), proto) {
if slices.Contains(s.Protocols, proto) {
return true
}
}
return false
}
func (s *Server) protocolsWithDefaults() []string {
if len(s.Protocols) == 0 {
return defaultProtocols
}
return s.Protocols
}
func (s *Server) listenerProtocolsWithDefaults(lnProtocols []string) []string {
serverProtocols := s.protocolsWithDefaults()
if len(lnProtocols) == 0 {
return serverProtocols
}
lnProtocolsDefault := false
lnProtocolsInclude := make([]string, 0, len(lnProtocols)+len(serverProtocols))
srvProtocolsInclude := make(map[string]struct{}, len(serverProtocols))
for _, srvProtocol := range serverProtocols {
srvProtocolsInclude[srvProtocol] = struct{}{}
}
for _, lnProtocol := range lnProtocols {
if lnProtocol == "" {
lnProtocolsDefault = true
continue
}
lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
delete(srvProtocolsInclude, lnProtocol)
}
if lnProtocolsDefault {
for _, srvProtocol := range serverProtocols {
if _, ok := srvProtocolsInclude[srvProtocol]; ok {
lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
} else {
for _, lnProtocols := range s.ListenProtocols {
for _, lnProtocol := range lnProtocols {
if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto {
return true
}
}
}
}
return lnProtocolsInclude
return false
}
// Listeners returns the server's listeners. These are active listeners,
@@ -1123,11 +1086,11 @@ func strictUntrustedClientIp(r *http.Request, headers []string, trusted []netip.
for _, headerName := range headers {
parts := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",")
for _, part := range slices.Backward(parts) {
for i := len(parts) - 1; i >= 0; i-- {
// Some proxies may retain the port number, so split if possible
host, _, err := net.SplitHostPort(part)
host, _, err := net.SplitHostPort(parts[i])
if err != nil {
host = part
host = parts[i]
}
// Remove any zone identifier from the IP address
+2 -30
View File
@@ -36,22 +36,13 @@ func init() {
// Templates is a middleware which executes response bodies as Go templates.
// The syntax is documented in the Go standard library's
// [text/template package](https://golang.org/pkg/text/template/).
// Note that ANY response body that matches and qualifies may be evaluated,
// even if it comes from a proxied backend.
//
// ⚠️ Template functions/actions can access the environment, files on disk,
// and make HTTP requests. This is extremely useful, but you need to make
// sure templates are only evaluated on content that you trust, control, or
// at least sanitize properly.
// ⚠️ Template functions/actions are still experimental, so they are subject to change.
//
// ⚠️ Templates are still experimental, so they are subject to change.
// Custom template functions can be registered by creating a plugin module under the `http.handlers.templates.functions.*` namespace that implements the `CustomFunctions` interface.
//
// [All Sprig functions](https://masterminds.github.io/sprig/) are supported.
//
// Custom template functions can be registered by creating a plugin module
// under the `http.handlers.templates.functions.*` namespace that implements
// the `CustomFunctions` interface.
//
// In addition to the standard functions and the Sprig library, Caddy adds
// extra functions and data that are available to a template:
//
@@ -171,25 +162,6 @@ func init() {
// {{listFiles "/mydir"}}
// ```
//
// ##### `fileExists`
//
// Returns true if the given file name, relative to the template context's file root,
// can be opened successfully.
//
// ```
// {{fileExists "path/to/file.html"}}
// ```
//
// ##### `fileStat`
//
// Returns [FileInfo](https://pkg.go.dev/io/fs#FileInfo) using [Stat](https://pkg.go.dev/io/fs#Stat)
// on the given file name, relative to the template context's file root.
//
// ```
// {{$css := fileStat "css/style.css" -}}
// <link rel="stylesheet" href="/css/style.css?v={{ $css.ModTime.Unix }}">
// ```
//
// ##### `markdown`
//
// Renders the given Markdown text as HTML and returns it. This uses the
+2 -1
View File
@@ -21,6 +21,7 @@ import (
)
const (
webEngineName = "Caddy"
defaultSpanName = "handler"
nextCallCtxKey caddy.CtxKey = "nextCall"
)
@@ -57,7 +58,7 @@ func newOpenTelemetryWrapper(
}
version, _ := caddy.Version()
res, err := ot.newResource(caddyhttp.ServerHeader, version)
res, err := ot.newResource(webEngineName, version)
if err != nil {
return ot, fmt.Errorf("creating resource error: %w", err)
}
-36
View File
@@ -140,42 +140,6 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
iss.Email = email
}
// expand CA endpoint, if non-empty
if iss.CA != "" {
ca, err := repl.ReplaceOrErr(iss.CA, true, true)
if err != nil {
return fmt.Errorf("expanding CA endpoint '%s': %v", iss.CA, err)
}
iss.CA = ca
}
// expand TestCA endpoint, if non-empty
if iss.TestCA != "" {
testca, err := repl.ReplaceOrErr(iss.TestCA, true, true)
if err != nil {
return fmt.Errorf("expanding TestCA endpoint '%s': %v", iss.TestCA, err)
}
iss.TestCA = testca
}
// expand EAB credentials, if non-empty
if iss.ExternalAccount != nil {
if iss.ExternalAccount.KeyID != "" {
keyID, err := repl.ReplaceOrErr(iss.ExternalAccount.KeyID, true, true)
if err != nil {
return fmt.Errorf("expanding EAB key ID '%s': %v", iss.ExternalAccount.KeyID, err)
}
iss.ExternalAccount.KeyID = keyID
}
if iss.ExternalAccount.MACKey != "" {
macKey, err := repl.ReplaceOrErr(iss.ExternalAccount.MACKey, true, true)
if err != nil {
return fmt.Errorf("expanding EAB MAC key (redacted): %v", err)
}
iss.ExternalAccount.MACKey = macKey
}
}
// expand account key, if non-empty
if iss.AccountKey != "" {
accountKey, err := repl.ReplaceOrErr(iss.AccountKey, true, true)
-43
View File
@@ -1,43 +0,0 @@
package caddytls
import (
"github.com/caddyserver/caddy/v2"
"github.com/mholt/acmez/v3/acme"
"testing"
)
func TestACMEIssuerExpandPlaceholders(t *testing.T) {
t.Setenv("CADDY_TEST_CA_URL", "https://acme.example.com/directory")
t.Setenv("CADDY_TEST_TEST_CA_URL", "https://acme2.example.com/directory")
t.Setenv("CADDY_TEST_EAB_KEY_ID", "example-key-id")
t.Setenv("CADDY_TEST_EAB_MAC_KEY", "example-mac-key")
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
defer cancel()
iss := &ACMEIssuer{
CA: "{env.CADDY_TEST_CA_URL}",
TestCA: "{env.CADDY_TEST_TEST_CA_URL}",
ExternalAccount: &acme.EAB{
KeyID: "{env.CADDY_TEST_EAB_KEY_ID}",
MACKey: "{env.CADDY_TEST_EAB_MAC_KEY}",
},
}
if err := iss.Provision(caddyCtx); err != nil {
t.Fatalf("Provision() returned unexpected error: %v", err)
}
if want := "https://acme.example.com/directory"; iss.CA != want {
t.Errorf("CA: got %q, want %q", iss.CA, want)
}
if want := "https://acme2.example.com/directory"; iss.TestCA != want {
t.Errorf("TestCA: got %q, want %q", iss.TestCA, want)
}
if want := "example-key-id"; iss.ExternalAccount.KeyID != want {
t.Errorf("ExternalAccount.KeyID: got %q, want %q", iss.ExternalAccount.KeyID, want)
}
if want := "example-mac-key"; iss.ExternalAccount.MACKey != want {
t.Errorf("ExternalAccount.MACKey: got %q, want %q", iss.ExternalAccount.MACKey, want)
}
}
+1 -1
View File
@@ -158,7 +158,7 @@ type AutomationPolicy struct {
DisableOCSPStapling bool `json:"disable_ocsp_stapling,omitempty"`
// Overrides the URLs of OCSP responders embedded in certificates.
// Each key is an OCSP server URL to override, and its value is the
// Each key is a OCSP server URL to override, and its value is the
// replacement. An empty value will disable querying of that server.
// EXPERIMENTAL. Subject to change.
OCSPOverrides map[string]string `json:"ocsp_overrides,omitempty"`
+9 -11
View File
@@ -107,8 +107,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
if sni, ok := m.(MatchServerName); ok {
for _, sniName := range sni {
// index for fast lookups during handshakes
indexName := asciiServerNameForMatch(sniName)
indexedBySNI[indexName] = append(indexedBySNI[indexName], p)
indexedBySNI[sniName] = append(indexedBySNI[sniName], p)
}
}
}
@@ -119,7 +118,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
// filter policies by SNI first, if possible, to speed things up
// when there may be lots of policies
possiblePolicies := cp
if indexedPolicies, ok := indexedBySNI[asciiServerNameForMatch(hello.ServerName)]; ok {
if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok {
possiblePolicies = indexedPolicies
}
@@ -154,9 +153,9 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
// in its config (remember, TLS connection policies are used by *other* apps to
// run TLS servers) -- we skip names with placeholders
if tlsApp.EncryptedClientHello.Publication == nil {
var echNames []string
repl := caddy.NewReplacer()
for _, p := range cp {
var echNames []string
for _, m := range p.matchers {
if sni, ok := m.(MatchServerName); ok {
for _, name := range sni {
@@ -165,8 +164,8 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
}
}
}
tlsApp.RegisterServerNames(echNames, p.ALPN)
}
tlsApp.RegisterServerNames(echNames)
}
tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
@@ -897,19 +896,18 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro
// Unlike VerifyPeerCertificate, VerifyConnection is called on every
// connection including resumed sessions, preventing session-resumption bypass.
func (clientauth *ClientAuthentication) verifyConnection(cs tls.ConnectionState) error {
rawCerts := make([][]byte, len(cs.PeerCertificates))
for i, cert := range cs.PeerCertificates {
rawCerts[i] = cert.Raw
}
// first use any pre-existing custom verification function
if clientauth.existingVerifyPeerCert != nil {
rawCerts := make([][]byte, len(cs.PeerCertificates))
for i, cert := range cs.PeerCertificates {
rawCerts[i] = cert.Raw
}
if err := clientauth.existingVerifyPeerCert(rawCerts, cs.VerifiedChains); err != nil {
return err
}
}
for _, verifier := range clientauth.verifiers {
if err := verifier.VerifyClientCertificate(rawCerts, cs.VerifiedChains); err != nil {
if err := verifier.VerifyClientCertificate(nil, cs.VerifiedChains); err != nil {
return err
}
}
-36
View File
@@ -15,8 +15,6 @@
package caddytls
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"reflect"
@@ -26,40 +24,6 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func TestConnectionPolicyIDNSNIMatcherFastPath(t *testing.T) {
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
targetTLSConfig := &tls.Config{ClientAuth: tls.RequireAnyClientCert}
policies := ConnectionPolicies{
{
matchers: []ConnectionMatcher{MatchServerName{"つ.Localhost"}},
TLSConfig: targetTLSConfig,
},
}
const sniFastPathThreshold = 30
for i := len(policies); i < sniFastPathThreshold; i++ {
policies = append(policies, &ConnectionPolicy{
matchers: []ConnectionMatcher{MatchServerName{fmt.Sprintf("example-%d.localhost", i)}},
TLSConfig: &tls.Config{},
})
}
policies = append(policies, &ConnectionPolicy{
matchers: []ConnectionMatcher{MatchServerName{"xn--k9j.localhost"}},
TLSConfig: &tls.Config{ClientAuth: tls.NoClientCert},
})
tlsConfig := policies.TLSConfig(ctx)
got, err := tlsConfig.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "XN--K9J.LOCALHOST"})
if err != nil {
t.Fatalf("GetConfigForClient() error = %v", err)
}
if got != targetTLSConfig {
t.Fatalf("expected Unicode IDN policy to match before later punycode policy")
}
}
func TestClientAuthenticationUnmarshalCaddyfileWithDirectiveName(t *testing.T) {
const test_der_1 = `MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==`
const test_cert_file_1 = "../../caddytest/caddy.ca.cer"
@@ -1,59 +0,0 @@
package caddytls
import (
"crypto/tls"
"crypto/x509"
"errors"
"reflect"
"testing"
)
type testClientCertificateVerifier struct {
rawCerts [][]byte
verifiedChains [][]*x509.Certificate
err error
}
func (v *testClientCertificateVerifier) VerifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
v.rawCerts = rawCerts
v.verifiedChains = verifiedChains
return v.err
}
func TestClientAuthenticationVerifyConnectionPassesRawCertsToVerifiers(t *testing.T) {
verifier := &testClientCertificateVerifier{}
clientauth := &ClientAuthentication{
verifiers: []ClientCertificateVerifier{verifier},
}
peerCert := &x509.Certificate{Raw: []byte("peer-cert-raw")}
verifiedChains := [][]*x509.Certificate{{peerCert}}
connState := tls.ConnectionState{
PeerCertificates: []*x509.Certificate{peerCert},
VerifiedChains: verifiedChains,
}
if err := clientauth.verifyConnection(connState); err != nil {
t.Fatalf("verifyConnection failed: %v", err)
}
if !reflect.DeepEqual(verifier.rawCerts, [][]byte{[]byte("peer-cert-raw")}) {
t.Fatalf("unexpected raw certs: got %#v", verifier.rawCerts)
}
if !reflect.DeepEqual(verifier.verifiedChains, verifiedChains) {
t.Fatalf("unexpected verified chains: got %#v", verifier.verifiedChains)
}
}
func TestClientAuthenticationVerifyConnectionReturnsVerifierError(t *testing.T) {
wantErr := errors.New("verify failed")
verifier := &testClientCertificateVerifier{err: wantErr}
clientauth := &ClientAuthentication{
verifiers: []ClientCertificateVerifier{verifier},
}
err := clientauth.verifyConnection(tls.ConnectionState{})
if !errors.Is(err, wantErr) {
t.Fatalf("expected error %v, got %v", wantErr, err)
}
}
+7 -26
View File
@@ -440,10 +440,6 @@ func (t *TLS) publishECHConfigs(logger *zap.Logger) error {
zap.Strings("domains", dnsNamesToPublish),
zap.Uint8s("config_ids", configIDs))
if dnsPublisher, ok := publisher.(*ECHDNSPublisher); ok {
dnsPublisher.alpnByDomain = t.alpnValuesForServerNames(dnsNamesToPublish)
}
// publish this ECH config list with this publisher
pubTime := time.Now()
err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin)
@@ -780,8 +776,7 @@ type ECHDNSPublisher struct {
ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
provider ECHDNSProvider
alpnByDomain map[string][]string
logger *zap.Logger
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
@@ -877,7 +872,12 @@ nextName:
continue
}
params := httpsRec.Params
params = dnsPub.publishedSvcParams(domain, params, configListBin)
if params == nil {
params = make(libdns.SvcParams)
}
// overwrite only the "ech" SvcParamKey
params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}
// publish record
_, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{
@@ -903,25 +903,6 @@ nextName:
return nil
}
func (dnsPub *ECHDNSPublisher) publishedSvcParams(domain string, existing libdns.SvcParams, configListBin []byte) libdns.SvcParams {
params := make(libdns.SvcParams, len(existing)+2)
for key, values := range existing {
params[key] = append([]string(nil), values...)
}
params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}
if len(dnsPub.alpnByDomain) == 0 {
return params
}
if alpn := dnsPub.alpnByDomain[strings.ToLower(domain)]; len(alpn) > 0 {
params["alpn"] = append([]string(nil), alpn...)
}
return params
}
// echConfig represents an ECHConfig from the specification,
// [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html).
type echConfig struct {
-65
View File
@@ -1,65 +0,0 @@
package caddytls
import (
"encoding/base64"
"reflect"
"sync"
"testing"
"github.com/libdns/libdns"
)
func TestRegisterServerNamesWithALPN(t *testing.T) {
tlsApp := &TLS{
serverNames: make(map[string]serverNameRegistration),
serverNamesMu: new(sync.Mutex),
}
tlsApp.RegisterServerNames([]string{
"Example.com:443",
"example.com",
"127.0.0.1:443",
}, []string{"h2", "http/1.1"})
tlsApp.RegisterServerNames([]string{"EXAMPLE.COM"}, []string{"h3"})
got := tlsApp.alpnValuesForServerNames([]string{"example.com:443", "127.0.0.1:443"})
want := map[string][]string{
"example.com": {"h3", "h2", "http/1.1"},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ALPN values: got %#v want %#v", got, want)
}
}
func TestECHDNSPublisherPublishedSvcParams(t *testing.T) {
dnsPub := &ECHDNSPublisher{
alpnByDomain: map[string][]string{
"example.com": {"h3", "h2", "http/1.1"},
},
}
existing := libdns.SvcParams{
"alpn": {"h2"},
"ipv4hint": {"203.0.113.10"},
}
got := dnsPub.publishedSvcParams("Example.com", existing, []byte{0x01, 0x02, 0x03})
if !reflect.DeepEqual(existing["alpn"], []string{"h2"}) {
t.Fatalf("existing params mutated: got %v", existing["alpn"])
}
if !reflect.DeepEqual(got["alpn"], []string{"h3", "h2", "http/1.1"}) {
t.Fatalf("unexpected ALPN params: got %v", got["alpn"])
}
if !reflect.DeepEqual(got["ipv4hint"], []string{"203.0.113.10"}) {
t.Fatalf("unexpected preserved params: got %v", got["ipv4hint"])
}
wantECH := base64.StdEncoding.EncodeToString([]byte{0x01, 0x02, 0x03})
if !reflect.DeepEqual(got["ech"], []string{wantECH}) {
t.Fatalf("unexpected ECH params: got %v want %v", got["ech"], wantECH)
}
}
+2 -33
View File
@@ -28,7 +28,6 @@ import (
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/net/idna"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -70,45 +69,15 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
repl = caddy.NewReplacer()
}
serverName := asciiServerNameForMatch(hello.ServerName)
for _, name := range m {
rs := asciiServerNameForMatch(repl.ReplaceAll(name, ""))
if certmagic.MatchWildcard(serverName, rs) {
rs := repl.ReplaceAll(name, "")
if certmagic.MatchWildcard(hello.ServerName, rs) {
return true
}
}
return false
}
func asciiServerNameForMatch(name string) string {
if name == "" {
return name
}
// SNI is ASCII on the wire, but config can use Unicode IDNs.
ascii, err := idna.ToASCII(name)
if err == nil {
return strings.ToLower(ascii)
}
if !strings.Contains(name, "*") {
return strings.ToLower(name)
}
labels := strings.Split(name, ".")
for i, label := range labels {
if label == "" || label == "*" {
continue
}
ascii, err := idna.ToASCII(label)
if err != nil {
return strings.ToLower(name)
}
labels[i] = strings.ToLower(ascii)
}
return strings.Join(labels, ".")
}
// UnmarshalCaddyfile sets up the MatchServerName from Caddyfile tokens. Syntax:
//
// sni <domains...>
-20
View File
@@ -79,26 +79,6 @@ func TestServerNameMatcher(t *testing.T) {
input: "sub2.sub.example.com",
expect: true,
},
{
names: []string{"つ.localhost"},
input: "xn--k9j.localhost",
expect: true,
},
{
names: []string{"つ.Localhost"},
input: "XN--K9J.LOCALHOST",
expect: true,
},
{
names: []string{"*.つ.localhost"},
input: "sub.xn--k9j.localhost",
expect: true,
},
{
names: []string{"*.つ.Localhost"},
input: "Sub.XN--K9J.LOCALHOST",
expect: true,
},
} {
chi := &tls.ClientHelloInfo{ServerName: tc.input}
actual := MatchServerName(tc.names).Match(chi)
+3 -2
View File
@@ -137,10 +137,11 @@ func (s *SessionTicketService) stayUpdated() {
case newKeys := <-keysChan:
s.mu.Lock()
s.currentKeys = newKeys
for cfg := range s.configs {
configs := s.configs
s.mu.Unlock()
for cfg := range configs {
cfg.SetSessionTicketKeys(newKeys)
}
s.mu.Unlock()
case <-s.stopChan:
return
}
+17 -107
View File
@@ -23,7 +23,6 @@ import (
"net"
"net/http"
"runtime/debug"
"slices"
"strings"
"sync"
"time"
@@ -141,7 +140,7 @@ type TLS struct {
logger *zap.Logger
events *caddyevents.App
serverNames map[string]serverNameRegistration
serverNames map[string]struct{}
serverNamesMu *sync.Mutex
// set of subjects with managed certificates,
@@ -169,7 +168,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
t.logger = ctx.Logger()
repl := caddy.NewReplacer()
t.managing, t.loaded = make(map[string]string), make(map[string]string)
t.serverNames = make(map[string]serverNameRegistration)
t.serverNames = make(map[string]struct{})
t.serverNamesMu = new(sync.Mutex)
// set up default DNS module, if any, and make sure it implements all the
@@ -440,7 +439,7 @@ func (t *TLS) Start() error {
t.EncryptedClientHello.configsMu.Unlock()
if err != nil {
echLogger.Error("rotating ECH configs failed", zap.Error(err))
continue
return
}
err := t.publishECHConfigs(echLogger)
if err != nil {
@@ -614,8 +613,8 @@ func (t *TLS) Manage(subjects map[string]struct{}) error {
// managingWildcardFor returns true if the app is managing a certificate that covers that
// subject name (including consideration of wildcards), either from its internal list of
// names that it IS managing certs for, from the otherSubjsToManage which includes names
// that WILL be managed, or from names configured in the 'automate' loader.
// names that it IS managing certs for, or from the otherSubjsToManage which includes names
// that WILL be managed.
func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]struct{}) bool {
// TODO: we could also consider manually-loaded certs using t.HasCertificateForSubject(),
// but that does not account for how manually-loaded certs may be restricted as to which
@@ -630,9 +629,7 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str
return managing
}
// replace labels of the domain with wildcards until we get a match from names
// already being managed, those about to be managed in this batch, or those
// configured for automation
// replace labels of the domain with wildcards until we get a match
labels := strings.Split(subj, ".")
for i := range labels {
if labels[i] == "*" {
@@ -646,117 +643,32 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str
if _, ok := otherSubjsToManage[candidate]; ok {
return true
}
if _, ok := t.automateNames[candidate]; ok {
return true
}
}
return false
}
// RegisterServerNames registers the provided DNS names with the TLS app and
// associates them with the given HTTPS RR ALPN values, if any. This is
// currently used to auto-publish Encrypted ClientHello (ECH) configurations,
// if enabled. Use of this function by apps using the TLS app removes the need
// for the user to redundantly specify domain names in their configuration.
// This function separates hostname and port, keeping only the hostname, and
// filters IP addresses which can't be used with ECH.
// RegisterServerNames registers the provided DNS names with the TLS app.
// This is currently used to auto-publish Encrypted ClientHello (ECH)
// configurations, if enabled. Use of this function by apps using the TLS
// app removes the need for the user to redundantly specify domain names
// in their configuration. This function separates hostname and port
// (keeping only the hotsname) and filters IP addresses, which can't be
// used with ECH.
//
// EXPERIMENTAL: This function and its semantics/behavior are subject to change.
func (t *TLS) RegisterServerNames(dnsNames, alpnValues []string) {
func (t *TLS) RegisterServerNames(dnsNames []string) {
t.serverNamesMu.Lock()
defer t.serverNamesMu.Unlock()
for _, name := range dnsNames {
host, _, err := net.SplitHostPort(name)
if err != nil {
host = name
}
host = strings.ToLower(strings.TrimSpace(host))
if host == "" || certmagic.SubjectIsIP(host) {
continue
}
registration := t.serverNames[host]
if len(alpnValues) == 0 {
t.serverNames[host] = registration
continue
}
if registration.alpnValues == nil {
registration.alpnValues = make(map[string]struct{}, len(alpnValues))
}
for _, alpn := range alpnValues {
if alpn == "" {
continue
}
registration.alpnValues[alpn] = struct{}{}
}
t.serverNames[host] = registration
}
}
func (t *TLS) alpnValuesForServerNames(dnsNames []string) map[string][]string {
t.serverNamesMu.Lock()
defer t.serverNamesMu.Unlock()
result := make(map[string][]string, len(dnsNames))
for _, name := range dnsNames {
host, _, err := net.SplitHostPort(name)
if err != nil {
host = name
}
host = strings.ToLower(strings.TrimSpace(host))
if host == "" {
continue
}
registration, ok := t.serverNames[host]
if !ok || len(registration.alpnValues) == 0 {
continue
}
result[host] = OrderedHTTPSRRALPN(registration.alpnValues)
}
return result
}
// OrderedHTTPSRRALPN returns the HTTPS RR ALPN values in preferred order.
func OrderedHTTPSRRALPN(alpnSet map[string]struct{}) []string {
if len(alpnSet) == 0 {
return nil
}
knownOrder := append([]string{"h3"}, defaultALPN...)
ordered := make([]string, 0, len(alpnSet))
seen := make(map[string]struct{}, len(alpnSet))
for _, alpn := range knownOrder {
if _, ok := alpnSet[alpn]; ok {
ordered = append(ordered, alpn)
seen[alpn] = struct{}{}
if strings.TrimSpace(host) != "" && !certmagic.SubjectIsIP(host) {
t.serverNames[strings.ToLower(host)] = struct{}{}
}
}
if len(ordered) == len(alpnSet) {
return ordered
}
var remaining []string
for alpn := range alpnSet {
if _, ok := seen[alpn]; ok {
continue
}
remaining = append(remaining, alpn)
}
slices.Sort(remaining)
return append(ordered, remaining...)
}
type serverNameRegistration struct {
alpnValues map[string]struct{}
t.serverNamesMu.Unlock()
}
// HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP
@@ -879,8 +791,6 @@ func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy {
// AllMatchingCertificates returns the list of all certificates in
// the cache which could be used to satisfy the given SAN.
func AllMatchingCertificates(san string) []certmagic.Certificate {
certCacheMu.RLock()
defer certCacheMu.RUnlock()
return certCache.AllMatchingCertificates(san)
}
-96
View File
@@ -1,96 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddytls
import (
"encoding/json"
"testing"
"github.com/caddyserver/caddy/v2"
)
func TestAvoidDuplicateAutomation(t *testing.T) {
tests := []struct {
name string
automateNames []string
expectedToManage bool
}{
{
name: "do not manage if wildcard is automated",
automateNames: []string{"*.example.com"},
expectedToManage: false,
},
{
name: "manage if no automation configured",
automateNames: []string{},
expectedToManage: true,
},
{
name: "manage if explicitly requested even when wildcard automated",
automateNames: []string{"*.example.com", "sub.example.com"},
expectedToManage: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
automateJSON, err := json.Marshal(tc.automateNames)
if err != nil {
t.Fatal(err)
}
tlsApp := &TLS{
Automation: &AutomationConfig{
Policies: []*AutomationPolicy{
{
IssuersRaw: []json.RawMessage{
[]byte(`{"module": "internal"}`),
},
},
},
},
CertificatesRaw: map[string]json.RawMessage{
"automate": automateJSON,
},
}
var cfg caddy.Config
ctx, err := caddy.ProvisionContext(&cfg)
if err != nil {
t.Fatal(err)
}
if err := tlsApp.Provision(ctx); err != nil {
t.Fatal(err)
}
// simulate a case wherein the HTTP app starts first and
// tells the TLS app about the following auto-HTTPS domains
httpDomains := map[string]struct{}{"sub.example.com": {}}
if err := tlsApp.Manage(httpDomains); err != nil {
t.Fatal(err)
}
_, actuallyManaged := tlsApp.managing["sub.example.com"]
if actuallyManaged != tc.expectedToManage {
t.Errorf(
"expected sub.example.com individually managed: %v, got: %v",
tc.expectedToManage,
actuallyManaged,
)
}
})
}
}
-41
View File
@@ -174,47 +174,6 @@ func TestFileRotationPreserveMode(t *testing.T) {
}
}
func TestFileRotationPreserveModeWithUmask(t *testing.T) {
m := syscall.Umask(0o022)
defer syscall.Umask(m)
dir, err := os.MkdirTemp("", "caddytest")
if err != nil {
t.Fatalf("failed to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
fpath := path.Join(dir, "test.log")
roll := true
mode := fileMode(0o660)
fw := FileWriter{
Filename: fpath,
Mode: mode,
Roll: &roll,
RollSizeMB: 1,
}
logger, err := fw.OpenWriter()
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
defer logger.Close()
b := make([]byte, 1024*1024-1000)
logger.Write(b)
logger.Write(b[0:2000])
st, err := os.Stat(fpath)
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
if got := st.Mode().Perm(); got != os.FileMode(mode) {
t.Errorf("file mode after rotation is %v, want %v", got, mode)
}
}
func TestFileModeConfig(t *testing.T) {
tests := []struct {
name string
+15 -15
View File
@@ -29,7 +29,7 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/internal"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
@@ -100,8 +100,8 @@ func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Filter filters the input field with the replacement value.
func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field {
if array, ok := in.Interface.(internal.LoggableStringArray); ok {
newArray := make(internal.LoggableStringArray, len(array))
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
newArray := make(caddyhttp.LoggableStringArray, len(array))
for i, s := range array {
newArray[i] = hash(s)
}
@@ -149,10 +149,10 @@ func (f *ReplaceFilter) Filter(in zapcore.Field) zapcore.Field {
// list of IP addresses, where all of the values
// will be masked.
type IPMaskFilter struct {
// The IPv4 mask, as a subnet size CIDR.
// The IPv4 mask, as an subnet size CIDR.
IPv4MaskRaw int `json:"ipv4_cidr,omitempty"`
// The IPv6 mask, as a subnet size CIDR.
// The IPv6 mask, as an subnet size CIDR.
IPv6MaskRaw int `json:"ipv6_cidr,omitempty"`
v4Mask net.IPMask
@@ -241,8 +241,8 @@ func (m *IPMaskFilter) Provision(ctx caddy.Context) error {
// Filter filters the input field.
func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
if array, ok := in.Interface.(internal.LoggableStringArray); ok {
newArray := make(internal.LoggableStringArray, len(array))
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
newArray := make(caddyhttp.LoggableStringArray, len(array))
for i, s := range array {
newArray[i] = m.mask(s)
}
@@ -392,8 +392,8 @@ func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Filter filters the input field.
func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
if array, ok := in.Interface.(internal.LoggableStringArray); ok {
newArray := make(internal.LoggableStringArray, len(array))
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
newArray := make(caddyhttp.LoggableStringArray, len(array))
for i, s := range array {
newArray[i] = m.processQueryString(s)
}
@@ -523,7 +523,7 @@ func (m *CookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Filter filters the input field.
func (m CookieFilter) Filter(in zapcore.Field) zapcore.Field {
cookiesSlice, ok := in.Interface.(internal.LoggableStringArray)
cookiesSlice, ok := in.Interface.(caddyhttp.LoggableStringArray)
if !ok {
return in
}
@@ -559,7 +559,7 @@ OUTER:
transformedRequest.AddCookie(c)
}
in.Interface = internal.LoggableStringArray(transformedRequest.Header["Cookie"])
in.Interface = caddyhttp.LoggableStringArray(transformedRequest.Header["Cookie"])
return in
}
@@ -613,8 +613,8 @@ func (m *RegexpFilter) Provision(ctx caddy.Context) error {
// Filter filters the input field with the replacement value if it matches the regexp.
func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field {
if array, ok := in.Interface.(internal.LoggableStringArray); ok {
newArray := make(internal.LoggableStringArray, len(array))
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
newArray := make(caddyhttp.LoggableStringArray, len(array))
for i, s := range array {
newArray[i] = f.regexp.ReplaceAllString(s, f.Value)
}
@@ -783,8 +783,8 @@ func (f *MultiRegexpFilter) Validate() error {
// Filter applies all regexp operations sequentially to the input field.
// Input is sanitized and validated for security.
func (f *MultiRegexpFilter) Filter(in zapcore.Field) zapcore.Field {
if array, ok := in.Interface.(internal.LoggableStringArray); ok {
newArray := make(internal.LoggableStringArray, len(array))
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
newArray := make(caddyhttp.LoggableStringArray, len(array))
for i, s := range array {
newArray[i] = f.processString(s)
}
+16 -16
View File
@@ -8,7 +8,7 @@ import (
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/internal"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestIPMaskSingleValue(t *testing.T) {
@@ -55,11 +55,11 @@ func TestIPMaskMultiValue(t *testing.T) {
f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32}
f.Provision(caddy.Context{})
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
"255.255.255.255",
"244.244.244.244",
}})
arr, ok := out.Interface.(internal.LoggableStringArray)
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
if !ok {
t.Fatalf("field is wrong type: %T", out.Integer)
}
@@ -70,11 +70,11 @@ func TestIPMaskMultiValue(t *testing.T) {
t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
}
out = f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
out = f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
}})
arr, ok = out.Interface.(internal.LoggableStringArray)
arr, ok = out.Interface.(caddyhttp.LoggableStringArray)
if !ok {
t.Fatalf("field is wrong type: %T", out.Integer)
}
@@ -120,11 +120,11 @@ func TestQueryFilterMultiValue(t *testing.T) {
t.Fatalf("the filter must be valid")
}
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
"/path1?foo=a&foo=b&bar=c&bar=d&baz=e&hash=hashed",
"/path2?foo=c&foo=d&bar=e&bar=f&baz=g&hash=hashed",
}})
arr, ok := out.Interface.(internal.LoggableStringArray)
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
if !ok {
t.Fatalf("field is wrong type: %T", out.Interface)
}
@@ -162,11 +162,11 @@ func TestCookieFilter(t *testing.T) {
{hashAction, "hash", ""},
}}
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
"foo=a; foo=b; bar=c; bar=d; baz=e; hash=hashed",
}})
outval := out.Interface.(internal.LoggableStringArray)
expected := internal.LoggableStringArray{
outval := out.Interface.(caddyhttp.LoggableStringArray)
expected := caddyhttp.LoggableStringArray{
"foo=REDACTED; foo=REDACTED; baz=e; hash=1a06df82",
}
if outval[0] != expected[0] {
@@ -204,8 +204,8 @@ func TestRegexpFilterMultiValue(t *testing.T) {
f := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"}
f.Provision(caddy.Context{})
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{"foo-secret-bar", "bar-secret-foo"}})
arr, ok := out.Interface.(internal.LoggableStringArray)
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{"foo-secret-bar", "bar-secret-foo"}})
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
if !ok {
t.Fatalf("field is wrong type: %T", out.Integer)
}
@@ -229,8 +229,8 @@ func TestHashFilterSingleValue(t *testing.T) {
func TestHashFilterMultiValue(t *testing.T) {
f := HashFilter{}
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{"foo", "bar"}})
arr, ok := out.Interface.(internal.LoggableStringArray)
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{"foo", "bar"}})
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
if !ok {
t.Fatalf("field is wrong type: %T", out.Integer)
}
@@ -292,11 +292,11 @@ func TestMultiRegexpFilterMultiValue(t *testing.T) {
t.Fatalf("unexpected error provisioning: %v", err)
}
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
"foo-secret-123",
"bar-secret-456",
}})
arr, ok := out.Interface.(internal.LoggableStringArray)
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
if !ok {
t.Fatalf("field is wrong type: %T", out.Interface)
}
-12
View File
@@ -121,18 +121,6 @@ func (r *Replacer) Delete(variable string) {
r.mapMutex.Unlock()
}
// DeleteByPrefix removes all static variables with
// keys starting with the given prefix
func (r *Replacer) DeleteByPrefix(prefix string) {
r.mapMutex.Lock()
for key := range r.static {
if strings.HasPrefix(key, prefix) {
delete(r.static, key)
}
}
r.mapMutex.Unlock()
}
// fromStatic provides values from r.static.
func (r *Replacer) fromStatic(key string) (any, bool) {
r.mapMutex.RLock()
+8 -10
View File
@@ -79,15 +79,14 @@ func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loade
up.Lock()
upv, loaded = up.pool[key]
if loaded {
upv.refs.Add(1)
atomic.AddInt32(&upv.refs, 1)
up.Unlock()
upv.RLock()
value = upv.value
err = upv.err
upv.RUnlock()
} else {
upv = &usagePoolVal{}
upv.refs.Store(1)
upv = &usagePoolVal{refs: 1}
upv.Lock()
up.pool[key] = upv
up.Unlock()
@@ -119,7 +118,7 @@ func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) {
up.Lock()
upv, loaded = up.pool[key]
if loaded {
upv.refs.Add(1)
atomic.AddInt32(&upv.refs, 1)
up.Unlock()
upv.Lock()
if upv.err == nil {
@@ -130,8 +129,7 @@ func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) {
}
upv.Unlock()
} else {
upv = &usagePoolVal{value: val}
upv.refs.Store(1)
upv = &usagePoolVal{refs: 1, value: val}
up.pool[key] = upv
up.Unlock()
value = val
@@ -175,7 +173,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) {
up.Unlock()
return false, nil
}
refs := upv.refs.Add(-1)
refs := atomic.AddInt32(&upv.refs, -1)
if refs == 0 {
delete(up.pool, key)
up.Unlock()
@@ -190,7 +188,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) {
up.Unlock()
if refs < 0 {
panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)",
upv.value, upv.refs.Load()))
upv.value, upv.refs))
}
}
return deleted, err
@@ -205,7 +203,7 @@ func (up *UsagePool) References(key any) (int, bool) {
if loaded {
// I wonder if it'd be safer to read this value during
// our lock on the UsagePool... guess we'll see...
refs := upv.refs.Load()
refs := atomic.LoadInt32(&upv.refs)
return int(refs), true
}
return 0, false
@@ -222,7 +220,7 @@ type Destructor interface {
}
type usagePoolVal struct {
refs atomic.Int32
refs int32 // accessed atomically; must be 64-bit aligned for 32-bit systems
value any
err error
sync.RWMutex