Compare commits

..

1 Commits

Author SHA1 Message Date
Mohammed Al Sahaf 2aca49d5f6 http: use sync.Map for request-scoped vars
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-03-26 19:50:33 +03:00
90 changed files with 490 additions and 5017 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
+6 -39
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
@@ -212,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.
@@ -718,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
}
@@ -747,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")
@@ -815,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(
@@ -1076,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{
@@ -1161,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
@@ -1236,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)
}
@@ -1341,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)
-195
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)
@@ -662,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
@@ -1107,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())
}
})
}
}
+6 -5
View File
@@ -127,9 +127,10 @@ func Load(cfgJSON []byte, forceReload bool) error {
zap.Error(notifyErr),
zap.String("reload_err", err.Error()))
}
return
}
if notifyErr := notify.Ready(); notifyErr != nil {
Log().Error("unable to notify to service manager of ready state", zap.Error(notifyErr))
if err := notify.Ready(); err != nil {
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
}
}()
@@ -766,7 +767,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
}
@@ -845,11 +846,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
+1 -39
View File
@@ -63,33 +63,8 @@ func Format(input []byte) []byte {
heredocClosingMarker []rune
nesting int // indentation level
currentToken strings.Builder
currentLineFirstToken string
previousLineWasTopLevelImport bool
openBraceOwnLine bool
)
finishToken := func() {
if currentToken.Len() == 0 {
return
}
if currentLineFirstToken == "" {
currentLineFirstToken = currentToken.String()
}
currentToken.Reset()
}
finishLine := func() {
finishToken()
if currentLineFirstToken != "" {
previousLineWasTopLevelImport = nesting == 0 && currentLineFirstToken == "import"
} else if !openBrace || !openBraceOwnLine || openBraceWritten {
previousLineWasTopLevelImport = false
}
currentLineFirstToken = ""
}
write := func(ch rune) {
out.WriteRune(ch)
last = ch
@@ -245,11 +220,9 @@ func Format(input []byte) []byte {
}
if unicode.IsSpace(ch) {
finishToken()
space = true
heredocEscaped = false
if ch == '\n' {
finishLine()
newLines++
}
continue
@@ -276,19 +249,13 @@ func Format(input []byte) []byte {
}
openBrace = false
if openBraceOwnLine && previousLineWasTopLevelImport {
if last != '\n' {
nextLine()
}
indent()
} else if beginningOfLine {
if beginningOfLine {
indent()
} else if !openBraceSpace || !unicode.IsSpace(last) {
write(' ')
}
write('{')
openBraceWritten = true
openBraceOwnLine = false
nextLine()
newLines = 0
// prevent infinite nesting from ridiculous inputs (issue #4169)
@@ -299,10 +266,8 @@ func Format(input []byte) []byte {
switch {
case ch == '{':
finishToken()
openBrace = true
openBraceSpace = spacePrior && !beginningOfLine
openBraceOwnLine = newLines > 0
if openBraceSpace && newLines == 0 {
write(' ')
}
@@ -310,13 +275,11 @@ func Format(input []byte) []byte {
if quotes == "`" {
write('{')
openBraceWritten = true
openBraceOwnLine = false
continue
}
continue
case ch == '}' && (spacePrior || !openBrace):
finishToken()
if quotes == "`" {
write('}')
continue
@@ -361,7 +324,6 @@ func Format(input []byte) []byte {
space = true
}
currentToken.WriteRune(ch)
write(ch)
beginningOfLine = false
-15
View File
@@ -475,21 +475,6 @@ Hope this helps.` + "`" + `
}`,
expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}",
},
{
description: "imports before global options block keep standalone brace",
input: `import ./conf.d/matcher_my_subnet.caddy
import ./conf.d/matcher_not_my_subnet.caddy
{
order crowdsec first
order appsec after crowdsec
}`,
expect: `import ./conf.d/matcher_my_subnet.caddy
import ./conf.d/matcher_not_my_subnet.caddy
{
order crowdsec first
order appsec after crowdsec
}`,
},
} {
// the formatter should output a trailing newline,
// even if the tests aren't written to expect that
+1 -22
View File
@@ -550,11 +550,7 @@ func (p *parser) doImport(nesting int) error {
}
if foundBlockDirective {
if maybeSnippet {
tokensCopy = append(tokensCopy, token)
} else {
tokensCopy = append(tokensCopy, tokensToAdd...)
}
tokensCopy = append(tokensCopy, tokensToAdd...)
continue
}
@@ -686,28 +682,11 @@ func (p *parser) directive() error {
// a opening curly brace. It does NOT advance the token.
func (p *parser) openCurlyBrace() error {
if p.Val() != "{" {
if p.valLooksLikeGlobalOptionsAfterImportedSnippets() {
return p.Err("global options block must appear before import directives; move the global options block to the top of the Caddyfile")
}
return p.SyntaxErr("{")
}
return nil
}
func (p *parser) valLooksLikeGlobalOptionsAfterImportedSnippets() bool {
if p.Val() != "import" || len(p.block.Keys) == 0 {
return false
}
for _, key := range p.block.Keys {
if !strings.HasPrefix(key.Text, "(") || !strings.HasSuffix(key.Text, ")") {
return false
}
}
return true
}
// closeCurlyBrace expects the current token to be
// a closing curly brace. This acts like an assertion
// because it returns an error if the token is not
-101
View File
@@ -930,107 +930,6 @@ func TestAcceptSiteImportWithBraces(t *testing.T) {
}
}
func TestGlobalOptionsAfterImportedSnippetsGivesHelpfulError(t *testing.T) {
tempDir := t.TempDir()
importFile1 := filepath.Join(tempDir, "matcher_snippet_1.caddy")
importFile2 := filepath.Join(tempDir, "matcher_snippet_2.caddy")
err := os.WriteFile(importFile1, []byte(`(matcher1)`), 0o644)
if err != nil {
t.Fatalf("writing first import file: %v", err)
}
err = os.WriteFile(importFile2, []byte(`(matcher2)`), 0o644)
if err != nil {
t.Fatalf("writing second import file: %v", err)
}
_, err = Parse("Testfile", []byte(`import `+importFile1+`
import `+importFile2+`
{
debug
}`))
if err == nil {
t.Fatal("Expected an error, but got nil")
}
expected := "global options block must appear before import directives; move the global options block to the top of the Caddyfile"
if !strings.HasPrefix(err.Error(), expected) {
t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
}
}
func TestImportedSnippetDefinitionRetainsBlockPlaceholder(t *testing.T) {
tempDir := t.TempDir()
importFile := filepath.Join(tempDir, "snippets.caddy")
err := os.WriteFile(importFile, []byte(`
(site) {
http://{args[0]} {
respond "before"
{block}
respond "after"
}
}
`), 0o644)
if err != nil {
t.Fatalf("writing imported snippet file: %v", err)
}
for _, tc := range []struct {
name string
input string
expectedDirectives []string
}{
{
name: "with nested block",
input: `
import ` + importFile + `
import site example.com {
redir https://example.net
}
`,
expectedDirectives: []string{"respond", "redir", "respond"},
},
{
name: "without nested block",
input: `
import ` + importFile + `
import site example.com
`,
expectedDirectives: []string{"respond", "respond"},
},
} {
t.Run(tc.name, func(t *testing.T) {
p := testParser(tc.input)
blocks, err := p.parseAll()
if err != nil {
t.Fatalf("parseAll: %v", err)
}
if len(blocks) != 1 {
t.Fatalf("expected exactly one server block, got %d", len(blocks))
}
if actual := blocks[0].GetKeysText(); len(actual) != 1 || actual[0] != "http://example.com" {
t.Fatalf("expected server block key http://example.com, got %v", actual)
}
if len(blocks[0].Segments) != len(tc.expectedDirectives) {
t.Fatalf("expected %d segments, got %d", len(tc.expectedDirectives), len(blocks[0].Segments))
}
for i, directive := range tc.expectedDirectives {
if actual := blocks[0].Segments[i].Directive(); actual != directive {
t.Fatalf("segment %d: expected directive %q, got %q", i, directive, actual)
}
}
})
}
}
func testParser(input string) parser {
return parser{Dispenser: NewTestDispenser(input)}
}
+20 -11
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,
@@ -653,8 +668,6 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
if !h.NextArg() {
return nil, h.ArgErr()
}
// store the unmatched root in block state so sibling directives can access it
h.BlockState["root"] = h.Val()
return h.NewRoute(nil, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
}
@@ -669,10 +682,6 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
if !h.NextArg() {
return nil, h.ArgErr()
}
// store the unmatched root in state so sibling/child directives can access it
if userMatcherSet == nil {
h.BlockState["root"] = h.Val()
}
// make the route with the matcher
return h.NewRoute(userMatcherSet, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
}
@@ -1053,7 +1062,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,
},
} {
+1 -10
View File
@@ -202,10 +202,7 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
type Helper struct {
*caddyfile.Dispenser
// State stores intermediate variables during caddyfile adaptation.
State map[string]any
// BlockState stores intermediate variables scoped to the current block.
// It propagates down, but unlike state not back up from child to parent.
BlockState map[string]any
State map[string]any
options map[string]any
warnings *[]caddyconfig.Warning
matcherDefs map[string]caddy.ModuleMap
@@ -388,11 +385,6 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
}
}
// clone BlockState once for the entire block so sibling directives
// can share state, but changes don't leak to the parent scope
subBlockState := make(map[string]any, len(h.BlockState))
maps.Copy(subBlockState, h.BlockState)
// with matchers ready to go, evaluate each directive's segment
for _, seg := range segments {
dir := seg.Directive()
@@ -404,7 +396,6 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
subHelper := h
subHelper.Dispenser = caddyfile.NewDispenser(seg)
subHelper.matcherDefs = matcherDefs
subHelper.BlockState = subBlockState
results, err := dirFunc(subHelper)
if err != nil {
-2
View File
@@ -143,7 +143,6 @@ func (st ServerType) Setup(
parentBlock: sb.block,
groupCounter: gc,
State: state,
BlockState: state,
}
results, err := dirFunc(h)
@@ -505,7 +504,6 @@ func (ServerType) extractNamedRoutes(
parentBlock: sb.block,
groupCounter: gc,
State: state,
BlockState: state,
}
handler, err := ParseSegmentAsSubroute(h)
-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)
}
}
+3 -303
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
@@ -981,31 +698,14 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
emptyAPCount := 0
origLenAPs := len(aps)
// compute the number of empty policies (disregarding subjects) - see #4128
// while we're at it,
emptyAP := new(caddytls.AutomationPolicy)
for i := 0; i < len(aps); i++ {
emptyAP.SubjectsRaw = aps[i].SubjectsRaw
emptyAP.ManagersRaw = nil
if reflect.DeepEqual(aps[i], emptyAP) {
// AP is empty
emptyAPCount++
// see if this AP shadows something later
shadowIdx := automationPolicyShadows(i, aps)
emptyAP.SubjectsRaw = nil
if shadowIdx >= 0 {
emptyAP.SubjectsRaw = aps[shadowIdx].SubjectsRaw
// allow the later policy, which is likely for a wildcard, to have cert
// managers ("get_certificate"), since wildcards now cover specific
// subdomains by default, when configured (see discussion in #7559)
emptyAP.ManagersRaw = aps[shadowIdx].ManagersRaw
}
// if this is the last AP, we can delete it, since auto-https should
// pick it up; if it shadows something later that is also empty, we
// can similarly delete this; but if it shadows something that is NOT
// empty, we must not delete it since the shadowing has a purpose
if i == len(aps)-1 || (shadowIdx >= 0 && reflect.DeepEqual(aps[shadowIdx], emptyAP)) {
if !automationPolicyHasAllPublicNames(aps[i]) {
// if this automation policy has internal names, we might as well remove it
// so auto-https can implicitly use the internal issuer
aps = slices.Delete(aps, i, i+1)
i--
}
-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(`
@@ -18,9 +18,7 @@ encode gzip zstd {
# Long way with a block for each encoding
encode {
zstd {
disable_checksum
}
zstd
gzip 5
}
@@ -73,9 +71,7 @@ encode
"gzip": {
"level": 5
},
"zstd": {
"checksum": false
}
"zstd": {}
},
"handler": "encode",
"prefer": [
@@ -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
}
@@ -1,15 +0,0 @@
{
admin off
auto_https off
}
import testdata/issue_7557_invalid_subdirective_snippet.conf
:8080 {
import test {
this_is_nonsense
}
}
----------
parsing caddyfile tokens for 'reverse_proxy': unrecognized subdirective this_is_nonsense
@@ -1,47 +0,0 @@
{
log {
format journald {
wrap console
}
}
}
:80 {
respond "Hello, World!"
}
----------
{
"logging": {
"logs": {
"default": {
"encoder": {
"format": "journald",
"wrap": {
"format": "console"
}
}
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"body": "Hello, World!",
"handler": "static_response"
}
]
}
]
}
}
}
}
}
@@ -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
}
}
}
}
@@ -11,9 +11,7 @@ reverse_proxy 127.0.0.1:65535 {
@accel header X-Accel-Redirect *
handle_response @accel {
rewrite * {rp.header.X-Accel-Redirect} {
force_modify_query
}
respond "Header X-Accel-Redirect!"
}
@another {
@@ -106,12 +104,10 @@ reverse_proxy 127.0.0.1:65535 {
},
"routes": [
{
"group": "group0",
"handle": [
{
"force_modify_query": true,
"handler": "rewrite",
"uri": "{http.reverse_proxy.header.X-Accel-Redirect}"
"body": "Header X-Accel-Redirect!",
"handler": "static_response"
}
]
}
@@ -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"
}
]
}
]
}
]
}
}
}
}
}
@@ -1,56 +0,0 @@
https://example.com {
reverse_proxy https://localhost:54321 {
stream_buffer_size 8KB
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"stream_buffer_size": 8000,
"transport": {
"protocol": "http",
"tls": {}
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -54,6 +54,11 @@ b.com {
"via": "http"
}
]
},
{
"subjects": [
"b.com"
]
}
]
}
@@ -1,96 +0,0 @@
# example from https://github.com/caddyserver/caddy/issues/7559
*.test.local {
tls {
get_certificate http http://cert-server:9000/certs
}
respond "wildcard"
}
# certificate for this subdomain is covered by wildcard above
subdomain.test.local {
respond "subdomain"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"subdomain.test.local"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "subdomain",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.test.local"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "wildcard",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"*.test.local"
],
"get_certificate": [
{
"url": "http://cert-server:9000/certs",
"via": "http"
}
]
}
]
}
}
}
}
@@ -1,87 +0,0 @@
localhost
respond "hello from localhost"
tls {
client_auth {
mode request
trust_pool combined {
source inline {
trust_der 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==
}
source file {
pem_file ../caddy.ca.cer
}
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"localhost"
]
},
"client_authentication": {
"ca": {
"provider": "combined",
"sources": [
{
"provider": "inline",
"trusted_ca_certs": [
"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=="
]
},
{
"pem_files": [
"../caddy.ca.cer"
],
"provider": "file"
}
]
},
"mode": "request"
}
},
{}
]
}
}
}
}
}
@@ -1,87 +0,0 @@
localhost
respond "hello from localhost"
tls {
client_auth {
mode require_and_verify
trust_pool combined {
source inline {
trust_der 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==
}
source pki_root {
authority local
}
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"localhost"
]
},
"client_authentication": {
"ca": {
"provider": "combined",
"sources": [
{
"provider": "inline",
"trusted_ca_certs": [
"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=="
]
},
{
"authority": [
"local"
],
"provider": "pki_root"
}
]
},
"mode": "require_and_verify"
}
},
{}
]
}
}
}
}
}
@@ -1,66 +0,0 @@
localhost
respond "hello from localhost"
tls {
client_auth {
mode request
trust_pool system
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"localhost"
]
},
"client_authentication": {
"ca": {
"provider": "system"
},
"mode": "request"
}
},
{}
]
}
}
}
}
}
+1 -1
View File
@@ -190,7 +190,7 @@ func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) {
// its own values. The backend must receive the auth service values.
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("X-User-Id", "forged-id") // must be overwritten
req.Header.Set("X-User-Id", "forged-id") // must be overwritten
req.Header.Set("X-User-Role", "forged-role") // must be overwritten
tester.AssertResponse(req, http.StatusOK, "ok")
-231
View File
@@ -7,7 +7,6 @@ import (
"os"
"runtime"
"strings"
"sync/atomic"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
@@ -563,233 +562,3 @@ 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)
}),
}
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")
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.status_code} in [502, 503]`"+`
}
}
}
`, 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\"`"+`
}
}
}
`, 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]`"+`
}
}
}
`, 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")
// /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()
if err != nil {
return
}
conn.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.is_transport_error} || {rp.status_code} in [502, 503]`"+`
}
}
}
`, goodLn.Addr().String(), brokenLn.Addr().String()), "caddyfile")
// Transport error on broken upstream should be retried to good upstream
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
}
@@ -1,7 +0,0 @@
# Used by import_block_snippet_invalid_subdirective.caddyfiletest
(test) {
reverse_proxy {
{block}
}
}
+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)
+44 -44
View File
@@ -4,17 +4,17 @@ 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.0
github.com/KimMachineGun/automemlimit v0.7.5
github.com/Masterminds/sprig/v3 v3.3.0
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.0
github.com/google/cel-go v0.27.0
github.com/google/uuid v1.6.0
github.com/klauspost/compress v1.18.5
github.com/klauspost/cpuid/v2 v2.3.0
@@ -30,22 +30,20 @@ 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.65.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
go.opentelemetry.io/contrib/propagators/autoprop v0.65.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.opentelemetry.io/contrib/exporters/autoexport v0.67.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
go.opentelemetry.io/contrib/propagators/autoprop v0.67.0
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/sdk v1.42.0
go.step.sm/crypto v0.77.1
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.1
go.uber.org/zap/exp v0.3.0
golang.org/x/crypto v0.50.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
golang.org/x/net v0.53.0
golang.org/x/crypto v0.49.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807
golang.org/x/net v0.52.0
golang.org/x/sync v0.20.0
golang.org/x/term v0.42.0
golang.org/x/term v0.41.0
golang.org/x/time v0.15.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -64,7 +62,7 @@ require (
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.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // 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
@@ -72,7 +70,7 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.18.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,29 +87,31 @@ 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/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
go.opentelemetry.io/contrib/propagators/ot v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect
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.yaml.in/yaml/v2 v2.4.4 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.42.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.42.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.42.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect
go.opentelemetry.io/otel/log v0.18.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.18.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // 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.271.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
)
@@ -163,16 +163,16 @@ require (
github.com/spf13/cast v1.7.0 // indirect
github.com/urfave/cli v1.22.17 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/grpc v1.80.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.42.0
golang.org/x/text v0.35.0
golang.org/x/tools v0.42.0 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
howett.net/plist v1.0.0 // indirect
)
+92 -92
View File
@@ -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.0 h1:Ipw9KjS/6K6A9D1xdhWebYJFqdQez5gXwfzmeKOroqE=
github.com/DeRuina/timberjack v1.4.0/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=
@@ -85,8 +85,8 @@ 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=
@@ -151,8 +151,8 @@ 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.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-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -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.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
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=
@@ -205,8 +205,8 @@ 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=
@@ -373,64 +373,64 @@ go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w=
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o=
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk=
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4=
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8=
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ=
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.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s=
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE=
go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg=
go.opentelemetry.io/contrib/propagators/aws v1.43.0/go.mod h1:CJjTym6F87tEdm61Qvnz5xrV8vKlH4C92djiqcn62k8=
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=
go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw=
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 h1:peiLMz1+aqJE+3L4mOVtR9wlmv+yh/JVYXCBjqmzJJE=
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0/go.mod h1:Agvif+4A8p/3UtZzJ0MCcDEuQwgtrzM71DueU41DCs8=
go.opentelemetry.io/contrib/propagators/ot v1.43.0 h1:Hh1HahlGc81AOE7siqi1tVOlbanY/UxMMWedpb0d5oQ=
go.opentelemetry.io/contrib/propagators/ot v1.43.0/go.mod h1:58MlyS7lghzYvAm5LN9gGmZpCMQEMB5vpZp9SRgOyE4=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE=
go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
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.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/contrib/propagators/autoprop v0.67.0 h1:XhcQRf4MeqwQw96FcnatDAj6gwE19SUrWZ1VwNg77iE=
go.opentelemetry.io/contrib/propagators/autoprop v0.67.0/go.mod h1:7OK06SuNIBIlc5Uq3JGQEsKHuXw29t9OJemvDYyP1dk=
go.opentelemetry.io/contrib/propagators/aws v1.42.0 h1:Kbr3xDxs6kcxp5ThXTKWK2OtwLhNoXBVtqguNYcsZL0=
go.opentelemetry.io/contrib/propagators/aws v1.42.0/go.mod h1:Jzw9hZHtxdpCN7x8S17UH59X/EiFivp6VXLs9bdM1OQ=
go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU=
go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc=
go.opentelemetry.io/contrib/propagators/jaeger v1.42.0 h1:jP8unWI6q5kcb3gpGLjKDGaUa+JW+nHKWvpS/q+YuWA=
go.opentelemetry.io/contrib/propagators/jaeger v1.42.0/go.mod h1:xd89e/pUyPatUP1C4z1UknD9jHptESO99tWyvd4mWD4=
go.opentelemetry.io/contrib/propagators/ot v1.42.0 h1:uQjD1NNqX1+DfcAoWParPt1egNg9vC9gH4xarJ9Khxo=
go.opentelemetry.io/contrib/propagators/ot v1.42.0/go.mod h1:yw/c2TCmQLIv109HBOCn6NlJ8Dp7MNfjMcqQZRnAMmg=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0/go.mod h1:W2m8P+d5Wn5kipj4/xmbt9uMqezEKfBjzVJadfABSBE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=
go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg=
go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw=
go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk=
go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA=
go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.step.sm/crypto v0.77.1 h1:4EEqfKdv0egQ1lqz2RhnU8Jv6QgXZfrgoxWMqJF9aDs=
go.step.sm/crypto v0.77.1/go.mod h1:U/SsmEm80mNnfD5WIkbhuW/B1eFp3fgFvdXyDLpU1AQ=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
@@ -445,8 +445,8 @@ 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=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -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.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-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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
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.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.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.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/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/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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
+5 -15
View File
@@ -69,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
@@ -99,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
@@ -208,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
@@ -222,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{
@@ -695,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()))
@@ -820,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
}
+7 -31
View File
@@ -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}"
-44
View File
@@ -1,44 +0,0 @@
package caddyhttp
import (
"testing"
"github.com/caddyserver/caddy/v2"
)
func TestRecordAutoHTTPSRedirectAddressPrefersHTTPSPort(t *testing.T) {
app := &App{HTTPSPort: 443}
redirDomains := make(map[string][]caddy.NetworkAddress)
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 2345, EndPort: 2345})
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 443, EndPort: 443})
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 8443, EndPort: 8443})
got := redirDomains["example.com"]
if len(got) != 1 {
t.Fatalf("expected 1 redirect address, got %d: %#v", len(got), got)
}
if got[0].StartPort != 443 {
t.Fatalf("expected redirect to prefer HTTPS port 443, got %#v", got[0])
}
}
func TestRecordAutoHTTPSRedirectAddressKeepsAllBindAddressesOnWinningPort(t *testing.T) {
app := &App{HTTPSPort: 443}
redirDomains := make(map[string][]caddy.NetworkAddress)
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "10.0.0.189", StartPort: 8443, EndPort: 8443})
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "10.0.0.189", StartPort: 443, EndPort: 443})
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "2603:c024:8002:9500:9eb:e5d3:3975:d056", StartPort: 443, EndPort: 443})
got := redirDomains["example.com"]
if len(got) != 2 {
t.Fatalf("expected 2 redirect addresses for both bind addresses on the winning port, got %d: %#v", len(got), got)
}
if got[0].StartPort != 443 || got[1].StartPort != 443 {
t.Fatalf("expected both redirect addresses to stay on HTTPS port 443, got %#v", got)
}
if got[0].Host != "10.0.0.189" || got[1].Host != "2603:c024:8002:9500:9eb:e5d3:3975:d056" {
t.Fatalf("expected both bind addresses to be preserved, got %#v", got)
}
}
+1 -4
View File
@@ -41,10 +41,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
//
// encode [<matcher>] <formats...> {
// gzip [<level>]
// zstd [<level>] {
// level <level>
// disable_checksum
// }
// zstd
// minimum_length <length>
// # response matcher block
// match {
+24 -84
View File
@@ -33,10 +33,6 @@ type Zstd struct {
// The compression level. Accepted values: fastest, better, best, default.
Level string `json:"level,omitempty"`
// Whether to include the optional 4-byte zstd frame checksum trailer.
// If unset, the upstream zstd library default is preserved.
Checksum *bool `json:"checksum,omitempty"`
// Compression level refer to type constants value from zstd.SpeedFastest to zstd.SpeedBestCompression
level zstd.EncoderLevel
}
@@ -52,48 +48,19 @@ func (Zstd) CaddyModule() caddy.ModuleInfo {
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume option name
args := d.RemainingArgs()
switch len(args) {
case 0:
case 1:
if _, err := parseEncoderLevel(args[0]); err != nil {
return d.Err(err.Error())
}
z.Level = args[0]
default:
return d.ArgErr()
if !d.NextArg() {
return nil
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "level":
args := d.RemainingArgs()
if len(args) != 1 {
return d.ArgErr()
}
if z.Level != "" {
return d.Err("compression level already specified")
}
if _, err := parseEncoderLevel(args[0]); err != nil {
return d.Err(err.Error())
}
z.Level = args[0]
case "disable_checksum":
if d.NextArg() {
return d.ArgErr()
}
if z.Checksum != nil {
return d.Err("checksum already specified")
}
disabled := false
z.Checksum = &disabled
default:
return d.Errf("unknown subdirective '%s'", d.Val())
}
levelStr := d.Val()
if ok, _ := zstd.EncoderLevelFromString(levelStr); !ok {
return d.Errf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
zstd.SpeedFastest,
zstd.SpeedBetterCompression,
zstd.SpeedBestCompression,
zstd.SpeedDefault,
)
}
z.Level = levelStr
return nil
}
@@ -102,11 +69,15 @@ func (z *Zstd) Provision(ctx caddy.Context) error {
if z.Level == "" {
z.Level = zstd.SpeedDefault.String()
}
level, err := parseEncoderLevel(z.Level)
if err != nil {
return err
var ok bool
if ok, z.level = zstd.EncoderLevelFromString(z.Level); !ok {
return fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
zstd.SpeedFastest,
zstd.SpeedDefault,
zstd.SpeedBetterCompression,
zstd.SpeedBestCompression,
)
}
z.level = level
return nil
}
@@ -119,45 +90,14 @@ func (z Zstd) NewEncoder() encode.Encoder {
// The default of 8MB for the window is
// too large for many clients, so we limit
// it to 128K to lighten their load.
writer, _ := zstd.NewWriter(nil, z.writerOptions(128<<10)...)
return writer
}
func (z Zstd) writerOptions(windowSize int) []zstd.EOption {
opts := []zstd.EOption{
zstd.WithWindowSize(windowSize),
writer, _ := zstd.NewWriter(
nil,
zstd.WithWindowSize(128<<10),
zstd.WithEncoderConcurrency(1),
zstd.WithZeroFrames(true),
zstd.WithEncoderLevel(z.encoderLevel()),
}
if z.Checksum != nil {
opts = append(opts, zstd.WithEncoderCRC(*z.Checksum))
}
return opts
}
func (z Zstd) encoderLevel() zstd.EncoderLevel {
if z.level != 0 {
return z.level
}
if z.Level != "" {
if level, err := parseEncoderLevel(z.Level); err == nil {
return level
}
}
return zstd.SpeedDefault
}
func parseEncoderLevel(level string) (zstd.EncoderLevel, error) {
if ok, encLevel := zstd.EncoderLevelFromString(level); ok {
return encLevel, nil
}
return 0, fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
zstd.SpeedFastest,
zstd.SpeedBetterCompression,
zstd.SpeedBestCompression,
zstd.SpeedDefault,
zstd.WithEncoderLevel(z.level),
)
return writer
}
// Interface guards
+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
@@ -20,6 +20,7 @@ import (
"net/url"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strconv"
@@ -99,7 +100,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
}
if fsrv.Browse.RevealSymlinks {
symLinkTarget, err := os.Readlink(path)
symLinkTarget, err := filepath.EvalSymlinks(path)
if err == nil {
symlinkPath = symLinkTarget
}
+1 -1
View File
@@ -785,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
}
+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)
)
-8
View File
@@ -1562,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()...)
}
+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
+5 -13
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>
@@ -96,7 +96,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// flush_interval <duration>
// request_buffers <size>
// response_buffers <size>
// stream_buffer_size <size>
// stream_timeout <duration>
// stream_close_delay <duration>
// verbose_logs
@@ -647,7 +646,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
h.FlushInterval = caddy.Duration(dur)
}
case "request_buffers", "response_buffers", "stream_buffer_size":
case "request_buffers", "response_buffers":
subdir := d.Val()
if !d.NextArg() {
return d.ArgErr()
@@ -671,8 +670,6 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
h.RequestBuffers = size
case "response_buffers":
h.ResponseBuffers = size
case "stream_buffer_size":
h.StreamBufferSize = int(size)
}
case "stream_timeout":
@@ -728,6 +725,9 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], "", nil)
case 2:
// some lint checks, I guess
if strings.EqualFold(args[0], "host") && (args[1] == "{hostport}" || args[1] == "{http.request.hostport}") {
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up Host: the reverse proxy's default behavior is to pass headers to the upstream")
}
if strings.EqualFold(args[0], "x-forwarded-for") && (args[1] == "{remote}" || args[1] == "{http.request.remote}" || args[1] == "{remote_host}" || args[1] == "{http.request.remote.host}") {
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up X-Forwarded-For: the reverse proxy's default behavior is to pass headers to the upstream")
}
@@ -885,14 +885,6 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return err
}
}
// check if the user set 'header_up host upstream_hostport' when proxying to HTTPS
// this is unnecessary because it's the default behavior already
if te.TLSEnabled() && h.Headers != nil && h.Headers.Request != nil {
hostVal := h.Headers.Request.Set.Get("Host")
if hostVal == "{upstream_hostport}" || hostVal == "{http.reverse_proxy.upstream.hostport}" {
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up Host: the reverse proxy's default behavior is to pass the configured upstream address to the upstream when proxying to HTTPS")
}
}
if commonScheme == "http" && te.TLSEnabled() {
return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
}
@@ -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)
}
}
@@ -17,7 +17,7 @@ func TestAddForwardedHeadersNonIP(t *testing.T) {
// Mock the context variables required by Caddy.
// We need to inject the variable map manually since we aren't running the full server.
vars := map[string]any{
vars := map[string]interface{}{
caddyhttp.TrustedProxyVarKey: false,
}
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
@@ -42,7 +42,7 @@ func TestAddForwardedHeaders_UnixSocketTrusted(t *testing.T) {
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "original.example.com")
vars := map[string]any{
vars := map[string]interface{}{
caddyhttp.TrustedProxyVarKey: true,
caddyhttp.ClientIPVarKey: "1.2.3.4",
}
@@ -74,7 +74,7 @@ func TestAddForwardedHeaders_UnixSocketUntrusted(t *testing.T) {
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "spoofed.example.com")
vars := map[string]any{
vars := map[string]interface{}{
caddyhttp.TrustedProxyVarKey: false,
caddyhttp.ClientIPVarKey: "",
}
@@ -103,7 +103,7 @@ func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/", nil)
req.RemoteAddr = "@"
vars := map[string]any{
vars := map[string]interface{}{
caddyhttp.TrustedProxyVarKey: true,
caddyhttp.ClientIPVarKey: "5.6.7.8",
}
+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
@@ -129,11 +129,11 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
defer cancel()
tests := []struct {
name string
tls *TLSConfig
proxyProtocol string
name string
tls *TLSConfig
proxyProtocol string
serverNameHasPlaceholder bool
expectDialTLSContext bool
expectDialTLSContext bool
}{
{
name: "no TLS, no proxy protocol",
@@ -194,3 +194,4 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
})
}
}
@@ -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,475 +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)
}
})
}
}
+19 -132
View File
@@ -171,12 +171,6 @@ type Handler struct {
// forcibly closed at the end of the timeout. Default: no timeout.
StreamTimeout caddy.Duration `json:"stream_timeout,omitempty"`
// The size of the buffer used for each direction of streaming
// requests such as WebSockets. If zero, the default size is 32 KiB.
// This only affects upgraded bidirectional streams, not normal
// request or response buffering.
StreamBufferSize int `json:"stream_buffer_size,omitempty"`
// If nonzero, streaming requests such as WebSockets will not be
// closed when the proxy config is unloaded, and instead the stream
// will remain open until the delay is complete. In other words,
@@ -574,17 +568,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 {
@@ -681,12 +664,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) {
@@ -1070,45 +1049,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) {
@@ -1128,6 +1068,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))
}
@@ -1312,29 +1260,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))
@@ -1564,12 +1501,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
@@ -1621,15 +1552,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:"-"`
@@ -1651,28 +1580,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
@@ -1745,34 +1656,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 {
+3 -17
View File
@@ -204,12 +204,7 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
defer deleteFrontConn()
defer deleteBackConn()
spc := switchProtocolCopier{
user: conn,
backend: backConn,
wg: wg,
bufferSize: h.StreamBufferSize,
}
spc := switchProtocolCopier{user: conn, backend: backConn, wg: wg}
// setup the timeout if requested
var timeoutc <-chan time.Time
@@ -641,29 +636,20 @@ func (m *maxLatencyWriter) stop() {
type switchProtocolCopier struct {
user, backend io.ReadWriteCloser
wg *sync.WaitGroup
bufferSize int
}
func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
_, err := io.CopyBuffer(c.user, c.backend, c.buffer())
_, err := io.Copy(c.user, c.backend)
errc <- err
c.wg.Done()
}
func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
_, err := io.CopyBuffer(c.backend, c.user, c.buffer())
_, err := io.Copy(c.backend, c.user)
errc <- err
c.wg.Done()
}
func (c switchProtocolCopier) buffer() []byte {
size := c.bufferSize
if size <= 0 {
size = defaultBufferSize
}
return make([]byte, size)
}
var streamingBufPool = sync.Pool{
New: func() any {
// The Pool's New function should generally only return pointer
@@ -2,10 +2,8 @@ package reverseproxy
import (
"bytes"
"io"
"net/http/httptest"
"strings"
"sync"
"testing"
"github.com/caddyserver/caddy/v2"
@@ -36,47 +34,3 @@ func TestHandlerCopyResponse(t *testing.T) {
}
}
}
func TestSwitchProtocolCopierBufferSize(t *testing.T) {
var wg sync.WaitGroup
var errc = make(chan error, 1)
var dst bytes.Buffer
copier := switchProtocolCopier{
user: nopReadWriteCloser{Reader: strings.NewReader("hello")},
backend: nopReadWriteCloser{Writer: &dst},
wg: &wg,
bufferSize: 7,
}
buf := copier.buffer()
if got := len(buf); got != 7 {
t.Fatalf("buffer len = %d, want 7", got)
}
wg.Add(1)
go copier.copyToBackend(errc)
wg.Wait()
if err := <-errc; err != nil {
t.Fatalf("copyToBackend() error = %v", err)
}
if got := dst.String(); got != "hello" {
t.Fatalf("copied data = %q, want %q", got, "hello")
}
}
func TestSwitchProtocolCopierDefaultBufferSize(t *testing.T) {
copier := switchProtocolCopier{}
buf := copier.buffer()
if got := len(buf); got != defaultBufferSize {
t.Fatalf("buffer len = %d, want %d", got, defaultBufferSize)
}
}
type nopReadWriteCloser struct {
io.Reader
io.Writer
}
func (nopReadWriteCloser) Close() error { return 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)
)
+3 -28
View File
@@ -34,9 +34,7 @@ func init() {
// parseCaddyfileRewrite sets up a basic rewrite handler from Caddyfile tokens. Syntax:
//
// rewrite [<matcher>] <to> {
// force_modify_query
// }
// rewrite [<matcher>] <to>
//
// Only URI components which are given in <to> will be set in the resulting URI.
// See the docs for the rewrite handler for more information.
@@ -52,30 +50,12 @@ func parseCaddyfileRewrite(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue,
return nil, h.Errf("too many arguments; should only be a matcher and a URI")
}
parseBlock := func(rewr *Rewrite) error {
for nesting := h.Nesting(); h.NextBlock(nesting); {
switch h.Val() {
case "force_modify_query":
rewr.ForceModifyQuery = true
default:
return h.Errf("unknown subdirective: %s", h.Val())
}
}
return nil
}
// with only one arg, assume it's a rewrite URI with no matcher token
if argsCount == 1 {
if !h.NextArg() {
return nil, h.ArgErr()
}
rewr := Rewrite{URI: h.Val()}
err := parseBlock(&rewr)
if err != nil {
return nil, err
}
return h.NewRoute(nil, rewr), nil
return h.NewRoute(nil, Rewrite{URI: h.Val()}), nil
}
// parse the matcher token into a matcher set
@@ -86,12 +66,7 @@ func parseCaddyfileRewrite(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue,
h.Next() // consume directive name again, matcher parsing does a reset
h.Next() // advance to the rewrite URI
rewr := Rewrite{URI: h.Val()}
err = parseBlock(&rewr)
if err != nil {
return nil, err
}
return h.NewRoute(userMatcherSet, rewr), nil
return h.NewRoute(userMatcherSet, Rewrite{URI: h.Val()}), nil
}
// parseCaddyfileMethod sets up a basic method rewrite handler from Caddyfile tokens. Syntax:
+3 -26
View File
@@ -92,17 +92,6 @@ type Rewrite struct {
// Mutates the query string of the URI.
Query *queryOps `json:"query,omitempty"`
// If true, the rewrite will be forced to also apply to the
// query part of the URL. This is only needed if the configured
// URI does not include a '?' character which is normally used
// to determine whether the query should be modified. In other
// words, this allows rewriting both the path and query when
// using a placeholder as the replacement value, whereas otherwise
// only the path would be rewritten because the placeholder itself
// does not contain a '?' character. Only use this if the placeholder
// is trusted to not be vulnerable to query injections.
ForceModifyQuery bool `json:"force_modify_query,omitempty"`
logger *zap.Logger
}
@@ -237,15 +226,10 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
// recompute; new path contains a query string
var injectedQuery string
newPath, injectedQuery = before, after
// don't overwrite explicitly-configured query string,
// unless configured explicitly to do so
if query == "" || rewr.ForceModifyQuery {
// don't overwrite explicitly-configured query string
if query == "" {
query = injectedQuery
}
if rewr.ForceModifyQuery {
qsStart = 0
}
}
if query != "" {
@@ -545,14 +529,7 @@ func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) {
if key == "" || val == "" {
continue
}
if key == val {
continue
}
originalValues, ok := query[key]
if !ok {
continue
}
query[val] = originalValues
query[val] = query[key]
delete(query, key)
}
-70
View File
@@ -16,7 +16,6 @@ package rewrite
import (
"net/http"
"reflect"
"regexp"
"testing"
@@ -225,23 +224,6 @@ func TestRewrite(t *testing.T) {
input: newRequest(t, "GET", "/foo#fragFirst?c=d"),
expect: newRequest(t, "GET", "/bar#fragFirst?c=d"),
},
{
rule: Rewrite{URI: "{test.path_and_query}"},
input: newRequest(t, "GET", "/"),
expect: newRequest(t, "GET", "/foo"),
},
{
// TODO: This might be an incorrect result, since it also replaces
// the path with empty string when that might not be the intent.
rule: Rewrite{URI: "{test.query}", ForceModifyQuery: true},
input: newRequest(t, "GET", "/foo"),
expect: newRequest(t, "GET", "?bar=1"),
},
{
rule: Rewrite{URI: "{test.path_and_query}", ForceModifyQuery: true},
input: newRequest(t, "GET", "/"),
expect: newRequest(t, "GET", "/foo?bar=1"),
},
{
rule: Rewrite{URI: "/api/admin/panel"},
input: newRequest(t, "GET", "/api/admin%2Fpanel"),
@@ -381,9 +363,6 @@ func TestRewrite(t *testing.T) {
repl.Set("http.request.uri", tc.input.RequestURI)
repl.Set("http.request.uri.path", tc.input.URL.Path)
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
repl.Set("test.path", "/foo")
repl.Set("test.query", "?bar=1")
repl.Set("test.path_and_query", "/foo?bar=1")
// we can't directly call Provision() without a valid caddy.Context
// (TODO: fix that) so here we ad-hoc compile the regex
@@ -418,55 +397,6 @@ func TestRewrite(t *testing.T) {
}
}
func TestQueryOpsRenameNoOpCases(t *testing.T) {
repl := caddy.NewReplacer()
for i, tc := range []struct {
input *http.Request
expect map[string][]string
ops *queryOps
}{
{
ops: &queryOps{
Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
},
input: newRequest(t, "GET", "/?page=test&id=5&test=100"),
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
},
{
ops: &queryOps{
Rename: []queryOpsArguments{{Key: "id", Val: "id"}},
},
input: newRequest(t, "GET", "/?page=test&id=5&test=100"),
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
},
{
ops: &queryOps{
Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
},
input: newRequest(t, "GET", "/?page=test&ID=5&test=100"),
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
},
{
ops: &queryOps{
Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
},
input: newRequest(t, "GET", "/?page=test&ID=5&id=7&test=100"),
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
},
} {
repl.Set("http.request.uri", tc.input.RequestURI)
repl.Set("http.request.uri.path", tc.input.URL.Path)
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
tc.ops.do(tc.input, repl)
if actual := tc.input.URL.Query(); !reflect.DeepEqual(tc.expect, map[string][]string(actual)) {
t.Errorf("Test %d: Expected query=%v but got %v", i, tc.expect, actual)
}
}
}
func newRequest(t *testing.T, method, uri string) *http.Request {
req, err := http.NewRequest(method, uri, nil)
if err != nil {
+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,
+10 -9
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)
@@ -934,10 +935,10 @@ func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter
ctx = context.WithValue(ctx, ServerCtxKey, s)
trusted, clientIP := determineTrustedProxy(r, s)
ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
TrustedProxyVarKey: trusted,
ClientIPVarKey: clientIP,
})
varsMap := &sync.Map{}
varsMap.Store(TrustedProxyVarKey, trusted)
varsMap.Store(ClientIPVarKey, clientIP)
ctx = context.WithValue(ctx, VarsCtxKey, varsMap)
ctx = context.WithValue(ctx, routeGroupCtxKey, make(map[string]struct{}))
@@ -1085,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 -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)
}
+30 -18
View File
@@ -20,6 +20,7 @@ import (
"net/http"
"reflect"
"strings"
"sync"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
@@ -181,15 +182,18 @@ func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) {
vars := r.Context().Value(VarsCtxKey).(map[string]any)
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var matcherValExpanded, varStr, v string
var fromPlaceholder bool
var matcherValExpanded, valExpanded, varStr, v string
var varValue any
for key, vals := range m {
if strings.HasPrefix(key, "{") &&
strings.HasSuffix(key, "}") &&
strings.Count(key, "{") == 1 {
varValue, _ = repl.Get(strings.Trim(key, "{}"))
fromPlaceholder = true
} else {
varValue = vars[key]
fromPlaceholder = false
}
switch vv := varValue.(type) {
@@ -205,15 +209,19 @@ func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) {
varStr = fmt.Sprintf("%v", vv)
}
// Don't expand placeholders in values from literal variable names
// (e.g. map outputs) or other placeholders. These values are
// Only expand placeholders in values from literal variable names
// (e.g. map outputs). Values resolved from placeholder keys are
// already final and must not be re-expanded, as that would allow
// user input like {env.SECRET} to be evaluated.
valExpanded = varStr
if !fromPlaceholder {
valExpanded = repl.ReplaceAll(varStr, "")
}
// see if any of the values given in the matcher match the actual value
for _, v = range vals {
matcherValExpanded = repl.ReplaceAll(v, "")
if varStr == matcherValExpanded {
if valExpanded == matcherValExpanded {
return true, nil
}
}
@@ -317,16 +325,18 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
vars := r.Context().Value(VarsCtxKey).(map[string]any)
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var match bool
var varStr string
var fromPlaceholder, match bool
var valExpanded, varStr string
var varValue any
for key, val := range m {
if strings.HasPrefix(key, "{") &&
strings.HasSuffix(key, "}") &&
strings.Count(key, "{") == 1 {
varValue, _ = repl.Get(strings.Trim(key, "{}"))
fromPlaceholder = true
} else {
varValue = vars[key]
fromPlaceholder = false
}
switch vv := varValue.(type) {
@@ -342,12 +352,15 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
varStr = fmt.Sprintf("%v", vv)
}
// Don't expand placeholders in values from literal variable names
// (e.g. map outputs) or other placeholders. These values are
// Only expand placeholders in values from literal variable names
// (e.g. map outputs). Values resolved from placeholder keys are
// already final and must not be re-expanded, as that would allow
// user input like {env.SECRET} to be evaluated.
if match = val.Match(varStr, repl); match {
valExpanded = varStr
if !fromPlaceholder {
valExpanded = repl.ReplaceAll(varStr, "")
}
if match = val.Match(valExpanded, repl); match {
return match, nil
}
}
@@ -431,11 +444,12 @@ func (m MatchVarsRE) Validate() error {
// GetVar gets a value out of the context's variable table by key.
// If the key does not exist, the return value will be nil.
func GetVar(ctx context.Context, key string) any {
varMap, ok := ctx.Value(VarsCtxKey).(map[string]any)
varMap, ok := ctx.Value(VarsCtxKey).(*sync.Map)
if !ok {
return nil
}
return varMap[key]
val, _ := varMap.Load(key)
return val
}
// SetVar sets a value in the context's variable table with
@@ -446,17 +460,15 @@ func GetVar(ctx context.Context, key string) any {
// underlying value does not count) and the key exists in
// the table, the key+value will be deleted from the table.
func SetVar(ctx context.Context, key string, value any) {
varMap, ok := ctx.Value(VarsCtxKey).(map[string]any)
varMap, ok := ctx.Value(VarsCtxKey).(*sync.Map)
if !ok {
return
}
if value == nil {
if _, ok := varMap[key]; ok {
delete(varMap, key)
return
}
varMap.Delete(key)
return
}
varMap[key] = value
varMap.Store(key, value)
}
// Interface guards
-159
View File
@@ -1,159 +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 caddyhttp
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/caddyserver/caddy/v2"
)
func newVarsTestRequest(t *testing.T, target string, headers http.Header, vars map[string]any) (*http.Request, *caddy.Replacer) {
t.Helper()
if target == "" {
target = "https://example.com/test"
}
req := httptest.NewRequest(http.MethodGet, target, nil)
req.Header = headers
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
if vars == nil {
vars = make(map[string]any)
}
// Inject vars directly so these tests exercise matcher-side handling of
// already-resolved values, not VarsMiddleware placeholder expansion.
ctx = context.WithValue(ctx, VarsCtxKey, vars)
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
return req, repl
}
func TestVarsMatcherDoesNotExpandResolvedValues(t *testing.T) {
t.Setenv("CADDY_VARS_TEST_SECRET", "topsecret")
for _, tc := range []struct {
name string
target string
match VarsMatcher
headers http.Header
vars map[string]any
expect bool
}{
{
name: "literal variable value containing placeholder syntax is not re-expanded",
match: VarsMatcher{"secret": []string{"topsecret"}},
vars: map[string]any{"secret": "{env.CADDY_VARS_TEST_SECRET}"},
expect: false,
},
{
name: "placeholder key value containing placeholder syntax is not re-expanded",
match: VarsMatcher{"{http.request.header.X-Input}": []string{"topsecret"}},
headers: http.Header{"X-Input": []string{"{env.CADDY_VARS_TEST_SECRET}"}},
expect: false,
},
{
name: "query placeholder value containing placeholder syntax is not re-expanded",
target: "https://example.com/test?foo=%7Benv.CADDY_VARS_TEST_SECRET%7D",
match: VarsMatcher{"{http.request.uri.query.foo}": []string{"topsecret"}},
expect: false,
},
{
name: "matcher values still expand placeholders",
match: VarsMatcher{"secret": []string{"{env.CADDY_VARS_TEST_SECRET}"}},
vars: map[string]any{"secret": "topsecret"},
expect: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
req, _ := newVarsTestRequest(t, tc.target, tc.headers, tc.vars)
actual, err := tc.match.MatchWithError(req)
if err != nil {
t.Fatalf("MatchWithError() error = %v", err)
}
if actual != tc.expect {
t.Fatalf("MatchWithError() = %t, want %t", actual, tc.expect)
}
})
}
}
func TestMatchVarsREDoesNotExpandResolvedValues(t *testing.T) {
t.Setenv("CADDY_VARS_TEST_SECRET", "topsecret")
for _, tc := range []struct {
name string
target string
match MatchVarsRE
headers http.Header
vars map[string]any
expect bool
}{
{
name: "literal variable value containing placeholder syntax is not re-expanded",
match: MatchVarsRE{"secret": &MatchRegexp{Pattern: "^topsecret$"}},
vars: map[string]any{"secret": "{env.CADDY_VARS_TEST_SECRET}"},
expect: false,
},
{
name: "placeholder key value containing placeholder syntax is not re-expanded",
match: MatchVarsRE{"{http.request.header.X-Input}": &MatchRegexp{Pattern: "^topsecret$"}},
headers: http.Header{"X-Input": []string{"{env.CADDY_VARS_TEST_SECRET}"}},
expect: false,
},
{
name: "query placeholder value containing placeholder syntax is not re-expanded",
target: "https://example.com/test?foo=%7Benv.CADDY_VARS_TEST_SECRET%7D",
match: MatchVarsRE{"{http.request.uri.query.foo}": &MatchRegexp{Pattern: "^topsecret$"}},
expect: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := tc.match.Provision(caddy.Context{})
if err != nil {
t.Fatalf("Provision() error = %v", err)
}
err = tc.match.Validate()
if err != nil {
t.Fatalf("Validate() error = %v", err)
}
req, _ := newVarsTestRequest(t, tc.target, tc.headers, tc.vars)
actual, err := tc.match.MatchWithError(req)
if err != nil {
t.Fatalf("MatchWithError() error = %v", err)
}
if actual != tc.expect {
t.Fatalf("MatchWithError() = %t, want %t", actual, tc.expect)
}
})
}
}
-45
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)
@@ -185,15 +149,6 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
iss.AccountKey = accountKey
}
// expand DNS override domain, if non-empty
if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.OverrideDomain != "" {
overrideDomain, err := repl.ReplaceOrErr(iss.Challenges.DNS.OverrideDomain, true, true)
if err != nil {
return fmt.Errorf("expanding DNS override domain '%s': %v", iss.Challenges.DNS.OverrideDomain, err)
}
iss.Challenges.DNS.OverrideDomain = overrideDomain
}
// DNS challenge provider, if not already established
if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.solver == nil {
var prov certmagic.DNSProvider
-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)
}
}
+15 -307
View File
@@ -4,7 +4,6 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
@@ -28,8 +27,6 @@ func init() {
caddy.RegisterModule(PKIIntermediateCAPool{})
caddy.RegisterModule(StoragePool{})
caddy.RegisterModule(HTTPCertPool{})
caddy.RegisterModule(SystemCAPool{})
caddy.RegisterModule(CombinedCAPool{})
}
// The interface to be implemented by all guest modules part of
@@ -38,12 +35,6 @@ type CA interface {
CertPool() *x509.CertPool
}
// CertificateProvider is an optional interface that CA pool sources
// can implement to expose their underlying certificates for combining.
type CertificateProvider interface {
Certificates() []*x509.Certificate
}
// InlineCAPool is a certificate authority pool provider coming from
// a DER-encoded certificates in the config
type InlineCAPool struct {
@@ -53,8 +44,7 @@ type InlineCAPool struct {
// these CAs will be rejected.
TrustedCACerts []string `json:"trusted_ca_certs,omitempty"`
pool *x509.CertPool
certs []*x509.Certificate
pool *x509.CertPool
}
// CaddyModule implements caddy.Module.
@@ -70,17 +60,14 @@ func (icp InlineCAPool) CaddyModule() caddy.ModuleInfo {
// Provision implements caddy.Provisioner.
func (icp *InlineCAPool) Provision(ctx caddy.Context) error {
caPool := x509.NewCertPool()
var certs []*x509.Certificate
for i, clientCAString := range icp.TrustedCACerts {
clientCA, err := decodeBase64DERCert(clientCAString)
if err != nil {
return fmt.Errorf("parsing certificate at index %d: %v", i, err)
}
caPool.AddCert(clientCA)
certs = append(certs, clientCA)
}
icp.pool = caPool
icp.certs = certs
return nil
}
@@ -116,11 +103,6 @@ func (icp InlineCAPool) CertPool() *x509.CertPool {
return icp.pool
}
// Certificates implements CertificateProvider.
func (icp InlineCAPool) Certificates() []*x509.Certificate {
return icp.certs
}
// FileCAPool generates trusted root certificates pool from the designated DER and PEM file
type FileCAPool struct {
// TrustedCACertPEMFiles is a list of PEM file names
@@ -129,8 +111,7 @@ type FileCAPool struct {
// these CA certificates will be rejected.
TrustedCACertPEMFiles []string `json:"pem_files,omitempty"`
pool *x509.CertPool
certs []*x509.Certificate
pool *x509.CertPool
}
// CaddyModule implements caddy.Module.
@@ -146,32 +127,14 @@ func (FileCAPool) CaddyModule() caddy.ModuleInfo {
// Loads and decodes the DER and pem files to generate the certificate pool
func (f *FileCAPool) Provision(ctx caddy.Context) error {
caPool := x509.NewCertPool()
var certs []*x509.Certificate
for _, pemFile := range f.TrustedCACertPEMFiles {
pemContents, err := os.ReadFile(pemFile)
if err != nil {
return fmt.Errorf("reading %s: %v", pemFile, err)
}
// Parse PEM to extract certificates
for len(pemContents) > 0 {
var block *pem.Block
block, pemContents = pem.Decode(pemContents)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("parsing certificate in %s: %v", pemFile, err)
}
caPool.AddCert(cert)
certs = append(certs, cert)
}
caPool.AppendCertsFromPEM(pemContents)
}
f.pool = caPool
f.certs = certs
return nil
}
@@ -203,19 +166,13 @@ func (f FileCAPool) CertPool() *x509.CertPool {
return f.pool
}
// Certificates implements CertificateProvider.
func (f FileCAPool) Certificates() []*x509.Certificate {
return f.certs
}
// PKIRootCAPool extracts the trusted root certificates from Caddy's native 'pki' app
type PKIRootCAPool struct {
// List of the Authority names that are configured in the `pki` app whose root certificates are trusted
Authority []string `json:"authority,omitempty"`
ca []*caddypki.CA
pool *x509.CertPool
certs []*x509.Certificate
ca []*caddypki.CA
pool *x509.CertPool
}
// CaddyModule implements caddy.Module.
@@ -244,17 +201,10 @@ func (p *PKIRootCAPool) Provision(ctx caddy.Context) error {
}
caPool := x509.NewCertPool()
var certs []*x509.Certificate
for _, ca := range p.ca {
rootCert := ca.RootCertificate()
if rootCert == nil {
return fmt.Errorf("CA %s has no root certificate", ca.ID)
}
caPool.AddCert(rootCert)
certs = append(certs, rootCert)
caPool.AddCert(ca.RootCertificate())
}
p.pool = caPool
p.certs = certs
return nil
}
@@ -288,19 +238,13 @@ func (p PKIRootCAPool) CertPool() *x509.CertPool {
return p.pool
}
// Certificates implements CertificateProvider.
func (p PKIRootCAPool) Certificates() []*x509.Certificate {
return p.certs
}
// PKIIntermediateCAPool extracts the trusted intermediate certificates from Caddy's native 'pki' app
type PKIIntermediateCAPool struct {
// List of the Authority names that are configured in the `pki` app whose intermediate certificates are trusted
Authority []string `json:"authority,omitempty"`
ca []*caddypki.CA
pool *x509.CertPool
certs []*x509.Certificate
ca []*caddypki.CA
pool *x509.CertPool
}
// CaddyModule implements caddy.Module.
@@ -329,18 +273,12 @@ func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error {
}
caPool := x509.NewCertPool()
var certs []*x509.Certificate
for _, ca := range p.ca {
for _, c := range ca.IntermediateCertificateChain() {
if c == nil {
return fmt.Errorf("CA %s has a nil certificate in its intermediate chain", ca.ID)
}
caPool.AddCert(c)
certs = append(certs, c)
}
}
p.pool = caPool
p.certs = certs
return nil
}
@@ -373,11 +311,6 @@ func (p PKIIntermediateCAPool) CertPool() *x509.CertPool {
return p.pool
}
// Certificates implements CertificateProvider.
func (p PKIIntermediateCAPool) Certificates() []*x509.Certificate {
return p.certs
}
// StoragePool extracts the trusted certificates root from Caddy storage
type StoragePool struct {
// The storage module where the trusted root certificates are stored. Absent
@@ -389,7 +322,6 @@ type StoragePool struct {
storage certmagic.Storage
pool *x509.CertPool
certs []*x509.Certificate
}
// CaddyModule implements caddy.Module.
@@ -422,33 +354,16 @@ func (ca *StoragePool) Provision(ctx caddy.Context) error {
return fmt.Errorf("no PEM keys specified")
}
caPool := x509.NewCertPool()
var certs []*x509.Certificate
for _, caID := range ca.PEMKeys {
bs, err := ca.storage.Load(ctx, caID)
if err != nil {
return fmt.Errorf("error loading cert '%s' from storage: %s", caID, err)
}
// Parse PEM to extract certificates
pemData := bs
for len(pemData) > 0 {
var block *pem.Block
block, pemData = pem.Decode(pemData)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("parsing certificate '%s': %v", caID, err)
}
caPool.AddCert(cert)
certs = append(certs, cert)
if !caPool.AppendCertsFromPEM(bs) {
return fmt.Errorf("failed to add certificate '%s' to pool", caID)
}
}
ca.pool = caPool
ca.certs = certs
return nil
}
@@ -498,13 +413,9 @@ func (p StoragePool) CertPool() *x509.CertPool {
return p.pool
}
// Certificates implements CertificateProvider.
func (p StoragePool) Certificates() []*x509.Certificate {
return p.certs
}
// TLSConfig holds configuration related to the TLS configuration for the
// transport/client.
// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go
type TLSConfig struct {
// Provides the guest module that provides the trusted certificate authority (CA) certificates
CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
@@ -589,6 +500,7 @@ func (t *TLSConfig) unmarshalCaddyfile(d *caddyfile.Dispenser) error {
// MakeTLSClientConfig returns a tls.Config usable by a client to a backend.
// If there is no custom TLS configuration, a nil config may be returned.
// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go
func (t *TLSConfig) makeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
if !ok || repl == nil {
@@ -642,8 +554,7 @@ type HTTPCertPool struct {
// Customize the TLS connection knobs to used during the HTTP call
TLS *TLSConfig `json:"tls,omitempty"`
pool *x509.CertPool
certs []*x509.Certificate
pool *x509.CertPool
}
// CaddyModule implements caddy.Module.
@@ -659,7 +570,6 @@ func (HTTPCertPool) CaddyModule() caddy.ModuleInfo {
// Provision implements caddy.Provisioner.
func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
caPool := x509.NewCertPool()
var certs []*x509.Certificate
customTransport := http.DefaultTransport.(*http.Transport).Clone()
if hcp.TLS != nil {
@@ -687,30 +597,11 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
if err != nil {
return err
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return fmt.Errorf("HTTP %d fetching CA certificate bundle from %s", res.StatusCode, uri)
}
// Parse PEM to extract certificates
pemData := pembs
for len(pemData) > 0 {
var block *pem.Block
block, pemData = pem.Decode(pemData)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("parsing certificate from URL %s: %v", uri, err)
}
caPool.AddCert(cert)
certs = append(certs, cert)
if !caPool.AppendCertsFromPEM(pembs) {
return fmt.Errorf("failed to add certs from URL: %s", uri)
}
}
hcp.pool = caPool
hcp.certs = certs
return nil
}
@@ -774,179 +665,6 @@ func (hcp HTTPCertPool) CertPool() *x509.CertPool {
return hcp.pool
}
// Certificates implements CertificateProvider.
func (hcp HTTPCertPool) Certificates() []*x509.Certificate {
return hcp.certs
}
// SystemCAPool obtains the trusted root certificates from the system's
// certificate pool using x509.SystemCertPool()
type SystemCAPool struct {
pool *x509.CertPool
}
// CaddyModule implements caddy.Module.
func (SystemCAPool) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "tls.ca_pool.source.system",
New: func() caddy.Module {
return new(SystemCAPool)
},
}
}
// Provision implements caddy.Provisioner.
func (scp *SystemCAPool) Provision(ctx caddy.Context) error {
pool, err := x509.SystemCertPool()
if err != nil {
return fmt.Errorf("failed to load system cert pool: %v", err)
}
scp.pool = pool
return nil
}
func (scp *SystemCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume module name
if d.CountRemainingArgs() > 0 {
return d.ArgErr()
}
if d.NextBlock(0) {
return d.Err("system trust pool does not support any configuration")
}
return nil
}
// CertPool implements CA.
func (scp SystemCAPool) CertPool() *x509.CertPool {
return scp.pool
}
// The `combined` pool type merges multiple pools. The `sources` pools must implement the
// `CertificateProvider` interface, which allows them to export their certificate set.
//
// Note: SystemCAPool does not implement CertificateProvider because
// x509.SystemCertPool() doesn't expose its certificates, so it cannot
// be used as a source in CombinedCAPool.
type CombinedCAPool struct {
// The CA pool sources to combine. Each source is a CA pool provider module.
SourcesRaw []json.RawMessage `json:"sources,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
sources []CA
pool *x509.CertPool
certs []*x509.Certificate
}
// CaddyModule implements caddy.Module.
func (CombinedCAPool) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "tls.ca_pool.source.combined",
New: func() caddy.Module {
return new(CombinedCAPool)
},
}
}
// Provision implements caddy.Provisioner.
func (ccp *CombinedCAPool) Provision(ctx caddy.Context) error {
if len(ccp.SourcesRaw) == 0 {
return fmt.Errorf("no sources specified for combined CA pool")
}
// Load all source modules
sources, err := ctx.LoadModule(ccp, "SourcesRaw")
if err != nil {
return fmt.Errorf("loading CA pool sources: %v", err)
}
caPool := x509.NewCertPool()
var allCerts []*x509.Certificate
for _, src := range sources.([]any) {
ca, ok := src.(CA)
if !ok {
return fmt.Errorf("source module is not a CA pool provider")
}
ccp.sources = append(ccp.sources, ca)
certProvider, ok := ca.(CertificateProvider)
if !ok {
return fmt.Errorf("source %T does not implement CertificateProvider (required for combining)", ca)
}
certs := certProvider.Certificates()
if certs == nil {
return fmt.Errorf("source %T returned nil certificates", ca)
}
for _, cert := range certs {
if cert == nil {
return fmt.Errorf("source %T returned a nil certificate", ca)
}
caPool.AddCert(cert)
allCerts = append(allCerts, cert)
}
}
ccp.pool = caPool
ccp.certs = allCerts
return nil
}
// Syntax:
//
// trust_pool combined {
// source <module_name> {
// <module_config>
// }
// }
//
// The 'source' directive can be specified multiple times. Sources that
// don't implement CertificateProvider (like 'system') cannot be combined.
func (ccp *CombinedCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume module name
if d.CountRemainingArgs() > 0 {
return d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "source":
if !d.NextArg() {
return d.ArgErr()
}
modStem := d.Val()
modID := "tls.ca_pool.source." + modStem
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
ca, ok := unm.(CA)
if !ok {
return d.Errf("module %s is not a CA pool provider", modID)
}
ccp.SourcesRaw = append(ccp.SourcesRaw, caddyconfig.JSONModuleObject(ca, "provider", modStem, nil))
default:
return d.Errf("unrecognized directive: %s", d.Val())
}
}
if len(ccp.SourcesRaw) == 0 {
return d.Err("no sources specified")
}
return nil
}
// CertPool implements CA.
func (ccp CombinedCAPool) CertPool() *x509.CertPool {
return ccp.pool
}
// Certificates implements CertificateProvider.
func (ccp CombinedCAPool) Certificates() []*x509.Certificate {
return ccp.certs
}
var (
_ caddy.Module = (*InlineCAPool)(nil)
_ caddy.Provisioner = (*InlineCAPool)(nil)
@@ -978,14 +696,4 @@ var (
_ caddy.Validator = (*HTTPCertPool)(nil)
_ CA = (*HTTPCertPool)(nil)
_ caddyfile.Unmarshaler = (*HTTPCertPool)(nil)
_ caddy.Module = (*SystemCAPool)(nil)
_ caddy.Provisioner = (*SystemCAPool)(nil)
_ CA = (*SystemCAPool)(nil)
_ caddyfile.Unmarshaler = (*SystemCAPool)(nil)
_ caddy.Module = (*CombinedCAPool)(nil)
_ caddy.Provisioner = (*CombinedCAPool)(nil)
_ CA = (*CombinedCAPool)(nil)
_ caddyfile.Unmarshaler = (*CombinedCAPool)(nil)
)
-217
View File
@@ -1,7 +1,6 @@
package caddytls
import (
"context"
"encoding/json"
"fmt"
"reflect"
@@ -777,219 +776,3 @@ func TestHTTPCertPoolUnmarshalCaddyfile(t *testing.T) {
})
}
}
func TestSystemCAPoolUnmarshalCaddyfile(t *testing.T) {
type args struct {
d *caddyfile.Dispenser
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "basic system pool configuration",
args: args{
d: caddyfile.NewTestDispenser(`system`),
},
wantErr: false,
},
{
name: "system pool with arguments produces error",
args: args{
d: caddyfile.NewTestDispenser(`system foo`),
},
wantErr: true,
},
{
name: "system pool with block produces error",
args: args{
d: caddyfile.NewTestDispenser(`system {
foo bar
}`),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scp := &SystemCAPool{}
if err := scp.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr {
t.Errorf("SystemCAPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestCombinedCAPoolUnmarshalCaddyfile(t *testing.T) {
type args struct {
d *caddyfile.Dispenser
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty block produces error",
args: args{
d: caddyfile.NewTestDispenser(`combined {
}`),
},
wantErr: true,
},
{
name: "arguments on same line as module name produces error",
args: args{
d: caddyfile.NewTestDispenser(`combined foo`),
},
wantErr: true,
},
{
name: "single source - system",
args: args{
d: caddyfile.NewTestDispenser(`combined {
source system
}`),
},
wantErr: false,
},
{
name: "single source - inline with config",
args: args{
d: caddyfile.NewTestDispenser(fmt.Sprintf(`combined {
source inline {
trust_der %s
}
}`, test_der_1)),
},
wantErr: false,
},
{
name: "multiple sources produces error due to limitation",
args: args{
d: caddyfile.NewTestDispenser(fmt.Sprintf(`combined {
source system
source inline {
trust_der %s
}
}`, test_der_1)),
},
wantErr: false, // UnmarshalCaddyfile succeeds, but Provision will fail
},
{
name: "source without module name produces error",
args: args{
d: caddyfile.NewTestDispenser(`combined {
source
}`),
},
wantErr: true,
},
{
name: "invalid directive produces error",
args: args{
d: caddyfile.NewTestDispenser(`combined {
invalid_directive foo
}`),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ccp := &CombinedCAPool{}
if err := ccp.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr {
t.Errorf("CombinedCAPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && len(ccp.SourcesRaw) == 0 {
t.Errorf("CombinedCAPool.UnmarshalCaddyfile() produced no sources")
}
})
}
}
func TestSystemCAPoolProvision(t *testing.T) {
scp := &SystemCAPool{}
ctx := caddy.Context{Context: context.Background()}
err := scp.Provision(ctx)
if err != nil {
t.Errorf("SystemCAPool.Provision() error = %v", err)
}
if scp.pool == nil {
t.Error("SystemCAPool.Provision() did not create a cert pool")
}
pool := scp.CertPool()
if pool == nil {
t.Error("SystemCAPool.CertPool() returned nil")
}
}
func TestCombinedCAPoolProvisionWithSystemFails(t *testing.T) {
// Test that combining system pool fails during Provision
// because SystemCAPool doesn't implement CertificateProvider
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
// Create a combined pool with system source
ccp := &CombinedCAPool{
SourcesRaw: []json.RawMessage{
json.RawMessage(`{"provider":"system"}`),
},
}
err := ccp.Provision(ctx)
if err == nil {
t.Error("CombinedCAPool.Provision() with system source should fail, but succeeded")
}
// Verify error message mentions CertificateProvider
if err != nil && !contains(err.Error(), "CertificateProvider") {
t.Errorf("Expected error to mention CertificateProvider, got: %v", err)
}
}
func TestCombinedCAPoolProvisionWithInlineSucceeds(t *testing.T) {
// Test that combining inline pools works
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
// Create a combined pool with inline source
ccp := &CombinedCAPool{
SourcesRaw: []json.RawMessage{
json.RawMessage(fmt.Sprintf(`{"provider":"inline","trusted_ca_certs":["%s"]}`, test_der_1)),
},
}
err := ccp.Provision(ctx)
if err != nil {
t.Errorf("CombinedCAPool.Provision() with inline source failed: %v", err)
}
if ccp.pool == nil {
t.Error("CombinedCAPool.Provision() did not create a cert pool")
}
pool := ccp.CertPool()
if pool == nil {
t.Error("CombinedCAPool.CertPool() returned nil")
}
}
// Helper function for string contains check
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
}
func findSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
-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
+13 -13
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)
}
@@ -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)
}
-218
View File
@@ -1,218 +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 logging
import (
"encoding/json"
"fmt"
"os"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
"golang.org/x/term"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func init() {
caddy.RegisterModule(JournaldEncoder{})
}
// JournaldEncoder wraps another encoder and prepends a systemd/journald
// priority prefix to each emitted log line. This lets journald classify
// stdout/stderr log lines by severity while leaving the underlying log
// structure to the wrapped encoder.
//
// This encoder does not write directly to journald; it only changes the
// encoded output by adding the priority marker that journald understands.
// The wrapped encoder still controls the actual log format, such as JSON
// or console output.
type JournaldEncoder struct {
zapcore.Encoder `json:"-"`
// The underlying encoder that actually encodes the log entries.
// If not specified, defaults to "json", unless the output is a
// terminal, in which case it defaults to "console".
WrappedRaw json.RawMessage `json:"wrap,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
wrappedIsDefault bool
ctx caddy.Context
}
// CaddyModule returns the Caddy module information.
func (JournaldEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.logging.encoders.journald",
New: func() caddy.Module { return new(JournaldEncoder) },
}
}
// Provision sets up the encoder.
func (je *JournaldEncoder) Provision(ctx caddy.Context) error {
je.ctx = ctx
if je.WrappedRaw == nil {
je.Encoder = &JSONEncoder{}
if p, ok := je.Encoder.(caddy.Provisioner); ok {
if err := p.Provision(ctx); err != nil {
return fmt.Errorf("provisioning fallback encoder module: %v", err)
}
}
je.wrappedIsDefault = true
} else {
val, err := ctx.LoadModule(je, "WrappedRaw")
if err != nil {
return fmt.Errorf("loading wrapped encoder module: %v", err)
}
je.Encoder = val.(zapcore.Encoder)
}
suppressConsoleEncoderTimestamp(je.Encoder)
return nil
}
// ConfigureDefaultFormat will set the default wrapped format to "console"
// if the writer is a terminal. If already configured, it passes through
// the writer so a deeply nested encoder can configure its own default format.
func (je *JournaldEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error {
if !je.wrappedIsDefault {
if cfd, ok := je.Encoder.(caddy.ConfiguresFormatterDefault); ok {
return cfd.ConfigureDefaultFormat(wo)
}
return nil
}
if caddy.IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) {
je.Encoder = &ConsoleEncoder{}
if p, ok := je.Encoder.(caddy.Provisioner); ok {
if err := p.Provision(je.ctx); err != nil {
return fmt.Errorf("provisioning fallback encoder module: %v", err)
}
}
}
suppressConsoleEncoderTimestamp(je.Encoder)
return nil
}
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
//
// journald {
// wrap <another encoder>
// }
//
// Example:
//
// log {
// format journald {
// wrap json
// }
// }
func (je *JournaldEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume encoder name
if d.NextArg() {
return d.ArgErr()
}
for d.NextBlock(0) {
if d.Val() != "wrap" {
return d.Errf("unrecognized subdirective %s", d.Val())
}
if !d.NextArg() {
return d.ArgErr()
}
moduleName := d.Val()
moduleID := "caddy.logging.encoders." + moduleName
unm, err := caddyfile.UnmarshalModule(d, moduleID)
if err != nil {
return err
}
enc, ok := unm.(zapcore.Encoder)
if !ok {
return d.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm)
}
je.WrappedRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, nil)
}
return nil
}
// Clone implements zapcore.Encoder.
func (je JournaldEncoder) Clone() zapcore.Encoder {
return JournaldEncoder{
Encoder: je.Encoder.Clone(),
}
}
// EncodeEntry implements zapcore.Encoder.
func (je JournaldEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
encoded, err := je.Encoder.Clone().EncodeEntry(ent, fields)
if err != nil {
return nil, err
}
out := bufferpool.Get()
out.AppendString(journaldPriorityPrefix(ent.Level))
out.AppendBytes(encoded.Bytes())
encoded.Free()
return out, nil
}
func journaldPriorityPrefix(level zapcore.Level) string {
switch level {
case zapcore.InvalidLevel:
return "<6>"
case zapcore.DebugLevel:
return "<7>"
case zapcore.InfoLevel:
return "<6>"
case zapcore.WarnLevel:
return "<4>"
case zapcore.ErrorLevel:
return "<3>"
case zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
return "<2>"
default:
return "<6>"
}
}
func suppressConsoleEncoderTimestamp(enc zapcore.Encoder) {
empty := ""
switch e := enc.(type) {
case *ConsoleEncoder:
e.TimeKey = &empty
_ = e.Provision(caddy.Context{})
case *AppendEncoder:
suppressConsoleEncoderTimestamp(e.wrapped)
case *FilterEncoder:
suppressConsoleEncoderTimestamp(e.wrapped)
case *JournaldEncoder:
suppressConsoleEncoderTimestamp(e.Encoder)
}
}
// Interface guards
var (
_ zapcore.Encoder = (*JournaldEncoder)(nil)
_ caddyfile.Unmarshaler = (*JournaldEncoder)(nil)
_ caddy.ConfiguresFormatterDefault = (*JournaldEncoder)(nil)
)
-155
View File
@@ -1,155 +0,0 @@
package logging
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func TestJournaldPriorityPrefix(t *testing.T) {
tests := []struct {
level zapcore.Level
want string
}{
{level: zapcore.InvalidLevel, want: "<6>"},
{level: zapcore.DebugLevel, want: "<7>"},
{level: zapcore.InfoLevel, want: "<6>"},
{level: zapcore.WarnLevel, want: "<4>"},
{level: zapcore.ErrorLevel, want: "<3>"},
{level: zapcore.DPanicLevel, want: "<2>"},
{level: zapcore.PanicLevel, want: "<2>"},
{level: zapcore.FatalLevel, want: "<2>"},
}
for _, tt := range tests {
t.Run(tt.level.String(), func(t *testing.T) {
if got := journaldPriorityPrefix(tt.level); got != tt.want {
t.Fatalf("got %s, want %s", got, tt.want)
}
})
}
}
func TestJournaldEncoderEncodeEntry(t *testing.T) {
tests := []struct {
name string
level zapcore.Level
want string
}{
{name: "debug", level: zapcore.DebugLevel, want: "<7>wrapped\n"},
{name: "info", level: zapcore.InfoLevel, want: "<6>wrapped\n"},
{name: "warn", level: zapcore.WarnLevel, want: "<4>wrapped\n"},
{name: "error", level: zapcore.ErrorLevel, want: "<3>wrapped\n"},
{name: "panic", level: zapcore.PanicLevel, want: "<2>wrapped\n"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
enc := JournaldEncoder{Encoder: staticEncoder{output: "wrapped\n"}}
buf, err := enc.EncodeEntry(zapcore.Entry{Level: tt.level}, nil)
if err != nil {
t.Fatalf("EncodeEntry() error = %v", err)
}
defer buf.Free()
if got := buf.String(); got != tt.want {
t.Fatalf("got %q, want %q", got, tt.want)
}
})
}
}
func TestJournaldEncoderUnmarshalCaddyfile(t *testing.T) {
d := caddyfile.NewTestDispenser(`
journald {
wrap console
}
`)
var enc JournaldEncoder
if err := enc.UnmarshalCaddyfile(d); err != nil {
t.Fatalf("UnmarshalCaddyfile() error = %v", err)
}
var got map[string]any
if err := json.Unmarshal(enc.WrappedRaw, &got); err != nil {
t.Fatalf("unmarshal wrapped encoder: %v", err)
}
if got["format"] != "console" {
t.Fatalf("wrapped format = %v, want console", got["format"])
}
}
func TestJournaldEncoderPreservesJSONTimestamp(t *testing.T) {
enc := &JournaldEncoder{
Encoder: &JSONEncoder{},
}
if err := enc.Provision(caddy.Context{Context: context.Background()}); err != nil {
t.Fatalf("Provision() error = %v", err)
}
buf, err := enc.EncodeEntry(zapcore.Entry{
Level: zapcore.InfoLevel,
Time: fixedEntryTime(),
Message: "hello",
}, nil)
if err != nil {
t.Fatalf("EncodeEntry() error = %v", err)
}
defer buf.Free()
got := buf.String()
if !strings.Contains(got, `"ts"`) {
t.Fatalf("got JSON output without ts field: %q", got)
}
}
func TestJournaldEncoderSuppressesConsoleTimestamp(t *testing.T) {
enc := &JournaldEncoder{
Encoder: &ConsoleEncoder{},
}
if err := enc.Provision(caddy.Context{Context: context.Background()}); err != nil {
t.Fatalf("Provision() error = %v", err)
}
buf, err := enc.EncodeEntry(zapcore.Entry{
Level: zapcore.InfoLevel,
Time: fixedEntryTime(),
Message: "hello",
}, nil)
if err != nil {
t.Fatalf("EncodeEntry() error = %v", err)
}
defer buf.Free()
got := buf.String()
if strings.Contains(got, "2001/02/03") {
t.Fatalf("got console output with timestamp: %q", got)
}
}
type staticEncoder struct {
nopEncoder
output string
}
func (se staticEncoder) Clone() zapcore.Encoder { return se }
func (se staticEncoder) EncodeEntry(zapcore.Entry, []zapcore.Field) (*buffer.Buffer, error) {
buf := bufferpool.Get()
buf.AppendString(se.output)
return buf, nil
}
func fixedEntryTime() (ts time.Time) {
return time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC)
}
-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