mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc58caa109 | |||
| d80774cb3f | |||
| a4a38c3e88 | |||
| 761347aa63 | |||
| 4ba16fe82c | |||
| 0fab9f0f7d | |||
| 5e76b5ee43 | |||
| 9c78b97f9e | |||
| fb324331f4 | |||
| 0780d4489c | |||
| d2172bea61 | |||
| c7c9f3108a | |||
| 7e77eec0ae | |||
| ef496e58ef | |||
| 18ab0f955f | |||
| 6a64bb2ce5 | |||
| 4d6945769d | |||
| 2d33271482 | |||
| c653e7d61a | |||
| c1918ff1ad | |||
| fdbef2a6ef | |||
| 2a3ed96f8c | |||
| 355c178213 | |||
| f6ee80be1b | |||
| 48c08e3890 | |||
| cf42f61566 | |||
| 41aee97386 | |||
| 441d5eb062 | |||
| aed1af5976 | |||
| 4430756d5c | |||
| af89c5ab02 | |||
| bd9f145321 | |||
| 24bebd0a07 | |||
| 7dedd1486c | |||
| 7586e68e27 | |||
| 0c7c91a447 | |||
| 1a3e900b35 | |||
| 0722cf6fd8 | |||
| 8e2dd5079c | |||
| 5f44ea0748 | |||
| c8e4ac2c8c | |||
| 7dcc041eec | |||
| ca0ca67fbd | |||
| 92b62004eb | |||
| 6c23ec2f3c | |||
| 5de1565ff6 | |||
| d7834676aa | |||
| 4f50458866 | |||
| ea4ee3ae5d | |||
| 30b80bece8 | |||
| 7a630f2910 | |||
| 62e9c05264 | |||
| 6f6771aa1d | |||
| acf8d6a1ae | |||
| e98ed6232d | |||
| c35ba5588d | |||
| 5d189aff40 | |||
| df65455b1f | |||
| 8499e34e10 |
+3
-1
@@ -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,6 +25,8 @@ 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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
# 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**: 1–2 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
|
||||
@@ -45,6 +45,8 @@ 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
|
||||
@@ -118,10 +120,6 @@ type AdminConfig struct {
|
||||
//
|
||||
// EXPERIMENTAL: This feature is subject to change.
|
||||
Remote *RemoteAdmin `json:"remote,omitempty"`
|
||||
|
||||
// Holds onto the routers so that we can later provision them
|
||||
// if they require provisioning.
|
||||
routers []AdminRouter
|
||||
}
|
||||
|
||||
// ConfigSettings configures the management of configuration.
|
||||
@@ -210,8 +208,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. Paths are simple prefix matches.
|
||||
// Any subpath of the specified paths will be allowed.
|
||||
// The API paths allowed. A request path must either equal an
|
||||
// allowed path or be a subpath with a path-segment boundary.
|
||||
Paths []string `json:"paths,omitempty"`
|
||||
|
||||
// The HTTP methods allowed for the given paths.
|
||||
@@ -220,7 +218,7 @@ type AdminPermissions struct {
|
||||
|
||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||
// for use in an admin endpoint server, which will be listening on listenAddr.
|
||||
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler {
|
||||
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) (adminHandler, error) {
|
||||
muxWrap := adminHandler{mux: http.NewServeMux()}
|
||||
|
||||
// secure the local or remote endpoint respectively
|
||||
@@ -277,34 +275,21 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co
|
||||
// register third-party module endpoints
|
||||
for _, m := range GetModules("admin.api") {
|
||||
router := m.New().(AdminRouter)
|
||||
|
||||
// provision the router before registering its routes, so
|
||||
// handlers have access to all provisioned state
|
||||
if provisioner, ok := router.(Provisioner); ok {
|
||||
if err := provisioner.Provision(ctx); err != nil {
|
||||
return adminHandler{}, fmt.Errorf("provisioning admin router module %s: %v", m.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, route := range router.Routes() {
|
||||
addRoute(route.Pattern, handlerLabel, route.Handler)
|
||||
}
|
||||
admin.routers = append(admin.routers, router)
|
||||
}
|
||||
|
||||
return muxWrap
|
||||
}
|
||||
|
||||
// provisionAdminRouters provisions all the router modules
|
||||
// in the admin.api namespace that need provisioning.
|
||||
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
|
||||
for _, router := range admin.routers {
|
||||
provisioner, ok := router.(Provisioner)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
err := provisioner.Provision(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// We no longer need the routers once provisioned, allow for GC
|
||||
admin.routers = nil
|
||||
|
||||
return nil
|
||||
return muxWrap, nil
|
||||
}
|
||||
|
||||
// allowedOrigins returns a list of origins that are allowed.
|
||||
@@ -428,11 +413,7 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := cfg.Admin.newAdminHandler(addr, false, ctx)
|
||||
|
||||
// run the provisioners for loaded modules to make sure local
|
||||
// state is properly re-initialized in the new admin server
|
||||
err = cfg.Admin.provisionAdminRouters(ctx)
|
||||
handler, err := cfg.Admin.newAdminHandler(addr, false, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -556,11 +537,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
||||
|
||||
// make the HTTP handler but disable Host/Origin enforcement
|
||||
// because we are using TLS authentication instead
|
||||
handler := cfg.Admin.newAdminHandler(addr, true, ctx)
|
||||
|
||||
// run the provisioners for loaded modules to make sure local
|
||||
// state is properly re-initialized in the new admin server
|
||||
err = cfg.Admin.provisionAdminRouters(ctx)
|
||||
handler, err := cfg.Admin.newAdminHandler(addr, true, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -716,7 +693,7 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
|
||||
// verify path
|
||||
pathFound := accessPerm.Paths == nil
|
||||
for _, allowedPath := range accessPerm.Paths {
|
||||
if strings.HasPrefix(r.URL.Path, allowedPath) {
|
||||
if adminPathAllowed(r.URL.Path, allowedPath) {
|
||||
pathFound = true
|
||||
break
|
||||
}
|
||||
@@ -745,6 +722,19 @@ 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")
|
||||
@@ -800,7 +790,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.Reflect("headers", r.Header),
|
||||
zap.Object("headers", internal.LoggableHTTPHeader{Header: r.Header}),
|
||||
)
|
||||
if r.TLS != nil {
|
||||
log = log.With(
|
||||
@@ -859,6 +849,7 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Err: errors.New("invalid origin 'null'"),
|
||||
Message: "Buggy browser is sending null Origin header.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if h.enforceHost {
|
||||
@@ -1060,6 +1051,9 @@ 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{
|
||||
@@ -1142,6 +1136,20 @@ 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
|
||||
@@ -1203,11 +1211,12 @@ traverseLoop:
|
||||
var idx int
|
||||
if method != http.MethodPost {
|
||||
idxStr := parts[len(parts)-1]
|
||||
idx, err = strconv.Atoi(idxStr)
|
||||
idx, err = parseCanonicalArrayIndex(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)
|
||||
}
|
||||
@@ -1307,7 +1316,7 @@ traverseLoop:
|
||||
}
|
||||
|
||||
case []any:
|
||||
partInt, err := strconv.Atoi(part)
|
||||
partInt, err := parseCanonicalArrayIndex(part)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
||||
strings.Join(parts[:i+1], "/"), part, err)
|
||||
|
||||
+204
-15
@@ -15,9 +15,13 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
@@ -31,6 +35,8 @@ 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(`{
|
||||
@@ -51,6 +57,13 @@ 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;
|
||||
@@ -242,6 +255,51 @@ 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)
|
||||
@@ -282,7 +340,10 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse address: %v", err)
|
||||
}
|
||||
handler := cfg.Admin.newAdminHandler(addr, false, Context{})
|
||||
handler, err := cfg.Admin.newAdminHandler(addr, false, Context{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create admin handler: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -403,7 +464,10 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
|
||||
admin := &AdminConfig{
|
||||
EnforceOrigin: false,
|
||||
}
|
||||
handler := admin.newAdminHandler(addr, false, Context{})
|
||||
handler, err := admin.newAdminHandler(addr, false, Context{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create admin handler: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/mock", nil)
|
||||
req.Host = "localhost:2019"
|
||||
@@ -415,10 +479,6 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
|
||||
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
|
||||
t.Logf("Response body: %s", rr.Body.String())
|
||||
}
|
||||
|
||||
if len(admin.routers) != 1 {
|
||||
t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
|
||||
}
|
||||
}
|
||||
|
||||
type mockProvisionableRouter struct {
|
||||
@@ -456,19 +516,16 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
||||
name string
|
||||
provisionErr error
|
||||
wantErr bool
|
||||
routersAfter int // expected number of routers after provisioning
|
||||
}{
|
||||
{
|
||||
name: "successful provisioning",
|
||||
provisionErr: nil,
|
||||
wantErr: false,
|
||||
routersAfter: 0,
|
||||
},
|
||||
{
|
||||
name: "provisioning error",
|
||||
provisionErr: fmt.Errorf("provision failed"),
|
||||
wantErr: true,
|
||||
routersAfter: 1,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -504,8 +561,7 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
||||
t.Fatalf("Failed to parse address: %v", err)
|
||||
}
|
||||
|
||||
_ = admin.newAdminHandler(addr, false, Context{})
|
||||
err = admin.provisionAdminRouters(Context{})
|
||||
_, err = admin.newAdminHandler(addr, false, Context{})
|
||||
|
||||
if test.wantErr {
|
||||
if err == nil {
|
||||
@@ -516,10 +572,6 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
||||
t.Errorf("Expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(admin.routers) != test.routersAfter {
|
||||
t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -604,6 +656,99 @@ 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
|
||||
@@ -956,3 +1101,47 @@ 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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,10 +127,9 @@ func Load(cfgJSON []byte, forceReload bool) error {
|
||||
zap.Error(notifyErr),
|
||||
zap.String("reload_err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := notify.Ready(); err != nil {
|
||||
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
|
||||
if notifyErr := notify.Ready(); notifyErr != nil {
|
||||
Log().Error("unable to notify to service manager of ready state", zap.Error(notifyErr))
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -441,13 +440,6 @@ func run(newCfg *Config, start bool) (Context, error) {
|
||||
}
|
||||
}()
|
||||
|
||||
// Provision any admin routers which may need to access
|
||||
// some of the other apps at runtime
|
||||
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// Start
|
||||
err = func() error {
|
||||
started := make([]string, 0, len(ctx.cfg.apps))
|
||||
@@ -767,7 +759,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 !atomic.CompareAndSwapInt32(exiting, 0, 1) {
|
||||
if !exiting.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -846,11 +838,11 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
|
||||
}()
|
||||
}
|
||||
|
||||
var exiting = new(int32) // accessed atomically
|
||||
var exiting atomic.Bool
|
||||
|
||||
// Exiting returns true if the process is exiting.
|
||||
// EXPERIMENTAL API: subject to change or removal.
|
||||
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
|
||||
func Exiting() bool { return exiting.Load() }
|
||||
|
||||
// OnExit registers a callback to invoke during process exit.
|
||||
// This registration is PROCESS-GLOBAL, meaning that each
|
||||
|
||||
@@ -63,8 +63,33 @@ 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
|
||||
@@ -220,9 +245,11 @@ func Format(input []byte) []byte {
|
||||
}
|
||||
|
||||
if unicode.IsSpace(ch) {
|
||||
finishToken()
|
||||
space = true
|
||||
heredocEscaped = false
|
||||
if ch == '\n' {
|
||||
finishLine()
|
||||
newLines++
|
||||
}
|
||||
continue
|
||||
@@ -249,13 +276,19 @@ func Format(input []byte) []byte {
|
||||
}
|
||||
|
||||
openBrace = false
|
||||
if beginningOfLine {
|
||||
if openBraceOwnLine && previousLineWasTopLevelImport {
|
||||
if last != '\n' {
|
||||
nextLine()
|
||||
}
|
||||
indent()
|
||||
} else 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)
|
||||
@@ -266,8 +299,10 @@ func Format(input []byte) []byte {
|
||||
|
||||
switch {
|
||||
case ch == '{':
|
||||
finishToken()
|
||||
openBrace = true
|
||||
openBraceSpace = spacePrior && !beginningOfLine
|
||||
openBraceOwnLine = newLines > 0
|
||||
if openBraceSpace && newLines == 0 {
|
||||
write(' ')
|
||||
}
|
||||
@@ -275,11 +310,13 @@ func Format(input []byte) []byte {
|
||||
if quotes == "`" {
|
||||
write('{')
|
||||
openBraceWritten = true
|
||||
openBraceOwnLine = false
|
||||
continue
|
||||
}
|
||||
continue
|
||||
|
||||
case ch == '}' && (spacePrior || !openBrace):
|
||||
finishToken()
|
||||
if quotes == "`" {
|
||||
write('}')
|
||||
continue
|
||||
@@ -324,6 +361,7 @@ func Format(input []byte) []byte {
|
||||
space = true
|
||||
}
|
||||
|
||||
currentToken.WriteRune(ch)
|
||||
write(ch)
|
||||
|
||||
beginningOfLine = false
|
||||
|
||||
@@ -475,6 +475,21 @@ 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
|
||||
|
||||
@@ -550,7 +550,11 @@ func (p *parser) doImport(nesting int) error {
|
||||
}
|
||||
|
||||
if foundBlockDirective {
|
||||
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||
if maybeSnippet {
|
||||
tokensCopy = append(tokensCopy, token)
|
||||
} else {
|
||||
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -682,11 +686,28 @@ 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
|
||||
|
||||
@@ -930,6 +930,107 @@ 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)}
|
||||
}
|
||||
|
||||
@@ -550,26 +550,11 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
}
|
||||
|
||||
case acmeIssuer != nil:
|
||||
// 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}
|
||||
}
|
||||
|
||||
// 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)
|
||||
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,
|
||||
@@ -668,6 +653,8 @@ 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
|
||||
}
|
||||
|
||||
@@ -682,6 +669,10 @@ 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
|
||||
}
|
||||
@@ -1062,7 +1053,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
interval, err := time.ParseDuration(d.Val() + "ns")
|
||||
interval, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return nil, d.Errf("failed to parse interval: %v", err)
|
||||
}
|
||||
|
||||
@@ -66,14 +66,14 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
||||
input: `:8080 {
|
||||
log {
|
||||
sampling {
|
||||
interval 2
|
||||
interval 2s
|
||||
first 3
|
||||
thereafter 4
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
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"}}}}}}`,
|
||||
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"}}}}}}`,
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
|
||||
@@ -202,7 +202,10 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
|
||||
type Helper struct {
|
||||
*caddyfile.Dispenser
|
||||
// State stores intermediate variables during caddyfile adaptation.
|
||||
State map[string]any
|
||||
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
|
||||
options map[string]any
|
||||
warnings *[]caddyconfig.Warning
|
||||
matcherDefs map[string]caddy.ModuleMap
|
||||
@@ -385,6 +388,11 @@ 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()
|
||||
@@ -396,6 +404,7 @@ 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 {
|
||||
|
||||
@@ -143,6 +143,7 @@ func (st ServerType) Setup(
|
||||
parentBlock: sb.block,
|
||||
groupCounter: gc,
|
||||
State: state,
|
||||
BlockState: state,
|
||||
}
|
||||
|
||||
results, err := dirFunc(h)
|
||||
@@ -504,6 +505,7 @@ func (ServerType) extractNamedRoutes(
|
||||
parentBlock: sb.block,
|
||||
groupCounter: gc,
|
||||
State: state,
|
||||
BlockState: state,
|
||||
}
|
||||
|
||||
handler, err := ParseSegmentAsSubroute(h)
|
||||
|
||||
@@ -484,6 +484,8 @@ 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())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ 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"
|
||||
@@ -166,3 +168,126 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,6 +612,289 @@ 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
|
||||
@@ -698,14 +981,31 @@ 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++
|
||||
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
|
||||
|
||||
// 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)) {
|
||||
aps = slices.Delete(aps, i, i+1)
|
||||
i--
|
||||
}
|
||||
|
||||
@@ -55,6 +55,28 @@ 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,7 +18,9 @@ encode gzip zstd {
|
||||
|
||||
# Long way with a block for each encoding
|
||||
encode {
|
||||
zstd
|
||||
zstd {
|
||||
disable_checksum
|
||||
}
|
||||
gzip 5
|
||||
}
|
||||
|
||||
@@ -71,7 +73,9 @@ encode
|
||||
"gzip": {
|
||||
"level": 5
|
||||
},
|
||||
"zstd": {}
|
||||
"zstd": {
|
||||
"checksum": false
|
||||
}
|
||||
},
|
||||
"handler": "encode",
|
||||
"prefer": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
log {
|
||||
sampling {
|
||||
interval 300
|
||||
interval 5m
|
||||
first 50
|
||||
thereafter 40
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"logs": {
|
||||
"default": {
|
||||
"sampling": {
|
||||
"interval": 300,
|
||||
"interval": 300000000000,
|
||||
"first": 50,
|
||||
"thereafter": 40
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
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
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
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 300
|
||||
interval 5m
|
||||
first 50
|
||||
thereafter 40
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"log0": {
|
||||
"sampling": {
|
||||
"interval": 300,
|
||||
"interval": 300000000000,
|
||||
"first": 50,
|
||||
"thereafter": 40
|
||||
},
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
metrics {
|
||||
otlp
|
||||
}
|
||||
}
|
||||
:80 {
|
||||
respond "Hello"
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "Hello",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"otlp": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
: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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
: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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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,11 +54,6 @@ b.com {
|
||||
"via": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"subjects": [
|
||||
"b.com"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
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"
|
||||
}
|
||||
},
|
||||
{}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
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"
|
||||
}
|
||||
},
|
||||
{}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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"
|
||||
}
|
||||
},
|
||||
{}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
@@ -562,3 +563,233 @@ 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")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Used by import_block_snippet_invalid_subdirective.caddyfiletest
|
||||
|
||||
(test) {
|
||||
reverse_proxy {
|
||||
{block}
|
||||
}
|
||||
}
|
||||
@@ -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.Ptr {
|
||||
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Pointer {
|
||||
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
|
||||
}
|
||||
modPkgPath := reflect.TypeOf(iface).PkgPath()
|
||||
|
||||
+1
-1
@@ -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.Ptr {
|
||||
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Pointer {
|
||||
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)
|
||||
|
||||
@@ -4,53 +4,55 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
github.com/DeRuina/timberjack v1.3.9
|
||||
github.com/DeRuina/timberjack v1.4.2
|
||||
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.2
|
||||
github.com/caddyserver/certmagic v0.25.3
|
||||
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.27.0
|
||||
github.com/google/cel-go v0.28.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/klauspost/compress v1.18.4
|
||||
github.com/klauspost/compress v1.18.5
|
||||
github.com/klauspost/cpuid/v2 v2.3.0
|
||||
github.com/mholt/acmez/v3 v3.1.6
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/quic-go/quic-go v0.59.0
|
||||
github.com/smallstep/certificates v0.30.0-rc3
|
||||
github.com/smallstep/nosql v0.7.0
|
||||
github.com/quic-go/quic-go v0.59.1
|
||||
github.com/smallstep/certificates v0.30.2
|
||||
github.com/smallstep/nosql v0.8.0
|
||||
github.com/smallstep/truststore v0.13.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
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.40.0
|
||||
go.opentelemetry.io/otel/sdk v1.40.0
|
||||
go.step.sm/crypto v0.76.2
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||
go.step.sm/crypto v0.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.48.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
|
||||
golang.org/x/net v0.51.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/term v0.40.0
|
||||
golang.org/x/time v0.14.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/term v0.42.0
|
||||
golang.org/x/time v0.15.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go/auth v0.18.1 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
@@ -62,16 +64,16 @@ 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.3 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
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/puddle/v2 v2.2.2 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
@@ -87,31 +89,29 @@ require (
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // 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.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
google.golang.org/api v0.266.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // 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/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
)
|
||||
|
||||
@@ -133,13 +133,13 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/libdns/libdns v1.1.1
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
@@ -153,7 +153,7 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
@@ -162,17 +162,17 @@ require (
|
||||
github.com/slackhq/nebula v1.10.3 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/urfave/cli v1.22.17 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // 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.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/text v0.34.0
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/grpc v1.79.1 // 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
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,16 +2,16 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
|
||||
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
|
||||
cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=
|
||||
cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=
|
||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
@@ -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.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo=
|
||||
github.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||
github.com/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
|
||||
github.com/DeRuina/timberjack v1.4.2/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=
|
||||
@@ -53,40 +53,40 @@ github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmO
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
|
||||
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
|
||||
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/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,15 +151,15 @@ 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.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
@@ -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.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
|
||||
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
|
||||
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
|
||||
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
||||
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
||||
@@ -187,12 +187,12 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=
|
||||
github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
@@ -203,16 +203,16 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
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/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -276,12 +276,12 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
@@ -301,16 +301,16 @@ github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU
|
||||
github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
||||
github.com/smallstep/certificates v0.30.0-rc3 h1:Lx/NNJ4n+L3Pyx5NtVRGXeqviPPXTFFGLRiC1fCwU50=
|
||||
github.com/smallstep/certificates v0.30.0-rc3/go.mod h1:e5/ylYYpvnjCVZz6RpyOkpTe73EGPYoL+8TZZ5EtLjI=
|
||||
github.com/smallstep/certificates v0.30.2 h1:1G3xBi8sJ740iA1mMPW2Svv7EIZKJ4Zf/iQtA5QlN0Y=
|
||||
github.com/smallstep/certificates v0.30.2/go.mod h1:oyaE/aEYUGDr+YiCZLAxxP22bOQqcSHTeDgp8Vv2rlY=
|
||||
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
|
||||
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
||||
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
|
||||
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
|
||||
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
|
||||
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
|
||||
github.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A=
|
||||
github.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo=
|
||||
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
|
||||
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
|
||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
|
||||
@@ -359,8 +359,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
@@ -369,12 +369,12 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
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.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
|
||||
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/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
@@ -383,56 +383,56 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb
|
||||
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.40.0 h1:4VIrh75jW4RTimUNx1DSk+6H9/nDr1FvmKoOVDh3K04=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.40.0/go.mod h1:B0dCov9KNQGlut3T8wZZjDnLXEXdBroM7bFsHh/gRos=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 h1:aXl9uobjJs5vquMLt9ZkI/3zIuz8XQ3TqOKSWx0/xdU=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0/go.mod h1:ioMePqe6k6c/ovXSkmkMr1mbN5qRBGJxNTVop7/2XO0=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.40.0 h1:Lon8J5SPmWaL1Ko2TIlCNHJ42/J1b5XbJlgJaE/9m7I=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.40.0/go.mod h1:dKWtJTlp1Yj+8Cneye5idO46eRPIbi23qVuJYKjNnvY=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
|
||||
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
|
||||
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
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.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4=
|
||||
go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q=
|
||||
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.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=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -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.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
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/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,8 +456,8 @@ 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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
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/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
@@ -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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
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/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,10 +477,10 @@ 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.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -488,8 +488,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -517,8 +517,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -528,31 +528,31 @@ 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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
|
||||
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/api v0.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/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=
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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)
|
||||
)
|
||||
@@ -30,10 +30,6 @@ 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
|
||||
|
||||
@@ -120,8 +116,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 int32 // accessed atomically; belongs to this struct only
|
||||
*sharedListener // embedded, so we also become a net.Listener
|
||||
closed atomic.Bool
|
||||
*sharedListener // embedded, so we also become a net.Listener
|
||||
keepAliveConfig net.KeepAliveConfig
|
||||
}
|
||||
|
||||
@@ -131,7 +127,7 @@ type canSetKeepAliveConfig interface {
|
||||
|
||||
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
||||
// if the listener is already "closed", return error
|
||||
if atomic.LoadInt32(&fcl.closed) == 1 {
|
||||
if fcl.closed.Load() {
|
||||
return nil, fakeClosedErr(fcl)
|
||||
}
|
||||
|
||||
@@ -155,7 +151,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 atomic.LoadInt32(&fcl.closed) == 1 {
|
||||
if fcl.closed.Load() {
|
||||
// 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
|
||||
@@ -175,7 +171,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 atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) {
|
||||
if fcl.closed.CompareAndSwap(false, true) {
|
||||
// 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
|
||||
@@ -238,13 +234,13 @@ func (sl *sharedListener) Destruct() error {
|
||||
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
|
||||
// or more specifically, *net.UDPConn
|
||||
type fakeClosePacketConn struct {
|
||||
closed int32 // accessed atomically; belongs to this struct only
|
||||
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
|
||||
closed atomic.Bool
|
||||
*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 atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||
if fcpc.closed.Load() {
|
||||
return 0, nil, &net.OpError{
|
||||
Op: "readfrom",
|
||||
Net: fcpc.LocalAddr().Network(),
|
||||
@@ -258,7 +254,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 atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||
if fcpc.closed.Load() {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
||||
return n, addr, err
|
||||
@@ -273,7 +269,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 atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
|
||||
if fcpc.closed.CompareAndSwap(false, true) {
|
||||
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
|
||||
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -63,7 +63,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
atomic.AddInt32(unixSocket.count, 1)
|
||||
unixSocket.count.Add(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
|
||||
}
|
||||
atomic.AddInt32(unixSocket.count, 1)
|
||||
unixSocket.count.Add(1)
|
||||
unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
|
||||
}
|
||||
|
||||
@@ -165,8 +165,9 @@ 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 {
|
||||
one := int32(1)
|
||||
ln = &unixConn{unix, lnKey, &one}
|
||||
cnt := new(atomic.Int32)
|
||||
cnt.Store(1)
|
||||
ln = &unixConn{unix, lnKey, cnt}
|
||||
unixSockets[lnKey] = ln.(*unixConn)
|
||||
}
|
||||
}
|
||||
@@ -181,8 +182,9 @@ 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)
|
||||
one := int32(1)
|
||||
ln = &unixListener{unix, lnKey, &one}
|
||||
cnt := new(atomic.Int32)
|
||||
cnt.Store(1)
|
||||
ln = &unixListener{unix, lnKey, cnt}
|
||||
unixSockets[lnKey] = ln.(*unixListener)
|
||||
}
|
||||
}
|
||||
@@ -216,11 +218,11 @@ func reusePort(network, address string, conn syscall.RawConn) error {
|
||||
type unixListener struct {
|
||||
*net.UnixListener
|
||||
mapKey string
|
||||
count *int32 // accessed atomically
|
||||
count *atomic.Int32
|
||||
}
|
||||
|
||||
func (uln *unixListener) Close() error {
|
||||
newCount := atomic.AddInt32(uln.count, -1)
|
||||
newCount := uln.count.Add(-1)
|
||||
if newCount == 0 {
|
||||
file, err := uln.File()
|
||||
var name string
|
||||
@@ -242,11 +244,11 @@ func (uln *unixListener) Close() error {
|
||||
type unixConn struct {
|
||||
*net.UnixConn
|
||||
mapKey string
|
||||
count *int32 // accessed atomically
|
||||
count *atomic.Int32
|
||||
}
|
||||
|
||||
func (uc *unixConn) Close() error {
|
||||
newCount := atomic.AddInt32(uc.count, -1)
|
||||
newCount := uc.count.Add(-1)
|
||||
if newCount == 0 {
|
||||
file, err := uc.File()
|
||||
var name string
|
||||
|
||||
+19
-6
@@ -462,7 +462,10 @@ 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}
|
||||
quicTlsConfig := &tls.Config{
|
||||
GetConfigForClient: sqs.getConfigForClient,
|
||||
GetEncryptedClientHelloKeys: sqs.getEncryptedClientHelloKeys,
|
||||
}
|
||||
// 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)
|
||||
@@ -540,6 +543,16 @@ 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) {
|
||||
@@ -611,8 +624,8 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
|
||||
var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉")
|
||||
|
||||
type fakeCloseQuicListener struct {
|
||||
closed int32 // accessed atomically; belongs to this struct only
|
||||
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
||||
closed atomic.Int32
|
||||
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
||||
context context.Context
|
||||
contextCancel context.CancelCauseFunc
|
||||
}
|
||||
@@ -629,16 +642,16 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
|
||||
}
|
||||
|
||||
// if the listener is "closed", return a fake closed error instead
|
||||
if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) {
|
||||
if fcql.closed.Load() == 1 && errors.Is(err, context.Canceled) {
|
||||
return nil, fakeClosedErr(fcql)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (fcql *fakeCloseQuicListener) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
||||
if fcql.closed.CompareAndSwap(0, 1) {
|
||||
fcql.contextCancel(errFakeClosed)
|
||||
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
|
||||
} else if fcql.closed.CompareAndSwap(1, 2) {
|
||||
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -175,6 +176,63 @@ 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
|
||||
|
||||
+17
-37
@@ -20,7 +20,6 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -69,6 +68,7 @@ func init() {
|
||||
// `{http.request.orig_uri.path.dir}` | The request's original directory
|
||||
// `{http.request.orig_uri.path.file}` | The request's original filename
|
||||
// `{http.request.orig_uri.query}` | The request's original query string (without `?`)
|
||||
// `{http.request.orig_uri.prefixed_query}` | The request's original query string with a `?` prefix, if non-empty
|
||||
// `{http.request.port}` | The port part of the request's Host header
|
||||
// `{http.request.proto}` | The protocol of the request
|
||||
// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
|
||||
@@ -98,11 +98,15 @@ 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
|
||||
@@ -203,6 +207,9 @@ 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
|
||||
@@ -214,8 +221,6 @@ 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{
|
||||
@@ -235,12 +240,7 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
|
||||
// if no protocols configured explicitly, enable all except h2c
|
||||
if len(srv.Protocols) == 0 {
|
||||
srv.Protocols = []string{"h1", "h2", "h3"}
|
||||
}
|
||||
|
||||
srvProtocolsUnique := map[string]struct{}{}
|
||||
for _, srvProtocol := range srv.Protocols {
|
||||
srvProtocolsUnique[srvProtocol] = struct{}{}
|
||||
srv.Protocols = srv.protocolsWithDefaults()
|
||||
}
|
||||
|
||||
if srv.ListenProtocols != nil {
|
||||
@@ -251,31 +251,7 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
|
||||
for i, lnProtocols := range srv.ListenProtocols {
|
||||
if lnProtocols != nil {
|
||||
// populate empty listen protocols with server protocols
|
||||
lnProtocolsDefault := false
|
||||
var lnProtocolsInclude []string
|
||||
srvProtocolsInclude := maps.Clone(srvProtocolsUnique)
|
||||
|
||||
// keep existing listener protocols unless they are empty
|
||||
for _, lnProtocol := range lnProtocols {
|
||||
if lnProtocol == "" {
|
||||
lnProtocolsDefault = true
|
||||
} else {
|
||||
lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
|
||||
delete(srvProtocolsInclude, lnProtocol)
|
||||
}
|
||||
}
|
||||
|
||||
// append server protocols to listener protocols if any listener protocols were empty
|
||||
if lnProtocolsDefault {
|
||||
for _, srvProtocol := range srv.Protocols {
|
||||
if _, ok := srvProtocolsInclude[srvProtocol]; ok {
|
||||
lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
srv.ListenProtocols[i] = lnProtocolsInclude
|
||||
srv.ListenProtocols[i] = srv.listenerProtocolsWithDefaults(lnProtocols)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -689,9 +665,7 @@ 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.shutdownAtMu.Lock()
|
||||
server.shutdownAt = scheduledTime
|
||||
server.shutdownAtMu.Unlock()
|
||||
server.shutdownAt.Store(&scheduledTime)
|
||||
delay = true
|
||||
} else {
|
||||
app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String()))
|
||||
@@ -816,6 +790,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
for d := range serverDomainSet {
|
||||
echDomains = append(echDomains, d)
|
||||
}
|
||||
app.tlsApp.RegisterServerNames(echDomains)
|
||||
app.tlsApp.RegisterServerNames(echDomains, httpsRRALPNs(srv))
|
||||
|
||||
// nothing more to do here if there are no domains that qualify for
|
||||
// automatic HTTPS and there are no explicit TLS connection policies:
|
||||
@@ -258,18 +258,13 @@ 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 {
|
||||
redirDomains[""] = append(redirDomains[""], addr)
|
||||
app.recordAutoHTTPSRedirectAddress(redirDomains, "", addr)
|
||||
continue
|
||||
}
|
||||
|
||||
// ...and associate it with each domain in this server
|
||||
for d := range serverDomainSet {
|
||||
// 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)
|
||||
}
|
||||
app.recordAutoHTTPSRedirectAddress(redirDomains, d, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,6 +512,35 @@ 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}"
|
||||
|
||||
@@ -550,6 +574,20 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
|
||||
}
|
||||
}
|
||||
|
||||
func httpsRRALPNs(srv *Server) []string {
|
||||
alpn := make(map[string]struct{}, 3)
|
||||
if srv.protocol("h3") {
|
||||
alpn["h3"] = struct{}{}
|
||||
}
|
||||
if srv.protocol("h2") {
|
||||
alpn["h2"] = struct{}{}
|
||||
}
|
||||
if srv.protocol("h1") {
|
||||
alpn["http/1.1"] = struct{}{}
|
||||
}
|
||||
return caddytls.OrderedHTTPSRRALPN(alpn)
|
||||
}
|
||||
|
||||
// createAutomationPolicies ensures that automated certificates for this
|
||||
// app are managed properly. This adds up to two automation policies:
|
||||
// one for the public names, and one for the internal names. If a catch-all
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPSRRALPNsDefaultProtocols(t *testing.T) {
|
||||
srv := &Server{}
|
||||
|
||||
got := httpsRRALPNs(srv)
|
||||
want := []string{"h3", "h2", "http/1.1"}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPSRRALPNsListenProtocolOverrides(t *testing.T) {
|
||||
srv := &Server{
|
||||
Protocols: []string{"h1", "h2"},
|
||||
ListenProtocols: [][]string{
|
||||
{"h1"},
|
||||
nil,
|
||||
{},
|
||||
{"h3", ""},
|
||||
},
|
||||
}
|
||||
|
||||
got := httpsRRALPNs(srv)
|
||||
want := []string{"h3", "h2", "http/1.1"}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPSRRALPNsIgnoresH2COnly(t *testing.T) {
|
||||
srv := &Server{
|
||||
Protocols: []string{"h2c"},
|
||||
}
|
||||
|
||||
got := httpsRRALPNs(srv)
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("unexpected ALPN values: got %v want none", got)
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
//
|
||||
// encode [<matcher>] <formats...> {
|
||||
// gzip [<level>]
|
||||
// zstd
|
||||
// zstd [<level>] {
|
||||
// level <level>
|
||||
// disable_checksum
|
||||
// }
|
||||
// minimum_length <length>
|
||||
// # response matcher block
|
||||
// match {
|
||||
|
||||
@@ -33,6 +33,10 @@ 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
|
||||
}
|
||||
@@ -48,19 +52,48 @@ 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
|
||||
if !d.NextArg() {
|
||||
return nil
|
||||
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()
|
||||
}
|
||||
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,
|
||||
)
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
z.Level = levelStr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,15 +102,11 @@ func (z *Zstd) Provision(ctx caddy.Context) error {
|
||||
if z.Level == "" {
|
||||
z.Level = zstd.SpeedDefault.String()
|
||||
}
|
||||
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,
|
||||
)
|
||||
level, err := parseEncoderLevel(z.Level)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
z.level = level
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -90,14 +119,45 @@ 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,
|
||||
zstd.WithWindowSize(128<<10),
|
||||
writer, _ := zstd.NewWriter(nil, z.writerOptions(128<<10)...)
|
||||
return writer
|
||||
}
|
||||
|
||||
func (z Zstd) writerOptions(windowSize int) []zstd.EOption {
|
||||
opts := []zstd.EOption{
|
||||
zstd.WithWindowSize(windowSize),
|
||||
zstd.WithEncoderConcurrency(1),
|
||||
zstd.WithZeroFrames(true),
|
||||
zstd.WithEncoderLevel(z.level),
|
||||
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,
|
||||
)
|
||||
return writer
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
|
||||
@@ -281,7 +281,13 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
|
||||
sortParam = sortCookie.Value
|
||||
}
|
||||
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
|
||||
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil})
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// then figure out the order
|
||||
@@ -292,7 +298,13 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
|
||||
orderParam = orderCookie.Value
|
||||
}
|
||||
case sortOrderAsc, sortOrderDesc:
|
||||
http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil})
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// finally, apply the sorting and limiting
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -100,7 +99,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
|
||||
}
|
||||
|
||||
if fsrv.Browse.RevealSymlinks {
|
||||
symLinkTarget, err := filepath.EvalSymlinks(path)
|
||||
symLinkTarget, err := os.Readlink(path)
|
||||
if err == nil {
|
||||
symlinkPath = symLinkTarget
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/internal/filesystems"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
)
|
||||
|
||||
type testCase struct {
|
||||
@@ -188,6 +189,105 @@ func fileMatcherTest(t *testing.T, i int, tc testCase) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryFilesRewriteEscapesMatchedPath(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
requestTarget string
|
||||
filename string
|
||||
extraFiles []string
|
||||
wantPath string
|
||||
wantRequestURI string
|
||||
skipWindows bool
|
||||
}{
|
||||
{
|
||||
name: "question mark in path",
|
||||
requestTarget: "/%3F.html",
|
||||
filename: "?.html",
|
||||
wantPath: "/?.html",
|
||||
wantRequestURI: "/%3F.html",
|
||||
skipWindows: true,
|
||||
},
|
||||
{
|
||||
name: "percent in path",
|
||||
requestTarget: "/%25.html",
|
||||
filename: "%.html",
|
||||
wantPath: "/%.html",
|
||||
wantRequestURI: "/%25.html",
|
||||
},
|
||||
{
|
||||
name: "encoded question mark remains percent-encoded",
|
||||
requestTarget: "/%253F.html",
|
||||
filename: "%3F.html",
|
||||
wantPath: "/%3F.html",
|
||||
wantRequestURI: "/%253F.html",
|
||||
},
|
||||
{
|
||||
name: "question mark in nested path",
|
||||
requestTarget: "/nested/%3F.html",
|
||||
filename: filepath.Join("nested", "?.html"),
|
||||
wantPath: "/nested/?.html",
|
||||
wantRequestURI: "/nested/%3F.html",
|
||||
skipWindows: true,
|
||||
},
|
||||
{
|
||||
name: "encoded slash in filename does not conflict with nesting",
|
||||
requestTarget: "/nested%252Ffile.html",
|
||||
filename: "nested%2Ffile.html",
|
||||
extraFiles: []string{filepath.Join("nested", "file.html")},
|
||||
wantPath: "/nested%2Ffile.html",
|
||||
wantRequestURI: "/nested%252Ffile.html",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.skipWindows && runtime.GOOS == "windows" {
|
||||
t.Skip("Windows file names cannot contain question marks")
|
||||
}
|
||||
|
||||
for _, name := range append([]string{tc.filename}, tc.extraFiles...) {
|
||||
filename := filepath.Join(root, name)
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
|
||||
t.Fatalf("creating test file parent directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filename, []byte(name), 0o600); err != nil {
|
||||
t.Fatalf("writing test file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
m := &MatchFile{
|
||||
fsmap: &filesystems.FileSystemMap{},
|
||||
Root: root,
|
||||
TryFiles: []string{"{http.request.uri.path}"},
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "http://example.com"+tc.requestTarget, nil)
|
||||
repl := caddyhttp.NewTestReplacer(req)
|
||||
|
||||
matched, err := m.MatchWithError(req)
|
||||
if err != nil {
|
||||
t.Fatalf("matching file: %v", err)
|
||||
}
|
||||
if !matched {
|
||||
t.Fatalf("expected request %s to match %s", tc.requestTarget, tc.filename)
|
||||
}
|
||||
|
||||
rewrite.Rewrite{URI: "{http.matchers.file.relative}"}.Rewrite(req, repl)
|
||||
|
||||
if req.URL.Path != tc.wantPath {
|
||||
t.Errorf("rewritten path = %q, want %q", req.URL.Path, tc.wantPath)
|
||||
}
|
||||
if req.RequestURI != tc.wantRequestURI {
|
||||
t.Errorf("rewritten request URI = %q, want %q", req.RequestURI, tc.wantRequestURI)
|
||||
}
|
||||
if req.URL.RawQuery != "" {
|
||||
t.Errorf("rewritten raw query = %q, want empty", req.URL.RawQuery)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPHPFileMatcher(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
path string
|
||||
|
||||
@@ -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)
|
||||
http.Redirect(w, r, toPath, http.StatusPermanentRedirect) //nolint:gosec // toPath is a same-origin path and leading // is stripped above
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,10 @@ 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().
|
||||
@@ -47,12 +48,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", LoggableHTTPHeader{
|
||||
enc.AddObject("headers", internal.LoggableHTTPHeader{
|
||||
Header: r.Header,
|
||||
ShouldLogCredentials: r.ShouldLogCredentials,
|
||||
})
|
||||
if r.TransferEncoding != nil {
|
||||
enc.AddArray("transfer_encoding", LoggableStringArray(r.TransferEncoding))
|
||||
enc.AddArray("transfer_encoding", internal.LoggableStringArray(r.TransferEncoding))
|
||||
}
|
||||
if r.TLS != nil {
|
||||
enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
|
||||
@@ -61,44 +62,10 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
type LoggableHTTPHeader = internal.LoggableHTTPHeader
|
||||
|
||||
// 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
|
||||
}
|
||||
type LoggableStringArray = internal.LoggableStringArray
|
||||
|
||||
// LoggableTLSConnState makes a TLS connection state loggable with zap.Object().
|
||||
type LoggableTLSConnState tls.ConnectionState
|
||||
@@ -121,7 +88,5 @@ 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)
|
||||
)
|
||||
|
||||
@@ -1562,6 +1562,14 @@ 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()...)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package caddyhttp
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -10,9 +11,14 @@ 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"
|
||||
"github.com/caddyserver/caddy/v2/internal/metrics"
|
||||
caddymetrics "github.com/caddyserver/caddy/v2/internal/metrics"
|
||||
)
|
||||
|
||||
// Metrics configures metrics observations.
|
||||
@@ -67,10 +73,20 @@ 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 {
|
||||
@@ -147,6 +163,70 @@ 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) {
|
||||
@@ -234,7 +314,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 := metrics.SanitizeMethod(r.Method)
|
||||
method := caddymetrics.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": ""}
|
||||
@@ -264,7 +344,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"] = metrics.SanitizeCode(status)
|
||||
statusLabels["code"] = caddymetrics.SanitizeCode(status)
|
||||
ttfb := time.Since(start).Seconds()
|
||||
h.metrics.httpMetrics.responseDuration.With(statusLabels).Observe(ttfb)
|
||||
return false
|
||||
@@ -280,7 +360,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"] = metrics.SanitizeCode(status)
|
||||
statusLabels["code"] = caddymetrics.SanitizeCode(status)
|
||||
}
|
||||
|
||||
h.metrics.httpMetrics.requestDuration.With(statusLabels).Observe(dur)
|
||||
|
||||
@@ -523,6 +523,56 @@ 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{
|
||||
|
||||
@@ -387,17 +387,14 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
switch key {
|
||||
case "http.shutting_down":
|
||||
server := req.Context().Value(ServerCtxKey).(*Server)
|
||||
server.shutdownAtMu.RLock()
|
||||
defer server.shutdownAtMu.RUnlock()
|
||||
return !server.shutdownAt.IsZero(), true
|
||||
return server.shutdownAt.Load() != nil, true
|
||||
case "http.time_until_shutdown":
|
||||
server := req.Context().Value(ServerCtxKey).(*Server)
|
||||
server.shutdownAtMu.RLock()
|
||||
defer server.shutdownAtMu.RUnlock()
|
||||
if server.shutdownAt.IsZero() {
|
||||
t := server.shutdownAt.Load()
|
||||
if t == nil {
|
||||
return nil, true
|
||||
}
|
||||
return time.Until(server.shutdownAt), true
|
||||
return time.Until(*t), true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
|
||||
@@ -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 <request-matcher>
|
||||
// lb_retry_match <matcher>
|
||||
//
|
||||
// # active health checking
|
||||
// health_uri <uri>
|
||||
@@ -96,6 +96,7 @@ 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
|
||||
@@ -646,7 +647,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
h.FlushInterval = caddy.Duration(dur)
|
||||
}
|
||||
|
||||
case "request_buffers", "response_buffers":
|
||||
case "request_buffers", "response_buffers", "stream_buffer_size":
|
||||
subdir := d.Val()
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
@@ -670,6 +671,8 @@ 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":
|
||||
@@ -725,9 +728,6 @@ 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,6 +885,14 @@ 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,8 +28,6 @@ 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"
|
||||
@@ -418,14 +416,19 @@ 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...
|
||||
@@ -438,31 +441,18 @@ 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 := range pathLen {
|
||||
if path[i] >= utf8.RuneSelf {
|
||||
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
|
||||
return end
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if i+splitLen > pathLen {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i <= pathLen-splitLen; i++ {
|
||||
match := true
|
||||
for j := range splitLen {
|
||||
c := path[i+j]
|
||||
|
||||
if c >= utf8.RuneSelf {
|
||||
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
|
||||
return end
|
||||
}
|
||||
match = false
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -191,6 +191,65 @@ 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: "/shell.php",
|
||||
splitPath: []string{".php"},
|
||||
wantPos: -1,
|
||||
},
|
||||
{
|
||||
name: "fullwidth p in extension must not match",
|
||||
path: "/shell.php",
|
||||
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 {
|
||||
@@ -244,3 +303,31 @@ 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
|
||||
"/shell.php", // U+FF0E fullwidth full stop
|
||||
"/shell.php", // U+FF50 fullwidth p
|
||||
"/shell.php", // U+FF48 fullwidth h
|
||||
"/shell.php", // 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]interface{}{
|
||||
vars := map[string]any{
|
||||
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]interface{}{
|
||||
vars := map[string]any{
|
||||
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]interface{}{
|
||||
vars := map[string]any{
|
||||
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]interface{}{
|
||||
vars := map[string]any{
|
||||
caddyhttp.TrustedProxyVarKey: true,
|
||||
caddyhttp.ClientIPVarKey: "5.6.7.8",
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ type Upstream struct {
|
||||
activeHealthCheckUpstream string
|
||||
healthCheckPolicy *PassiveHealthChecks
|
||||
cb CircuitBreaker
|
||||
unhealthy int32 // accessed atomically; status from active health checker
|
||||
unhealthy atomic.Int32 // status from active health checker
|
||||
}
|
||||
|
||||
// (pointer receiver necessary to avoid a race condition, since
|
||||
@@ -174,36 +174,36 @@ 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 int64 // must be 64-bit aligned on 32-bit systems (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG)
|
||||
fails int64
|
||||
activePasses int64
|
||||
activeFails int64
|
||||
numRequests atomic.Int64
|
||||
fails atomic.Int64
|
||||
activePasses atomic.Int64
|
||||
activeFails atomic.Int64
|
||||
}
|
||||
|
||||
// NumRequests returns the number of active requests to the upstream.
|
||||
func (h *Host) NumRequests() int {
|
||||
return int(atomic.LoadInt64(&h.numRequests))
|
||||
return int(h.numRequests.Load())
|
||||
}
|
||||
|
||||
// Fails returns the number of recent failures with the upstream.
|
||||
func (h *Host) Fails() int {
|
||||
return int(atomic.LoadInt64(&h.fails))
|
||||
return int(h.fails.Load())
|
||||
}
|
||||
|
||||
// activeHealthPasses returns the number of consecutive active health check passes with the upstream.
|
||||
func (h *Host) activeHealthPasses() int {
|
||||
return int(atomic.LoadInt64(&h.activePasses))
|
||||
return int(h.activePasses.Load())
|
||||
}
|
||||
|
||||
// activeHealthFails returns the number of consecutive active health check failures with the upstream.
|
||||
func (h *Host) activeHealthFails() int {
|
||||
return int(atomic.LoadInt64(&h.activeFails))
|
||||
return int(h.activeFails.Load())
|
||||
}
|
||||
|
||||
// countRequest mutates the active request count by
|
||||
// delta. It returns an error if the adjustment fails.
|
||||
func (h *Host) countRequest(delta int) error {
|
||||
result := atomic.AddInt64(&h.numRequests, int64(delta))
|
||||
result := h.numRequests.Add(int64(delta))
|
||||
if result < 0 {
|
||||
return fmt.Errorf("count below 0: %d", result)
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func (h *Host) countRequest(delta int) error {
|
||||
// countFail mutates the recent failures count by
|
||||
// delta. It returns an error if the adjustment fails.
|
||||
func (h *Host) countFail(delta int) error {
|
||||
result := atomic.AddInt64(&h.fails, int64(delta))
|
||||
result := h.fails.Add(int64(delta))
|
||||
if result < 0 {
|
||||
return fmt.Errorf("count below 0: %d", result)
|
||||
}
|
||||
@@ -223,7 +223,7 @@ func (h *Host) countFail(delta int) error {
|
||||
// countHealthPass mutates the recent passes count by
|
||||
// delta. It returns an error if the adjustment fails.
|
||||
func (h *Host) countHealthPass(delta int) error {
|
||||
result := atomic.AddInt64(&h.activePasses, int64(delta))
|
||||
result := h.activePasses.Add(int64(delta))
|
||||
if result < 0 {
|
||||
return fmt.Errorf("count below 0: %d", result)
|
||||
}
|
||||
@@ -233,7 +233,7 @@ func (h *Host) countHealthPass(delta int) error {
|
||||
// countHealthFail mutates the recent failures count by
|
||||
// delta. It returns an error if the adjustment fails.
|
||||
func (h *Host) countHealthFail(delta int) error {
|
||||
result := atomic.AddInt64(&h.activeFails, int64(delta))
|
||||
result := h.activeFails.Add(int64(delta))
|
||||
if result < 0 {
|
||||
return fmt.Errorf("count below 0: %d", result)
|
||||
}
|
||||
@@ -242,14 +242,14 @@ func (h *Host) countHealthFail(delta int) error {
|
||||
|
||||
// resetHealth resets the health check counters.
|
||||
func (h *Host) resetHealth() {
|
||||
atomic.StoreInt64(&h.activePasses, 0)
|
||||
atomic.StoreInt64(&h.activeFails, 0)
|
||||
h.activePasses.Store(0)
|
||||
h.activeFails.Store(0)
|
||||
}
|
||||
|
||||
// healthy returns true if the upstream is not actively marked as unhealthy.
|
||||
// (This returns the status only from the "active" health checks.)
|
||||
func (u *Upstream) healthy() bool {
|
||||
return atomic.LoadInt32(&u.unhealthy) == 0
|
||||
return u.unhealthy.Load() == 0
|
||||
}
|
||||
|
||||
// SetHealthy sets the upstream has healthy or unhealthy
|
||||
@@ -260,7 +260,7 @@ func (u *Upstream) setHealthy(healthy bool) bool {
|
||||
if healthy {
|
||||
unhealthy, compare = 0, 1
|
||||
}
|
||||
return atomic.CompareAndSwapInt32(&u.unhealthy, compare, unhealthy)
|
||||
return u.unhealthy.CompareAndSwap(compare, unhealthy)
|
||||
}
|
||||
|
||||
// DialInfo contains information needed to dial a
|
||||
|
||||
@@ -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,4 +194,3 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
@@ -8,11 +9,13 @@ 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"
|
||||
)
|
||||
|
||||
@@ -255,3 +258,475 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,12 @@ 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,
|
||||
@@ -568,6 +574,17 @@ 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 {
|
||||
@@ -664,8 +681,12 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
|
||||
return true, succ.error
|
||||
}
|
||||
|
||||
// remember this failure (if enabled)
|
||||
h.countFailure(upstream)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// if we've tried long enough, break
|
||||
if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
|
||||
@@ -1049,6 +1070,45 @@ 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) {
|
||||
@@ -1068,14 +1128,6 @@ 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))
|
||||
}
|
||||
@@ -1260,18 +1312,29 @@ 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
|
||||
if !isDialError && (!isHandlerError || !errors.Is(herr, errNoUpstream)) {
|
||||
// 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)) {
|
||||
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))
|
||||
@@ -1501,6 +1564,12 @@ 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
|
||||
|
||||
@@ -1552,13 +1621,15 @@ 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 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.
|
||||
// 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
|
||||
RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty" caddy:"namespace=http.matchers"`
|
||||
|
||||
SelectionPolicy Selector `json:"-"`
|
||||
@@ -1580,10 +1651,28 @@ 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
|
||||
@@ -1656,10 +1745,34 @@ 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 {
|
||||
|
||||
@@ -40,8 +40,8 @@ func init() {
|
||||
caddy.RegisterModule(RandomSelection{})
|
||||
caddy.RegisterModule(RandomChoiceSelection{})
|
||||
caddy.RegisterModule(LeastConnSelection{})
|
||||
caddy.RegisterModule(RoundRobinSelection{})
|
||||
caddy.RegisterModule(WeightedRoundRobinSelection{})
|
||||
caddy.RegisterModule(new(RoundRobinSelection))
|
||||
caddy.RegisterModule(new(WeightedRoundRobinSelection))
|
||||
caddy.RegisterModule(FirstSelection{})
|
||||
caddy.RegisterModule(IPHashSelection{})
|
||||
caddy.RegisterModule(ClientIPHashSelection{})
|
||||
@@ -83,12 +83,12 @@ type WeightedRoundRobinSelection struct {
|
||||
// The weight of each upstream in order,
|
||||
// corresponding with the list of upstreams configured.
|
||||
Weights []int `json:"weights,omitempty"`
|
||||
index uint32
|
||||
index atomic.Uint32
|
||||
totalWeight int
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo {
|
||||
func (*WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.reverse_proxy.selection_policies.weighted_round_robin",
|
||||
New: func() caddy.Module {
|
||||
@@ -143,7 +143,7 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request,
|
||||
weights = append(weights, w)
|
||||
}
|
||||
}
|
||||
currentWeight := int(atomic.AddUint32(&r.index, 1)) % r.totalWeight
|
||||
currentWeight := int(r.index.Add(1)) % r.totalWeight
|
||||
for i, weight := range weights {
|
||||
totalWeight += weight
|
||||
if currentWeight < totalWeight {
|
||||
@@ -295,11 +295,11 @@ func (r *LeastConnSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// RoundRobinSelection is a policy that selects
|
||||
// a host based on round-robin ordering.
|
||||
type RoundRobinSelection struct {
|
||||
robin uint32
|
||||
robin atomic.Uint32
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (RoundRobinSelection) CaddyModule() caddy.ModuleInfo {
|
||||
func (*RoundRobinSelection) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.reverse_proxy.selection_policies.round_robin",
|
||||
New: func() caddy.Module { return new(RoundRobinSelection) },
|
||||
@@ -313,7 +313,7 @@ func (r *RoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http.
|
||||
return nil
|
||||
}
|
||||
for range n {
|
||||
robin := atomic.AddUint32(&r.robin, 1)
|
||||
robin := r.robin.Add(1)
|
||||
host := pool[robin%n]
|
||||
if host.Available() {
|
||||
return host
|
||||
@@ -664,10 +664,12 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
|
||||
return upstream
|
||||
}
|
||||
cookie := &http.Cookie{
|
||||
Name: s.Name,
|
||||
Value: sha,
|
||||
Path: "/",
|
||||
Secure: false,
|
||||
Name: s.Name,
|
||||
Value: sha,
|
||||
Path: "/",
|
||||
Secure: false,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
isProxyHttps := false
|
||||
if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted {
|
||||
|
||||
@@ -204,7 +204,12 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
|
||||
defer deleteFrontConn()
|
||||
defer deleteBackConn()
|
||||
|
||||
spc := switchProtocolCopier{user: conn, backend: backConn, wg: wg}
|
||||
spc := switchProtocolCopier{
|
||||
user: conn,
|
||||
backend: backConn,
|
||||
wg: wg,
|
||||
bufferSize: h.StreamBufferSize,
|
||||
}
|
||||
|
||||
// setup the timeout if requested
|
||||
var timeoutc <-chan time.Time
|
||||
@@ -636,20 +641,29 @@ 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.Copy(c.user, c.backend)
|
||||
_, err := io.CopyBuffer(c.user, c.backend, c.buffer())
|
||||
errc <- err
|
||||
c.wg.Done()
|
||||
}
|
||||
|
||||
func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
|
||||
_, err := io.Copy(c.backend, c.user)
|
||||
_, err := io.CopyBuffer(c.backend, c.user, c.buffer())
|
||||
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,8 +2,10 @@ package reverseproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
@@ -34,3 +36,47 @@ 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 }
|
||||
|
||||
@@ -119,6 +119,18 @@ 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)
|
||||
|
||||
@@ -554,8 +566,9 @@ var (
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*SRVUpstreams)(nil)
|
||||
_ UpstreamSource = (*SRVUpstreams)(nil)
|
||||
_ caddy.Provisioner = (*AUpstreams)(nil)
|
||||
_ UpstreamSource = (*AUpstreams)(nil)
|
||||
_ caddy.Provisioner = (*SRVUpstreams)(nil)
|
||||
_ UpstreamSource = (*SRVUpstreams)(nil)
|
||||
_ CachingUpstreamSource = (*SRVUpstreams)(nil)
|
||||
_ caddy.Provisioner = (*AUpstreams)(nil)
|
||||
_ UpstreamSource = (*AUpstreams)(nil)
|
||||
)
|
||||
|
||||
@@ -211,12 +211,7 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
|
||||
var newPath, newQuery, newFrag string
|
||||
|
||||
if path != "" {
|
||||
// replace the `path` placeholder to escaped path
|
||||
pathPlaceholder := "{http.request.uri.path}"
|
||||
if strings.Contains(path, pathPlaceholder) {
|
||||
path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
|
||||
}
|
||||
|
||||
path = escapePathPlaceholders(path, r, repl)
|
||||
newPath = repl.ReplaceAll(path, "")
|
||||
}
|
||||
|
||||
@@ -300,6 +295,31 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
|
||||
return r.Method != oldMethod || r.RequestURI != oldURI
|
||||
}
|
||||
|
||||
func escapePathPlaceholders(path string, r *http.Request, repl *caddy.Replacer) string {
|
||||
// Replace path-valued placeholders in escaped form before the URI is parsed,
|
||||
// otherwise literal '?' and '%' bytes from the path can be interpreted as URI
|
||||
// delimiters or percent-escape sequences during the rewrite.
|
||||
pathPlaceholder := "{http.request.uri.path}"
|
||||
if strings.Contains(path, pathPlaceholder) {
|
||||
path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
|
||||
}
|
||||
|
||||
fileMatchRelativePlaceholder := "{http.matchers.file.relative}"
|
||||
if strings.Contains(path, fileMatchRelativePlaceholder) {
|
||||
if val, ok := repl.Get("http.matchers.file.relative"); ok {
|
||||
if relativePath, ok := val.(string); ok {
|
||||
path = strings.ReplaceAll(path, fileMatchRelativePlaceholder, escapePathPreservingSlashes(relativePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func escapePathPreservingSlashes(path string) string {
|
||||
return strings.ReplaceAll(url.PathEscape(path), "%2F", "/")
|
||||
}
|
||||
|
||||
// buildQueryString takes an input query string and
|
||||
// performs replacements on each component, returning
|
||||
// the resulting query string. This function appends
|
||||
@@ -529,7 +549,14 @@ func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) {
|
||||
if key == "" || val == "" {
|
||||
continue
|
||||
}
|
||||
query[val] = query[key]
|
||||
if key == val {
|
||||
continue
|
||||
}
|
||||
originalValues, ok := query[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
query[val] = originalValues
|
||||
delete(query, key)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ package rewrite
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
@@ -397,6 +398,55 @@ 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 {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
@@ -241,8 +242,8 @@ func (routes RouteList) Compile(next Handler) Handler {
|
||||
mid = append(mid, wrapRoute(route))
|
||||
}
|
||||
stack := next
|
||||
for i := len(mid) - 1; i >= 0; i-- {
|
||||
stack = mid[i](stack)
|
||||
for _, middleware := range slices.Backward(mid) {
|
||||
stack = middleware(stack)
|
||||
}
|
||||
return stack
|
||||
}
|
||||
@@ -305,8 +306,8 @@ func wrapRoute(route Route) Middleware {
|
||||
}
|
||||
|
||||
// compile this route's handler stack
|
||||
for i := len(route.middleware) - 1; i >= 0; i-- {
|
||||
nextCopy = route.middleware[i](nextCopy)
|
||||
for _, middleware := range slices.Backward(route.middleware) {
|
||||
nextCopy = middleware(nextCopy)
|
||||
}
|
||||
|
||||
// Apply metrics instrumentation once for the entire route,
|
||||
|
||||
+52
-15
@@ -28,7 +28,7 @@ import (
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
@@ -291,8 +291,7 @@ type Server struct {
|
||||
|
||||
trustedProxies IPRangeSource
|
||||
|
||||
shutdownAt time.Time
|
||||
shutdownAtMu *sync.RWMutex
|
||||
shutdownAt atomic.Pointer[time.Time]
|
||||
|
||||
// registered callback functions
|
||||
connStateFuncs []func(net.Conn, http.ConnState)
|
||||
@@ -301,6 +300,8 @@ type Server struct {
|
||||
onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023)
|
||||
}
|
||||
|
||||
var defaultProtocols = []string{"h1", "h2", "h3"}
|
||||
|
||||
var (
|
||||
ServerHeader = "Caddy"
|
||||
serverHeader = []string{ServerHeader}
|
||||
@@ -900,22 +901,58 @@ func (s *Server) logRequest(
|
||||
// protocol returns true if the protocol proto is configured/enabled.
|
||||
func (s *Server) protocol(proto string) bool {
|
||||
if s.ListenProtocols == nil {
|
||||
if slices.Contains(s.Protocols, proto) {
|
||||
return slices.Contains(s.protocolsWithDefaults(), proto)
|
||||
}
|
||||
|
||||
for _, lnProtocols := range s.ListenProtocols {
|
||||
if slices.Contains(s.listenerProtocolsWithDefaults(lnProtocols), proto) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
for _, lnProtocols := range s.ListenProtocols {
|
||||
for _, lnProtocol := range lnProtocols {
|
||||
if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Server) protocolsWithDefaults() []string {
|
||||
if len(s.Protocols) == 0 {
|
||||
return defaultProtocols
|
||||
}
|
||||
return s.Protocols
|
||||
}
|
||||
|
||||
func (s *Server) listenerProtocolsWithDefaults(lnProtocols []string) []string {
|
||||
serverProtocols := s.protocolsWithDefaults()
|
||||
if len(lnProtocols) == 0 {
|
||||
return serverProtocols
|
||||
}
|
||||
|
||||
lnProtocolsDefault := false
|
||||
lnProtocolsInclude := make([]string, 0, len(lnProtocols)+len(serverProtocols))
|
||||
srvProtocolsInclude := make(map[string]struct{}, len(serverProtocols))
|
||||
for _, srvProtocol := range serverProtocols {
|
||||
srvProtocolsInclude[srvProtocol] = struct{}{}
|
||||
}
|
||||
|
||||
for _, lnProtocol := range lnProtocols {
|
||||
if lnProtocol == "" {
|
||||
lnProtocolsDefault = true
|
||||
continue
|
||||
}
|
||||
lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
|
||||
delete(srvProtocolsInclude, lnProtocol)
|
||||
}
|
||||
|
||||
if lnProtocolsDefault {
|
||||
for _, srvProtocol := range serverProtocols {
|
||||
if _, ok := srvProtocolsInclude[srvProtocol]; ok {
|
||||
lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lnProtocolsInclude
|
||||
}
|
||||
|
||||
// Listeners returns the server's listeners. These are active listeners,
|
||||
// so calling Accept() or Close() on them will probably break things.
|
||||
// They are made available here for read-only purposes (e.g. Addr())
|
||||
@@ -1086,11 +1123,11 @@ func strictUntrustedClientIp(r *http.Request, headers []string, trusted []netip.
|
||||
for _, headerName := range headers {
|
||||
parts := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",")
|
||||
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
for _, part := range slices.Backward(parts) {
|
||||
// Some proxies may retain the port number, so split if possible
|
||||
host, _, err := net.SplitHostPort(parts[i])
|
||||
host, _, err := net.SplitHostPort(part)
|
||||
if err != nil {
|
||||
host = parts[i]
|
||||
host = part
|
||||
}
|
||||
|
||||
// Remove any zone identifier from the IP address
|
||||
|
||||
@@ -36,13 +36,22 @@ func init() {
|
||||
// Templates is a middleware which executes response bodies as Go templates.
|
||||
// The syntax is documented in the Go standard library's
|
||||
// [text/template package](https://golang.org/pkg/text/template/).
|
||||
// Note that ANY response body that matches and qualifies may be evaluated,
|
||||
// even if it comes from a proxied backend.
|
||||
//
|
||||
// ⚠️ Template functions/actions are still experimental, so they are subject to change.
|
||||
// ⚠️ Template functions/actions can access the environment, files on disk,
|
||||
// and make HTTP requests. This is extremely useful, but you need to make
|
||||
// sure templates are only evaluated on content that you trust, control, or
|
||||
// at least sanitize properly.
|
||||
//
|
||||
// Custom template functions can be registered by creating a plugin module under the `http.handlers.templates.functions.*` namespace that implements the `CustomFunctions` interface.
|
||||
// ⚠️ Templates are still experimental, so they are subject to change.
|
||||
//
|
||||
// [All Sprig functions](https://masterminds.github.io/sprig/) are supported.
|
||||
//
|
||||
// Custom template functions can be registered by creating a plugin module
|
||||
// under the `http.handlers.templates.functions.*` namespace that implements
|
||||
// the `CustomFunctions` interface.
|
||||
//
|
||||
// In addition to the standard functions and the Sprig library, Caddy adds
|
||||
// extra functions and data that are available to a template:
|
||||
//
|
||||
@@ -162,6 +171,25 @@ func init() {
|
||||
// {{listFiles "/mydir"}}
|
||||
// ```
|
||||
//
|
||||
// ##### `fileExists`
|
||||
//
|
||||
// Returns true if the given file name, relative to the template context's file root,
|
||||
// can be opened successfully.
|
||||
//
|
||||
// ```
|
||||
// {{fileExists "path/to/file.html"}}
|
||||
// ```
|
||||
//
|
||||
// ##### `fileStat`
|
||||
//
|
||||
// Returns [FileInfo](https://pkg.go.dev/io/fs#FileInfo) using [Stat](https://pkg.go.dev/io/fs#Stat)
|
||||
// on the given file name, relative to the template context's file root.
|
||||
//
|
||||
// ```
|
||||
// {{$css := fileStat "css/style.css" -}}
|
||||
// <link rel="stylesheet" href="/css/style.css?v={{ $css.ModTime.Unix }}">
|
||||
// ```
|
||||
//
|
||||
// ##### `markdown`
|
||||
//
|
||||
// Renders the given Markdown text as HTML and returns it. This uses the
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
webEngineName = "Caddy"
|
||||
defaultSpanName = "handler"
|
||||
nextCallCtxKey caddy.CtxKey = "nextCall"
|
||||
)
|
||||
@@ -58,7 +57,7 @@ func newOpenTelemetryWrapper(
|
||||
}
|
||||
|
||||
version, _ := caddy.Version()
|
||||
res, err := ot.newResource(webEngineName, version)
|
||||
res, err := ot.newResource(caddyhttp.ServerHeader, version)
|
||||
if err != nil {
|
||||
return ot, fmt.Errorf("creating resource error: %w", err)
|
||||
}
|
||||
|
||||
+30
-27
@@ -181,8 +181,9 @@ 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 varValue any
|
||||
for key, vals := range m {
|
||||
var varValue any
|
||||
if strings.HasPrefix(key, "{") &&
|
||||
strings.HasSuffix(key, "}") &&
|
||||
strings.Count(key, "{") == 1 {
|
||||
@@ -191,22 +192,27 @@ func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) {
|
||||
varValue = vars[key]
|
||||
}
|
||||
|
||||
switch vv := varValue.(type) {
|
||||
case string:
|
||||
varStr = vv
|
||||
case fmt.Stringer:
|
||||
varStr = vv.String()
|
||||
case error:
|
||||
varStr = vv.Error()
|
||||
case nil:
|
||||
varStr = ""
|
||||
default:
|
||||
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
|
||||
// already final and must not be re-expanded, as that would allow
|
||||
// user input like {env.SECRET} to be evaluated.
|
||||
|
||||
// see if any of the values given in the matcher match the actual value
|
||||
for _, v := range vals {
|
||||
matcherValExpanded := repl.ReplaceAll(v, "")
|
||||
var varStr string
|
||||
switch vv := varValue.(type) {
|
||||
case string:
|
||||
varStr = vv
|
||||
case fmt.Stringer:
|
||||
varStr = vv.String()
|
||||
case error:
|
||||
varStr = vv.Error()
|
||||
case nil:
|
||||
varStr = ""
|
||||
default:
|
||||
varStr = fmt.Sprintf("%v", vv)
|
||||
}
|
||||
for _, v = range vals {
|
||||
matcherValExpanded = repl.ReplaceAll(v, "")
|
||||
if varStr == matcherValExpanded {
|
||||
return true, nil
|
||||
}
|
||||
@@ -310,19 +316,19 @@ func (m MatchVarsRE) Match(r *http.Request) bool {
|
||||
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 varValue any
|
||||
for key, val := range m {
|
||||
var varValue any
|
||||
var fromPlaceholder bool
|
||||
if strings.HasPrefix(key, "{") &&
|
||||
strings.HasSuffix(key, "}") &&
|
||||
strings.Count(key, "{") == 1 {
|
||||
varValue, _ = repl.Get(strings.Trim(key, "{}"))
|
||||
fromPlaceholder = true
|
||||
} else {
|
||||
varValue = vars[key]
|
||||
}
|
||||
|
||||
var varStr string
|
||||
switch vv := varValue.(type) {
|
||||
case string:
|
||||
varStr = vv
|
||||
@@ -336,15 +342,12 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
|
||||
varStr = fmt.Sprintf("%v", vv)
|
||||
}
|
||||
|
||||
// Only expand placeholders in values from literal variable names
|
||||
// (e.g. map outputs). Values resolved from placeholder keys are
|
||||
// Don't expand placeholders in values from literal variable names
|
||||
// (e.g. map outputs) or other placeholders. These values 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, "")
|
||||
}
|
||||
if match := val.Match(valExpanded, repl); match {
|
||||
|
||||
if match = val.Match(varStr, repl); match {
|
||||
return match, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,42 @@ 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)
|
||||
@@ -149,6 +185,15 @@ 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
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -235,7 +235,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
|
||||
}
|
||||
|
||||
issuers := ap.Issuers
|
||||
if len(issuers) == 0 {
|
||||
if len(issuers) == 0 && !ap.implicitTailscaleManagersOnly() {
|
||||
var err error
|
||||
issuers, err = DefaultIssuersProvisioned(tlsApp.ctx)
|
||||
if err != nil {
|
||||
@@ -429,6 +429,29 @@ func (ap *AutomationPolicy) AllInternalSubjects() bool {
|
||||
})
|
||||
}
|
||||
|
||||
// implicitTailscaleManagersOnly returns true if this policy is configured to
|
||||
// serve only Tailscale names from the Tailscale manager at handshake-time.
|
||||
func (ap *AutomationPolicy) implicitTailscaleManagersOnly() bool {
|
||||
if len(ap.subjects) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, subject := range ap.subjects {
|
||||
if !strings.HasSuffix(strings.ToLower(subject), tailscaleDomainAliasEnding) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, manager := range ap.Managers {
|
||||
switch manager.(type) {
|
||||
case Tailscale, *Tailscale:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ap *AutomationPolicy) onlyInternalIssuer() bool {
|
||||
if len(ap.Issuers) != 1 {
|
||||
return false
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestAutomationPolicyMakeCertMagicConfigImplicitTailscaleManagersOnly(t *testing.T) {
|
||||
ap := AutomationPolicy{
|
||||
Managers: []certmagic.Manager{Tailscale{}},
|
||||
subjects: []string{"test-node.example.ts.net"},
|
||||
}
|
||||
|
||||
cfg, err := ap.makeCertMagicConfig(&TLS{
|
||||
logger: zap.NewNop(),
|
||||
}, nil, &certmagic.FileStorage{Path: t.TempDir()})
|
||||
if err != nil {
|
||||
t.Fatalf("making certmagic config: %v", err)
|
||||
}
|
||||
if cfg.OnDemand == nil {
|
||||
t.Fatal("expected on-demand config to be set")
|
||||
}
|
||||
if len(cfg.Issuers) != 0 {
|
||||
t.Fatalf("expected no issuers for tailscale-managed ts.net policy, got %d", len(cfg.Issuers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutomationPolicyImplicitTailscaleManagersOnlyCatchAll(t *testing.T) {
|
||||
ap := AutomationPolicy{
|
||||
Managers: []certmagic.Manager{Tailscale{}},
|
||||
}
|
||||
if ap.implicitTailscaleManagersOnly() {
|
||||
t.Fatal("expected catch-all manager policy to remain outside tailscale-only special case")
|
||||
}
|
||||
}
|
||||
+307
-15
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -27,6 +28,8 @@ 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
|
||||
@@ -35,6 +38,12 @@ 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 {
|
||||
@@ -44,7 +53,8 @@ type InlineCAPool struct {
|
||||
// these CAs will be rejected.
|
||||
TrustedCACerts []string `json:"trusted_ca_certs,omitempty"`
|
||||
|
||||
pool *x509.CertPool
|
||||
pool *x509.CertPool
|
||||
certs []*x509.Certificate
|
||||
}
|
||||
|
||||
// CaddyModule implements caddy.Module.
|
||||
@@ -60,14 +70,17 @@ 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
|
||||
}
|
||||
@@ -103,6 +116,11 @@ 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
|
||||
@@ -111,7 +129,8 @@ type FileCAPool struct {
|
||||
// these CA certificates will be rejected.
|
||||
TrustedCACertPEMFiles []string `json:"pem_files,omitempty"`
|
||||
|
||||
pool *x509.CertPool
|
||||
pool *x509.CertPool
|
||||
certs []*x509.Certificate
|
||||
}
|
||||
|
||||
// CaddyModule implements caddy.Module.
|
||||
@@ -127,14 +146,32 @@ 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)
|
||||
}
|
||||
caPool.AppendCertsFromPEM(pemContents)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
f.pool = caPool
|
||||
f.certs = certs
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -166,13 +203,19 @@ 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
|
||||
ca []*caddypki.CA
|
||||
pool *x509.CertPool
|
||||
certs []*x509.Certificate
|
||||
}
|
||||
|
||||
// CaddyModule implements caddy.Module.
|
||||
@@ -201,10 +244,17 @@ func (p *PKIRootCAPool) Provision(ctx caddy.Context) error {
|
||||
}
|
||||
|
||||
caPool := x509.NewCertPool()
|
||||
var certs []*x509.Certificate
|
||||
for _, ca := range p.ca {
|
||||
caPool.AddCert(ca.RootCertificate())
|
||||
rootCert := ca.RootCertificate()
|
||||
if rootCert == nil {
|
||||
return fmt.Errorf("CA %s has no root certificate", ca.ID)
|
||||
}
|
||||
caPool.AddCert(rootCert)
|
||||
certs = append(certs, rootCert)
|
||||
}
|
||||
p.pool = caPool
|
||||
p.certs = certs
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -238,13 +288,19 @@ 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
|
||||
ca []*caddypki.CA
|
||||
pool *x509.CertPool
|
||||
certs []*x509.Certificate
|
||||
}
|
||||
|
||||
// CaddyModule implements caddy.Module.
|
||||
@@ -273,12 +329,18 @@ 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
|
||||
}
|
||||
|
||||
@@ -311,6 +373,11 @@ 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
|
||||
@@ -322,6 +389,7 @@ type StoragePool struct {
|
||||
|
||||
storage certmagic.Storage
|
||||
pool *x509.CertPool
|
||||
certs []*x509.Certificate
|
||||
}
|
||||
|
||||
// CaddyModule implements caddy.Module.
|
||||
@@ -354,16 +422,33 @@ 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)
|
||||
}
|
||||
if !caPool.AppendCertsFromPEM(bs) {
|
||||
return fmt.Errorf("failed to add certificate '%s' to pool", caID)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
ca.pool = caPool
|
||||
ca.certs = certs
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -413,9 +498,13 @@ 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"`
|
||||
@@ -500,7 +589,6 @@ 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 {
|
||||
@@ -554,7 +642,8 @@ type HTTPCertPool struct {
|
||||
// Customize the TLS connection knobs to used during the HTTP call
|
||||
TLS *TLSConfig `json:"tls,omitempty"`
|
||||
|
||||
pool *x509.CertPool
|
||||
pool *x509.CertPool
|
||||
certs []*x509.Certificate
|
||||
}
|
||||
|
||||
// CaddyModule implements caddy.Module.
|
||||
@@ -570,6 +659,7 @@ 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 {
|
||||
@@ -597,11 +687,30 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !caPool.AppendCertsFromPEM(pembs) {
|
||||
return fmt.Errorf("failed to add certs from URL: %s", uri)
|
||||
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)
|
||||
}
|
||||
}
|
||||
hcp.pool = caPool
|
||||
hcp.certs = certs
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -665,6 +774,179 @@ 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)
|
||||
@@ -696,4 +978,14 @@ 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)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -776,3 +777,219 @@ 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
|
||||
}
|
||||
|
||||
@@ -153,9 +153,9 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
|
||||
// in its config (remember, TLS connection policies are used by *other* apps to
|
||||
// run TLS servers) -- we skip names with placeholders
|
||||
if tlsApp.EncryptedClientHello.Publication == nil {
|
||||
var echNames []string
|
||||
repl := caddy.NewReplacer()
|
||||
for _, p := range cp {
|
||||
var echNames []string
|
||||
for _, m := range p.matchers {
|
||||
if sni, ok := m.(MatchServerName); ok {
|
||||
for _, name := range sni {
|
||||
@@ -164,8 +164,14 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
tlsApp.RegisterServerNames(echNames, p.ALPN)
|
||||
}
|
||||
tlsApp.RegisterServerNames(echNames)
|
||||
}
|
||||
|
||||
tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
|
||||
tlsApp.EncryptedClientHello.configsMu.RLock()
|
||||
defer tlsApp.EncryptedClientHello.configsMu.RUnlock()
|
||||
return tlsApp.EncryptedClientHello.stdlibReady, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,19 +376,6 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
|
||||
cfg.MaxVersion = SupportedProtocols[p.ProtocolMax]
|
||||
}
|
||||
|
||||
// enable ECH (Encrypted ClientHello) if configured
|
||||
if tlsApp.EncryptedClientHello != nil {
|
||||
cfg.GetEncryptedClientHelloKeys = func(_ *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
|
||||
tlsApp.EncryptedClientHello.configsMu.RLock()
|
||||
defer tlsApp.EncryptedClientHello.configsMu.RUnlock()
|
||||
return tlsApp.EncryptedClientHello.stdlibReady, nil
|
||||
}
|
||||
// TLS 1.3 is the first version that supports ECH
|
||||
if cfg.MinVersion < tls.VersionTLS13 {
|
||||
cfg.MaxVersion = tls.VersionTLS13
|
||||
}
|
||||
}
|
||||
|
||||
// client authentication
|
||||
if p.ClientAuthentication != nil {
|
||||
if err := p.ClientAuthentication.provision(ctx); err != nil {
|
||||
|
||||
+35
-11
@@ -132,7 +132,10 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// ensure old keys are rotated out
|
||||
// convert the configs into a structure ready for the std lib to use
|
||||
ech.updateKeyList()
|
||||
|
||||
// ensure any old keys are rotated out
|
||||
if err = ech.rotateECHKeys(ctx, logger, true); err != nil {
|
||||
return nil, fmt.Errorf("rotating ECH configs: %w", err)
|
||||
}
|
||||
@@ -179,9 +182,11 @@ func (ech *ECH) setConfigsFromStorage(ctx caddy.Context, logger *zap.Logger) ([]
|
||||
return outerNames, nil
|
||||
}
|
||||
|
||||
// rotateECHKeys updates the ECH keys/configs that are outdated. It should be called
|
||||
// in a write lock on ech.configsMu. If a lock is already obtained in storage, then
|
||||
// pass true for storageSynced.
|
||||
// rotateECHKeys updates the ECH keys/configs that are outdated if rotation is needed.
|
||||
// It should be called in a write lock on ech.configsMu. If a lock is already obtained
|
||||
// in storage, then pass true for storageSynced.
|
||||
//
|
||||
// This function sets/updates the stdlib-ready key list only if a rotation occurs.
|
||||
func (ech *ECH) rotateECHKeys(ctx caddy.Context, logger *zap.Logger, storageSynced bool) error {
|
||||
storage := ctx.Storage()
|
||||
|
||||
@@ -435,6 +440,10 @@ func (t *TLS) publishECHConfigs(logger *zap.Logger) error {
|
||||
zap.Strings("domains", dnsNamesToPublish),
|
||||
zap.Uint8s("config_ids", configIDs))
|
||||
|
||||
if dnsPublisher, ok := publisher.(*ECHDNSPublisher); ok {
|
||||
dnsPublisher.alpnByDomain = t.alpnValuesForServerNames(dnsNamesToPublish)
|
||||
}
|
||||
|
||||
// publish this ECH config list with this publisher
|
||||
pubTime := time.Now()
|
||||
err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin)
|
||||
@@ -771,7 +780,8 @@ type ECHDNSPublisher struct {
|
||||
ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
|
||||
provider ECHDNSProvider
|
||||
|
||||
logger *zap.Logger
|
||||
alpnByDomain map[string][]string
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
@@ -867,12 +877,7 @@ nextName:
|
||||
continue
|
||||
}
|
||||
params := httpsRec.Params
|
||||
if params == nil {
|
||||
params = make(libdns.SvcParams)
|
||||
}
|
||||
|
||||
// overwrite only the "ech" SvcParamKey
|
||||
params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}
|
||||
params = dnsPub.publishedSvcParams(domain, params, configListBin)
|
||||
|
||||
// publish record
|
||||
_, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{
|
||||
@@ -898,6 +903,25 @@ nextName:
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dnsPub *ECHDNSPublisher) publishedSvcParams(domain string, existing libdns.SvcParams, configListBin []byte) libdns.SvcParams {
|
||||
params := make(libdns.SvcParams, len(existing)+2)
|
||||
for key, values := range existing {
|
||||
params[key] = append([]string(nil), values...)
|
||||
}
|
||||
|
||||
params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}
|
||||
|
||||
if len(dnsPub.alpnByDomain) == 0 {
|
||||
return params
|
||||
}
|
||||
|
||||
if alpn := dnsPub.alpnByDomain[strings.ToLower(domain)]; len(alpn) > 0 {
|
||||
params["alpn"] = append([]string(nil), alpn...)
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// echConfig represents an ECHConfig from the specification,
|
||||
// [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html).
|
||||
type echConfig struct {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/libdns/libdns"
|
||||
)
|
||||
|
||||
func TestRegisterServerNamesWithALPN(t *testing.T) {
|
||||
tlsApp := &TLS{
|
||||
serverNames: make(map[string]serverNameRegistration),
|
||||
serverNamesMu: new(sync.Mutex),
|
||||
}
|
||||
|
||||
tlsApp.RegisterServerNames([]string{
|
||||
"Example.com:443",
|
||||
"example.com",
|
||||
"127.0.0.1:443",
|
||||
}, []string{"h2", "http/1.1"})
|
||||
tlsApp.RegisterServerNames([]string{"EXAMPLE.COM"}, []string{"h3"})
|
||||
|
||||
got := tlsApp.alpnValuesForServerNames([]string{"example.com:443", "127.0.0.1:443"})
|
||||
want := map[string][]string{
|
||||
"example.com": {"h3", "h2", "http/1.1"},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected ALPN values: got %#v want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestECHDNSPublisherPublishedSvcParams(t *testing.T) {
|
||||
dnsPub := &ECHDNSPublisher{
|
||||
alpnByDomain: map[string][]string{
|
||||
"example.com": {"h3", "h2", "http/1.1"},
|
||||
},
|
||||
}
|
||||
|
||||
existing := libdns.SvcParams{
|
||||
"alpn": {"h2"},
|
||||
"ipv4hint": {"203.0.113.10"},
|
||||
}
|
||||
|
||||
got := dnsPub.publishedSvcParams("Example.com", existing, []byte{0x01, 0x02, 0x03})
|
||||
|
||||
if !reflect.DeepEqual(existing["alpn"], []string{"h2"}) {
|
||||
t.Fatalf("existing params mutated: got %v", existing["alpn"])
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got["alpn"], []string{"h3", "h2", "http/1.1"}) {
|
||||
t.Fatalf("unexpected ALPN params: got %v", got["alpn"])
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got["ipv4hint"], []string{"203.0.113.10"}) {
|
||||
t.Fatalf("unexpected preserved params: got %v", got["ipv4hint"])
|
||||
}
|
||||
|
||||
wantECH := base64.StdEncoding.EncodeToString([]byte{0x01, 0x02, 0x03})
|
||||
if !reflect.DeepEqual(got["ech"], []string{wantECH}) {
|
||||
t.Fatalf("unexpected ECH params: got %v want %v", got["ech"], wantECH)
|
||||
}
|
||||
}
|
||||
+104
-16
@@ -23,6 +23,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -140,7 +141,7 @@ type TLS struct {
|
||||
logger *zap.Logger
|
||||
events *caddyevents.App
|
||||
|
||||
serverNames map[string]struct{}
|
||||
serverNames map[string]serverNameRegistration
|
||||
serverNamesMu *sync.Mutex
|
||||
|
||||
// set of subjects with managed certificates,
|
||||
@@ -168,7 +169,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
|
||||
t.logger = ctx.Logger()
|
||||
repl := caddy.NewReplacer()
|
||||
t.managing, t.loaded = make(map[string]string), make(map[string]string)
|
||||
t.serverNames = make(map[string]struct{})
|
||||
t.serverNames = make(map[string]serverNameRegistration)
|
||||
t.serverNamesMu = new(sync.Mutex)
|
||||
|
||||
// set up default DNS module, if any, and make sure it implements all the
|
||||
@@ -613,8 +614,8 @@ func (t *TLS) Manage(subjects map[string]struct{}) error {
|
||||
|
||||
// managingWildcardFor returns true if the app is managing a certificate that covers that
|
||||
// subject name (including consideration of wildcards), either from its internal list of
|
||||
// names that it IS managing certs for, or from the otherSubjsToManage which includes names
|
||||
// that WILL be managed.
|
||||
// names that it IS managing certs for, from the otherSubjsToManage which includes names
|
||||
// that WILL be managed, or from names configured in the 'automate' loader.
|
||||
func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]struct{}) bool {
|
||||
// TODO: we could also consider manually-loaded certs using t.HasCertificateForSubject(),
|
||||
// but that does not account for how manually-loaded certs may be restricted as to which
|
||||
@@ -629,7 +630,9 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str
|
||||
return managing
|
||||
}
|
||||
|
||||
// replace labels of the domain with wildcards until we get a match
|
||||
// replace labels of the domain with wildcards until we get a match from names
|
||||
// already being managed, those about to be managed in this batch, or those
|
||||
// configured for automation
|
||||
labels := strings.Split(subj, ".")
|
||||
for i := range labels {
|
||||
if labels[i] == "*" {
|
||||
@@ -643,32 +646,117 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str
|
||||
if _, ok := otherSubjsToManage[candidate]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := t.automateNames[candidate]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// RegisterServerNames registers the provided DNS names with the TLS app.
|
||||
// This is currently used to auto-publish Encrypted ClientHello (ECH)
|
||||
// configurations, if enabled. Use of this function by apps using the TLS
|
||||
// app removes the need for the user to redundantly specify domain names
|
||||
// in their configuration. This function separates hostname and port
|
||||
// (keeping only the hotsname) and filters IP addresses, which can't be
|
||||
// used with ECH.
|
||||
// RegisterServerNames registers the provided DNS names with the TLS app and
|
||||
// associates them with the given HTTPS RR ALPN values, if any. This is
|
||||
// currently used to auto-publish Encrypted ClientHello (ECH) configurations,
|
||||
// if enabled. Use of this function by apps using the TLS app removes the need
|
||||
// for the user to redundantly specify domain names in their configuration.
|
||||
// This function separates hostname and port, keeping only the hostname, and
|
||||
// filters IP addresses which can't be used with ECH.
|
||||
//
|
||||
// EXPERIMENTAL: This function and its semantics/behavior are subject to change.
|
||||
func (t *TLS) RegisterServerNames(dnsNames []string) {
|
||||
func (t *TLS) RegisterServerNames(dnsNames, alpnValues []string) {
|
||||
t.serverNamesMu.Lock()
|
||||
defer t.serverNamesMu.Unlock()
|
||||
|
||||
for _, name := range dnsNames {
|
||||
host, _, err := net.SplitHostPort(name)
|
||||
if err != nil {
|
||||
host = name
|
||||
}
|
||||
if strings.TrimSpace(host) != "" && !certmagic.SubjectIsIP(host) {
|
||||
t.serverNames[strings.ToLower(host)] = struct{}{}
|
||||
host = strings.ToLower(strings.TrimSpace(host))
|
||||
if host == "" || certmagic.SubjectIsIP(host) {
|
||||
continue
|
||||
}
|
||||
|
||||
registration := t.serverNames[host]
|
||||
|
||||
if len(alpnValues) == 0 {
|
||||
t.serverNames[host] = registration
|
||||
continue
|
||||
}
|
||||
|
||||
if registration.alpnValues == nil {
|
||||
registration.alpnValues = make(map[string]struct{}, len(alpnValues))
|
||||
}
|
||||
for _, alpn := range alpnValues {
|
||||
if alpn == "" {
|
||||
continue
|
||||
}
|
||||
registration.alpnValues[alpn] = struct{}{}
|
||||
}
|
||||
t.serverNames[host] = registration
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TLS) alpnValuesForServerNames(dnsNames []string) map[string][]string {
|
||||
t.serverNamesMu.Lock()
|
||||
defer t.serverNamesMu.Unlock()
|
||||
|
||||
result := make(map[string][]string, len(dnsNames))
|
||||
for _, name := range dnsNames {
|
||||
host, _, err := net.SplitHostPort(name)
|
||||
if err != nil {
|
||||
host = name
|
||||
}
|
||||
host = strings.ToLower(strings.TrimSpace(host))
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
registration, ok := t.serverNames[host]
|
||||
if !ok || len(registration.alpnValues) == 0 {
|
||||
continue
|
||||
}
|
||||
result[host] = OrderedHTTPSRRALPN(registration.alpnValues)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// OrderedHTTPSRRALPN returns the HTTPS RR ALPN values in preferred order.
|
||||
func OrderedHTTPSRRALPN(alpnSet map[string]struct{}) []string {
|
||||
if len(alpnSet) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
knownOrder := append([]string{"h3"}, defaultALPN...)
|
||||
ordered := make([]string, 0, len(alpnSet))
|
||||
seen := make(map[string]struct{}, len(alpnSet))
|
||||
|
||||
for _, alpn := range knownOrder {
|
||||
if _, ok := alpnSet[alpn]; ok {
|
||||
ordered = append(ordered, alpn)
|
||||
seen[alpn] = struct{}{}
|
||||
}
|
||||
}
|
||||
t.serverNamesMu.Unlock()
|
||||
|
||||
if len(ordered) == len(alpnSet) {
|
||||
return ordered
|
||||
}
|
||||
|
||||
var remaining []string
|
||||
for alpn := range alpnSet {
|
||||
if _, ok := seen[alpn]; ok {
|
||||
continue
|
||||
}
|
||||
remaining = append(remaining, alpn)
|
||||
}
|
||||
slices.Sort(remaining)
|
||||
|
||||
return append(ordered, remaining...)
|
||||
}
|
||||
|
||||
type serverNameRegistration struct {
|
||||
alpnValues map[string]struct{}
|
||||
}
|
||||
|
||||
// HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func TestAvoidDuplicateAutomation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
automateNames []string
|
||||
expectedToManage bool
|
||||
}{
|
||||
{
|
||||
name: "do not manage if wildcard is automated",
|
||||
automateNames: []string{"*.example.com"},
|
||||
expectedToManage: false,
|
||||
},
|
||||
{
|
||||
name: "manage if no automation configured",
|
||||
automateNames: []string{},
|
||||
expectedToManage: true,
|
||||
},
|
||||
{
|
||||
name: "manage if explicitly requested even when wildcard automated",
|
||||
automateNames: []string{"*.example.com", "sub.example.com"},
|
||||
expectedToManage: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
automateJSON, err := json.Marshal(tc.automateNames)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tlsApp := &TLS{
|
||||
Automation: &AutomationConfig{
|
||||
Policies: []*AutomationPolicy{
|
||||
{
|
||||
IssuersRaw: []json.RawMessage{
|
||||
[]byte(`{"module": "internal"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CertificatesRaw: map[string]json.RawMessage{
|
||||
"automate": automateJSON,
|
||||
},
|
||||
}
|
||||
|
||||
var cfg caddy.Config
|
||||
ctx, err := caddy.ProvisionContext(&cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := tlsApp.Provision(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// simulate a case wherein the HTTP app starts first and
|
||||
// tells the TLS app about the following auto-HTTPS domains
|
||||
httpDomains := map[string]struct{}{"sub.example.com": {}}
|
||||
if err := tlsApp.Manage(httpDomains); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, actuallyManaged := tlsApp.managing["sub.example.com"]
|
||||
if actuallyManaged != tc.expectedToManage {
|
||||
t.Errorf(
|
||||
"expected sub.example.com individually managed: %v, got: %v",
|
||||
tc.expectedToManage,
|
||||
actuallyManaged,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,47 @@ 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
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
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.(caddyhttp.LoggableStringArray); ok {
|
||||
newArray := make(caddyhttp.LoggableStringArray, len(array))
|
||||
if array, ok := in.Interface.(internal.LoggableStringArray); ok {
|
||||
newArray := make(internal.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.(caddyhttp.LoggableStringArray); ok {
|
||||
newArray := make(caddyhttp.LoggableStringArray, len(array))
|
||||
if array, ok := in.Interface.(internal.LoggableStringArray); ok {
|
||||
newArray := make(internal.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.(caddyhttp.LoggableStringArray); ok {
|
||||
newArray := make(caddyhttp.LoggableStringArray, len(array))
|
||||
if array, ok := in.Interface.(internal.LoggableStringArray); ok {
|
||||
newArray := make(internal.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.(caddyhttp.LoggableStringArray)
|
||||
cookiesSlice, ok := in.Interface.(internal.LoggableStringArray)
|
||||
if !ok {
|
||||
return in
|
||||
}
|
||||
@@ -559,7 +559,7 @@ OUTER:
|
||||
transformedRequest.AddCookie(c)
|
||||
}
|
||||
|
||||
in.Interface = caddyhttp.LoggableStringArray(transformedRequest.Header["Cookie"])
|
||||
in.Interface = internal.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.(caddyhttp.LoggableStringArray); ok {
|
||||
newArray := make(caddyhttp.LoggableStringArray, len(array))
|
||||
if array, ok := in.Interface.(internal.LoggableStringArray); ok {
|
||||
newArray := make(internal.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.(caddyhttp.LoggableStringArray); ok {
|
||||
newArray := make(caddyhttp.LoggableStringArray, len(array))
|
||||
if array, ok := in.Interface.(internal.LoggableStringArray); ok {
|
||||
newArray := make(internal.LoggableStringArray, len(array))
|
||||
for i, s := range array {
|
||||
newArray[i] = f.processString(s)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
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: caddyhttp.LoggableStringArray{
|
||||
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
|
||||
"255.255.255.255",
|
||||
"244.244.244.244",
|
||||
}})
|
||||
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
|
||||
arr, ok := out.Interface.(internal.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: caddyhttp.LoggableStringArray{
|
||||
out = f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
|
||||
"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
|
||||
"ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
|
||||
}})
|
||||
arr, ok = out.Interface.(caddyhttp.LoggableStringArray)
|
||||
arr, ok = out.Interface.(internal.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: caddyhttp.LoggableStringArray{
|
||||
out := f.Filter(zapcore.Field{Interface: internal.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.(caddyhttp.LoggableStringArray)
|
||||
arr, ok := out.Interface.(internal.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: caddyhttp.LoggableStringArray{
|
||||
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
|
||||
"foo=a; foo=b; bar=c; bar=d; baz=e; hash=hashed",
|
||||
}})
|
||||
outval := out.Interface.(caddyhttp.LoggableStringArray)
|
||||
expected := caddyhttp.LoggableStringArray{
|
||||
outval := out.Interface.(internal.LoggableStringArray)
|
||||
expected := internal.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: caddyhttp.LoggableStringArray{"foo-secret-bar", "bar-secret-foo"}})
|
||||
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
|
||||
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{"foo-secret-bar", "bar-secret-foo"}})
|
||||
arr, ok := out.Interface.(internal.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: caddyhttp.LoggableStringArray{"foo", "bar"}})
|
||||
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
|
||||
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{"foo", "bar"}})
|
||||
arr, ok := out.Interface.(internal.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: caddyhttp.LoggableStringArray{
|
||||
out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
|
||||
"foo-secret-123",
|
||||
"bar-secret-456",
|
||||
}})
|
||||
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
|
||||
arr, ok := out.Interface.(internal.LoggableStringArray)
|
||||
if !ok {
|
||||
t.Fatalf("field is wrong type: %T", out.Interface)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
// 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)
|
||||
)
|
||||
@@ -0,0 +1,155 @@
|
||||
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
@@ -121,6 +121,18 @@ 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()
|
||||
|
||||
+10
-8
@@ -79,14 +79,15 @@ func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loade
|
||||
up.Lock()
|
||||
upv, loaded = up.pool[key]
|
||||
if loaded {
|
||||
atomic.AddInt32(&upv.refs, 1)
|
||||
upv.refs.Add(1)
|
||||
up.Unlock()
|
||||
upv.RLock()
|
||||
value = upv.value
|
||||
err = upv.err
|
||||
upv.RUnlock()
|
||||
} else {
|
||||
upv = &usagePoolVal{refs: 1}
|
||||
upv = &usagePoolVal{}
|
||||
upv.refs.Store(1)
|
||||
upv.Lock()
|
||||
up.pool[key] = upv
|
||||
up.Unlock()
|
||||
@@ -118,7 +119,7 @@ func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) {
|
||||
up.Lock()
|
||||
upv, loaded = up.pool[key]
|
||||
if loaded {
|
||||
atomic.AddInt32(&upv.refs, 1)
|
||||
upv.refs.Add(1)
|
||||
up.Unlock()
|
||||
upv.Lock()
|
||||
if upv.err == nil {
|
||||
@@ -129,7 +130,8 @@ func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) {
|
||||
}
|
||||
upv.Unlock()
|
||||
} else {
|
||||
upv = &usagePoolVal{refs: 1, value: val}
|
||||
upv = &usagePoolVal{value: val}
|
||||
upv.refs.Store(1)
|
||||
up.pool[key] = upv
|
||||
up.Unlock()
|
||||
value = val
|
||||
@@ -173,7 +175,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) {
|
||||
up.Unlock()
|
||||
return false, nil
|
||||
}
|
||||
refs := atomic.AddInt32(&upv.refs, -1)
|
||||
refs := upv.refs.Add(-1)
|
||||
if refs == 0 {
|
||||
delete(up.pool, key)
|
||||
up.Unlock()
|
||||
@@ -188,7 +190,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))
|
||||
upv.value, upv.refs.Load()))
|
||||
}
|
||||
}
|
||||
return deleted, err
|
||||
@@ -203,7 +205,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 := atomic.LoadInt32(&upv.refs)
|
||||
refs := upv.refs.Load()
|
||||
return int(refs), true
|
||||
}
|
||||
return 0, false
|
||||
@@ -220,7 +222,7 @@ type Destructor interface {
|
||||
}
|
||||
|
||||
type usagePoolVal struct {
|
||||
refs int32 // accessed atomically; must be 64-bit aligned for 32-bit systems
|
||||
refs atomic.Int32
|
||||
value any
|
||||
err error
|
||||
sync.RWMutex
|
||||
|
||||
Reference in New Issue
Block a user