mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cef20d257 |
+1
-3
@@ -8,7 +8,7 @@ The Caddy project would like to make sure that it stays on top of all relevant a
|
|||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ----------- | ----------|
|
| ----------- | ----------|
|
||||||
| 2.latest | ✔️ |
|
| 2.latest | ✔️ |
|
||||||
| < 2.latest | :x: |
|
| <= 2.latest | :x: |
|
||||||
|
|
||||||
|
|
||||||
## Acceptable Scope
|
## Acceptable Scope
|
||||||
@@ -25,8 +25,6 @@ Client-side exploits are out of scope. In other words, it is not a bug in Caddy
|
|||||||
|
|
||||||
Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
|
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.
|
We accept security reports and patches, but do not assign CVEs, for code that has not been released with a non-prerelease tag.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
# Caddy Project Guidelines
|
|
||||||
|
|
||||||
## Mission
|
|
||||||
|
|
||||||
**Every site on HTTPS.** Caddy is a security-first, modular, extensible server platform.
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
### Go Idioms
|
|
||||||
|
|
||||||
Follow [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments):
|
|
||||||
|
|
||||||
- **Error flow**: Early return, indent error handling—not else blocks
|
|
||||||
```go
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// normal code
|
|
||||||
```
|
|
||||||
- **Naming**: initialisms (`URL`, `HTTP`, `ID`—not `Url`, `Http`, `Id`)
|
|
||||||
- **Receiver names**: 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,8 +45,6 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/internal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// testCertMagicStorageOverride is a package-level test hook. Tests may set
|
// testCertMagicStorageOverride is a package-level test hook. Tests may set
|
||||||
@@ -120,6 +118,10 @@ type AdminConfig struct {
|
|||||||
//
|
//
|
||||||
// EXPERIMENTAL: This feature is subject to change.
|
// EXPERIMENTAL: This feature is subject to change.
|
||||||
Remote *RemoteAdmin `json:"remote,omitempty"`
|
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.
|
// ConfigSettings configures the management of configuration.
|
||||||
@@ -208,8 +210,8 @@ type AdminAccess struct {
|
|||||||
// AdminPermissions specifies what kinds of requests are allowed
|
// AdminPermissions specifies what kinds of requests are allowed
|
||||||
// to be made to the admin endpoint.
|
// to be made to the admin endpoint.
|
||||||
type AdminPermissions struct {
|
type AdminPermissions struct {
|
||||||
// The API paths allowed. A request path must either equal an
|
// The API paths allowed. Paths are simple prefix matches.
|
||||||
// allowed path or be a subpath with a path-segment boundary.
|
// Any subpath of the specified paths will be allowed.
|
||||||
Paths []string `json:"paths,omitempty"`
|
Paths []string `json:"paths,omitempty"`
|
||||||
|
|
||||||
// The HTTP methods allowed for the given paths.
|
// The HTTP methods allowed for the given paths.
|
||||||
@@ -218,7 +220,7 @@ type AdminPermissions struct {
|
|||||||
|
|
||||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||||
// for use in an admin endpoint server, which will be listening on listenAddr.
|
// for use in an admin endpoint server, which will be listening on listenAddr.
|
||||||
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) (adminHandler, error) {
|
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler {
|
||||||
muxWrap := adminHandler{mux: http.NewServeMux()}
|
muxWrap := adminHandler{mux: http.NewServeMux()}
|
||||||
|
|
||||||
// secure the local or remote endpoint respectively
|
// secure the local or remote endpoint respectively
|
||||||
@@ -275,21 +277,34 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx
|
|||||||
// register third-party module endpoints
|
// register third-party module endpoints
|
||||||
for _, m := range GetModules("admin.api") {
|
for _, m := range GetModules("admin.api") {
|
||||||
router := m.New().(AdminRouter)
|
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() {
|
for _, route := range router.Routes() {
|
||||||
addRoute(route.Pattern, handlerLabel, route.Handler)
|
addRoute(route.Pattern, handlerLabel, route.Handler)
|
||||||
}
|
}
|
||||||
|
admin.routers = append(admin.routers, router)
|
||||||
}
|
}
|
||||||
|
|
||||||
return muxWrap, nil
|
return muxWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
// provisionAdminRouters provisions all the router modules
|
||||||
|
// in the admin.api namespace that need provisioning.
|
||||||
|
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
|
||||||
|
for _, router := range admin.routers {
|
||||||
|
provisioner, ok := router.(Provisioner)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := provisioner.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We no longer need the routers once provisioned, allow for GC
|
||||||
|
admin.routers = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// allowedOrigins returns a list of origins that are allowed.
|
// allowedOrigins returns a list of origins that are allowed.
|
||||||
@@ -413,7 +428,11 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := cfg.Admin.newAdminHandler(addr, false, ctx)
|
handler := cfg.Admin.newAdminHandler(addr, false, ctx)
|
||||||
|
|
||||||
|
// run the provisioners for loaded modules to make sure local
|
||||||
|
// state is properly re-initialized in the new admin server
|
||||||
|
err = cfg.Admin.provisionAdminRouters(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -537,7 +556,11 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
|||||||
|
|
||||||
// make the HTTP handler but disable Host/Origin enforcement
|
// make the HTTP handler but disable Host/Origin enforcement
|
||||||
// because we are using TLS authentication instead
|
// because we are using TLS authentication instead
|
||||||
handler, err := cfg.Admin.newAdminHandler(addr, true, ctx)
|
handler := cfg.Admin.newAdminHandler(addr, true, ctx)
|
||||||
|
|
||||||
|
// run the provisioners for loaded modules to make sure local
|
||||||
|
// state is properly re-initialized in the new admin server
|
||||||
|
err = cfg.Admin.provisionAdminRouters(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -693,7 +716,7 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
|
|||||||
// verify path
|
// verify path
|
||||||
pathFound := accessPerm.Paths == nil
|
pathFound := accessPerm.Paths == nil
|
||||||
for _, allowedPath := range accessPerm.Paths {
|
for _, allowedPath := range accessPerm.Paths {
|
||||||
if adminPathAllowed(r.URL.Path, allowedPath) {
|
if strings.HasPrefix(r.URL.Path, allowedPath) {
|
||||||
pathFound = true
|
pathFound = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -722,19 +745,6 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminPathAllowed(reqPath, allowedPath string) bool {
|
|
||||||
if allowedPath == "" || allowedPath == "/" {
|
|
||||||
return strings.HasPrefix(reqPath, allowedPath)
|
|
||||||
}
|
|
||||||
if reqPath == allowedPath {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(allowedPath, "/") {
|
|
||||||
return strings.HasPrefix(reqPath, allowedPath)
|
|
||||||
}
|
|
||||||
return strings.HasPrefix(reqPath, allowedPath+"/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopAdminServer(srv *http.Server) error {
|
func stopAdminServer(srv *http.Server) error {
|
||||||
if srv == nil {
|
if srv == nil {
|
||||||
return fmt.Errorf("no admin server")
|
return fmt.Errorf("no admin server")
|
||||||
@@ -790,7 +800,7 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
zap.String("uri", r.RequestURI),
|
zap.String("uri", r.RequestURI),
|
||||||
zap.String("remote_ip", ip),
|
zap.String("remote_ip", ip),
|
||||||
zap.String("remote_port", port),
|
zap.String("remote_port", port),
|
||||||
zap.Object("headers", internal.LoggableHTTPHeader{Header: r.Header}),
|
zap.Reflect("headers", r.Header),
|
||||||
)
|
)
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
log = log.With(
|
log = log.With(
|
||||||
@@ -849,7 +859,6 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
Err: errors.New("invalid origin 'null'"),
|
Err: errors.New("invalid origin 'null'"),
|
||||||
Message: "Buggy browser is sending null Origin header.",
|
Message: "Buggy browser is sending null Origin header.",
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.enforceHost {
|
if h.enforceHost {
|
||||||
@@ -1051,9 +1060,6 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
|||||||
buf.Reset()
|
buf.Reset()
|
||||||
defer bufPool.Put(buf)
|
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)
|
_, err := io.Copy(buf, r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return APIError{
|
return APIError{
|
||||||
@@ -1136,20 +1142,6 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
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
|
// unsyncedConfigAccess traverses into the current config and performs
|
||||||
// the operation at path according to method, using body and out as
|
// the operation at path according to method, using body and out as
|
||||||
// needed. This is a low-level, unsynchronized function; most callers
|
// needed. This is a low-level, unsynchronized function; most callers
|
||||||
@@ -1211,12 +1203,11 @@ traverseLoop:
|
|||||||
var idx int
|
var idx int
|
||||||
if method != http.MethodPost {
|
if method != http.MethodPost {
|
||||||
idxStr := parts[len(parts)-1]
|
idxStr := parts[len(parts)-1]
|
||||||
idx, err = parseCanonicalArrayIndex(idxStr)
|
idx, err = strconv.Atoi(idxStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[%s] invalid array index '%s': %v",
|
return fmt.Errorf("[%s] invalid array index '%s': %v",
|
||||||
path, idxStr, err)
|
path, idxStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
|
if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
|
||||||
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
|
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
|
||||||
}
|
}
|
||||||
@@ -1316,7 +1307,7 @@ traverseLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
case []any:
|
case []any:
|
||||||
partInt, err := parseCanonicalArrayIndex(part)
|
partInt, err := strconv.Atoi(part)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
||||||
strings.Join(parts[:i+1], "/"), part, err)
|
strings.Join(parts[:i+1], "/"), part, err)
|
||||||
|
|||||||
+15
-204
@@ -15,13 +15,9 @@
|
|||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -35,8 +31,6 @@ import (
|
|||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
dto "github.com/prometheus/client_model/go"
|
dto "github.com/prometheus/client_model/go"
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zaptest/observer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var testCfg = []byte(`{
|
var testCfg = []byte(`{
|
||||||
@@ -57,13 +51,6 @@ var testCfg = []byte(`{
|
|||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
type testAdminPublicKey string
|
|
||||||
|
|
||||||
func (k testAdminPublicKey) Equal(x crypto.PublicKey) bool {
|
|
||||||
other, ok := x.(testAdminPublicKey)
|
|
||||||
return ok && k == other
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnsyncedConfigAccess(t *testing.T) {
|
func TestUnsyncedConfigAccess(t *testing.T) {
|
||||||
// each test is performed in sequence, so
|
// each test is performed in sequence, so
|
||||||
// each change builds on the previous ones;
|
// each change builds on the previous ones;
|
||||||
@@ -255,51 +242,6 @@ func TestAdminHandlerErrorHandling(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminHandlerServeHTTPRedactsSensitiveHeadersInLogs(t *testing.T) {
|
|
||||||
core, logs := observer.New(zap.InfoLevel)
|
|
||||||
|
|
||||||
defaultLoggerMu.Lock()
|
|
||||||
origLogger := defaultLogger.logger
|
|
||||||
defaultLogger.logger = zap.New(core)
|
|
||||||
defaultLoggerMu.Unlock()
|
|
||||||
t.Cleanup(func() {
|
|
||||||
defaultLoggerMu.Lock()
|
|
||||||
defaultLogger.logger = origLogger
|
|
||||||
defaultLoggerMu.Unlock()
|
|
||||||
})
|
|
||||||
|
|
||||||
handler := adminHandler{
|
|
||||||
mux: http.NewServeMux(),
|
|
||||||
}
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer secret")
|
|
||||||
req.Header.Set("Cookie", "session=secret")
|
|
||||||
req.Header.Set("X-Test", "ok")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
if logs.Len() == 0 {
|
|
||||||
t.Fatal("expected request log entry")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := logs.All()[0].ContextMap()
|
|
||||||
headers, ok := ctx["headers"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected headers field in log context, got %T", ctx["headers"])
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := headers["Authorization"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
|
|
||||||
t.Fatalf("expected redacted Authorization header, got %#v", got)
|
|
||||||
}
|
|
||||||
if got := headers["Cookie"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
|
|
||||||
t.Fatalf("expected redacted Cookie header, got %#v", got)
|
|
||||||
}
|
|
||||||
if got := headers["X-Test"]; !reflect.DeepEqual(got, []any{"ok"}) {
|
|
||||||
t.Fatalf("expected X-Test header to remain visible, got %#v", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initAdminMetrics() {
|
func initAdminMetrics() {
|
||||||
if adminMetrics.requestErrors != nil {
|
if adminMetrics.requestErrors != nil {
|
||||||
prometheus.Unregister(adminMetrics.requestErrors)
|
prometheus.Unregister(adminMetrics.requestErrors)
|
||||||
@@ -340,10 +282,7 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to parse address: %v", err)
|
t.Fatalf("Failed to parse address: %v", err)
|
||||||
}
|
}
|
||||||
handler, err := cfg.Admin.newAdminHandler(addr, false, Context{})
|
handler := cfg.Admin.newAdminHandler(addr, false, Context{})
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create admin handler: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -464,10 +403,7 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
|
|||||||
admin := &AdminConfig{
|
admin := &AdminConfig{
|
||||||
EnforceOrigin: false,
|
EnforceOrigin: false,
|
||||||
}
|
}
|
||||||
handler, err := admin.newAdminHandler(addr, false, Context{})
|
handler := admin.newAdminHandler(addr, false, Context{})
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create admin handler: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/mock", nil)
|
req := httptest.NewRequest("GET", "/mock", nil)
|
||||||
req.Host = "localhost:2019"
|
req.Host = "localhost:2019"
|
||||||
@@ -479,6 +415,10 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
|
|||||||
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
|
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
|
||||||
t.Logf("Response body: %s", rr.Body.String())
|
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 {
|
type mockProvisionableRouter struct {
|
||||||
@@ -516,16 +456,19 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
provisionErr error
|
provisionErr error
|
||||||
wantErr bool
|
wantErr bool
|
||||||
|
routersAfter int // expected number of routers after provisioning
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "successful provisioning",
|
name: "successful provisioning",
|
||||||
provisionErr: nil,
|
provisionErr: nil,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
|
routersAfter: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "provisioning error",
|
name: "provisioning error",
|
||||||
provisionErr: fmt.Errorf("provision failed"),
|
provisionErr: fmt.Errorf("provision failed"),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
|
routersAfter: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,7 +504,8 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
|||||||
t.Fatalf("Failed to parse address: %v", err)
|
t.Fatalf("Failed to parse address: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = admin.newAdminHandler(addr, false, Context{})
|
_ = admin.newAdminHandler(addr, false, Context{})
|
||||||
|
err = admin.provisionAdminRouters(Context{})
|
||||||
|
|
||||||
if test.wantErr {
|
if test.wantErr {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -572,6 +516,10 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
|||||||
t.Errorf("Expected no error but got: %v", err)
|
t.Errorf("Expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(admin.routers) != test.routersAfter {
|
||||||
|
t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -656,99 +604,6 @@ func TestAllowedOriginsUnixSocket(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoteAdminAccessControlPathSegmentMatching(t *testing.T) {
|
|
||||||
const authorizedKey testAdminPublicKey = "authorized"
|
|
||||||
peerCert := &x509.Certificate{PublicKey: authorizedKey}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
allowedPath string
|
|
||||||
requestPath string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "exact path",
|
|
||||||
allowedPath: "/pki/ca/prod",
|
|
||||||
requestPath: "/pki/ca/prod",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "subpath",
|
|
||||||
allowedPath: "/pki/ca/prod",
|
|
||||||
requestPath: "/pki/ca/prod/certificates",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "trailing slash subpath",
|
|
||||||
allowedPath: "/pki/ca/prod/",
|
|
||||||
requestPath: "/pki/ca/prod/certificates",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sibling with shared prefix",
|
|
||||||
allowedPath: "/pki/ca/prod",
|
|
||||||
requestPath: "/pki/ca/prod-backup",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "same segment plus digit",
|
|
||||||
allowedPath: "/pki/ca/prod",
|
|
||||||
requestPath: "/pki/ca/prod1",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "root path",
|
|
||||||
allowedPath: "/",
|
|
||||||
requestPath: "/pki/ca/prod",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
remote := RemoteAdmin{
|
|
||||||
AccessControl: []*AdminAccess{
|
|
||||||
{
|
|
||||||
Permissions: []AdminPermissions{
|
|
||||||
{
|
|
||||||
Methods: []string{http.MethodGet},
|
|
||||||
Paths: []string{test.allowedPath},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
publicKeys: []crypto.PublicKey{authorizedKey},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "https://localhost:2021"+test.requestPath, nil)
|
|
||||||
req.TLS = &tls.ConnectionState{
|
|
||||||
VerifiedChains: [][]*x509.Certificate{{peerCert}},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := remote.enforceAccessControls(req)
|
|
||||||
if test.wantErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("test %d (%s): allowed path %q, request path %q: expected forbidden error, got nil", i, test.name, test.allowedPath, test.requestPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var apiErr APIError
|
|
||||||
if !errors.As(err, &apiErr) {
|
|
||||||
t.Errorf("test %d (%s): allowed path %q, request path %q: expected APIError with HTTP status %d, got %T: %v", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, err, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if apiErr.HTTPStatus != http.StatusForbidden {
|
|
||||||
t.Errorf("test %d (%s): allowed path %q, request path %q: expected HTTP status %d, got %d", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, apiErr.HTTPStatus)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d (%s): allowed path %q, request path %q: expected no error, got %v", i, test.name, test.allowedPath, test.requestPath, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplaceRemoteAdminServer(t *testing.T) {
|
func TestReplaceRemoteAdminServer(t *testing.T) {
|
||||||
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
|
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
|
||||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
|
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
|
||||||
@@ -1101,47 +956,3 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnsyncedConfigAccessCanonicalArrayIndices(t *testing.T) {
|
|
||||||
rawCfg = map[string]any{
|
|
||||||
rawConfigKey: map[string]any{
|
|
||||||
"list": []any{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
wantOutput string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{name: "allow zero", path: "/" + rawConfigKey + "/list/0", wantOutput: "\"zero\"\n"},
|
|
||||||
{name: "allow one", path: "/" + rawConfigKey + "/list/1", wantOutput: "\"one\"\n"},
|
|
||||||
{name: "allow ten", path: "/" + rawConfigKey + "/list/10", wantOutput: "\"ten\"\n"},
|
|
||||||
{name: "reject leading zero", path: "/" + rawConfigKey + "/list/01", wantErr: true},
|
|
||||||
{name: "reject multiple leading zeros", path: "/" + rawConfigKey + "/list/002", wantErr: true},
|
|
||||||
{name: "reject plus sign", path: "/" + rawConfigKey + "/list/+1", wantErr: true},
|
|
||||||
{name: "reject negative zero", path: "/" + rawConfigKey + "/list/-0", wantErr: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
var gotOutput bytes.Buffer
|
|
||||||
err := unsyncedConfigAccess(http.MethodGet, tc.path, nil, &gotOutput)
|
|
||||||
|
|
||||||
if tc.wantErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("test %d (%s): input path %q: expected error, got nil with output %q", i, tc.name, tc.path, gotOutput.String())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d (%s): input path %q: expected no error with output %q, got error %v with output %q", i, tc.name, tc.path, tc.wantOutput, err, gotOutput.String())
|
|
||||||
}
|
|
||||||
if gotOutput.String() != tc.wantOutput {
|
|
||||||
t.Errorf("test %d (%s): input path %q: expected output %q, got %q", i, tc.name, tc.path, tc.wantOutput, gotOutput.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,377 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package caddy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAPIError_Error_WithErr(t *testing.T) {
|
|
||||||
underlyingErr := errors.New("underlying error")
|
|
||||||
apiErr := APIError{
|
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Err: underlyingErr,
|
|
||||||
Message: "API error message",
|
|
||||||
}
|
|
||||||
|
|
||||||
result := apiErr.Error()
|
|
||||||
expected := "underlying error"
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected '%s', got '%s'", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIError_Error_WithoutErr(t *testing.T) {
|
|
||||||
apiErr := APIError{
|
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Err: nil,
|
|
||||||
Message: "API error message",
|
|
||||||
}
|
|
||||||
|
|
||||||
result := apiErr.Error()
|
|
||||||
expected := "API error message"
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected '%s', got '%s'", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIError_Error_BothNil(t *testing.T) {
|
|
||||||
apiErr := APIError{
|
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Err: nil,
|
|
||||||
Message: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
result := apiErr.Error()
|
|
||||||
expected := ""
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected empty string, got '%s'", result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIError_JSON_Serialization(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
apiErr APIError
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "with message only",
|
|
||||||
apiErr: APIError{
|
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Message: "validation failed",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with underlying error only",
|
|
||||||
apiErr: APIError{
|
|
||||||
HTTPStatus: http.StatusInternalServerError,
|
|
||||||
Err: errors.New("internal error"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with both message and error",
|
|
||||||
apiErr: APIError{
|
|
||||||
HTTPStatus: http.StatusConflict,
|
|
||||||
Err: errors.New("underlying"),
|
|
||||||
Message: "conflict detected",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "minimal error",
|
|
||||||
apiErr: APIError{
|
|
||||||
HTTPStatus: http.StatusNotFound,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
// Marshal to JSON
|
|
||||||
jsonData, err := json.Marshal(test.apiErr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to marshal APIError: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal back
|
|
||||||
var unmarshaled APIError
|
|
||||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal APIError: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only Message field should survive JSON round-trip
|
|
||||||
// HTTPStatus and Err are marked with json:"-"
|
|
||||||
if unmarshaled.Message != test.apiErr.Message {
|
|
||||||
t.Errorf("Message mismatch: expected '%s', got '%s'",
|
|
||||||
test.apiErr.Message, unmarshaled.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPStatus and Err should be zero values after unmarshal
|
|
||||||
if unmarshaled.HTTPStatus != 0 {
|
|
||||||
t.Errorf("HTTPStatus should be 0 after unmarshal, got %d", unmarshaled.HTTPStatus)
|
|
||||||
}
|
|
||||||
if unmarshaled.Err != nil {
|
|
||||||
t.Errorf("Err should be nil after unmarshal, got %v", unmarshaled.Err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIError_HTTPStatus_Values(t *testing.T) {
|
|
||||||
// Test common HTTP status codes
|
|
||||||
statusCodes := []int{
|
|
||||||
http.StatusBadRequest,
|
|
||||||
http.StatusUnauthorized,
|
|
||||||
http.StatusForbidden,
|
|
||||||
http.StatusNotFound,
|
|
||||||
http.StatusMethodNotAllowed,
|
|
||||||
http.StatusConflict,
|
|
||||||
http.StatusPreconditionFailed,
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
http.StatusNotImplemented,
|
|
||||||
http.StatusServiceUnavailable,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, status := range statusCodes {
|
|
||||||
t.Run(fmt.Sprintf("status_%d", status), func(t *testing.T) {
|
|
||||||
apiErr := APIError{
|
|
||||||
HTTPStatus: status,
|
|
||||||
Message: http.StatusText(status),
|
|
||||||
}
|
|
||||||
|
|
||||||
if apiErr.HTTPStatus != status {
|
|
||||||
t.Errorf("Expected status %d, got %d", status, apiErr.HTTPStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that error message is reasonable
|
|
||||||
if apiErr.Message == "" && status >= 400 {
|
|
||||||
t.Errorf("Status %d should have a message", status)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIError_ErrorInterface_Compliance(t *testing.T) {
|
|
||||||
// Verify APIError properly implements error interface
|
|
||||||
var err error = APIError{
|
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Message: "test error",
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMsg := err.Error()
|
|
||||||
if errorMsg != "test error" {
|
|
||||||
t.Errorf("Expected 'test error', got '%s'", errorMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with underlying error
|
|
||||||
underlyingErr := errors.New("underlying")
|
|
||||||
err2 := APIError{
|
|
||||||
HTTPStatus: http.StatusInternalServerError,
|
|
||||||
Err: underlyingErr,
|
|
||||||
Message: "wrapper",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err2.Error() != "underlying" {
|
|
||||||
t.Errorf("Expected 'underlying', got '%s'", err2.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIError_JSON_EdgeCases(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
message string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty message",
|
|
||||||
message: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unicode message",
|
|
||||||
message: "Error: 🚨 Something went wrong! 你好",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "json characters in message",
|
|
||||||
message: `Error with "quotes" and {brackets}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "newlines in message",
|
|
||||||
message: "Line 1\nLine 2\r\nLine 3",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "very long message",
|
|
||||||
message: string(make([]byte, 10000)), // 10KB message
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
apiErr := APIError{
|
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Message: test.message,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be JSON serializable
|
|
||||||
jsonData, err := json.Marshal(apiErr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to marshal APIError: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be deserializable
|
|
||||||
var unmarshaled APIError
|
|
||||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal APIError: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if unmarshaled.Message != test.message {
|
|
||||||
t.Errorf("Message corrupted during JSON round-trip")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIError_Chaining(t *testing.T) {
|
|
||||||
// Test error chaining scenarios
|
|
||||||
rootErr := errors.New("root cause")
|
|
||||||
wrappedErr := fmt.Errorf("wrapped: %w", rootErr)
|
|
||||||
|
|
||||||
apiErr := APIError{
|
|
||||||
HTTPStatus: http.StatusInternalServerError,
|
|
||||||
Err: wrappedErr,
|
|
||||||
Message: "API wrapper",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error() should return the underlying error message
|
|
||||||
if apiErr.Error() != wrappedErr.Error() {
|
|
||||||
t.Errorf("Expected underlying error message, got '%s'", apiErr.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be able to unwrap
|
|
||||||
if !errors.Is(apiErr.Err, rootErr) {
|
|
||||||
t.Error("Should be able to unwrap to root cause")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIError_StatusCode_Boundaries(t *testing.T) {
|
|
||||||
// Test edge cases for HTTP status codes
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
status int
|
|
||||||
valid bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "negative status",
|
|
||||||
status: -1,
|
|
||||||
valid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "zero status",
|
|
||||||
status: 0,
|
|
||||||
valid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid 1xx",
|
|
||||||
status: http.StatusContinue,
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid 2xx",
|
|
||||||
status: http.StatusOK,
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid 4xx",
|
|
||||||
status: http.StatusBadRequest,
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid 5xx",
|
|
||||||
status: http.StatusInternalServerError,
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "too large status",
|
|
||||||
status: 9999,
|
|
||||||
valid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
err := APIError{
|
|
||||||
HTTPStatus: test.status,
|
|
||||||
Message: "test",
|
|
||||||
}
|
|
||||||
|
|
||||||
// The struct allows any int value, but we can test
|
|
||||||
// if it's a valid HTTP status
|
|
||||||
statusText := http.StatusText(test.status)
|
|
||||||
isValidStatus := statusText != ""
|
|
||||||
|
|
||||||
if isValidStatus != test.valid {
|
|
||||||
t.Errorf("Status %d validity: expected %v, got %v",
|
|
||||||
test.status, test.valid, isValidStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the struct holds the status
|
|
||||||
if err.HTTPStatus != test.status {
|
|
||||||
t.Errorf("Status not preserved: expected %d, got %d", test.status, err.HTTPStatus)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkAPIError_Error(b *testing.B) {
|
|
||||||
apiErr := APIError{
|
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Err: errors.New("benchmark error"),
|
|
||||||
Message: "benchmark message",
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
apiErr.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkAPIError_JSON_Marshal(b *testing.B) {
|
|
||||||
apiErr := APIError{
|
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Err: errors.New("benchmark error"),
|
|
||||||
Message: "benchmark message",
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
json.Marshal(apiErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkAPIError_JSON_Unmarshal(b *testing.B) {
|
|
||||||
jsonData := []byte(`{"error": "benchmark message"}`)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
var result APIError
|
|
||||||
_ = json.Unmarshal(jsonData, &result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -127,9 +127,10 @@ func Load(cfgJSON []byte, forceReload bool) error {
|
|||||||
zap.Error(notifyErr),
|
zap.Error(notifyErr),
|
||||||
zap.String("reload_err", err.Error()))
|
zap.String("reload_err", err.Error()))
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if notifyErr := notify.Ready(); notifyErr != nil {
|
if err := notify.Ready(); err != nil {
|
||||||
Log().Error("unable to notify to service manager of ready state", zap.Error(notifyErr))
|
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -440,6 +441,13 @@ func run(newCfg *Config, start bool) (Context, error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Provision any admin routers which may need to access
|
||||||
|
// some of the other apps at runtime
|
||||||
|
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
err = func() error {
|
err = func() error {
|
||||||
started := make([]string, 0, len(ctx.cfg.apps))
|
started := make([]string, 0, len(ctx.cfg.apps))
|
||||||
@@ -759,7 +767,7 @@ func Validate(cfg *Config) error {
|
|||||||
// code is emitted.
|
// code is emitted.
|
||||||
func exitProcess(ctx context.Context, logger *zap.Logger) {
|
func exitProcess(ctx context.Context, logger *zap.Logger) {
|
||||||
// let the rest of the program know we're quitting; only do it once
|
// let the rest of the program know we're quitting; only do it once
|
||||||
if !exiting.CompareAndSwap(false, true) {
|
if !atomic.CompareAndSwapInt32(exiting, 0, 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,11 +846,11 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
var exiting atomic.Bool
|
var exiting = new(int32) // accessed atomically
|
||||||
|
|
||||||
// Exiting returns true if the process is exiting.
|
// Exiting returns true if the process is exiting.
|
||||||
// EXPERIMENTAL API: subject to change or removal.
|
// EXPERIMENTAL API: subject to change or removal.
|
||||||
func Exiting() bool { return exiting.Load() }
|
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
|
||||||
|
|
||||||
// OnExit registers a callback to invoke during process exit.
|
// OnExit registers a callback to invoke during process exit.
|
||||||
// This registration is PROCESS-GLOBAL, meaning that each
|
// This registration is PROCESS-GLOBAL, meaning that each
|
||||||
|
|||||||
@@ -63,33 +63,8 @@ func Format(input []byte) []byte {
|
|||||||
heredocClosingMarker []rune
|
heredocClosingMarker []rune
|
||||||
|
|
||||||
nesting int // indentation level
|
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) {
|
write := func(ch rune) {
|
||||||
out.WriteRune(ch)
|
out.WriteRune(ch)
|
||||||
last = ch
|
last = ch
|
||||||
@@ -245,11 +220,9 @@ func Format(input []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if unicode.IsSpace(ch) {
|
if unicode.IsSpace(ch) {
|
||||||
finishToken()
|
|
||||||
space = true
|
space = true
|
||||||
heredocEscaped = false
|
heredocEscaped = false
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
finishLine()
|
|
||||||
newLines++
|
newLines++
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -276,19 +249,13 @@ func Format(input []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openBrace = false
|
openBrace = false
|
||||||
if openBraceOwnLine && previousLineWasTopLevelImport {
|
if beginningOfLine {
|
||||||
if last != '\n' {
|
|
||||||
nextLine()
|
|
||||||
}
|
|
||||||
indent()
|
|
||||||
} else if beginningOfLine {
|
|
||||||
indent()
|
indent()
|
||||||
} else if !openBraceSpace || !unicode.IsSpace(last) {
|
} else if !openBraceSpace || !unicode.IsSpace(last) {
|
||||||
write(' ')
|
write(' ')
|
||||||
}
|
}
|
||||||
write('{')
|
write('{')
|
||||||
openBraceWritten = true
|
openBraceWritten = true
|
||||||
openBraceOwnLine = false
|
|
||||||
nextLine()
|
nextLine()
|
||||||
newLines = 0
|
newLines = 0
|
||||||
// prevent infinite nesting from ridiculous inputs (issue #4169)
|
// prevent infinite nesting from ridiculous inputs (issue #4169)
|
||||||
@@ -299,10 +266,8 @@ func Format(input []byte) []byte {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case ch == '{':
|
case ch == '{':
|
||||||
finishToken()
|
|
||||||
openBrace = true
|
openBrace = true
|
||||||
openBraceSpace = spacePrior && !beginningOfLine
|
openBraceSpace = spacePrior && !beginningOfLine
|
||||||
openBraceOwnLine = newLines > 0
|
|
||||||
if openBraceSpace && newLines == 0 {
|
if openBraceSpace && newLines == 0 {
|
||||||
write(' ')
|
write(' ')
|
||||||
}
|
}
|
||||||
@@ -310,13 +275,11 @@ func Format(input []byte) []byte {
|
|||||||
if quotes == "`" {
|
if quotes == "`" {
|
||||||
write('{')
|
write('{')
|
||||||
openBraceWritten = true
|
openBraceWritten = true
|
||||||
openBraceOwnLine = false
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case ch == '}' && (spacePrior || !openBrace):
|
case ch == '}' && (spacePrior || !openBrace):
|
||||||
finishToken()
|
|
||||||
if quotes == "`" {
|
if quotes == "`" {
|
||||||
write('}')
|
write('}')
|
||||||
continue
|
continue
|
||||||
@@ -361,7 +324,6 @@ func Format(input []byte) []byte {
|
|||||||
space = true
|
space = true
|
||||||
}
|
}
|
||||||
|
|
||||||
currentToken.WriteRune(ch)
|
|
||||||
write(ch)
|
write(ch)
|
||||||
|
|
||||||
beginningOfLine = false
|
beginningOfLine = false
|
||||||
|
|||||||
@@ -475,21 +475,6 @@ Hope this helps.` + "`" + `
|
|||||||
}`,
|
}`,
|
||||||
expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}",
|
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,
|
// the formatter should output a trailing newline,
|
||||||
// even if the tests aren't written to expect that
|
// even if the tests aren't written to expect that
|
||||||
|
|||||||
@@ -1,268 +0,0 @@
|
|||||||
package caddyfile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestImportGraphAddNode(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
|
|
||||||
g.addNode("a")
|
|
||||||
if !g.exists("a") {
|
|
||||||
t.Error("expected node 'a' to exist after addNode")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adding again should not error
|
|
||||||
g.addNode("a")
|
|
||||||
if !g.exists("a") {
|
|
||||||
t.Error("expected node 'a' to still exist after duplicate addNode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphAddNodes(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
|
|
||||||
g.addNodes([]string{"a", "b", "c"})
|
|
||||||
for _, name := range []string{"a", "b", "c"} {
|
|
||||||
if !g.exists(name) {
|
|
||||||
t.Errorf("expected node %q to exist", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphRemoveNode(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
|
|
||||||
g.addNode("a")
|
|
||||||
g.addNode("b")
|
|
||||||
g.removeNode("a")
|
|
||||||
|
|
||||||
if g.exists("a") {
|
|
||||||
t.Error("expected node 'a' to not exist after removeNode")
|
|
||||||
}
|
|
||||||
if !g.exists("b") {
|
|
||||||
t.Error("expected node 'b' to still exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphRemoveNodes(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
|
|
||||||
g.addNodes([]string{"a", "b", "c", "d"})
|
|
||||||
g.removeNodes([]string{"a", "c"})
|
|
||||||
|
|
||||||
if g.exists("a") {
|
|
||||||
t.Error("expected node 'a' to be removed")
|
|
||||||
}
|
|
||||||
if g.exists("c") {
|
|
||||||
t.Error("expected node 'c' to be removed")
|
|
||||||
}
|
|
||||||
if !g.exists("b") {
|
|
||||||
t.Error("expected node 'b' to still exist")
|
|
||||||
}
|
|
||||||
if !g.exists("d") {
|
|
||||||
t.Error("expected node 'd' to still exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphAddEdge(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
g.addNodes([]string{"a", "b"})
|
|
||||||
|
|
||||||
err := g.addEdge("a", "b")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("addEdge() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !g.areConnected("a", "b") {
|
|
||||||
t.Error("expected 'a' -> 'b' edge to exist")
|
|
||||||
}
|
|
||||||
if g.areConnected("b", "a") {
|
|
||||||
t.Error("expected no 'b' -> 'a' edge (directed)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphAddEdgeNonExistentNode(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
g.addNode("a")
|
|
||||||
|
|
||||||
err := g.addEdge("a", "nonexistent")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error when adding edge to nonexistent node")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = g.addEdge("nonexistent", "a")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error when adding edge from nonexistent node")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphAddEdgeDuplicate(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
g.addNodes([]string{"a", "b"})
|
|
||||||
|
|
||||||
_ = g.addEdge("a", "b")
|
|
||||||
err := g.addEdge("a", "b")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("duplicate addEdge() should not error, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphCycleDetectionDirect(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
g.addNodes([]string{"a", "b"})
|
|
||||||
|
|
||||||
_ = g.addEdge("a", "b")
|
|
||||||
|
|
||||||
// Adding b -> a should create a cycle
|
|
||||||
err := g.addEdge("b", "a")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for cycle: a -> b -> a")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphCycleDetectionIndirect(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
g.addNodes([]string{"a", "b", "c"})
|
|
||||||
|
|
||||||
_ = g.addEdge("a", "b")
|
|
||||||
_ = g.addEdge("b", "c")
|
|
||||||
|
|
||||||
// Adding c -> a should create a cycle: a -> b -> c -> a
|
|
||||||
err := g.addEdge("c", "a")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for indirect cycle: a -> b -> c -> a")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphCycleDetectionLongChain(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
nodes := []string{"a", "b", "c", "d", "e"}
|
|
||||||
g.addNodes(nodes)
|
|
||||||
|
|
||||||
_ = g.addEdge("a", "b")
|
|
||||||
_ = g.addEdge("b", "c")
|
|
||||||
_ = g.addEdge("c", "d")
|
|
||||||
_ = g.addEdge("d", "e")
|
|
||||||
|
|
||||||
// Adding e -> a should create a cycle
|
|
||||||
err := g.addEdge("e", "a")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for long cycle: a -> b -> c -> d -> e -> a")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adding e -> c should also create a cycle
|
|
||||||
err = g.addEdge("e", "c")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for cycle: c -> d -> e -> c")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphNoCycleDAG(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
g.addNodes([]string{"a", "b", "c", "d"})
|
|
||||||
|
|
||||||
// Create a diamond DAG: a -> b, a -> c, b -> d, c -> d
|
|
||||||
_ = g.addEdge("a", "b")
|
|
||||||
_ = g.addEdge("a", "c")
|
|
||||||
_ = g.addEdge("b", "d")
|
|
||||||
|
|
||||||
err := g.addEdge("c", "d")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no cycle in DAG, got error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphSelfLoop(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
g.addNode("a")
|
|
||||||
|
|
||||||
// BUG: Self-loops are not detected by willCycle(). The function checks if
|
|
||||||
// adding edge from→to would create a cycle by traversing edges from "to"
|
|
||||||
// to see if "from" is reachable. But for a self-loop (from==to), the edge
|
|
||||||
// doesn't exist yet, so the DFS finds nothing and returns false.
|
|
||||||
// A self-importing file would NOT be caught by this cycle detection.
|
|
||||||
err := g.addEdge("a", "a")
|
|
||||||
if err != nil {
|
|
||||||
t.Log("Self-loop was correctly detected (bug may have been fixed)")
|
|
||||||
} else {
|
|
||||||
t.Log("BUG CONFIRMED: addEdge('a', 'a') did not detect self-loop cycle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphExistsNonExistent(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
if g.exists("nonexistent") {
|
|
||||||
t.Error("expected false for nonexistent node on empty graph")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphAreConnectedEmpty(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
if g.areConnected("a", "b") {
|
|
||||||
t.Error("expected false for areConnected on empty graph")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphAddEdges(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
g.addNodes([]string{"a", "b", "c", "d"})
|
|
||||||
|
|
||||||
err := g.addEdges("a", []string{"b", "c", "d"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("addEdges() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !g.areConnected("a", "b") || !g.areConnected("a", "c") || !g.areConnected("a", "d") {
|
|
||||||
t.Error("expected all edges from 'a' to exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphAddEdgesWithCycle(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
g.addNodes([]string{"a", "b", "c"})
|
|
||||||
|
|
||||||
_ = g.addEdge("b", "c")
|
|
||||||
_ = g.addEdge("c", "a")
|
|
||||||
|
|
||||||
// This should fail because a -> b -> c -> a creates a cycle
|
|
||||||
err := g.addEdges("a", []string{"b"})
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error when addEdges creates a cycle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphRemoveNodeEdgeLeakBug(t *testing.T) {
|
|
||||||
// This test documents a known bug: removeNode doesn't clean up edges.
|
|
||||||
// Edges FROM the removed node remain in the adjacency list.
|
|
||||||
g := &importGraph{}
|
|
||||||
g.addNodes([]string{"a", "b", "c"})
|
|
||||||
_ = g.addEdge("a", "b")
|
|
||||||
_ = g.addEdge("b", "c")
|
|
||||||
|
|
||||||
g.removeNode("b")
|
|
||||||
|
|
||||||
// Bug: "b" is removed from nodes, but edges from "b" are still in the adjacency list.
|
|
||||||
// This means the graph is now inconsistent.
|
|
||||||
// The node doesn't exist...
|
|
||||||
if g.exists("b") {
|
|
||||||
t.Error("node 'b' should not exist after removeNode")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...but edges from "b" may still be present in the edges map (this is a bug).
|
|
||||||
// We test this to document the behavior.
|
|
||||||
if g.edges != nil {
|
|
||||||
if targets, ok := g.edges["b"]; ok && len(targets) > 0 {
|
|
||||||
t.Log("BUG CONFIRMED: removeNode does not clean up outgoing edges. " +
|
|
||||||
"Edges from removed node 'b' still exist in adjacency list.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportGraphWillCycleEmptyGraph(t *testing.T) {
|
|
||||||
g := &importGraph{}
|
|
||||||
// willCycle on empty graph should return false
|
|
||||||
if g.willCycle("a", "b") {
|
|
||||||
t.Error("expected no cycle on empty graph")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -550,11 +550,7 @@ func (p *parser) doImport(nesting int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if foundBlockDirective {
|
if foundBlockDirective {
|
||||||
if maybeSnippet {
|
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||||
tokensCopy = append(tokensCopy, token)
|
|
||||||
} else {
|
|
||||||
tokensCopy = append(tokensCopy, tokensToAdd...)
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,28 +682,11 @@ func (p *parser) directive() error {
|
|||||||
// a opening curly brace. It does NOT advance the token.
|
// a opening curly brace. It does NOT advance the token.
|
||||||
func (p *parser) openCurlyBrace() error {
|
func (p *parser) openCurlyBrace() error {
|
||||||
if p.Val() != "{" {
|
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 p.SyntaxErr("{")
|
||||||
}
|
}
|
||||||
return nil
|
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
|
// closeCurlyBrace expects the current token to be
|
||||||
// a closing curly brace. This acts like an assertion
|
// a closing curly brace. This acts like an assertion
|
||||||
// because it returns an error if the token is not
|
// because it returns an error if the token is not
|
||||||
|
|||||||
@@ -930,107 +930,6 @@ func TestAcceptSiteImportWithBraces(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGlobalOptionsAfterImportedSnippetsGivesHelpfulError(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
importFile1 := filepath.Join(tempDir, "matcher_snippet_1.caddy")
|
|
||||||
importFile2 := filepath.Join(tempDir, "matcher_snippet_2.caddy")
|
|
||||||
|
|
||||||
err := os.WriteFile(importFile1, []byte(`(matcher1)`), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("writing first import file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(importFile2, []byte(`(matcher2)`), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("writing second import file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = Parse("Testfile", []byte(`import `+importFile1+`
|
|
||||||
import `+importFile2+`
|
|
||||||
{
|
|
||||||
debug
|
|
||||||
}`))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected an error, but got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := "global options block must appear before import directives; move the global options block to the top of the Caddyfile"
|
|
||||||
if !strings.HasPrefix(err.Error(), expected) {
|
|
||||||
t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportedSnippetDefinitionRetainsBlockPlaceholder(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
importFile := filepath.Join(tempDir, "snippets.caddy")
|
|
||||||
|
|
||||||
err := os.WriteFile(importFile, []byte(`
|
|
||||||
(site) {
|
|
||||||
http://{args[0]} {
|
|
||||||
respond "before"
|
|
||||||
{block}
|
|
||||||
respond "after"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("writing imported snippet file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expectedDirectives []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "with nested block",
|
|
||||||
input: `
|
|
||||||
import ` + importFile + `
|
|
||||||
|
|
||||||
import site example.com {
|
|
||||||
redir https://example.net
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
expectedDirectives: []string{"respond", "redir", "respond"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "without nested block",
|
|
||||||
input: `
|
|
||||||
import ` + importFile + `
|
|
||||||
|
|
||||||
import site example.com
|
|
||||||
`,
|
|
||||||
expectedDirectives: []string{"respond", "respond"},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
p := testParser(tc.input)
|
|
||||||
blocks, err := p.parseAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parseAll: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(blocks) != 1 {
|
|
||||||
t.Fatalf("expected exactly one server block, got %d", len(blocks))
|
|
||||||
}
|
|
||||||
|
|
||||||
if actual := blocks[0].GetKeysText(); len(actual) != 1 || actual[0] != "http://example.com" {
|
|
||||||
t.Fatalf("expected server block key http://example.com, got %v", actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(blocks[0].Segments) != len(tc.expectedDirectives) {
|
|
||||||
t.Fatalf("expected %d segments, got %d", len(tc.expectedDirectives), len(blocks[0].Segments))
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, directive := range tc.expectedDirectives {
|
|
||||||
if actual := blocks[0].Segments[i].Directive(); actual != directive {
|
|
||||||
t.Fatalf("segment %d: expected directive %q, got %q", i, directive, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testParser(input string) parser {
|
func testParser(input string) parser {
|
||||||
return parser{Dispenser: NewTestDispenser(input)}
|
return parser{Dispenser: NewTestDispenser(input)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
package caddyconfig
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestJSON(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
val any
|
|
||||||
wantNil bool
|
|
||||||
wantWarnings int
|
|
||||||
nilWarnings bool // pass nil warnings pointer
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple string",
|
|
||||||
val: "hello",
|
|
||||||
wantNil: false,
|
|
||||||
wantWarnings: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "struct",
|
|
||||||
val: struct{ Name string }{"test"},
|
|
||||||
wantNil: false,
|
|
||||||
wantWarnings: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil value",
|
|
||||||
val: nil,
|
|
||||||
wantNil: false, // json.Marshal(nil) returns "null"
|
|
||||||
wantWarnings: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "map",
|
|
||||||
val: map[string]string{"key": "val"},
|
|
||||||
wantNil: false,
|
|
||||||
wantWarnings: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unmarshalable value produces warning",
|
|
||||||
val: make(chan int),
|
|
||||||
wantNil: true,
|
|
||||||
wantWarnings: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unmarshalable value with nil warnings pointer",
|
|
||||||
val: make(chan int),
|
|
||||||
wantNil: true,
|
|
||||||
nilWarnings: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
var warnings *[]Warning
|
|
||||||
if !tt.nilWarnings {
|
|
||||||
w := []Warning{}
|
|
||||||
warnings = &w
|
|
||||||
}
|
|
||||||
|
|
||||||
result := JSON(tt.val, warnings)
|
|
||||||
|
|
||||||
if tt.wantNil && result != nil {
|
|
||||||
t.Errorf("JSON() = %v, want nil", string(result))
|
|
||||||
}
|
|
||||||
if !tt.wantNil && result == nil {
|
|
||||||
t.Error("JSON() = nil, want non-nil")
|
|
||||||
}
|
|
||||||
if warnings != nil && len(*warnings) != tt.wantWarnings {
|
|
||||||
t.Errorf("JSON() produced %d warnings, want %d", len(*warnings), tt.wantWarnings)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJSONModuleObject(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
val any
|
|
||||||
fieldName string
|
|
||||||
fieldVal string
|
|
||||||
wantNil bool
|
|
||||||
wantField bool
|
|
||||||
wantWarnings int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple struct",
|
|
||||||
val: struct{ Name string }{"test"},
|
|
||||||
fieldName: "handler",
|
|
||||||
fieldVal: "file_server",
|
|
||||||
wantNil: false,
|
|
||||||
wantField: true,
|
|
||||||
wantWarnings: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "map value",
|
|
||||||
val: map[string]any{"key": "val"},
|
|
||||||
fieldName: "module",
|
|
||||||
fieldVal: "my_module",
|
|
||||||
wantNil: false,
|
|
||||||
wantField: true,
|
|
||||||
wantWarnings: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-object type (string) produces warning",
|
|
||||||
val: "not-an-object",
|
|
||||||
fieldName: "handler",
|
|
||||||
fieldVal: "test",
|
|
||||||
wantNil: true,
|
|
||||||
wantField: false,
|
|
||||||
wantWarnings: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unmarshalable value produces warning",
|
|
||||||
val: make(chan int),
|
|
||||||
fieldName: "handler",
|
|
||||||
fieldVal: "test",
|
|
||||||
wantNil: true,
|
|
||||||
wantField: false,
|
|
||||||
wantWarnings: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
warnings := []Warning{}
|
|
||||||
result := JSONModuleObject(tt.val, tt.fieldName, tt.fieldVal, &warnings)
|
|
||||||
|
|
||||||
if tt.wantNil && result != nil {
|
|
||||||
t.Errorf("JSONModuleObject() = %v, want nil", string(result))
|
|
||||||
}
|
|
||||||
if !tt.wantNil && result == nil {
|
|
||||||
t.Error("JSONModuleObject() = nil, want non-nil")
|
|
||||||
}
|
|
||||||
if len(warnings) != tt.wantWarnings {
|
|
||||||
t.Errorf("JSONModuleObject() produced %d warnings, want %d", len(warnings), tt.wantWarnings)
|
|
||||||
}
|
|
||||||
if tt.wantField && result != nil {
|
|
||||||
var m map[string]any
|
|
||||||
if err := json.Unmarshal(result, &m); err != nil {
|
|
||||||
t.Fatalf("failed to unmarshal result: %v", err)
|
|
||||||
}
|
|
||||||
if v, ok := m[tt.fieldName]; !ok {
|
|
||||||
t.Errorf("expected field %q in result", tt.fieldName)
|
|
||||||
} else if v != tt.fieldVal {
|
|
||||||
t.Errorf("field %q = %v, want %v", tt.fieldName, v, tt.fieldVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJSONModuleObjectPreservesExistingFields(t *testing.T) {
|
|
||||||
val := struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Port int `json:"port"`
|
|
||||||
}{"example", 8080}
|
|
||||||
|
|
||||||
warnings := []Warning{}
|
|
||||||
result := JSONModuleObject(val, "handler", "static", &warnings)
|
|
||||||
|
|
||||||
if result == nil {
|
|
||||||
t.Fatal("expected non-nil result")
|
|
||||||
}
|
|
||||||
|
|
||||||
var m map[string]any
|
|
||||||
if err := json.Unmarshal(result, &m); err != nil {
|
|
||||||
t.Fatalf("failed to unmarshal: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m["name"] != "example" {
|
|
||||||
t.Errorf("name = %v, want 'example'", m["name"])
|
|
||||||
}
|
|
||||||
if m["port"] != float64(8080) {
|
|
||||||
t.Errorf("port = %v, want 8080", m["port"])
|
|
||||||
}
|
|
||||||
if m["handler"] != "static" {
|
|
||||||
t.Errorf("handler = %v, want 'static'", m["handler"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetAdapterNil(t *testing.T) {
|
|
||||||
adapter := GetAdapter("nonexistent_adapter_xyz")
|
|
||||||
if adapter != nil {
|
|
||||||
t.Error("expected nil for unregistered adapter")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWarningString(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
warning Warning
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "all fields",
|
|
||||||
warning: Warning{File: "Caddyfile", Line: 10, Directive: "reverse_proxy", Message: "upstream not found"},
|
|
||||||
want: "Caddyfile:10 (reverse_proxy): upstream not found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no directive",
|
|
||||||
warning: Warning{File: "Caddyfile", Line: 5, Message: "something off"},
|
|
||||||
want: "Caddyfile:5: something off",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "zero line",
|
|
||||||
warning: Warning{File: "config.json", Line: 0, Message: "invalid"},
|
|
||||||
want: "config.json:0: invalid",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := tt.warning.String()
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("Warning.String() = %q, want %q", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -550,11 +550,26 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case acmeIssuer != nil:
|
case acmeIssuer != nil:
|
||||||
// implicit ACME issuers (from various subdirectives) should inherit from
|
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
|
||||||
// any globally-configured ACME issuer templates, then apply the local
|
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
|
||||||
// shortcut settings as overrides.
|
|
||||||
defaultIssuers := implicitACMEIssuers(h, acmeIssuer)
|
// if an ACME CA endpoint was set, the user expects to use that specific one,
|
||||||
|
// not any others that may be defaults, so replace all defaults with that ACME CA
|
||||||
|
if acmeIssuer.CA != "" {
|
||||||
|
defaultIssuers = []certmagic.Issuer{acmeIssuer}
|
||||||
|
}
|
||||||
|
|
||||||
for _, issuer := range defaultIssuers {
|
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{
|
configVals = append(configVals, ConfigValue{
|
||||||
Class: "tls.cert_issuer",
|
Class: "tls.cert_issuer",
|
||||||
Value: issuer,
|
Value: issuer,
|
||||||
@@ -653,8 +668,6 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
|
|||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
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
|
return h.NewRoute(nil, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,10 +682,6 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
|
|||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
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
|
// make the route with the matcher
|
||||||
return h.NewRoute(userMatcherSet, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
return h.NewRoute(userMatcherSet, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
||||||
}
|
}
|
||||||
@@ -1053,7 +1062,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
|||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
interval, err := caddy.ParseDuration(d.Val())
|
interval, err := time.ParseDuration(d.Val() + "ns")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, d.Errf("failed to parse interval: %v", err)
|
return nil, d.Errf("failed to parse interval: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,14 +66,14 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
input: `:8080 {
|
input: `:8080 {
|
||||||
log {
|
log {
|
||||||
sampling {
|
sampling {
|
||||||
interval 2s
|
interval 2
|
||||||
first 3
|
first 3
|
||||||
thereafter 4
|
thereafter 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2000000000,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -202,10 +202,7 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
|
|||||||
type Helper struct {
|
type Helper struct {
|
||||||
*caddyfile.Dispenser
|
*caddyfile.Dispenser
|
||||||
// State stores intermediate variables during caddyfile adaptation.
|
// 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
|
options map[string]any
|
||||||
warnings *[]caddyconfig.Warning
|
warnings *[]caddyconfig.Warning
|
||||||
matcherDefs map[string]caddy.ModuleMap
|
matcherDefs map[string]caddy.ModuleMap
|
||||||
@@ -388,11 +385,6 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clone BlockState once for the entire block so sibling directives
|
|
||||||
// can share state, but changes don't leak to the parent scope
|
|
||||||
subBlockState := make(map[string]any, len(h.BlockState))
|
|
||||||
maps.Copy(subBlockState, h.BlockState)
|
|
||||||
|
|
||||||
// with matchers ready to go, evaluate each directive's segment
|
// with matchers ready to go, evaluate each directive's segment
|
||||||
for _, seg := range segments {
|
for _, seg := range segments {
|
||||||
dir := seg.Directive()
|
dir := seg.Directive()
|
||||||
@@ -404,7 +396,6 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
|||||||
subHelper := h
|
subHelper := h
|
||||||
subHelper.Dispenser = caddyfile.NewDispenser(seg)
|
subHelper.Dispenser = caddyfile.NewDispenser(seg)
|
||||||
subHelper.matcherDefs = matcherDefs
|
subHelper.matcherDefs = matcherDefs
|
||||||
subHelper.BlockState = subBlockState
|
|
||||||
|
|
||||||
results, err := dirFunc(subHelper)
|
results, err := dirFunc(subHelper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ func (st ServerType) Setup(
|
|||||||
parentBlock: sb.block,
|
parentBlock: sb.block,
|
||||||
groupCounter: gc,
|
groupCounter: gc,
|
||||||
State: state,
|
State: state,
|
||||||
BlockState: state,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := dirFunc(h)
|
results, err := dirFunc(h)
|
||||||
@@ -505,7 +504,6 @@ func (ServerType) extractNamedRoutes(
|
|||||||
parentBlock: sb.block,
|
parentBlock: sb.block,
|
||||||
groupCounter: gc,
|
groupCounter: gc,
|
||||||
State: state,
|
State: state,
|
||||||
BlockState: state,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := ParseSegmentAsSubroute(h)
|
handler, err := ParseSegmentAsSubroute(h)
|
||||||
|
|||||||
@@ -484,8 +484,6 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
metrics.PerHost = true
|
metrics.PerHost = true
|
||||||
case "observe_catchall_hosts":
|
case "observe_catchall_hosts":
|
||||||
metrics.ObserveCatchallHosts = true
|
metrics.ObserveCatchallHosts = true
|
||||||
case "otlp":
|
|
||||||
metrics.OTLP = true
|
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package httpcaddyfile
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
||||||
@@ -168,126 +166,3 @@ func TestGlobalResolversOption(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGlobalCertIssuerAppliesToImplicitACMEIssuer(t *testing.T) {
|
|
||||||
adapter := caddyfile.Adapter{
|
|
||||||
ServerType: ServerType{},
|
|
||||||
}
|
|
||||||
|
|
||||||
input := `{
|
|
||||||
cert_issuer acme {
|
|
||||||
disable_tlsalpn_challenge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
report.company.intern {
|
|
||||||
tls {
|
|
||||||
ca https://deglacme01.company.intern/acme/acme/directory
|
|
||||||
ca_root /etc/certs/company_root2.crt
|
|
||||||
}
|
|
||||||
respond "ok"
|
|
||||||
}`
|
|
||||||
|
|
||||||
out, _, err := adapter.Adapt([]byte(input), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("adapting caddyfile: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var config struct {
|
|
||||||
Apps struct {
|
|
||||||
TLS *caddytls.TLS `json:"tls"`
|
|
||||||
} `json:"apps"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(out, &config); err != nil {
|
|
||||||
t.Fatalf("unmarshaling adapted config: %v", err)
|
|
||||||
}
|
|
||||||
if config.Apps.TLS == nil || config.Apps.TLS.Automation == nil {
|
|
||||||
t.Fatal("expected tls automation config")
|
|
||||||
}
|
|
||||||
|
|
||||||
var subjectPolicy *caddytls.AutomationPolicy
|
|
||||||
for _, ap := range config.Apps.TLS.Automation.Policies {
|
|
||||||
if len(ap.SubjectsRaw) == 1 && ap.SubjectsRaw[0] == "report.company.intern" {
|
|
||||||
subjectPolicy = ap
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if subjectPolicy == nil {
|
|
||||||
t.Fatal("expected subject-specific automation policy")
|
|
||||||
}
|
|
||||||
if len(subjectPolicy.IssuersRaw) != 1 {
|
|
||||||
t.Fatalf("expected one issuer for subject-specific policy, got %d", len(subjectPolicy.IssuersRaw))
|
|
||||||
}
|
|
||||||
|
|
||||||
var issuer caddytls.ACMEIssuer
|
|
||||||
if err := json.Unmarshal(subjectPolicy.IssuersRaw[0], &issuer); err != nil {
|
|
||||||
t.Fatalf("unmarshaling issuer: %v", err)
|
|
||||||
}
|
|
||||||
if issuer.CA != "https://deglacme01.company.intern/acme/acme/directory" {
|
|
||||||
t.Fatalf("expected custom ACME CA, got %q", issuer.CA)
|
|
||||||
}
|
|
||||||
if len(issuer.TrustedRootsPEMFiles) != 1 || issuer.TrustedRootsPEMFiles[0] != "/etc/certs/company_root2.crt" {
|
|
||||||
t.Fatalf("expected trusted roots to include site CA root, got %v", issuer.TrustedRootsPEMFiles)
|
|
||||||
}
|
|
||||||
if issuer.Challenges == nil || issuer.Challenges.TLSALPN == nil || !issuer.Challenges.TLSALPN.Disabled {
|
|
||||||
t.Fatalf("expected tls-alpn challenge to be disabled, got %#v", issuer.Challenges)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMergeACMEIssuers(t *testing.T) {
|
|
||||||
base := &caddytls.ACMEIssuer{
|
|
||||||
Email: "ops@example.com",
|
|
||||||
Challenges: &caddytls.ChallengesConfig{
|
|
||||||
HTTP: &caddytls.HTTPChallengeConfig{
|
|
||||||
AlternatePort: 8080,
|
|
||||||
},
|
|
||||||
TLSALPN: &caddytls.TLSALPNChallengeConfig{
|
|
||||||
Disabled: true,
|
|
||||||
AlternatePort: 8443,
|
|
||||||
},
|
|
||||||
DNS: &caddytls.DNSChallengeConfig{
|
|
||||||
Resolvers: []string{"1.1.1.1"},
|
|
||||||
OverrideDomain: "_acme-challenge.example.net",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TrustedRootsPEMFiles: []string{"global.pem"},
|
|
||||||
}
|
|
||||||
overrides := &caddytls.ACMEIssuer{
|
|
||||||
CA: "https://deglacme01.company.intern/acme/acme/directory",
|
|
||||||
Challenges: &caddytls.ChallengesConfig{
|
|
||||||
HTTP: &caddytls.HTTPChallengeConfig{
|
|
||||||
Disabled: true,
|
|
||||||
},
|
|
||||||
DNS: &caddytls.DNSChallengeConfig{
|
|
||||||
PropagationTimeout: caddy.Duration(time.Minute),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TrustedRootsPEMFiles: []string{"site.pem"},
|
|
||||||
}
|
|
||||||
|
|
||||||
merged := mergeACMEIssuers(base, overrides)
|
|
||||||
if merged.CA != overrides.CA {
|
|
||||||
t.Fatalf("expected merged CA %q, got %q", overrides.CA, merged.CA)
|
|
||||||
}
|
|
||||||
if merged.Email != base.Email {
|
|
||||||
t.Fatalf("expected merged email %q, got %q", base.Email, merged.Email)
|
|
||||||
}
|
|
||||||
if len(merged.TrustedRootsPEMFiles) != 2 || merged.TrustedRootsPEMFiles[0] != "global.pem" || merged.TrustedRootsPEMFiles[1] != "site.pem" {
|
|
||||||
t.Fatalf("expected merged roots [global.pem site.pem], got %v", merged.TrustedRootsPEMFiles)
|
|
||||||
}
|
|
||||||
if merged.Challenges == nil || merged.Challenges.HTTP == nil || !merged.Challenges.HTTP.Disabled || merged.Challenges.HTTP.AlternatePort != 8080 {
|
|
||||||
t.Fatalf("expected merged HTTP challenge config to preserve alternate port and apply disable flag, got %#v", merged.Challenges)
|
|
||||||
}
|
|
||||||
if merged.Challenges.TLSALPN == nil || !merged.Challenges.TLSALPN.Disabled || merged.Challenges.TLSALPN.AlternatePort != 8443 {
|
|
||||||
t.Fatalf("expected merged TLS-ALPN challenge config to preserve global settings, got %#v", merged.Challenges)
|
|
||||||
}
|
|
||||||
if merged.Challenges.DNS == nil || merged.Challenges.DNS.PropagationTimeout != caddy.Duration(time.Minute) || len(merged.Challenges.DNS.Resolvers) != 1 || merged.Challenges.DNS.Resolvers[0] != "1.1.1.1" || merged.Challenges.DNS.OverrideDomain != "_acme-challenge.example.net" {
|
|
||||||
t.Fatalf("expected merged DNS challenge config to preserve global values and apply overrides, got %#v", merged.Challenges)
|
|
||||||
}
|
|
||||||
|
|
||||||
if base.CA != "" {
|
|
||||||
t.Fatalf("expected base issuer to remain unchanged, got CA %q", base.CA)
|
|
||||||
}
|
|
||||||
if len(base.TrustedRootsPEMFiles) != 1 || base.TrustedRootsPEMFiles[0] != "global.pem" {
|
|
||||||
t.Fatalf("expected base roots to remain unchanged, got %v", base.TrustedRootsPEMFiles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,299 +0,0 @@
|
|||||||
package httpcaddyfile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestShorthandReplacerSimpleReplacements(t *testing.T) {
|
|
||||||
sr := NewShorthandReplacer()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "host",
|
|
||||||
input: "{host}",
|
|
||||||
want: "{http.request.host}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "hostport",
|
|
||||||
input: "{hostport}",
|
|
||||||
want: "{http.request.hostport}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "port",
|
|
||||||
input: "{port}",
|
|
||||||
want: "{http.request.port}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "method",
|
|
||||||
input: "{method}",
|
|
||||||
want: "{http.request.method}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "uri",
|
|
||||||
input: "{uri}",
|
|
||||||
want: "{http.request.uri}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "path",
|
|
||||||
input: "{path}",
|
|
||||||
want: "{http.request.uri.path}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "query",
|
|
||||||
input: "{query}",
|
|
||||||
want: "{http.request.uri.query}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scheme",
|
|
||||||
input: "{scheme}",
|
|
||||||
want: "{http.request.scheme}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remote_host",
|
|
||||||
input: "{remote_host}",
|
|
||||||
want: "{http.request.remote.host}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remote_port",
|
|
||||||
input: "{remote_port}",
|
|
||||||
want: "{http.request.remote.port}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "uuid",
|
|
||||||
input: "{uuid}",
|
|
||||||
want: "{http.request.uuid}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tls_cipher",
|
|
||||||
input: "{tls_cipher}",
|
|
||||||
want: "{http.request.tls.cipher_suite}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tls_version",
|
|
||||||
input: "{tls_version}",
|
|
||||||
want: "{http.request.tls.version}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "client_ip",
|
|
||||||
input: "{client_ip}",
|
|
||||||
want: "{http.vars.client_ip}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "upstream_hostport",
|
|
||||||
input: "{upstream_hostport}",
|
|
||||||
want: "{http.reverse_proxy.upstream.hostport}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dir",
|
|
||||||
input: "{dir}",
|
|
||||||
want: "{http.request.uri.path.dir}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "file",
|
|
||||||
input: "{file}",
|
|
||||||
want: "{http.request.uri.path.file}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "orig_method",
|
|
||||||
input: "{orig_method}",
|
|
||||||
want: "{http.request.orig_method}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "orig_uri",
|
|
||||||
input: "{orig_uri}",
|
|
||||||
want: "{http.request.orig_uri}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "orig_path",
|
|
||||||
input: "{orig_path}",
|
|
||||||
want: "{http.request.orig_uri.path}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no matching placeholder",
|
|
||||||
input: "{unknown}",
|
|
||||||
want: "{unknown}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "not a placeholder",
|
|
||||||
input: "plain text",
|
|
||||||
want: "plain text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty string",
|
|
||||||
input: "",
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple placeholders in one string",
|
|
||||||
input: "{host}:{port}",
|
|
||||||
want: "{http.request.host}:{http.request.port}",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
segment := caddyfile.Segment{{Text: tt.input}}
|
|
||||||
sr.ApplyToSegment(&segment)
|
|
||||||
got := segment[0].Text
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("ApplyToSegment(%q) = %q, want %q", tt.input, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShorthandReplacerComplexReplacements(t *testing.T) {
|
|
||||||
sr := NewShorthandReplacer()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "header placeholder",
|
|
||||||
input: "{header.X-Forwarded-For}",
|
|
||||||
want: "{http.request.header.X-Forwarded-For}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cookie placeholder",
|
|
||||||
input: "{cookie.session_id}",
|
|
||||||
want: "{http.request.cookie.session_id}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "labels placeholder",
|
|
||||||
input: "{labels.0}",
|
|
||||||
want: "{http.request.host.labels.0}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "path segment placeholder",
|
|
||||||
input: "{path.0}",
|
|
||||||
want: "{http.request.uri.path.0}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "query placeholder",
|
|
||||||
input: "{query.page}",
|
|
||||||
want: "{http.request.uri.query.page}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "re placeholder with dots",
|
|
||||||
input: "{re.name.group}",
|
|
||||||
want: "{http.regexp.name.group}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "vars placeholder",
|
|
||||||
input: "{vars.my_var}",
|
|
||||||
want: "{http.vars.my_var}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rp placeholder",
|
|
||||||
input: "{rp.upstream.address}",
|
|
||||||
want: "{http.reverse_proxy.upstream.address}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "resp placeholder",
|
|
||||||
input: "{resp.status_code}",
|
|
||||||
want: "{http.intercept.status_code}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "err placeholder",
|
|
||||||
input: "{err.status_code}",
|
|
||||||
want: "{http.error.status_code}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "file_match placeholder",
|
|
||||||
input: "{file_match.relative}",
|
|
||||||
want: "{http.matchers.file.relative}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "header with hyphen",
|
|
||||||
input: "{header.Content-Type}",
|
|
||||||
want: "{http.request.header.Content-Type}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "header with underscore",
|
|
||||||
input: "{header.X_Custom_Header}",
|
|
||||||
want: "{http.request.header.X_Custom_Header}",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
segment := caddyfile.Segment{{Text: tt.input}}
|
|
||||||
sr.ApplyToSegment(&segment)
|
|
||||||
got := segment[0].Text
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("ApplyToSegment(%q) = %q, want %q", tt.input, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShorthandReplacerApplyToNilSegment(t *testing.T) {
|
|
||||||
sr := NewShorthandReplacer()
|
|
||||||
// Should not panic
|
|
||||||
sr.ApplyToSegment(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShorthandReplacerMultipleTokens(t *testing.T) {
|
|
||||||
sr := NewShorthandReplacer()
|
|
||||||
|
|
||||||
segment := caddyfile.Segment{
|
|
||||||
{Text: "{host}"},
|
|
||||||
{Text: "{path}"},
|
|
||||||
{Text: "{header.X-Test}"},
|
|
||||||
{Text: "plain"},
|
|
||||||
}
|
|
||||||
|
|
||||||
sr.ApplyToSegment(&segment)
|
|
||||||
|
|
||||||
expected := []string{
|
|
||||||
"{http.request.host}",
|
|
||||||
"{http.request.uri.path}",
|
|
||||||
"{http.request.header.X-Test}",
|
|
||||||
"plain",
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, want := range expected {
|
|
||||||
if segment[i].Text != want {
|
|
||||||
t.Errorf("token %d: got %q, want %q", i, segment[i].Text, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShorthandReplacerEmptySegment(t *testing.T) {
|
|
||||||
sr := NewShorthandReplacer()
|
|
||||||
segment := caddyfile.Segment{}
|
|
||||||
sr.ApplyToSegment(&segment) // should not panic
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShorthandReplacerEscapedPlaceholders(t *testing.T) {
|
|
||||||
sr := NewShorthandReplacer()
|
|
||||||
|
|
||||||
// Percent-escaped path placeholder
|
|
||||||
segment := caddyfile.Segment{{Text: "{%path}"}}
|
|
||||||
sr.ApplyToSegment(&segment)
|
|
||||||
if segment[0].Text != "{http.request.uri.path_escaped}" {
|
|
||||||
t.Errorf("got %q, want {http.request.uri.path_escaped}", segment[0].Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Percent-escaped query placeholder
|
|
||||||
segment = caddyfile.Segment{{Text: "{%query}"}}
|
|
||||||
sr.ApplyToSegment(&segment)
|
|
||||||
if segment[0].Text != "{http.request.uri.query_escaped}" {
|
|
||||||
t.Errorf("got %q, want {http.request.uri.query_escaped}", segment[0].Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefixed query
|
|
||||||
segment = caddyfile.Segment{{Text: "{?query}"}}
|
|
||||||
sr.ApplyToSegment(&segment)
|
|
||||||
if segment[0].Text != "{http.request.uri.prefixed_query}" {
|
|
||||||
t.Errorf("got %q, want {http.request.uri.prefixed_query}", segment[0].Text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -612,289 +612,6 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
|||||||
return nil
|
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
|
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
||||||
// its values from the global options map. It should be used as the base
|
// 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
|
// for any other automation policies. A nil policy (and no error) will be
|
||||||
@@ -981,31 +698,14 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
|||||||
emptyAPCount := 0
|
emptyAPCount := 0
|
||||||
origLenAPs := len(aps)
|
origLenAPs := len(aps)
|
||||||
// compute the number of empty policies (disregarding subjects) - see #4128
|
// compute the number of empty policies (disregarding subjects) - see #4128
|
||||||
// while we're at it,
|
|
||||||
emptyAP := new(caddytls.AutomationPolicy)
|
emptyAP := new(caddytls.AutomationPolicy)
|
||||||
for i := 0; i < len(aps); i++ {
|
for i := 0; i < len(aps); i++ {
|
||||||
emptyAP.SubjectsRaw = aps[i].SubjectsRaw
|
emptyAP.SubjectsRaw = aps[i].SubjectsRaw
|
||||||
emptyAP.ManagersRaw = nil
|
|
||||||
if reflect.DeepEqual(aps[i], emptyAP) {
|
if reflect.DeepEqual(aps[i], emptyAP) {
|
||||||
// AP is empty
|
|
||||||
emptyAPCount++
|
emptyAPCount++
|
||||||
|
if !automationPolicyHasAllPublicNames(aps[i]) {
|
||||||
// see if this AP shadows something later
|
// if this automation policy has internal names, we might as well remove it
|
||||||
shadowIdx := automationPolicyShadows(i, aps)
|
// so auto-https can implicitly use the internal issuer
|
||||||
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)
|
aps = slices.Delete(aps, i, i+1)
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,28 +55,6 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
|
|||||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
|
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) {
|
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ encode gzip zstd {
|
|||||||
|
|
||||||
# Long way with a block for each encoding
|
# Long way with a block for each encoding
|
||||||
encode {
|
encode {
|
||||||
zstd {
|
zstd
|
||||||
disable_checksum
|
|
||||||
}
|
|
||||||
gzip 5
|
gzip 5
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +71,7 @@ encode
|
|||||||
"gzip": {
|
"gzip": {
|
||||||
"level": 5
|
"level": 5
|
||||||
},
|
},
|
||||||
"zstd": {
|
"zstd": {}
|
||||||
"checksum": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"handler": "encode",
|
"handler": "encode",
|
||||||
"prefer": [
|
"prefer": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
log {
|
log {
|
||||||
sampling {
|
sampling {
|
||||||
interval 5m
|
interval 300
|
||||||
first 50
|
first 50
|
||||||
thereafter 40
|
thereafter 40
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"logs": {
|
"logs": {
|
||||||
"default": {
|
"default": {
|
||||||
"sampling": {
|
"sampling": {
|
||||||
"interval": 300000000000,
|
"interval": 300,
|
||||||
"first": 50,
|
"first": 50,
|
||||||
"thereafter": 40
|
"thereafter": 40
|
||||||
}
|
}
|
||||||
|
|||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
admin off
|
|
||||||
auto_https off
|
|
||||||
}
|
|
||||||
|
|
||||||
import testdata/issue_7557_invalid_subdirective_snippet.conf
|
|
||||||
|
|
||||||
:8080 {
|
|
||||||
import test {
|
|
||||||
this_is_nonsense
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
----------
|
|
||||||
parsing caddyfile tokens for 'reverse_proxy': unrecognized subdirective this_is_nonsense
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
log {
|
|
||||||
format journald {
|
|
||||||
wrap console
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:80 {
|
|
||||||
respond "Hello, World!"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"logging": {
|
|
||||||
"logs": {
|
|
||||||
"default": {
|
|
||||||
"encoder": {
|
|
||||||
"format": "journald",
|
|
||||||
"wrap": {
|
|
||||||
"format": "console"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":80"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Hello, World!",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
:80 {
|
:80 {
|
||||||
log {
|
log {
|
||||||
sampling {
|
sampling {
|
||||||
interval 5m
|
interval 300
|
||||||
first 50
|
first 50
|
||||||
thereafter 40
|
thereafter 40
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"log0": {
|
"log0": {
|
||||||
"sampling": {
|
"sampling": {
|
||||||
"interval": 300000000000,
|
"interval": 300,
|
||||||
"first": 50,
|
"first": 50,
|
||||||
"thereafter": 40
|
"thereafter": 40
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
metrics {
|
|
||||||
otlp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:80 {
|
|
||||||
respond "Hello"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":80"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Hello",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"metrics": {
|
|
||||||
"otlp": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
:8884
|
|
||||||
|
|
||||||
reverse_proxy 127.0.0.1:65535 {
|
|
||||||
lb_retries 3
|
|
||||||
lb_retry_match expression `{rp.status_code} in [502, 503]`
|
|
||||||
lb_retry_match expression `{rp.is_transport_error} || {rp.status_code} == 502`
|
|
||||||
lb_retry_match expression `method('POST') && {rp.status_code} == 503`
|
|
||||||
lb_retry_match `{rp.status_code} == 504`
|
|
||||||
lb_retry_match `{rp.is_transport_error} && method('PUT')`
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":8884"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"load_balancing": {
|
|
||||||
"retries": 3,
|
|
||||||
"retry_match": [
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.status_code} in [502, 503]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} == 502"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} == 503"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.status_code} == 504"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('PUT')"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{
|
|
||||||
"dial": "127.0.0.1:65535"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-147
@@ -1,147 +0,0 @@
|
|||||||
:8884
|
|
||||||
|
|
||||||
reverse_proxy 127.0.0.1:65535 {
|
|
||||||
lb_retries 5
|
|
||||||
|
|
||||||
# request matchers (backward-compatible, non-expression)
|
|
||||||
lb_retry_match {
|
|
||||||
method POST PUT
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
path /foo*
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
header X-Idempotency-Key *
|
|
||||||
}
|
|
||||||
|
|
||||||
# response status code via expression
|
|
||||||
lb_retry_match {
|
|
||||||
expression `{rp.status_code} in [502, 503, 504]`
|
|
||||||
}
|
|
||||||
|
|
||||||
# response header via expression
|
|
||||||
lb_retry_match {
|
|
||||||
expression `{rp.header.X-Retry} == "true"`
|
|
||||||
}
|
|
||||||
|
|
||||||
# CEL request functions combined with response placeholders
|
|
||||||
lb_retry_match {
|
|
||||||
expression `method('POST') && {rp.status_code} >= 500`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `path('/api*') && {rp.status_code} in [502, 503]`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `host('example.com') && {rp.status_code} == 503`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `query({'retry': 'true'}) && {rp.status_code} >= 500`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `header({'X-Idempotency-Key': '*'}) && {rp.status_code} in [502, 503]`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `protocol('https') && {rp.status_code} == 502`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `path_regexp('^/api/v[0-9]+/') && {rp.status_code} >= 500`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `header_regexp('Content-Type', '^application/json') && {rp.status_code} == 502`
|
|
||||||
}
|
|
||||||
|
|
||||||
# transport error handling via placeholder
|
|
||||||
lb_retry_match {
|
|
||||||
expression `{rp.is_transport_error} || {rp.status_code} in [502, 503]`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `{rp.is_transport_error} && method('POST')`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":8884"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"load_balancing": {
|
|
||||||
"retries": 5,
|
|
||||||
"retry_match": [
|
|
||||||
{
|
|
||||||
"method": [
|
|
||||||
"POST",
|
|
||||||
"PUT"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": [
|
|
||||||
"/foo*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header": {
|
|
||||||
"X-Idempotency-Key": [
|
|
||||||
"*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.status_code} in [502, 503, 504]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.header.X-Retry} == \"true\""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "path('/api*') \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "host('example.com') \u0026\u0026 {http.reverse_proxy.status_code} == 503"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "query({'retry': 'true'}) \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "header({'X-Idempotency-Key': '*'}) \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "protocol('https') \u0026\u0026 {http.reverse_proxy.status_code} == 502"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "path_regexp('^/api/v[0-9]+/') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "header_regexp('Content-Type', '^application/json') \u0026\u0026 {http.reverse_proxy.status_code} == 502"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} in [502, 503]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('POST')"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{
|
|
||||||
"dial": "127.0.0.1:65535"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
https://example.com {
|
|
||||||
reverse_proxy https://localhost:54321 {
|
|
||||||
stream_buffer_size 8KB
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"stream_buffer_size": 8000,
|
|
||||||
"transport": {
|
|
||||||
"protocol": "http",
|
|
||||||
"tls": {}
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{
|
|
||||||
"dial": "localhost:54321"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -54,6 +54,11 @@ b.com {
|
|||||||
"via": "http"
|
"via": "http"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"b.com"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
# example from https://github.com/caddyserver/caddy/issues/7559
|
|
||||||
*.test.local {
|
|
||||||
tls {
|
|
||||||
get_certificate http http://cert-server:9000/certs
|
|
||||||
}
|
|
||||||
respond "wildcard"
|
|
||||||
}
|
|
||||||
|
|
||||||
# certificate for this subdomain is covered by wildcard above
|
|
||||||
subdomain.test.local {
|
|
||||||
respond "subdomain"
|
|
||||||
}
|
|
||||||
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"subdomain.test.local"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "subdomain",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"*.test.local"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "wildcard",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"subjects": [
|
|
||||||
"*.test.local"
|
|
||||||
],
|
|
||||||
"get_certificate": [
|
|
||||||
{
|
|
||||||
"url": "http://cert-server:9000/certs",
|
|
||||||
"via": "http"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-87
@@ -1,87 +0,0 @@
|
|||||||
localhost
|
|
||||||
|
|
||||||
respond "hello from localhost"
|
|
||||||
tls {
|
|
||||||
client_auth {
|
|
||||||
mode request
|
|
||||||
trust_pool combined {
|
|
||||||
source inline {
|
|
||||||
trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
|
|
||||||
}
|
|
||||||
source file {
|
|
||||||
pem_file ../caddy.ca.cer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "hello from localhost",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tls_connection_policies": [
|
|
||||||
{
|
|
||||||
"match": {
|
|
||||||
"sni": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"client_authentication": {
|
|
||||||
"ca": {
|
|
||||||
"provider": "combined",
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"provider": "inline",
|
|
||||||
"trusted_ca_certs": [
|
|
||||||
"MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pem_files": [
|
|
||||||
"../caddy.ca.cer"
|
|
||||||
],
|
|
||||||
"provider": "file"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"mode": "request"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-87
@@ -1,87 +0,0 @@
|
|||||||
localhost
|
|
||||||
|
|
||||||
respond "hello from localhost"
|
|
||||||
tls {
|
|
||||||
client_auth {
|
|
||||||
mode require_and_verify
|
|
||||||
trust_pool combined {
|
|
||||||
source inline {
|
|
||||||
trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
|
|
||||||
}
|
|
||||||
source pki_root {
|
|
||||||
authority local
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "hello from localhost",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tls_connection_policies": [
|
|
||||||
{
|
|
||||||
"match": {
|
|
||||||
"sni": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"client_authentication": {
|
|
||||||
"ca": {
|
|
||||||
"provider": "combined",
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"provider": "inline",
|
|
||||||
"trusted_ca_certs": [
|
|
||||||
"MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"authority": [
|
|
||||||
"local"
|
|
||||||
],
|
|
||||||
"provider": "pki_root"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"mode": "require_and_verify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
localhost
|
|
||||||
|
|
||||||
respond "hello from localhost"
|
|
||||||
tls {
|
|
||||||
client_auth {
|
|
||||||
mode request
|
|
||||||
trust_pool system
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "hello from localhost",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tls_connection_policies": [
|
|
||||||
{
|
|
||||||
"match": {
|
|
||||||
"sni": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"client_authentication": {
|
|
||||||
"ca": {
|
|
||||||
"provider": "system"
|
|
||||||
},
|
|
||||||
"mode": "request"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -190,7 +190,7 @@ func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) {
|
|||||||
// its own values. The backend must receive the auth service values.
|
// its own values. The backend must receive the auth service values.
|
||||||
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
||||||
req.Header.Set("Authorization", "Bearer token123")
|
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
|
req.Header.Set("X-User-Role", "forged-role") // must be overwritten
|
||||||
tester.AssertResponse(req, http.StatusOK, "ok")
|
tester.AssertResponse(req, http.StatusOK, "ok")
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddytest"
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
@@ -563,233 +562,3 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
|
|||||||
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestReverseProxyRetryMatchStatusCode verifies that lb_retry_match with a
|
|
||||||
// CEL expression matching on {rp.status_code} causes the request to be
|
|
||||||
// retried on the next upstream when the first upstream returns a matching
|
|
||||||
// status code
|
|
||||||
func TestReverseProxyRetryMatchStatusCode(t *testing.T) {
|
|
||||||
// Bad upstream: returns 502
|
|
||||||
badSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
badLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go badSrv.Serve(badLn)
|
|
||||||
t.Cleanup(func() { badSrv.Close(); badLn.Close() })
|
|
||||||
|
|
||||||
// Good upstream: returns 200
|
|
||||||
goodSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go goodSrv.Serve(goodLn)
|
|
||||||
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(fmt.Sprintf(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
reverse_proxy %s %s {
|
|
||||||
lb_policy round_robin
|
|
||||||
lb_retries 1
|
|
||||||
lb_retry_match {
|
|
||||||
expression `+"`{rp.status_code} in [502, 503]`"+`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
|
|
||||||
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestReverseProxyRetryMatchHeader verifies that lb_retry_match with a CEL
|
|
||||||
// expression matching on {rp.header.*} causes the request to be retried when
|
|
||||||
// the upstream sets a matching response header
|
|
||||||
func TestReverseProxyRetryMatchHeader(t *testing.T) {
|
|
||||||
var badHits atomic.Int32
|
|
||||||
|
|
||||||
// Bad upstream: returns 200 but signals retry via header
|
|
||||||
badSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
badHits.Add(1)
|
|
||||||
w.Header().Set("X-Upstream-Retry", "true")
|
|
||||||
w.Write([]byte("bad"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
badLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go badSrv.Serve(badLn)
|
|
||||||
t.Cleanup(func() { badSrv.Close(); badLn.Close() })
|
|
||||||
|
|
||||||
// Good upstream: returns 200 without retry header
|
|
||||||
goodSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("good"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go goodSrv.Serve(goodLn)
|
|
||||||
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(fmt.Sprintf(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
reverse_proxy %s %s {
|
|
||||||
lb_policy round_robin
|
|
||||||
lb_retries 1
|
|
||||||
lb_retry_match {
|
|
||||||
expression `+"`{rp.header.X-Upstream-Retry} == \"true\"`"+`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
|
|
||||||
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "good")
|
|
||||||
|
|
||||||
if badHits.Load() != 1 {
|
|
||||||
t.Errorf("bad upstream hits: got %d, want 1", badHits.Load())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestReverseProxyRetryMatchCombined verifies that a CEL expression combining
|
|
||||||
// request path matching with response status code matching works correctly -
|
|
||||||
// only retrying when both conditions are met
|
|
||||||
func TestReverseProxyRetryMatchCombined(t *testing.T) {
|
|
||||||
// Upstream: returns 502 for all requests
|
|
||||||
srv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go srv.Serve(ln)
|
|
||||||
t.Cleanup(func() { srv.Close(); ln.Close() })
|
|
||||||
|
|
||||||
// Good upstream
|
|
||||||
goodSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go goodSrv.Serve(goodLn)
|
|
||||||
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(fmt.Sprintf(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
reverse_proxy %s %s {
|
|
||||||
lb_policy round_robin
|
|
||||||
lb_retries 1
|
|
||||||
lb_retry_match {
|
|
||||||
expression `+"`path('/retry*') && {rp.status_code} in [502, 503]`"+`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, goodLn.Addr().String(), ln.Addr().String()), "caddyfile")
|
|
||||||
|
|
||||||
// /retry path matches the expression - should retry to good upstream
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/retry", 200, "ok")
|
|
||||||
|
|
||||||
// /other path does NOT match - should return the 502
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/other", nil)
|
|
||||||
tester.AssertResponse(req, 502, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestReverseProxyRetryMatchIsTransportError verifies that the
|
|
||||||
// {rp.is_transport_error} == true CEL function correctly identifies transport errors
|
|
||||||
// and allows retrying them alongside response-based matching
|
|
||||||
func TestReverseProxyRetryMatchIsTransportError(t *testing.T) {
|
|
||||||
// Good upstream: returns 200
|
|
||||||
goodSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go goodSrv.Serve(goodLn)
|
|
||||||
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
|
||||||
|
|
||||||
// Broken upstream: accepts connections but closes immediately
|
|
||||||
brokenLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { brokenLn.Close() })
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
conn, err := brokenLn.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(fmt.Sprintf(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
reverse_proxy %s %s {
|
|
||||||
lb_policy round_robin
|
|
||||||
lb_retries 1
|
|
||||||
lb_retry_match {
|
|
||||||
expression `+"`{rp.is_transport_error} || {rp.status_code} in [502, 503]`"+`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, goodLn.Addr().String(), brokenLn.Addr().String()), "caddyfile")
|
|
||||||
|
|
||||||
// Transport error on broken upstream should be retried to good upstream
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# Used by import_block_snippet_invalid_subdirective.caddyfiletest
|
|
||||||
|
|
||||||
(test) {
|
|
||||||
reverse_proxy {
|
|
||||||
{block}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -234,7 +234,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
|
|||||||
// not sure why), and since New() should return a pointer
|
// not sure why), and since New() should return a pointer
|
||||||
// value, we need to dereference it first
|
// value, we need to dereference it first
|
||||||
iface := any(modInfo.New())
|
iface := any(modInfo.New())
|
||||||
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Pointer {
|
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
|
||||||
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
|
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
|
||||||
}
|
}
|
||||||
modPkgPath := reflect.TypeOf(iface).PkgPath()
|
modPkgPath := reflect.TypeOf(iface).PkgPath()
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package caddycmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSplitModule(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expectedModule string
|
|
||||||
expectedVersion string
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple module without version",
|
|
||||||
input: "github.com/caddyserver/caddy",
|
|
||||||
expectedModule: "github.com/caddyserver/caddy",
|
|
||||||
expectedVersion: "",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "module with version",
|
|
||||||
input: "github.com/caddyserver/caddy@v2.0.0",
|
|
||||||
expectedModule: "github.com/caddyserver/caddy",
|
|
||||||
expectedVersion: "v2.0.0",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "module with semantic version",
|
|
||||||
input: "github.com/user/module@v1.2.3",
|
|
||||||
expectedModule: "github.com/user/module",
|
|
||||||
expectedVersion: "v1.2.3",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "module with prerelease version",
|
|
||||||
input: "github.com/user/module@v1.0.0-beta.1",
|
|
||||||
expectedModule: "github.com/user/module",
|
|
||||||
expectedVersion: "v1.0.0-beta.1",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "module with commit hash",
|
|
||||||
input: "github.com/user/module@abc123def",
|
|
||||||
expectedModule: "github.com/user/module",
|
|
||||||
expectedVersion: "abc123def",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "module with @ in path and version",
|
|
||||||
input: "github.com/@user/module@v1.0.0",
|
|
||||||
expectedModule: "github.com/@user/module",
|
|
||||||
expectedVersion: "v1.0.0",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "module with multiple @ in path",
|
|
||||||
input: "github.com/@org/@user/module@v2.3.4",
|
|
||||||
expectedModule: "github.com/@org/@user/module",
|
|
||||||
expectedVersion: "v2.3.4",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
// TODO: decide on the behavior for this case; it fails currently
|
|
||||||
// {
|
|
||||||
// name: "module with @ in path but no version",
|
|
||||||
// input: "github.com/@user/module",
|
|
||||||
// expectedModule: "github.com/@user/module",
|
|
||||||
// expectedVersion: "",
|
|
||||||
// expectError: false,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
name: "empty string",
|
|
||||||
input: "",
|
|
||||||
expectedModule: "",
|
|
||||||
expectedVersion: "",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "only @ symbol",
|
|
||||||
input: "@",
|
|
||||||
expectedModule: "",
|
|
||||||
expectedVersion: "",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "@ at start",
|
|
||||||
input: "@v1.0.0",
|
|
||||||
expectedModule: "",
|
|
||||||
expectedVersion: "v1.0.0",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "@ at end",
|
|
||||||
input: "github.com/user/module@",
|
|
||||||
expectedModule: "github.com/user/module",
|
|
||||||
expectedVersion: "",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple consecutive @",
|
|
||||||
input: "github.com/user/module@@v1.0.0",
|
|
||||||
expectedModule: "github.com/user/module@",
|
|
||||||
expectedVersion: "v1.0.0",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "version with latest tag",
|
|
||||||
input: "github.com/user/module@latest",
|
|
||||||
expectedModule: "github.com/user/module",
|
|
||||||
expectedVersion: "latest",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "long module path",
|
|
||||||
input: "github.com/organization/team/project/subproject/module@v3.14.159",
|
|
||||||
expectedModule: "github.com/organization/team/project/subproject/module",
|
|
||||||
expectedVersion: "v3.14.159",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "module with dots in name",
|
|
||||||
input: "github.com/user/my.module.name@v1.0",
|
|
||||||
expectedModule: "github.com/user/my.module.name",
|
|
||||||
expectedVersion: "v1.0",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "module with hyphens",
|
|
||||||
input: "github.com/user/my-module-name@v1.0.0",
|
|
||||||
expectedModule: "github.com/user/my-module-name",
|
|
||||||
expectedVersion: "v1.0.0",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "gitlab module",
|
|
||||||
input: "gitlab.com/user/module@v2.0.0",
|
|
||||||
expectedModule: "gitlab.com/user/module",
|
|
||||||
expectedVersion: "v2.0.0",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bitbucket module",
|
|
||||||
input: "bitbucket.org/user/module@v1.5.0",
|
|
||||||
expectedModule: "bitbucket.org/user/module",
|
|
||||||
expectedVersion: "v1.5.0",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom domain",
|
|
||||||
input: "example.com/custom/module@v1.0.0",
|
|
||||||
expectedModule: "example.com/custom/module",
|
|
||||||
expectedVersion: "v1.0.0",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
module, version, err := splitModule(tt.input)
|
|
||||||
|
|
||||||
// Check error expectation
|
|
||||||
if tt.expectError {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error but got none")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check module
|
|
||||||
if module != tt.expectedModule {
|
|
||||||
t.Errorf("module: got %q, want %q", module, tt.expectedModule)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check version
|
|
||||||
if version != tt.expectedVersion {
|
|
||||||
t.Errorf("version: got %q, want %q", version, tt.expectedVersion)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSplitModule_ErrorCases(t *testing.T) {
|
|
||||||
errorCases := []string{
|
|
||||||
"",
|
|
||||||
"@",
|
|
||||||
"@version",
|
|
||||||
"@v1.0.0",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range errorCases {
|
|
||||||
t.Run("error_"+tc, func(t *testing.T) {
|
|
||||||
_, _, err := splitModule(tc)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("splitModule(%q) should return error", tc)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkSplitModule benchmarks the splitModule function
|
|
||||||
func BenchmarkSplitModule(b *testing.B) {
|
|
||||||
testCases := []string{
|
|
||||||
"github.com/user/module",
|
|
||||||
"github.com/user/module@v1.0.0",
|
|
||||||
"github.com/@org/@user/module@v2.3.4",
|
|
||||||
"github.com/organization/team/project/subproject/module@v3.14.159",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
b.Run(tc, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
splitModule(tc)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-720
@@ -1,720 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package caddy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConfig_Start_Stop_Basic(t *testing.T) {
|
|
||||||
cfg := &Config{
|
|
||||||
Admin: &AdminConfig{Disabled: true}, // Disable admin to avoid port conflicts
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, err := run(cfg, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to run config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify context is valid
|
|
||||||
if ctx.cfg == nil {
|
|
||||||
t.Error("Expected non-nil config in context")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the config
|
|
||||||
unsyncedStop(ctx)
|
|
||||||
|
|
||||||
// Verify cleanup was called
|
|
||||||
if ctx.cfg.cancelFunc == nil {
|
|
||||||
t.Error("Expected cancel function to be set")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_Validate_InvalidConfig(t *testing.T) {
|
|
||||||
// Create a config with an invalid app module
|
|
||||||
cfg := &Config{
|
|
||||||
AppsRaw: ModuleMap{
|
|
||||||
"non-existent-app": json.RawMessage(`{}`),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Validate(cfg)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected validation error for invalid app module")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_Validate_ValidConfig(t *testing.T) {
|
|
||||||
cfg := &Config{
|
|
||||||
Admin: &AdminConfig{Disabled: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Validate(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected validation error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeConfig_ConcurrentAccess(t *testing.T) {
|
|
||||||
// Save original config state
|
|
||||||
originalRawCfg := rawCfg[rawConfigKey]
|
|
||||||
originalRawCfgJSON := rawCfgJSON
|
|
||||||
defer func() {
|
|
||||||
rawCfg[rawConfigKey] = originalRawCfg
|
|
||||||
rawCfgJSON = originalRawCfgJSON
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Initialize with a basic config
|
|
||||||
initialCfg := map[string]any{
|
|
||||||
"test": "value",
|
|
||||||
}
|
|
||||||
rawCfg[rawConfigKey] = initialCfg
|
|
||||||
|
|
||||||
const numGoroutines = 10 // Reduced for more controlled testing
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
errors := make([]error, numGoroutines)
|
|
||||||
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(index int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
// Only test read operations to avoid complex state changes
|
|
||||||
// that could cause nil pointer issues in concurrent scenarios
|
|
||||||
var buf bytes.Buffer
|
|
||||||
errors[index] = readConfig("/"+rawConfigKey+"/test", &buf)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Check that read operations succeeded
|
|
||||||
for i, err := range errors {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Goroutine %d: Unexpected read error: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeConfig_MethodValidation(t *testing.T) {
|
|
||||||
// Save original config state
|
|
||||||
originalRawCfg := rawCfg[rawConfigKey]
|
|
||||||
defer func() {
|
|
||||||
rawCfg[rawConfigKey] = originalRawCfg
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Set up a simple valid config for testing
|
|
||||||
rawCfg[rawConfigKey] = map[string]any{}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
method string
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{http.MethodPost, false},
|
|
||||||
{http.MethodPut, true}, // because key 'admin' already exists
|
|
||||||
{http.MethodPatch, false},
|
|
||||||
{http.MethodDelete, false},
|
|
||||||
{http.MethodGet, true},
|
|
||||||
{http.MethodHead, true},
|
|
||||||
{http.MethodOptions, true},
|
|
||||||
{http.MethodConnect, true},
|
|
||||||
{http.MethodTrace, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.method, func(t *testing.T) {
|
|
||||||
// Use a simple admin config path that won't cause complex validation
|
|
||||||
err := changeConfig(test.method, "/"+rawConfigKey+"/admin", []byte(`{"disabled": true}`), "", false)
|
|
||||||
|
|
||||||
if test.expectErr && err == nil {
|
|
||||||
t.Error("Expected error for invalid method")
|
|
||||||
}
|
|
||||||
if !test.expectErr && err != nil && (err != errSameConfig) {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeConfig_IfMatchHeader_Validation(t *testing.T) {
|
|
||||||
// Set up initial config
|
|
||||||
initialCfg := map[string]any{"test": "value"}
|
|
||||||
rawCfg[rawConfigKey] = initialCfg
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ifMatch string
|
|
||||||
expectErr bool
|
|
||||||
expectStatusCode int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "malformed - no quotes",
|
|
||||||
ifMatch: "path hash",
|
|
||||||
expectErr: true,
|
|
||||||
expectStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "malformed - single quote",
|
|
||||||
ifMatch: `"path hash`,
|
|
||||||
expectErr: true,
|
|
||||||
expectStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "malformed - wrong number of parts",
|
|
||||||
ifMatch: `"path"`,
|
|
||||||
expectErr: true,
|
|
||||||
expectStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "malformed - too many parts",
|
|
||||||
ifMatch: `"path hash extra"`,
|
|
||||||
expectErr: true,
|
|
||||||
expectStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wrong hash",
|
|
||||||
ifMatch: `"/config/test wronghash"`,
|
|
||||||
expectErr: true,
|
|
||||||
expectStatusCode: http.StatusPreconditionFailed,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
err := changeConfig(http.MethodPost, "/"+rawConfigKey+"/test", []byte(`"newvalue"`), test.ifMatch, false)
|
|
||||||
|
|
||||||
if test.expectErr && err == nil {
|
|
||||||
t.Error("Expected error")
|
|
||||||
}
|
|
||||||
if !test.expectErr && err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if test.expectErr && err != nil {
|
|
||||||
if apiErr, ok := err.(APIError); ok {
|
|
||||||
if apiErr.HTTPStatus != test.expectStatusCode {
|
|
||||||
t.Errorf("Expected status %d, got %d", test.expectStatusCode, apiErr.HTTPStatus)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t.Error("Expected APIError type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIndexConfigObjects_Basic(t *testing.T) {
|
|
||||||
config := map[string]any{
|
|
||||||
"app1": map[string]any{
|
|
||||||
"@id": "my-app",
|
|
||||||
"config": "value",
|
|
||||||
},
|
|
||||||
"nested": map[string]any{
|
|
||||||
"array": []any{
|
|
||||||
map[string]any{
|
|
||||||
"@id": "nested-item",
|
|
||||||
"data": "test",
|
|
||||||
},
|
|
||||||
map[string]any{
|
|
||||||
"@id": 123.0, // JSON numbers are float64
|
|
||||||
"more": "data",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
index := make(map[string]string)
|
|
||||||
err := indexConfigObjects(config, "/config", index)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := map[string]string{
|
|
||||||
"my-app": "/config/app1",
|
|
||||||
"nested-item": "/config/nested/array/0",
|
|
||||||
"123": "/config/nested/array/1",
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(index) != len(expected) {
|
|
||||||
t.Errorf("Expected %d indexed items, got %d", len(expected), len(index))
|
|
||||||
}
|
|
||||||
|
|
||||||
for id, expectedPath := range expected {
|
|
||||||
if actualPath, exists := index[id]; !exists || actualPath != expectedPath {
|
|
||||||
t.Errorf("ID %s: expected path '%s', got '%s'", id, expectedPath, actualPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIndexConfigObjects_InvalidID(t *testing.T) {
|
|
||||||
config := map[string]any{
|
|
||||||
"app": map[string]any{
|
|
||||||
"@id": map[string]any{"invalid": "id"}, // Invalid ID type
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
index := make(map[string]string)
|
|
||||||
err := indexConfigObjects(config, "/config", index)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for invalid ID type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRun_AppStartFailure(t *testing.T) {
|
|
||||||
// Register a mock app that fails to start
|
|
||||||
RegisterModule(&failingApp{})
|
|
||||||
defer func() {
|
|
||||||
// Clean up module registry
|
|
||||||
delete(modules, "failing-app")
|
|
||||||
}()
|
|
||||||
|
|
||||||
cfg := &Config{
|
|
||||||
Admin: &AdminConfig{Disabled: true},
|
|
||||||
AppsRaw: ModuleMap{
|
|
||||||
"failing-app": json.RawMessage(`{}`),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := run(cfg, true)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error when app fails to start")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should contain the app name in the error
|
|
||||||
if err.Error() == "" {
|
|
||||||
t.Error("Expected descriptive error message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRun_AppStopFailure_During_Cleanup(t *testing.T) {
|
|
||||||
// Register apps where one fails to start and another fails to stop
|
|
||||||
RegisterModule(&workingApp{})
|
|
||||||
RegisterModule(&failingStopApp{})
|
|
||||||
defer func() {
|
|
||||||
delete(modules, "working-app")
|
|
||||||
delete(modules, "failing-stop-app")
|
|
||||||
}()
|
|
||||||
|
|
||||||
cfg := &Config{
|
|
||||||
Admin: &AdminConfig{Disabled: true},
|
|
||||||
AppsRaw: ModuleMap{
|
|
||||||
"working-app": json.RawMessage(`{}`),
|
|
||||||
"failing-stop-app": json.RawMessage(`{}`),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start both apps
|
|
||||||
ctx, err := run(cfg, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error starting apps: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop context - this should handle stop failures gracefully
|
|
||||||
unsyncedStop(ctx)
|
|
||||||
|
|
||||||
// Test passed if we reach here without panic
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionContext_NilConfig(t *testing.T) {
|
|
||||||
ctx, err := provisionContext(nil, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.cfg == nil {
|
|
||||||
t.Error("Expected non-nil config even when input is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
// TODO: Investigate
|
|
||||||
ctx.cfg.cancelFunc(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDuration_UnmarshalJSON_EdgeCases(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expectErr bool
|
|
||||||
expected time.Duration
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty input",
|
|
||||||
input: "",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "integer nanoseconds",
|
|
||||||
input: "1000000000",
|
|
||||||
expected: time.Second,
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "string duration",
|
|
||||||
input: `"5m30s"`,
|
|
||||||
expected: 5*time.Minute + 30*time.Second,
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "days conversion",
|
|
||||||
input: `"2d"`,
|
|
||||||
expected: 48 * time.Hour,
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mixed days and hours",
|
|
||||||
input: `"1d12h"`,
|
|
||||||
expected: 36 * time.Hour,
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid duration",
|
|
||||||
input: `"invalid"`,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
var d Duration
|
|
||||||
err := d.UnmarshalJSON([]byte(test.input))
|
|
||||||
|
|
||||||
if test.expectErr && err == nil {
|
|
||||||
t.Error("Expected error")
|
|
||||||
}
|
|
||||||
if !test.expectErr && err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if !test.expectErr && time.Duration(d) != test.expected {
|
|
||||||
t.Errorf("Expected %v, got %v", test.expected, time.Duration(d))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDuration_LongInput(t *testing.T) {
|
|
||||||
// Test input length limit
|
|
||||||
longInput := string(make([]byte, 1025)) // Exceeds 1024 limit
|
|
||||||
for i := range longInput {
|
|
||||||
longInput = longInput[:i] + "1"
|
|
||||||
}
|
|
||||||
longInput += "d"
|
|
||||||
|
|
||||||
_, err := ParseDuration(longInput)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for input longer than 1024 characters")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersion_Deterministic(t *testing.T) {
|
|
||||||
// Test that Version() returns consistent results
|
|
||||||
simple1, full1 := Version()
|
|
||||||
simple2, full2 := Version()
|
|
||||||
|
|
||||||
if simple1 != simple2 {
|
|
||||||
t.Errorf("Version() simple form not deterministic: '%s' != '%s'", simple1, simple2)
|
|
||||||
}
|
|
||||||
if full1 != full2 {
|
|
||||||
t.Errorf("Version() full form not deterministic: '%s' != '%s'", full1, full2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInstanceID_Consistency(t *testing.T) {
|
|
||||||
// Test that InstanceID returns the same ID on subsequent calls
|
|
||||||
id1, err := InstanceID()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get instance ID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
id2, err := InstanceID()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get instance ID on second call: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if id1 != id2 {
|
|
||||||
t.Errorf("InstanceID not consistent: %v != %v", id1, id2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveMetaFields_EdgeCases(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no meta fields",
|
|
||||||
input: `{"normal": "field"}`,
|
|
||||||
expected: `{"normal": "field"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single @id field",
|
|
||||||
input: `{"@id": "test", "other": "field"}`,
|
|
||||||
expected: `{"other": "field"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "@id at beginning",
|
|
||||||
input: `{"@id": "test", "other": "field"}`,
|
|
||||||
expected: `{"other": "field"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "@id at end",
|
|
||||||
input: `{"other": "field", "@id": "test"}`,
|
|
||||||
expected: `{"other": "field"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "@id in middle",
|
|
||||||
input: `{"first": "value", "@id": "test", "last": "value"}`,
|
|
||||||
expected: `{"first": "value", "last": "value"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple @id fields",
|
|
||||||
input: `{"@id": "test1", "other": "field", "@id": "test2"}`,
|
|
||||||
expected: `{"other": "field"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "numeric @id",
|
|
||||||
input: `{"@id": 123, "other": "field"}`,
|
|
||||||
expected: `{"other": "field"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nested objects with @id",
|
|
||||||
input: `{"outer": {"@id": "nested", "data": "value"}}`,
|
|
||||||
expected: `{"outer": {"data": "value"}}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
result := RemoveMetaFields([]byte(test.input))
|
|
||||||
// resultStr := string(result)
|
|
||||||
|
|
||||||
// Parse both to ensure valid JSON and compare structures
|
|
||||||
var expectedObj, resultObj any
|
|
||||||
if err := json.Unmarshal([]byte(test.expected), &expectedObj); err != nil {
|
|
||||||
t.Fatalf("Expected result is not valid JSON: %v", err)
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(result, &resultObj); err != nil {
|
|
||||||
t.Fatalf("Result is not valid JSON: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: We can't do exact string comparison due to potential field ordering
|
|
||||||
// Instead, verify the structure matches
|
|
||||||
expectedJSON, _ := json.Marshal(expectedObj)
|
|
||||||
resultJSON, _ := json.Marshal(resultObj)
|
|
||||||
|
|
||||||
if string(expectedJSON) != string(resultJSON) {
|
|
||||||
t.Errorf("Expected %s, got %s", string(expectedJSON), string(resultJSON))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnsyncedConfigAccess_ArrayOperations_EdgeCases(t *testing.T) {
|
|
||||||
// Test array boundary conditions and edge cases
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
initialState map[string]any
|
|
||||||
method string
|
|
||||||
path string
|
|
||||||
payload string
|
|
||||||
expectErr bool
|
|
||||||
expectState map[string]any
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "delete from empty array",
|
|
||||||
initialState: map[string]any{"arr": []any{}},
|
|
||||||
method: http.MethodDelete,
|
|
||||||
path: "/config/arr/0",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "access negative index",
|
|
||||||
initialState: map[string]any{"arr": []any{"a", "b"}},
|
|
||||||
method: http.MethodGet,
|
|
||||||
path: "/config/arr/-1",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "put at index beyond end",
|
|
||||||
initialState: map[string]any{"arr": []any{"a"}},
|
|
||||||
method: http.MethodPut,
|
|
||||||
path: "/config/arr/5",
|
|
||||||
payload: `"new"`,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "patch non-existent index",
|
|
||||||
initialState: map[string]any{"arr": []any{"a"}},
|
|
||||||
method: http.MethodPatch,
|
|
||||||
path: "/config/arr/5",
|
|
||||||
payload: `"new"`,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "put at exact end of array",
|
|
||||||
initialState: map[string]any{"arr": []any{"a", "b"}},
|
|
||||||
method: http.MethodPut,
|
|
||||||
path: "/config/arr/2",
|
|
||||||
payload: `"c"`,
|
|
||||||
expectState: map[string]any{"arr": []any{"a", "b", "c"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ellipses with non-array payload",
|
|
||||||
initialState: map[string]any{"arr": []any{"a"}},
|
|
||||||
method: http.MethodPost,
|
|
||||||
path: "/config/arr/...",
|
|
||||||
payload: `"not-array"`,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
// Set up initial state
|
|
||||||
rawCfg[rawConfigKey] = test.initialState
|
|
||||||
|
|
||||||
err := unsyncedConfigAccess(test.method, test.path, []byte(test.payload), nil)
|
|
||||||
|
|
||||||
if test.expectErr && err == nil {
|
|
||||||
t.Error("Expected error")
|
|
||||||
}
|
|
||||||
if !test.expectErr && err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if test.expectState != nil {
|
|
||||||
// Compare resulting state
|
|
||||||
expectedJSON, _ := json.Marshal(test.expectState)
|
|
||||||
actualJSON, _ := json.Marshal(rawCfg[rawConfigKey])
|
|
||||||
|
|
||||||
if string(expectedJSON) != string(actualJSON) {
|
|
||||||
t.Errorf("Expected state %s, got %s", string(expectedJSON), string(actualJSON))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExitProcess_ConcurrentCalls(t *testing.T) {
|
|
||||||
// Test that multiple concurrent calls to exitProcess are safe
|
|
||||||
// We can't test the actual exit, but we can test the atomic flag
|
|
||||||
|
|
||||||
// Reset the exiting flag
|
|
||||||
oldExiting := exiting
|
|
||||||
exiting = new(int32)
|
|
||||||
defer func() { exiting = oldExiting }()
|
|
||||||
|
|
||||||
const numGoroutines = 10
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
results := make([]bool, numGoroutines)
|
|
||||||
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(index int) {
|
|
||||||
defer wg.Done()
|
|
||||||
// Check the Exiting() function which reads the atomic flag
|
|
||||||
wasExitingBefore := Exiting()
|
|
||||||
|
|
||||||
// This would call exitProcess, but we don't want to actually exit
|
|
||||||
// So we just test the atomic operation directly
|
|
||||||
results[index] = atomic.CompareAndSwapInt32(exiting, 0, 1)
|
|
||||||
|
|
||||||
wasExitingAfter := Exiting()
|
|
||||||
|
|
||||||
// At least one should succeed in setting the flag
|
|
||||||
if !wasExitingBefore && wasExitingAfter && !results[index] {
|
|
||||||
t.Errorf("Goroutine %d: Flag was set but CAS failed", index)
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Exactly one goroutine should have successfully set the flag
|
|
||||||
successCount := 0
|
|
||||||
for _, success := range results {
|
|
||||||
if success {
|
|
||||||
successCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if successCount != 1 {
|
|
||||||
t.Errorf("Expected exactly 1 successful flag set, got %d", successCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flag should be set
|
|
||||||
if !Exiting() {
|
|
||||||
t.Error("Exiting flag should be set")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock apps for testing
|
|
||||||
type failingApp struct{}
|
|
||||||
|
|
||||||
func (fa *failingApp) CaddyModule() ModuleInfo {
|
|
||||||
return ModuleInfo{
|
|
||||||
ID: "failing-app",
|
|
||||||
New: func() Module { return new(failingApp) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fa *failingApp) Start() error {
|
|
||||||
return fmt.Errorf("simulated start failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fa *failingApp) Stop() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type workingApp struct{}
|
|
||||||
|
|
||||||
func (wa *workingApp) CaddyModule() ModuleInfo {
|
|
||||||
return ModuleInfo{
|
|
||||||
ID: "working-app",
|
|
||||||
New: func() Module { return new(workingApp) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *workingApp) Start() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *workingApp) Stop() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type failingStopApp struct{}
|
|
||||||
|
|
||||||
func (fsa *failingStopApp) CaddyModule() ModuleInfo {
|
|
||||||
return ModuleInfo{
|
|
||||||
ID: "failing-stop-app",
|
|
||||||
New: func() Module { return new(failingStopApp) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fsa *failingStopApp) Start() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fsa *failingStopApp) Stop() error {
|
|
||||||
return fmt.Errorf("simulated stop failure")
|
|
||||||
}
|
|
||||||
+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
|
// 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
|
// the module's concrete type is a slice or map; New() *should* return
|
||||||
// a pointer, otherwise unmarshaling errors or panics will occur
|
// a pointer, otherwise unmarshaling errors or panics will occur
|
||||||
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Pointer {
|
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr {
|
||||||
log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+
|
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"+
|
" 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)
|
" using new(Type) or &Type notation in your module's New() function.", id)
|
||||||
|
|||||||
@@ -1,407 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package caddy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"math"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseDuration_EdgeCases(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expectErr bool
|
|
||||||
expected time.Duration
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "zero duration",
|
|
||||||
input: "0",
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid format",
|
|
||||||
input: "abc",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative days",
|
|
||||||
input: "-2d",
|
|
||||||
expected: -48 * time.Hour,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "decimal days",
|
|
||||||
input: "0.5d",
|
|
||||||
expected: 12 * time.Hour,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "large decimal days",
|
|
||||||
input: "365.25d",
|
|
||||||
expected: time.Duration(365.25*24) * time.Hour,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple days in same string",
|
|
||||||
input: "1d2d3d",
|
|
||||||
expected: (24 * 6) * time.Hour, // 6 days total
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "days with other units",
|
|
||||||
input: "1d30m15s",
|
|
||||||
expected: 24*time.Hour + 30*time.Minute + 15*time.Second,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "malformed days",
|
|
||||||
input: "d",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid day value",
|
|
||||||
input: "abcd",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "overflow protection",
|
|
||||||
input: "9999999999999999999999999d",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "zero days",
|
|
||||||
input: "0d",
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "input at limit",
|
|
||||||
input: strings.Repeat("1", 1024) + "ns",
|
|
||||||
expectErr: true, // Likely to cause parsing error due to size
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
result, err := ParseDuration(test.input)
|
|
||||||
|
|
||||||
if test.expectErr && err == nil {
|
|
||||||
t.Error("Expected error but got none")
|
|
||||||
}
|
|
||||||
if !test.expectErr && err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if !test.expectErr && result != test.expected {
|
|
||||||
t.Errorf("Expected %v, got %v", test.expected, result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDuration_InputLengthLimit(t *testing.T) {
|
|
||||||
// Test the 1024 character limit
|
|
||||||
longInput := strings.Repeat("1", 1025) + "s"
|
|
||||||
|
|
||||||
_, err := ParseDuration(longInput)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for input longer than 1024 characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedErrMsg := "parsing duration: input string too long"
|
|
||||||
if err.Error() != expectedErrMsg {
|
|
||||||
t.Errorf("Expected error message '%s', got '%s'", expectedErrMsg, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDuration_ComplexNumberFormats(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expected time.Duration
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
input: "+1d",
|
|
||||||
expected: 24 * time.Hour,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "-1.5d",
|
|
||||||
expected: -36 * time.Hour,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "1.0d",
|
|
||||||
expected: 24 * time.Hour,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "0.25d",
|
|
||||||
expected: 6 * time.Hour,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "1.5d30m",
|
|
||||||
expected: 36*time.Hour + 30*time.Minute,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "2.5d1h30m45s",
|
|
||||||
expected: 60*time.Hour + time.Hour + 30*time.Minute + 45*time.Second,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.input, func(t *testing.T) {
|
|
||||||
result, err := ParseDuration(test.input)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if result != test.expected {
|
|
||||||
t.Errorf("Expected %v, got %v", test.expected, result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDuration_UnmarshalJSON_TypeValidation(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expectErr bool
|
|
||||||
expected time.Duration
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "null value",
|
|
||||||
input: "null",
|
|
||||||
expectErr: false,
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean value",
|
|
||||||
input: "true",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "array value",
|
|
||||||
input: `[1,2,3]`,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "object value",
|
|
||||||
input: `{"duration": "5m"}`,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative integer",
|
|
||||||
input: "-1000000000",
|
|
||||||
expected: -time.Second,
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "zero integer",
|
|
||||||
input: "0",
|
|
||||||
expected: 0,
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "large integer",
|
|
||||||
input: "9223372036854775807", // Max int64
|
|
||||||
expected: time.Duration(math.MaxInt64),
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "float as integer (invalid JSON for int)",
|
|
||||||
input: "1.5",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "string with special characters",
|
|
||||||
input: `"5m\"30s"`,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "string with unicode",
|
|
||||||
input: `"5m🚀"`,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
var d Duration
|
|
||||||
err := d.UnmarshalJSON([]byte(test.input))
|
|
||||||
|
|
||||||
if test.expectErr && err == nil {
|
|
||||||
t.Error("Expected error but got none")
|
|
||||||
}
|
|
||||||
if !test.expectErr && err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if !test.expectErr && time.Duration(d) != test.expected {
|
|
||||||
t.Errorf("Expected %v, got %v", test.expected, time.Duration(d))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDuration_JSON_RoundTrip(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
duration time.Duration
|
|
||||||
asString bool
|
|
||||||
}{
|
|
||||||
{duration: 5 * time.Minute, asString: true},
|
|
||||||
{duration: 24 * time.Hour, asString: false}, // Will be stored as nanoseconds
|
|
||||||
{duration: 0, asString: false},
|
|
||||||
{duration: -time.Hour, asString: true},
|
|
||||||
{duration: time.Nanosecond, asString: false},
|
|
||||||
{duration: time.Second, asString: false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.duration.String(), func(t *testing.T) {
|
|
||||||
d := Duration(test.duration)
|
|
||||||
|
|
||||||
// Marshal to JSON
|
|
||||||
jsonData, err := json.Marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to marshal: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal back
|
|
||||||
var unmarshaled Duration
|
|
||||||
err = unmarshaled.UnmarshalJSON(jsonData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be equal
|
|
||||||
if time.Duration(unmarshaled) != test.duration {
|
|
||||||
t.Errorf("Round trip failed: expected %v, got %v", test.duration, time.Duration(unmarshaled))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDuration_Precision(t *testing.T) {
|
|
||||||
// Test floating point precision with days
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expected time.Duration
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
input: "0.1d",
|
|
||||||
expected: time.Duration(0.1 * 24 * float64(time.Hour)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "0.01d",
|
|
||||||
expected: time.Duration(0.01 * 24 * float64(time.Hour)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "0.001d",
|
|
||||||
expected: time.Duration(0.001 * 24 * float64(time.Hour)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "1.23456789d",
|
|
||||||
expected: time.Duration(1.23456789 * 24 * float64(time.Hour)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.input, func(t *testing.T) {
|
|
||||||
result, err := ParseDuration(test.input)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow for small floating point differences
|
|
||||||
diff := result - test.expected
|
|
||||||
if diff < 0 {
|
|
||||||
diff = -diff
|
|
||||||
}
|
|
||||||
if diff > time.Nanosecond {
|
|
||||||
t.Errorf("Expected %v, got %v (diff: %v)", test.expected, result, diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDuration_Boundary_Values(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "minimum day value",
|
|
||||||
input: "0.000000001d", // Very small but valid
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "very large day value",
|
|
||||||
input: "999999999999999999999d",
|
|
||||||
expectErr: true, // Should overflow
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative zero",
|
|
||||||
input: "-0d",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "positive zero",
|
|
||||||
input: "+0d",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
_, err := ParseDuration(test.input)
|
|
||||||
|
|
||||||
if test.expectErr && err == nil {
|
|
||||||
t.Error("Expected error but got none")
|
|
||||||
}
|
|
||||||
if !test.expectErr && err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkParseDuration_SimpleDay(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
ParseDuration("1d")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkParseDuration_ComplexDay(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
ParseDuration("1.5d30m15.5s")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkParseDuration_MultipleDays(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
ParseDuration("1d2d3d4d5d")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDuration_UnmarshalJSON_String(b *testing.B) {
|
|
||||||
input := []byte(`"5m30s"`)
|
|
||||||
var d Duration
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
d.UnmarshalJSON(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDuration_UnmarshalJSON_Integer(b *testing.B) {
|
|
||||||
input := []byte("300000000000") // 5 minutes in nanoseconds
|
|
||||||
var d Duration
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
d.UnmarshalJSON(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-642
@@ -1,642 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package caddy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewEvent_Basic(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
eventName := "test.event"
|
|
||||||
eventData := map[string]any{
|
|
||||||
"key1": "value1",
|
|
||||||
"key2": 42,
|
|
||||||
}
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, eventName, eventData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify event properties
|
|
||||||
if event.Name() != eventName {
|
|
||||||
t.Errorf("Expected name '%s', got '%s'", eventName, event.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Data == nil {
|
|
||||||
t.Error("Expected non-nil data")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(event.Data) != len(eventData) {
|
|
||||||
t.Errorf("Expected %d data items, got %d", len(eventData), len(event.Data))
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, expectedValue := range eventData {
|
|
||||||
if actualValue, exists := event.Data[key]; !exists || actualValue != expectedValue {
|
|
||||||
t.Errorf("Data key '%s': expected %v, got %v", key, expectedValue, actualValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify ID is generated
|
|
||||||
if event.ID().String() == "" {
|
|
||||||
t.Error("Event ID should not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify timestamp is recent
|
|
||||||
if time.Since(event.Timestamp()) > time.Second {
|
|
||||||
t.Error("Event timestamp should be recent")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewEvent_NameNormalization(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"UPPERCASE", "uppercase"},
|
|
||||||
{"MixedCase", "mixedcase"},
|
|
||||||
{"already.lower", "already.lower"},
|
|
||||||
{"With-Dashes", "with-dashes"},
|
|
||||||
{"With_Underscores", "with_underscores"},
|
|
||||||
{"", ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.input, func(t *testing.T) {
|
|
||||||
event, err := NewEvent(ctx, test.input, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Name() != test.expected {
|
|
||||||
t.Errorf("Expected normalized name '%s', got '%s'", test.expected, event.Name())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_CloudEvent_NilData(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, "test", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudEvent := event.CloudEvent()
|
|
||||||
|
|
||||||
// Should not panic with nil data
|
|
||||||
if cloudEvent.Data == nil {
|
|
||||||
t.Error("CloudEvent data should not be nil even with nil input")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be valid JSON
|
|
||||||
var parsed any
|
|
||||||
if err := json.Unmarshal(cloudEvent.Data, &parsed); err != nil {
|
|
||||||
t.Errorf("CloudEvent data should be valid JSON: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_CloudEvent_WithModule(t *testing.T) {
|
|
||||||
// Create a context with a mock module
|
|
||||||
mockMod := &mockModule{}
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Simulate module ancestry
|
|
||||||
ctx.ancestry = []Module{mockMod}
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, "test", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudEvent := event.CloudEvent()
|
|
||||||
|
|
||||||
// Source should be the module ID
|
|
||||||
expectedSource := string(mockMod.CaddyModule().ID)
|
|
||||||
if cloudEvent.Source != expectedSource {
|
|
||||||
t.Errorf("Expected source '%s', got '%s'", expectedSource, cloudEvent.Source)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Origin should be the module
|
|
||||||
if event.Origin() != mockMod {
|
|
||||||
t.Error("Expected event origin to be the mock module")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_CloudEvent_Fields(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
eventName := "test.event"
|
|
||||||
eventData := map[string]any{"test": "data"}
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, eventName, eventData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudEvent := event.CloudEvent()
|
|
||||||
|
|
||||||
// Verify CloudEvent fields
|
|
||||||
if cloudEvent.ID == "" {
|
|
||||||
t.Error("CloudEvent ID should not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cloudEvent.Source != "caddy" {
|
|
||||||
t.Errorf("Expected source 'caddy' for nil module, got '%s'", cloudEvent.Source)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cloudEvent.SpecVersion != "1.0" {
|
|
||||||
t.Errorf("Expected spec version '1.0', got '%s'", cloudEvent.SpecVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cloudEvent.Type != eventName {
|
|
||||||
t.Errorf("Expected type '%s', got '%s'", eventName, cloudEvent.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cloudEvent.DataContentType != "application/json" {
|
|
||||||
t.Errorf("Expected content type 'application/json', got '%s'", cloudEvent.DataContentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify data is valid JSON
|
|
||||||
var parsedData map[string]any
|
|
||||||
if err := json.Unmarshal(cloudEvent.Data, &parsedData); err != nil {
|
|
||||||
t.Errorf("CloudEvent data is not valid JSON: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedData["test"] != "data" {
|
|
||||||
t.Errorf("Expected data to contain test='data', got %v", parsedData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_ConcurrentAccess(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, "concurrent.test", map[string]any{
|
|
||||||
"counter": 0,
|
|
||||||
"data": "shared",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const numGoroutines = 50
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
// Test concurrent read access to event properties
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
// These should be safe for concurrent access
|
|
||||||
_ = event.ID()
|
|
||||||
_ = event.Name()
|
|
||||||
_ = event.Timestamp()
|
|
||||||
_ = event.Origin()
|
|
||||||
_ = event.CloudEvent()
|
|
||||||
|
|
||||||
// Data map is not synchronized, so read-only access should be safe
|
|
||||||
if data, exists := event.Data["data"]; !exists || data != "shared" {
|
|
||||||
t.Errorf("Goroutine %d: Expected shared data", id)
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_DataModification_Warning(t *testing.T) {
|
|
||||||
// This test documents the non-thread-safe nature of event data
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, "data.test", map[string]any{
|
|
||||||
"mutable": "original",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modifying data after creation (this is allowed but not thread-safe)
|
|
||||||
event.Data["mutable"] = "modified"
|
|
||||||
event.Data["new_key"] = "new_value"
|
|
||||||
|
|
||||||
// Verify modifications are visible
|
|
||||||
if event.Data["mutable"] != "modified" {
|
|
||||||
t.Error("Data modification should be visible")
|
|
||||||
}
|
|
||||||
if event.Data["new_key"] != "new_value" {
|
|
||||||
t.Error("New data should be visible")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloudEvent should reflect the current state
|
|
||||||
cloudEvent := event.CloudEvent()
|
|
||||||
var parsedData map[string]any
|
|
||||||
json.Unmarshal(cloudEvent.Data, &parsedData)
|
|
||||||
|
|
||||||
if parsedData["mutable"] != "modified" {
|
|
||||||
t.Error("CloudEvent should reflect modified data")
|
|
||||||
}
|
|
||||||
if parsedData["new_key"] != "new_value" {
|
|
||||||
t.Error("CloudEvent should reflect new data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_Aborted_State(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, "abort.test", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initially not aborted
|
|
||||||
if event.Aborted != nil {
|
|
||||||
t.Error("Event should not be aborted initially")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate aborting the event
|
|
||||||
event.Aborted = ErrEventAborted
|
|
||||||
|
|
||||||
if event.Aborted != ErrEventAborted {
|
|
||||||
t.Error("Event should be marked as aborted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrEventAborted_Value(t *testing.T) {
|
|
||||||
if ErrEventAborted == nil {
|
|
||||||
t.Error("ErrEventAborted should not be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ErrEventAborted.Error() != "event aborted" {
|
|
||||||
t.Errorf("Expected 'event aborted', got '%s'", ErrEventAborted.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_UniqueIDs(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
const numEvents = 1000
|
|
||||||
ids := make(map[string]bool)
|
|
||||||
|
|
||||||
for i := 0; i < numEvents; i++ {
|
|
||||||
event, err := NewEvent(ctx, "unique.test", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event %d: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
idStr := event.ID().String()
|
|
||||||
if ids[idStr] {
|
|
||||||
t.Errorf("Duplicate event ID: %s", idStr)
|
|
||||||
}
|
|
||||||
ids[idStr] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_TimestampProgression(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Create events with small delays
|
|
||||||
events := make([]Event, 5)
|
|
||||||
for i := range events {
|
|
||||||
var err error
|
|
||||||
events[i], err = NewEvent(ctx, "time.test", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event %d: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if i < len(events)-1 {
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify timestamps are in ascending order
|
|
||||||
for i := 1; i < len(events); i++ {
|
|
||||||
if !events[i].Timestamp().After(events[i-1].Timestamp()) {
|
|
||||||
t.Errorf("Event %d timestamp (%v) should be after event %d timestamp (%v)",
|
|
||||||
i, events[i].Timestamp(), i-1, events[i-1].Timestamp())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_JSON_Serialization(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
eventData := map[string]any{
|
|
||||||
"string": "value",
|
|
||||||
"number": 42,
|
|
||||||
"boolean": true,
|
|
||||||
"array": []any{1, 2, 3},
|
|
||||||
"object": map[string]any{"nested": "value"},
|
|
||||||
}
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, "json.test", eventData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudEvent := event.CloudEvent()
|
|
||||||
|
|
||||||
// CloudEvent should be JSON serializable
|
|
||||||
cloudEventJSON, err := json.Marshal(cloudEvent)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to marshal CloudEvent: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be able to unmarshal back
|
|
||||||
var parsed CloudEvent
|
|
||||||
err = json.Unmarshal(cloudEventJSON, &parsed)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal CloudEvent: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify key fields survived round-trip
|
|
||||||
if parsed.ID != cloudEvent.ID {
|
|
||||||
t.Errorf("ID mismatch after round-trip")
|
|
||||||
}
|
|
||||||
if parsed.Source != cloudEvent.Source {
|
|
||||||
t.Errorf("Source mismatch after round-trip")
|
|
||||||
}
|
|
||||||
if parsed.Type != cloudEvent.Type {
|
|
||||||
t.Errorf("Type mismatch after round-trip")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_EmptyData(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Test with empty map
|
|
||||||
event1, err := NewEvent(ctx, "empty.map", map[string]any{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event with empty map: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudEvent1 := event1.CloudEvent()
|
|
||||||
var parsed1 map[string]any
|
|
||||||
json.Unmarshal(cloudEvent1.Data, &parsed1)
|
|
||||||
if len(parsed1) != 0 {
|
|
||||||
t.Error("Expected empty data map")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with nil data
|
|
||||||
event2, err := NewEvent(ctx, "nil.data", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event with nil data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudEvent2 := event2.CloudEvent()
|
|
||||||
if cloudEvent2.Data == nil {
|
|
||||||
t.Error("CloudEvent data should not be nil even with nil input")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_Origin_WithModule(t *testing.T) {
|
|
||||||
mockMod := &mockEventModule{}
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Set module in ancestry
|
|
||||||
ctx.ancestry = []Module{mockMod}
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, "module.test", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Origin() != mockMod {
|
|
||||||
t.Error("Expected event origin to be the mock module")
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudEvent := event.CloudEvent()
|
|
||||||
expectedSource := string(mockMod.CaddyModule().ID)
|
|
||||||
if cloudEvent.Source != expectedSource {
|
|
||||||
t.Errorf("Expected source '%s', got '%s'", expectedSource, cloudEvent.Source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_LargeData(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Create event with large data
|
|
||||||
largeData := make(map[string]any)
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
largeData[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, "large.data", largeData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event with large data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloudEvent should handle large data
|
|
||||||
cloudEvent := event.CloudEvent()
|
|
||||||
|
|
||||||
var parsedData map[string]any
|
|
||||||
err = json.Unmarshal(cloudEvent.Data, &parsedData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse large data in CloudEvent: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parsedData) != len(largeData) {
|
|
||||||
t.Errorf("Expected %d data items, got %d", len(largeData), len(parsedData))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_SpecialCharacters_InData(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
specialData := map[string]any{
|
|
||||||
"unicode": "🚀✨",
|
|
||||||
"newlines": "line1\nline2\r\nline3",
|
|
||||||
"quotes": `"double" and 'single' quotes`,
|
|
||||||
"backslashes": "\\path\\to\\file",
|
|
||||||
"json_chars": `{"key": "value"}`,
|
|
||||||
"empty": "",
|
|
||||||
"null_value": nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, "special.chars", specialData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudEvent := event.CloudEvent()
|
|
||||||
|
|
||||||
// Should produce valid JSON
|
|
||||||
var parsedData map[string]any
|
|
||||||
err = json.Unmarshal(cloudEvent.Data, &parsedData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse data with special characters: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify some special cases survived JSON round-trip
|
|
||||||
if parsedData["unicode"] != "🚀✨" {
|
|
||||||
t.Error("Unicode characters should survive JSON encoding")
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedData["quotes"] != `"double" and 'single' quotes` {
|
|
||||||
t.Error("Quotes should be properly escaped in JSON")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_ConcurrentCreation(t *testing.T) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
const numGoroutines = 100
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
events := make([]Event, numGoroutines)
|
|
||||||
errors := make([]error, numGoroutines)
|
|
||||||
|
|
||||||
// Create events concurrently
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(index int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
eventData := map[string]any{
|
|
||||||
"goroutine": index,
|
|
||||||
"timestamp": time.Now().UnixNano(),
|
|
||||||
}
|
|
||||||
|
|
||||||
events[index], errors[index] = NewEvent(ctx, "concurrent.test", eventData)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Verify all events were created successfully
|
|
||||||
ids := make(map[string]bool)
|
|
||||||
for i, event := range events {
|
|
||||||
if errors[i] != nil {
|
|
||||||
t.Errorf("Goroutine %d: Failed to create event: %v", i, errors[i])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify unique IDs
|
|
||||||
idStr := event.ID().String()
|
|
||||||
if ids[idStr] {
|
|
||||||
t.Errorf("Duplicate event ID: %s", idStr)
|
|
||||||
}
|
|
||||||
ids[idStr] = true
|
|
||||||
|
|
||||||
// Verify data integrity
|
|
||||||
if goroutineID, exists := event.Data["goroutine"]; !exists || goroutineID != i {
|
|
||||||
t.Errorf("Event %d: Data corruption detected", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock module for event testing
|
|
||||||
type mockEventModule struct{}
|
|
||||||
|
|
||||||
func (m *mockEventModule) CaddyModule() ModuleInfo {
|
|
||||||
return ModuleInfo{
|
|
||||||
ID: "test.event.module",
|
|
||||||
New: func() Module { return new(mockEventModule) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvent_TimeAccuracy(t *testing.T) {
|
|
||||||
before := time.Now()
|
|
||||||
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
event, err := NewEvent(ctx, "time.accuracy", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create event: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
after := time.Now()
|
|
||||||
eventTime := event.Timestamp()
|
|
||||||
|
|
||||||
// Event timestamp should be between before and after
|
|
||||||
if eventTime.Before(before) || eventTime.After(after) {
|
|
||||||
t.Errorf("Event timestamp %v should be between %v and %v", eventTime, before, after)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkNewEvent(b *testing.B) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
eventData := map[string]any{
|
|
||||||
"key1": "value1",
|
|
||||||
"key2": 42,
|
|
||||||
"key3": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
NewEvent(ctx, "benchmark.test", eventData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkEvent_CloudEvent(b *testing.B) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
event, _ := NewEvent(ctx, "benchmark.cloud", map[string]any{
|
|
||||||
"data": "test",
|
|
||||||
"num": 123,
|
|
||||||
})
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
event.CloudEvent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkEvent_CloudEvent_LargeData(b *testing.B) {
|
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Create event with substantial data
|
|
||||||
largeData := make(map[string]any)
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
largeData[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
event, _ := NewEvent(ctx, "benchmark.large", largeData)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
event.CloudEvent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
//go:build !windows
|
|
||||||
|
|
||||||
package caddy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFastAbs(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
checkFunc func(result string, err error) error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "absolute path",
|
|
||||||
input: "/usr/local/bin",
|
|
||||||
checkFunc: func(result string, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
if result != "/usr/local/bin" {
|
|
||||||
t.Errorf("expected /usr/local/bin, got %s", result)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "absolute path with dots",
|
|
||||||
input: "/usr/local/../bin",
|
|
||||||
checkFunc: func(result string, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
if result != "/usr/bin" {
|
|
||||||
t.Errorf("expected /usr/bin, got %s", result)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "relative path",
|
|
||||||
input: "relative/path",
|
|
||||||
checkFunc: func(result string, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(result) {
|
|
||||||
t.Errorf("expected absolute path, got %s", result)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(result, "relative/path") {
|
|
||||||
t.Errorf("expected path to end with 'relative/path', got %s", result)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dot",
|
|
||||||
input: ".",
|
|
||||||
checkFunc: func(result string, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(result) {
|
|
||||||
t.Errorf("expected absolute path, got %s", result)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dot dot",
|
|
||||||
input: "..",
|
|
||||||
checkFunc: func(result string, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(result) {
|
|
||||||
t.Errorf("expected absolute path, got %s", result)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty string",
|
|
||||||
input: "",
|
|
||||||
checkFunc: func(result string, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
// Empty string should resolve to current directory
|
|
||||||
if !filepath.IsAbs(result) {
|
|
||||||
t.Errorf("expected absolute path, got %s", result)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "complex relative path",
|
|
||||||
input: "./foo/../bar/./baz",
|
|
||||||
checkFunc: func(result string, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(result) {
|
|
||||||
t.Errorf("expected absolute path, got %s", result)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(result, "bar/baz") {
|
|
||||||
t.Errorf("expected path to end with 'bar/baz', got %s", result)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result, err := FastAbs(tt.input)
|
|
||||||
tt.checkFunc(result, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFastAbsVsFilepathAbs compares FastAbs with filepath.Abs to ensure consistent behavior
|
|
||||||
func TestFastAbsVsFilepathAbs(t *testing.T) {
|
|
||||||
// Skip if working directory cannot be determined
|
|
||||||
if wderr != nil {
|
|
||||||
t.Skip("working directory error, skipping comparison test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testPaths := []string{
|
|
||||||
".",
|
|
||||||
"..",
|
|
||||||
"foo",
|
|
||||||
"foo/bar",
|
|
||||||
"./foo",
|
|
||||||
"../foo",
|
|
||||||
"/absolute/path",
|
|
||||||
"/usr/local/bin",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range testPaths {
|
|
||||||
t.Run(path, func(t *testing.T) {
|
|
||||||
fast, fastErr := FastAbs(path)
|
|
||||||
std, stdErr := filepath.Abs(path)
|
|
||||||
|
|
||||||
// Both should succeed or fail together
|
|
||||||
if (fastErr != nil) != (stdErr != nil) {
|
|
||||||
t.Errorf("error mismatch: FastAbs=%v, filepath.Abs=%v", fastErr, stdErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If both succeed, results should be the same
|
|
||||||
if fastErr == nil && stdErr == nil && fast != std {
|
|
||||||
t.Errorf("result mismatch for %q: FastAbs=%s, filepath.Abs=%s", path, fast, std)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFastAbsErrorHandling tests error handling when working directory is unavailable
|
|
||||||
func TestFastAbsErrorHandling(t *testing.T) {
|
|
||||||
// This tests the cached wderr behavior
|
|
||||||
if wderr != nil {
|
|
||||||
// Test that FastAbs properly returns the cached error for relative paths
|
|
||||||
_, err := FastAbs("relative/path")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for relative path when working directory is unavailable")
|
|
||||||
}
|
|
||||||
if err != wderr {
|
|
||||||
t.Errorf("expected cached wderr, got different error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkFastAbs benchmarks FastAbs
|
|
||||||
func BenchmarkFastAbs(b *testing.B) {
|
|
||||||
paths := []string{
|
|
||||||
"relative/path",
|
|
||||||
"/absolute/path",
|
|
||||||
".",
|
|
||||||
"..",
|
|
||||||
"./foo/bar",
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
FastAbs(paths[i%len(paths)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkFastAbsVsStdLib compares performance of FastAbs vs filepath.Abs
|
|
||||||
func BenchmarkFastAbsVsStdLib(b *testing.B) {
|
|
||||||
path := "relative/path/to/file"
|
|
||||||
|
|
||||||
b.Run("FastAbs", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
FastAbs(path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("filepath.Abs", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
filepath.Abs(path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package caddy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Mock filesystem implementation for testing
|
|
||||||
type mockFileSystem struct {
|
|
||||||
name string
|
|
||||||
files map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFileSystem) Open(name string) (fs.File, error) {
|
|
||||||
if content, exists := m.files[name]; exists {
|
|
||||||
return &mockFile{name: name, content: content}, nil
|
|
||||||
}
|
|
||||||
return nil, fs.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockFile struct {
|
|
||||||
name string
|
|
||||||
content string
|
|
||||||
pos int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Stat() (fs.FileInfo, error) {
|
|
||||||
return &mockFileInfo{name: m.name, size: int64(len(m.content))}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Read(b []byte) (int, error) {
|
|
||||||
if m.pos >= len(m.content) {
|
|
||||||
return 0, fs.ErrClosed
|
|
||||||
}
|
|
||||||
n := copy(b, m.content[m.pos:])
|
|
||||||
m.pos += n
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockFileInfo struct {
|
|
||||||
name string
|
|
||||||
size int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFileInfo) Name() string { return m.name }
|
|
||||||
func (m *mockFileInfo) Size() int64 { return m.size }
|
|
||||||
func (m *mockFileInfo) Mode() fs.FileMode { return 0o644 }
|
|
||||||
func (m *mockFileInfo) ModTime() time.Time {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
func (m *mockFileInfo) IsDir() bool { return false }
|
|
||||||
func (m *mockFileInfo) Sys() any { return nil }
|
|
||||||
|
|
||||||
// Mock FileSystems implementation for testing
|
|
||||||
type mockFileSystems struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
filesystems map[string]fs.FS
|
|
||||||
defaultFS fs.FS
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMockFileSystems() *mockFileSystems {
|
|
||||||
return &mockFileSystems{
|
|
||||||
filesystems: make(map[string]fs.FS),
|
|
||||||
defaultFS: &mockFileSystem{name: "default", files: map[string]string{"default.txt": "default content"}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFileSystems) Register(k string, v fs.FS) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
m.filesystems[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFileSystems) Unregister(k string) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
delete(m.filesystems, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFileSystems) Get(k string) (fs.FS, bool) {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
v, ok := m.filesystems[k]
|
|
||||||
return v, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFileSystems) Default() fs.FS {
|
|
||||||
return m.defaultFS
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystems_Register_Get(t *testing.T) {
|
|
||||||
fsys := newMockFileSystems()
|
|
||||||
mockFS := &mockFileSystem{
|
|
||||||
name: "test",
|
|
||||||
files: map[string]string{"test.txt": "test content"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register filesystem
|
|
||||||
fsys.Register("test", mockFS)
|
|
||||||
|
|
||||||
// Retrieve filesystem
|
|
||||||
retrieved, exists := fsys.Get("test")
|
|
||||||
if !exists {
|
|
||||||
t.Error("Expected filesystem to exist after registration")
|
|
||||||
}
|
|
||||||
if retrieved != mockFS {
|
|
||||||
t.Error("Retrieved filesystem is not the same as registered")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystems_Unregister(t *testing.T) {
|
|
||||||
fsys := newMockFileSystems()
|
|
||||||
mockFS := &mockFileSystem{name: "test"}
|
|
||||||
|
|
||||||
// Register then unregister
|
|
||||||
fsys.Register("test", mockFS)
|
|
||||||
fsys.Unregister("test")
|
|
||||||
|
|
||||||
// Should not exist after unregistration
|
|
||||||
_, exists := fsys.Get("test")
|
|
||||||
if exists {
|
|
||||||
t.Error("Filesystem should not exist after unregistration")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystems_Default(t *testing.T) {
|
|
||||||
fsys := newMockFileSystems()
|
|
||||||
|
|
||||||
defaultFS := fsys.Default()
|
|
||||||
if defaultFS == nil {
|
|
||||||
t.Error("Default filesystem should not be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that default filesystem works
|
|
||||||
file, err := defaultFS.Open("default.txt")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to open default file: %v", err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
data := make([]byte, 100)
|
|
||||||
n, err := file.Read(data)
|
|
||||||
if err != nil && err != fs.ErrClosed {
|
|
||||||
t.Fatalf("Failed to read default file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
content := string(data[:n])
|
|
||||||
if content != "default content" {
|
|
||||||
t.Errorf("Expected 'default content', got '%s'", content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystems_Concurrent_Access(t *testing.T) {
|
|
||||||
fsys := newMockFileSystems()
|
|
||||||
|
|
||||||
const numGoroutines = 50
|
|
||||||
const numOperations = 10
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
// Concurrent register/unregister/get operations
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
key := fmt.Sprintf("fs-%d", id)
|
|
||||||
mockFS := &mockFileSystem{
|
|
||||||
name: key,
|
|
||||||
files: map[string]string{key + ".txt": "content"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for j := 0; j < numOperations; j++ {
|
|
||||||
// Register
|
|
||||||
fsys.Register(key, mockFS)
|
|
||||||
|
|
||||||
// Get
|
|
||||||
retrieved, exists := fsys.Get(key)
|
|
||||||
if !exists {
|
|
||||||
t.Errorf("Filesystem %s should exist", key)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if retrieved != mockFS {
|
|
||||||
t.Errorf("Retrieved filesystem for %s is not correct", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test file access
|
|
||||||
file, err := retrieved.Open(key + ".txt")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to open file in %s: %v", key, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
file.Close()
|
|
||||||
|
|
||||||
// Unregister
|
|
||||||
fsys.Unregister(key)
|
|
||||||
|
|
||||||
// Should not exist after unregister
|
|
||||||
_, stillExists := fsys.Get(key)
|
|
||||||
if stillExists {
|
|
||||||
t.Errorf("Filesystem %s should not exist after unregister", key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystems_Get_NonExistent(t *testing.T) {
|
|
||||||
fsys := newMockFileSystems()
|
|
||||||
|
|
||||||
_, exists := fsys.Get("non-existent")
|
|
||||||
if exists {
|
|
||||||
t.Error("Non-existent filesystem should not exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystems_Register_Overwrite(t *testing.T) {
|
|
||||||
fsys := newMockFileSystems()
|
|
||||||
key := "overwrite-test"
|
|
||||||
|
|
||||||
// Register first filesystem
|
|
||||||
fs1 := &mockFileSystem{name: "fs1"}
|
|
||||||
fsys.Register(key, fs1)
|
|
||||||
|
|
||||||
// Register second filesystem with same key (should overwrite)
|
|
||||||
fs2 := &mockFileSystem{name: "fs2"}
|
|
||||||
fsys.Register(key, fs2)
|
|
||||||
|
|
||||||
// Should get the second filesystem
|
|
||||||
retrieved, exists := fsys.Get(key)
|
|
||||||
if !exists {
|
|
||||||
t.Error("Filesystem should exist")
|
|
||||||
}
|
|
||||||
if retrieved != fs2 {
|
|
||||||
t.Error("Should get the overwritten filesystem")
|
|
||||||
}
|
|
||||||
if retrieved == fs1 {
|
|
||||||
t.Error("Should not get the original filesystem")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystems_Concurrent_RegisterUnregister_SameKey(t *testing.T) {
|
|
||||||
fsys := newMockFileSystems()
|
|
||||||
key := "concurrent-key"
|
|
||||||
|
|
||||||
const numGoroutines = 20
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
// Half the goroutines register, half unregister
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
if i%2 == 0 {
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
mockFS := &mockFileSystem{name: fmt.Sprintf("fs-%d", id)}
|
|
||||||
fsys.Register(key, mockFS)
|
|
||||||
}(i)
|
|
||||||
} else {
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
fsys.Unregister(key)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// The final state is unpredictable due to race conditions,
|
|
||||||
// but the operations should not panic or cause corruption
|
|
||||||
// Test passes if we reach here without issues
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystems_StressTest(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping stress test in short mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
fsys := newMockFileSystems()
|
|
||||||
|
|
||||||
const numGoroutines = 100
|
|
||||||
const duration = 100 * time.Millisecond
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
stopChan := make(chan struct{})
|
|
||||||
|
|
||||||
// Start timer
|
|
||||||
go func() {
|
|
||||||
time.Sleep(duration)
|
|
||||||
close(stopChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Stress test with continuous operations
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
key := fmt.Sprintf("stress-fs-%d", id%10) // Use limited set of keys
|
|
||||||
mockFS := &mockFileSystem{
|
|
||||||
name: key,
|
|
||||||
files: map[string]string{key + ".txt": "stress content"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stopChan:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// Rapid register/get/unregister cycles
|
|
||||||
fsys.Register(key, mockFS)
|
|
||||||
|
|
||||||
if retrieved, exists := fsys.Get(key); exists {
|
|
||||||
// Try to use the filesystem
|
|
||||||
if file, err := retrieved.Open(key + ".txt"); err == nil {
|
|
||||||
file.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fsys.Unregister(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Test passes if we reach here without panics or deadlocks
|
|
||||||
}
|
|
||||||
@@ -1,58 +1,56 @@
|
|||||||
module github.com/caddyserver/caddy/v2
|
module github.com/caddyserver/caddy/v2
|
||||||
|
|
||||||
go 1.25.1
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
github.com/DeRuina/timberjack v1.4.2
|
github.com/DeRuina/timberjack v1.3.9
|
||||||
github.com/KimMachineGun/automemlimit v0.7.5
|
github.com/KimMachineGun/automemlimit v0.7.5
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0
|
github.com/Masterminds/sprig/v3 v3.3.0
|
||||||
github.com/alecthomas/chroma/v2 v2.23.1
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
||||||
github.com/caddyserver/certmagic v0.25.3
|
github.com/caddyserver/certmagic v0.25.2
|
||||||
github.com/caddyserver/zerossl v0.1.5
|
github.com/caddyserver/zerossl v0.1.5
|
||||||
github.com/cloudflare/circl v1.6.3
|
github.com/cloudflare/circl v1.6.3
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/google/cel-go v0.28.0
|
github.com/google/cel-go v0.27.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/klauspost/compress v1.18.5
|
github.com/klauspost/compress v1.18.4
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0
|
github.com/klauspost/cpuid/v2 v2.3.0
|
||||||
github.com/mholt/acmez/v3 v3.1.6
|
github.com/mholt/acmez/v3 v3.1.6
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/quic-go/quic-go v0.59.1
|
github.com/quic-go/quic-go v0.59.0
|
||||||
github.com/smallstep/certificates v0.30.2
|
github.com/smallstep/certificates v0.30.0-rc3
|
||||||
github.com/smallstep/nosql v0.8.0
|
github.com/smallstep/nosql v0.7.0
|
||||||
github.com/smallstep/truststore v0.13.0
|
github.com/smallstep/truststore v0.13.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747
|
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747
|
||||||
github.com/yuin/goldmark v1.8.2
|
github.com/yuin/goldmark v1.7.16
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
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/exporters/autoexport v0.65.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0
|
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0
|
||||||
go.opentelemetry.io/otel v1.43.0
|
go.opentelemetry.io/otel v1.40.0
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0
|
go.opentelemetry.io/otel/sdk v1.40.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
go.step.sm/crypto v0.76.2
|
||||||
go.step.sm/crypto v0.81.0
|
|
||||||
go.uber.org/automaxprocs v1.6.0
|
go.uber.org/automaxprocs v1.6.0
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
go.uber.org/zap/exp v0.3.0
|
go.uber.org/zap/exp v0.3.0
|
||||||
golang.org/x/crypto v0.51.0
|
golang.org/x/crypto v0.48.0
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
|
||||||
golang.org/x/net v0.53.0
|
golang.org/x/net v0.51.0
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.19.0
|
||||||
golang.org/x/term v0.43.0
|
golang.org/x/term v0.40.0
|
||||||
golang.org/x/time v0.15.0
|
golang.org/x/time v0.14.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cel.dev/expr v0.25.1 // indirect
|
cel.dev/expr v0.25.1 // indirect
|
||||||
cloud.google.com/go/auth v0.20.0 // indirect
|
cloud.google.com/go/auth v0.18.1 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
@@ -63,17 +61,17 @@ require (
|
|||||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
|
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
|
||||||
github.com/google/go-tpm v0.9.8 // indirect
|
github.com/google/go-tpm v0.9.8 // indirect
|
||||||
github.com/google/go-tspi v0.3.0 // indirect
|
github.com/google/go-tspi v0.3.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||||
@@ -89,29 +87,31 @@ require (
|
|||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/aws v1.43.0 // indirect
|
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 // indirect
|
go.opentelemetry.io/contrib/propagators/aws v1.40.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 // indirect
|
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/ot v1.43.0 // indirect
|
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect
|
go.opentelemetry.io/contrib/propagators/ot v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect
|
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/log v0.19.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
|
go.opentelemetry.io/otel/log v0.16.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.4 // 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.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/oauth2 v0.36.0 // indirect
|
golang.org/x/oauth2 v0.35.0 // indirect
|
||||||
google.golang.org/api v0.277.0 // indirect
|
google.golang.org/api v0.266.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // 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/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/libdns/libdns v1.1.1
|
github.com/libdns/libdns v1.1.1
|
||||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // 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/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2
|
github.com/prometheus/client_model v0.6.2
|
||||||
github.com/prometheus/common v0.67.5 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.20.1 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.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/slackhq/nebula v1.10.3 // indirect
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
github.com/urfave/cli v1.22.17 // indirect
|
github.com/urfave/cli v1.22.17 // indirect
|
||||||
go.etcd.io/bbolt v1.4.3 // indirect
|
go.etcd.io/bbolt v1.3.10 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0
|
go.opentelemetry.io/otel/trace v1.40.0
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/mod v0.35.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.44.0
|
golang.org/x/sys v0.41.0
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.34.0
|
||||||
golang.org/x/tools v0.44.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
google.golang.org/grpc v1.81.0 // indirect
|
google.golang.org/grpc v1.79.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
howett.net/plist v1.0.0 // indirect
|
howett.net/plist v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
|||||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
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 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||||
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||||
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
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/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 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
|
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||||
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
|
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||||
cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE=
|
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
|
||||||
cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U=
|
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
|
||||||
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
|
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||||
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
|
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
@@ -28,8 +28,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
|
github.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo=
|
||||||
github.com/DeRuina/timberjack v1.4.2/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
github.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||||
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
|
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
|
||||||
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
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/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 h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
|
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.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
|
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
|
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.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
|
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.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
|
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.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
|
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.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
|
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.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
|
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.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
|
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.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
|
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/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
|
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.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
|
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.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
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.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
|
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.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
|
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.51.1 h1:zuSf4olLKZW8cF/W9Y5wvGT+/0raY/3kVp49KsGs0QY=
|
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.51.1/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI=
|
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.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
|
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.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
|
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.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
|
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.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
|
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.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
|
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.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
|
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.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
|
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.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
|
||||||
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
|
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||||
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
|
github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
|
||||||
github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
|
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
|
||||||
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
||||||
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||||
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
|
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
|
||||||
@@ -149,17 +149,17 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
|||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.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 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
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/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 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
|
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
|
||||||
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
|
||||||
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
github.com/google/certificate-transparency-go v1.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 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
||||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
||||||
@@ -179,20 +179,20 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
github.com/google/go-tpm-tools v0.4.8 h1:V4oIYyAD3BykOycwYQzO29WefDouQMTsYZqmG3HxOfM=
|
github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws=
|
||||||
github.com/google/go-tpm-tools v0.4.8/go.mod h1:4DfiOtiS1KppJjwf1+tqtW4K3PrCJjAAqFKj/TYTJKg=
|
github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ=
|
||||||
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
||||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
||||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||||
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
|
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||||
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
|
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
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 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
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/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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
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.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
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/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
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/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 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||||
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
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/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
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/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 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/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
||||||
github.com/smallstep/certificates v0.30.2 h1:1G3xBi8sJ740iA1mMPW2Svv7EIZKJ4Zf/iQtA5QlN0Y=
|
github.com/smallstep/certificates v0.30.0-rc3 h1:Lx/NNJ4n+L3Pyx5NtVRGXeqviPPXTFFGLRiC1fCwU50=
|
||||||
github.com/smallstep/certificates v0.30.2/go.mod h1:oyaE/aEYUGDr+YiCZLAxxP22bOQqcSHTeDgp8Vv2rlY=
|
github.com/smallstep/certificates v0.30.0-rc3/go.mod h1:e5/ylYYpvnjCVZz6RpyOkpTe73EGPYoL+8TZZ5EtLjI=
|
||||||
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
|
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/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 h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
||||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
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 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
|
||||||
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
|
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
|
||||||
github.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A=
|
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
|
||||||
github.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo=
|
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
|
||||||
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
|
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/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
|
||||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
|
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/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.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.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.16/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 h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||||
@@ -369,70 +369,70 @@ 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/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 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
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/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w=
|
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o=
|
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
|
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/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
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 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s=
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE=
|
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE=
|
||||||
go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg=
|
go.opentelemetry.io/contrib/propagators/aws v1.40.0 h1:4VIrh75jW4RTimUNx1DSk+6H9/nDr1FvmKoOVDh3K04=
|
||||||
go.opentelemetry.io/contrib/propagators/aws v1.43.0/go.mod h1:CJjTym6F87tEdm61Qvnz5xrV8vKlH4C92djiqcn62k8=
|
go.opentelemetry.io/contrib/propagators/aws v1.40.0/go.mod h1:B0dCov9KNQGlut3T8wZZjDnLXEXdBroM7bFsHh/gRos=
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=
|
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg=
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw=
|
go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs=
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 h1:peiLMz1+aqJE+3L4mOVtR9wlmv+yh/JVYXCBjqmzJJE=
|
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 h1:aXl9uobjJs5vquMLt9ZkI/3zIuz8XQ3TqOKSWx0/xdU=
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0/go.mod h1:Agvif+4A8p/3UtZzJ0MCcDEuQwgtrzM71DueU41DCs8=
|
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0/go.mod h1:ioMePqe6k6c/ovXSkmkMr1mbN5qRBGJxNTVop7/2XO0=
|
||||||
go.opentelemetry.io/contrib/propagators/ot v1.43.0 h1:Hh1HahlGc81AOE7siqi1tVOlbanY/UxMMWedpb0d5oQ=
|
go.opentelemetry.io/contrib/propagators/ot v1.40.0 h1:Lon8J5SPmWaL1Ko2TIlCNHJ42/J1b5XbJlgJaE/9m7I=
|
||||||
go.opentelemetry.io/contrib/propagators/ot v1.43.0/go.mod h1:58MlyS7lghzYvAm5LN9gGmZpCMQEMB5vpZp9SRgOyE4=
|
go.opentelemetry.io/contrib/propagators/ot v1.40.0/go.mod h1:dKWtJTlp1Yj+8Cneye5idO46eRPIbi23qVuJYKjNnvY=
|
||||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
|
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.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
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.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
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.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
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.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE=
|
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo=
|
go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
|
||||||
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
|
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
|
||||||
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
|
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||||
go.step.sm/crypto v0.81.0 h1:e+ouzpNt3Xm4dp7HGXhgYB5y4iFik3vh3phHKWmvugU=
|
go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4=
|
||||||
go.step.sm/crypto v0.81.0/go.mod h1:fsTizqQeASjTXnbv9O00XtRlIuXRkCdoRiJNyXGQujc=
|
go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q=
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
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/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
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 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 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
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=
|
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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 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.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.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.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.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.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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.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.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.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
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.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.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.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk=
|
google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
|
||||||
google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
|
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
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-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
|
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||||
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
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=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
package filesystems
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"testing"
|
|
||||||
"testing/fstest"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFileSystemMapDefaultKey(t *testing.T) {
|
|
||||||
m := &FileSystemMap{}
|
|
||||||
|
|
||||||
// Empty key should map to default
|
|
||||||
if m.key("") != DefaultFileSystemKey {
|
|
||||||
t.Errorf("empty key should map to %q, got %q", DefaultFileSystemKey, m.key(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-empty key should be returned as-is
|
|
||||||
if m.key("custom") != "custom" {
|
|
||||||
t.Errorf("non-empty key should be returned as-is, got %q", m.key("custom"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystemMapRegisterAndGet(t *testing.T) {
|
|
||||||
m := &FileSystemMap{}
|
|
||||||
testFS := fstest.MapFS{
|
|
||||||
"hello.txt": &fstest.MapFile{Data: []byte("hello")},
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Register("test", testFS)
|
|
||||||
|
|
||||||
got, ok := m.Get("test")
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected to find registered filesystem")
|
|
||||||
}
|
|
||||||
if got == nil {
|
|
||||||
t.Fatal("expected non-nil filesystem")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the filesystem works
|
|
||||||
f, err := got.Open("hello.txt")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Open() error = %v", err)
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystemMapGetNonExistent(t *testing.T) {
|
|
||||||
m := &FileSystemMap{}
|
|
||||||
|
|
||||||
_, ok := m.Get("nonexistent")
|
|
||||||
if ok {
|
|
||||||
t.Error("expected Get to return false for nonexistent key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystemMapDefault(t *testing.T) {
|
|
||||||
m := &FileSystemMap{}
|
|
||||||
|
|
||||||
d := m.Default()
|
|
||||||
if d == nil {
|
|
||||||
t.Fatal("Default() should never return nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystemMapGetDefaultLazyInit(t *testing.T) {
|
|
||||||
m := &FileSystemMap{}
|
|
||||||
|
|
||||||
// Getting the default key before any registration should
|
|
||||||
// auto-initialize to DefaultFileSystem
|
|
||||||
got, ok := m.Get(DefaultFileSystemKey)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected default filesystem to be auto-initialized")
|
|
||||||
}
|
|
||||||
if got == nil {
|
|
||||||
t.Fatal("expected non-nil default filesystem")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystemMapUnregister(t *testing.T) {
|
|
||||||
m := &FileSystemMap{}
|
|
||||||
testFS := fstest.MapFS{}
|
|
||||||
|
|
||||||
m.Register("test", testFS)
|
|
||||||
m.Unregister("test")
|
|
||||||
|
|
||||||
_, ok := m.Get("test")
|
|
||||||
if ok {
|
|
||||||
t.Error("expected filesystem to be unregistered")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystemMapUnregisterDefault(t *testing.T) {
|
|
||||||
m := &FileSystemMap{}
|
|
||||||
customFS := fstest.MapFS{}
|
|
||||||
|
|
||||||
// Override default
|
|
||||||
m.Register("", customFS)
|
|
||||||
// Unregister default should reset to OsFS, not delete
|
|
||||||
m.Unregister("")
|
|
||||||
|
|
||||||
d := m.Default()
|
|
||||||
if d == nil {
|
|
||||||
t.Fatal("unregistering default should reset it, not delete it")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystemMapRegisterNil(t *testing.T) {
|
|
||||||
m := &FileSystemMap{}
|
|
||||||
testFS := fstest.MapFS{}
|
|
||||||
|
|
||||||
// Register then register nil (should unregister)
|
|
||||||
m.Register("test", testFS)
|
|
||||||
m.Register("test", nil)
|
|
||||||
|
|
||||||
_, ok := m.Get("test")
|
|
||||||
if ok {
|
|
||||||
t.Error("registering nil should unregister the filesystem")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystemMapEmptyKeyIsDefault(t *testing.T) {
|
|
||||||
m := &FileSystemMap{}
|
|
||||||
testFS := fstest.MapFS{
|
|
||||||
"test.txt": &fstest.MapFile{Data: []byte("test")},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register with empty key should register as default
|
|
||||||
m.Register("", testFS)
|
|
||||||
|
|
||||||
got, ok := m.Get("")
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected to find filesystem registered with empty key")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should also be accessible via default key
|
|
||||||
got2, ok := m.Get(DefaultFileSystemKey)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected to find filesystem via default key")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both should work
|
|
||||||
if got == nil || got2 == nil {
|
|
||||||
t.Fatal("expected non-nil filesystems")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystemMapGetTrimsWhitespace(t *testing.T) {
|
|
||||||
m := &FileSystemMap{}
|
|
||||||
testFS := fstest.MapFS{}
|
|
||||||
|
|
||||||
m.Register("test", testFS)
|
|
||||||
|
|
||||||
// Get with whitespace-padded key should match
|
|
||||||
got, ok := m.Get("test ")
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected Get to trim whitespace from key")
|
|
||||||
}
|
|
||||||
if got == nil {
|
|
||||||
t.Fatal("expected non-nil filesystem")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOsFSInterfaces(t *testing.T) {
|
|
||||||
var osFS OsFS
|
|
||||||
|
|
||||||
// Verify interface compliance at compile time (already done with var _ checks)
|
|
||||||
// but test that the methods exist and are callable
|
|
||||||
var _ fs.FS = osFS
|
|
||||||
var _ fs.StatFS = osFS
|
|
||||||
var _ fs.GlobFS = osFS
|
|
||||||
var _ fs.ReadDirFS = osFS
|
|
||||||
var _ fs.ReadFileFS = osFS
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
"go.uber.org/zap/zaptest/observer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLogBufferCoreEnabled(t *testing.T) {
|
|
||||||
core := NewLogBufferCore(zapcore.InfoLevel)
|
|
||||||
|
|
||||||
if !core.Enabled(zapcore.InfoLevel) {
|
|
||||||
t.Error("expected InfoLevel to be enabled")
|
|
||||||
}
|
|
||||||
if !core.Enabled(zapcore.ErrorLevel) {
|
|
||||||
t.Error("expected ErrorLevel to be enabled")
|
|
||||||
}
|
|
||||||
if core.Enabled(zapcore.DebugLevel) {
|
|
||||||
t.Error("expected DebugLevel to be disabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogBufferCoreWriteAndFlush(t *testing.T) {
|
|
||||||
core := NewLogBufferCore(zapcore.InfoLevel)
|
|
||||||
|
|
||||||
// Write entries
|
|
||||||
entry1 := zapcore.Entry{Level: zapcore.InfoLevel, Message: "message1"}
|
|
||||||
entry2 := zapcore.Entry{Level: zapcore.WarnLevel, Message: "message2"}
|
|
||||||
|
|
||||||
if err := core.Write(entry1, []zapcore.Field{zap.String("key1", "val1")}); err != nil {
|
|
||||||
t.Fatalf("Write() error = %v", err)
|
|
||||||
}
|
|
||||||
if err := core.Write(entry2, []zapcore.Field{zap.String("key2", "val2")}); err != nil {
|
|
||||||
t.Fatalf("Write() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify entries are buffered
|
|
||||||
if len(core.entries) != 2 {
|
|
||||||
t.Errorf("expected 2 entries, got %d", len(core.entries))
|
|
||||||
}
|
|
||||||
if len(core.fields) != 2 {
|
|
||||||
t.Errorf("expected 2 field sets, got %d", len(core.fields))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up an observed logger to capture flushed entries
|
|
||||||
observedCore, logs := observer.New(zapcore.InfoLevel)
|
|
||||||
logger := zap.New(observedCore)
|
|
||||||
|
|
||||||
core.FlushTo(logger)
|
|
||||||
|
|
||||||
// Verify entries were flushed
|
|
||||||
if logs.Len() != 2 {
|
|
||||||
t.Errorf("expected 2 flushed log entries, got %d", logs.Len())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify buffer is cleared after flush
|
|
||||||
if len(core.entries) != 0 {
|
|
||||||
t.Errorf("expected entries to be cleared after flush, got %d", len(core.entries))
|
|
||||||
}
|
|
||||||
if len(core.fields) != 0 {
|
|
||||||
t.Errorf("expected fields to be cleared after flush, got %d", len(core.fields))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogBufferCoreSync(t *testing.T) {
|
|
||||||
core := NewLogBufferCore(zapcore.InfoLevel)
|
|
||||||
if err := core.Sync(); err != nil {
|
|
||||||
t.Errorf("Sync() error = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogBufferCoreWith(t *testing.T) {
|
|
||||||
core := NewLogBufferCore(zapcore.InfoLevel)
|
|
||||||
|
|
||||||
// With() currently returns the same core (known limitation)
|
|
||||||
result := core.With([]zapcore.Field{zap.String("test", "val")})
|
|
||||||
if result != core {
|
|
||||||
t.Error("With() should return the same core instance")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogBufferCoreCheck(t *testing.T) {
|
|
||||||
core := NewLogBufferCore(zapcore.InfoLevel)
|
|
||||||
|
|
||||||
// Check for enabled level should add core
|
|
||||||
entry := zapcore.Entry{Level: zapcore.InfoLevel, Message: "test"}
|
|
||||||
ce := &zapcore.CheckedEntry{}
|
|
||||||
result := core.Check(entry, ce)
|
|
||||||
if result == nil {
|
|
||||||
t.Error("Check() should return non-nil for enabled level")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for disabled level should not add core
|
|
||||||
debugEntry := zapcore.Entry{Level: zapcore.DebugLevel, Message: "test"}
|
|
||||||
ce2 := &zapcore.CheckedEntry{}
|
|
||||||
result2 := core.Check(debugEntry, ce2)
|
|
||||||
// The ce2 should be returned unchanged (no core added)
|
|
||||||
if result2 != ce2 {
|
|
||||||
t.Error("Check() should return unchanged CheckedEntry for disabled level")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogBufferCoreEmptyFlush(t *testing.T) {
|
|
||||||
core := NewLogBufferCore(zapcore.InfoLevel)
|
|
||||||
|
|
||||||
// Flushing with no entries should not panic
|
|
||||||
observedCore, logs := observer.New(zapcore.InfoLevel)
|
|
||||||
logger := zap.New(observedCore)
|
|
||||||
|
|
||||||
core.FlushTo(logger)
|
|
||||||
|
|
||||||
if logs.Len() != 0 {
|
|
||||||
t.Errorf("expected 0 flushed entries for empty buffer, got %d", logs.Len())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogBufferCoreConcurrentWrites(t *testing.T) {
|
|
||||||
core := NewLogBufferCore(zapcore.InfoLevel)
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
const numWriters = 10
|
|
||||||
const numWrites = 100
|
|
||||||
|
|
||||||
for i := 0; i < numWriters; i++ {
|
|
||||||
go func() {
|
|
||||||
defer func() { done <- struct{}{} }()
|
|
||||||
for j := 0; j < numWrites; j++ {
|
|
||||||
entry := zapcore.Entry{Level: zapcore.InfoLevel, Message: "concurrent"}
|
|
||||||
_ = core.Write(entry, nil)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < numWriters; i++ {
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
|
|
||||||
core.mu.Lock()
|
|
||||||
count := len(core.entries)
|
|
||||||
core.mu.Unlock()
|
|
||||||
|
|
||||||
if count != numWriters*numWrites {
|
|
||||||
t.Errorf("expected %d entries, got %d", numWriters*numWrites, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
|
|
||||||
// Headers with potentially sensitive information (Cookie, Set-Cookie,
|
|
||||||
// Authorization, and Proxy-Authorization) are logged with empty values.
|
|
||||||
type LoggableHTTPHeader struct {
|
|
||||||
http.Header
|
|
||||||
|
|
||||||
ShouldLogCredentials bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
|
|
||||||
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
|
||||||
if h.Header == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for key, val := range h.Header {
|
|
||||||
if !h.ShouldLogCredentials {
|
|
||||||
switch strings.ToLower(key) {
|
|
||||||
case "cookie", "set-cookie", "authorization", "proxy-authorization":
|
|
||||||
val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enc.AddArray(key, LoggableStringArray(val))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoggableStringArray makes a slice of strings marshalable for logging.
|
|
||||||
type LoggableStringArray []string
|
|
||||||
|
|
||||||
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
|
|
||||||
func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
|
|
||||||
if sa == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, s := range sa {
|
|
||||||
enc.AppendString(s)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface guards
|
|
||||||
var (
|
|
||||||
_ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil)
|
|
||||||
_ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil)
|
|
||||||
)
|
|
||||||
@@ -17,37 +17,6 @@ func TestSanitizeMethod(t *testing.T) {
|
|||||||
{method: "trace", expected: "TRACE"},
|
{method: "trace", expected: "TRACE"},
|
||||||
{method: "UNKNOWN", expected: "OTHER"},
|
{method: "UNKNOWN", expected: "OTHER"},
|
||||||
{method: strings.Repeat("ohno", 9999), expected: "OTHER"},
|
{method: strings.Repeat("ohno", 9999), expected: "OTHER"},
|
||||||
|
|
||||||
// Test all standard HTTP methods in uppercase
|
|
||||||
{method: "GET", expected: "GET"},
|
|
||||||
{method: "HEAD", expected: "HEAD"},
|
|
||||||
{method: "POST", expected: "POST"},
|
|
||||||
{method: "PUT", expected: "PUT"},
|
|
||||||
{method: "DELETE", expected: "DELETE"},
|
|
||||||
{method: "CONNECT", expected: "CONNECT"},
|
|
||||||
{method: "OPTIONS", expected: "OPTIONS"},
|
|
||||||
{method: "TRACE", expected: "TRACE"},
|
|
||||||
{method: "PATCH", expected: "PATCH"},
|
|
||||||
|
|
||||||
// Test all standard HTTP methods in lowercase
|
|
||||||
{method: "get", expected: "GET"},
|
|
||||||
{method: "head", expected: "HEAD"},
|
|
||||||
{method: "post", expected: "POST"},
|
|
||||||
{method: "put", expected: "PUT"},
|
|
||||||
{method: "delete", expected: "DELETE"},
|
|
||||||
{method: "connect", expected: "CONNECT"},
|
|
||||||
{method: "options", expected: "OPTIONS"},
|
|
||||||
{method: "trace", expected: "TRACE"},
|
|
||||||
{method: "patch", expected: "PATCH"},
|
|
||||||
|
|
||||||
// Test mixed case and non-standard methods
|
|
||||||
{method: "Get", expected: "OTHER"},
|
|
||||||
{method: "gEt", expected: "OTHER"},
|
|
||||||
{method: "UNKNOWN", expected: "OTHER"},
|
|
||||||
{method: "PROPFIND", expected: "OTHER"},
|
|
||||||
{method: "MKCOL", expected: "OTHER"},
|
|
||||||
{method: "", expected: "OTHER"},
|
|
||||||
{method: " ", expected: "OTHER"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range tests {
|
for _, d := range tests {
|
||||||
@@ -57,79 +26,3 @@ func TestSanitizeMethod(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSanitizeCode(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
code int
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "zero returns 200",
|
|
||||||
code: 0,
|
|
||||||
expected: "200",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "200 returns 200",
|
|
||||||
code: 200,
|
|
||||||
expected: "200",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "404 returns 404",
|
|
||||||
code: 404,
|
|
||||||
expected: "404",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "500 returns 500",
|
|
||||||
code: 500,
|
|
||||||
expected: "500",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "301 returns 301",
|
|
||||||
code: 301,
|
|
||||||
expected: "301",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "418 teapot returns 418",
|
|
||||||
code: 418,
|
|
||||||
expected: "418",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "999 custom code",
|
|
||||||
code: 999,
|
|
||||||
expected: "999",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative code",
|
|
||||||
code: -1,
|
|
||||||
expected: "-1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := SanitizeCode(tt.code)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("SanitizeCode(%d) = %s; want %s", tt.code, result, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkSanitizeCode benchmarks the SanitizeCode function
|
|
||||||
func BenchmarkSanitizeCode(b *testing.B) {
|
|
||||||
codes := []int{0, 200, 404, 500, 301, 418}
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
SanitizeCode(codes[i%len(codes)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkSanitizeMethod benchmarks the SanitizeMethod function
|
|
||||||
func BenchmarkSanitizeMethod(b *testing.B) {
|
|
||||||
methods := []string{"GET", "POST", "PUT", "DELETE", "UNKNOWN"}
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
SanitizeMethod(methods[i%len(methods)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPrivateRangesCIDR(t *testing.T) {
|
|
||||||
ranges := PrivateRangesCIDR()
|
|
||||||
|
|
||||||
// Should include standard private IP ranges
|
|
||||||
expected := map[string]bool{
|
|
||||||
"192.168.0.0/16": false,
|
|
||||||
"172.16.0.0/12": false,
|
|
||||||
"10.0.0.0/8": false,
|
|
||||||
"127.0.0.1/8": false,
|
|
||||||
"fd00::/8": false,
|
|
||||||
"::1": false,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range ranges {
|
|
||||||
if _, ok := expected[r]; ok {
|
|
||||||
expected[r] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for cidr, found := range expected {
|
|
||||||
if !found {
|
|
||||||
t.Errorf("expected private range %q not found in PrivateRangesCIDR()", cidr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ranges) < 6 {
|
|
||||||
t.Errorf("expected at least 6 private ranges, got %d", len(ranges))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMaxSizeSubjectsListForLog(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
subjects map[string]struct{}
|
|
||||||
maxToDisplay int
|
|
||||||
wantLen int
|
|
||||||
wantSuffix bool // whether "(and N more...)" is expected
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty map",
|
|
||||||
subjects: map[string]struct{}{},
|
|
||||||
maxToDisplay: 5,
|
|
||||||
wantLen: 0,
|
|
||||||
wantSuffix: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fewer than max",
|
|
||||||
subjects: map[string]struct{}{
|
|
||||||
"example.com": {},
|
|
||||||
"example.org": {},
|
|
||||||
},
|
|
||||||
maxToDisplay: 5,
|
|
||||||
wantLen: 2,
|
|
||||||
wantSuffix: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "equal to max",
|
|
||||||
subjects: map[string]struct{}{
|
|
||||||
"a.com": {},
|
|
||||||
"b.com": {},
|
|
||||||
"c.com": {},
|
|
||||||
},
|
|
||||||
maxToDisplay: 3,
|
|
||||||
wantLen: 3,
|
|
||||||
wantSuffix: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "more than max",
|
|
||||||
subjects: map[string]struct{}{
|
|
||||||
"a.com": {},
|
|
||||||
"b.com": {},
|
|
||||||
"c.com": {},
|
|
||||||
"d.com": {},
|
|
||||||
"e.com": {},
|
|
||||||
},
|
|
||||||
maxToDisplay: 2,
|
|
||||||
wantLen: 3, // 2 domains + suffix
|
|
||||||
wantSuffix: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "max is zero",
|
|
||||||
subjects: map[string]struct{}{
|
|
||||||
"a.com": {},
|
|
||||||
"b.com": {},
|
|
||||||
},
|
|
||||||
maxToDisplay: 0,
|
|
||||||
// BUG: When maxToDisplay is 0, code still appends one domain
|
|
||||||
// because append happens before the break check in the loop.
|
|
||||||
// Expected behavior: 1 item (just suffix). Actual: 2 items
|
|
||||||
// (1 leaked domain + suffix).
|
|
||||||
wantLen: 2,
|
|
||||||
wantSuffix: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single subject with max 1",
|
|
||||||
subjects: map[string]struct{}{
|
|
||||||
"example.com": {},
|
|
||||||
},
|
|
||||||
maxToDisplay: 1,
|
|
||||||
wantLen: 1,
|
|
||||||
wantSuffix: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := MaxSizeSubjectsListForLog(tt.subjects, tt.maxToDisplay)
|
|
||||||
if len(result) != tt.wantLen {
|
|
||||||
t.Errorf("MaxSizeSubjectsListForLog() returned %d items, want %d; got: %v", len(result), tt.wantLen, result)
|
|
||||||
}
|
|
||||||
if tt.wantSuffix {
|
|
||||||
last := result[len(result)-1]
|
|
||||||
if len(last) < 4 || last[:4] != "(and" {
|
|
||||||
t.Errorf("expected suffix '(and N more...)' but got %q", last)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSplitUnixSocketPermissionsBits(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
wantPath string
|
|
||||||
wantFileMode fs.FileMode
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no permission bits defaults to 0200",
|
|
||||||
input: "/run/caddy.sock",
|
|
||||||
wantPath: "/run/caddy.sock",
|
|
||||||
wantFileMode: 0o200,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid permission 0222",
|
|
||||||
input: "/run/caddy.sock|0222",
|
|
||||||
wantPath: "/run/caddy.sock",
|
|
||||||
wantFileMode: 0o222,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid permission 0200",
|
|
||||||
input: "/run/caddy.sock|0200",
|
|
||||||
wantPath: "/run/caddy.sock",
|
|
||||||
wantFileMode: 0o200,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid permission 0777",
|
|
||||||
input: "/run/caddy.sock|0777",
|
|
||||||
wantPath: "/run/caddy.sock",
|
|
||||||
wantFileMode: 0o777,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid permission 0755",
|
|
||||||
input: "/run/caddy.sock|0755",
|
|
||||||
wantPath: "/run/caddy.sock",
|
|
||||||
wantFileMode: 0o755,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid permission 0666",
|
|
||||||
input: "/tmp/test.sock|0666",
|
|
||||||
wantPath: "/tmp/test.sock",
|
|
||||||
wantFileMode: 0o666,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing owner write permission 0444",
|
|
||||||
input: "/run/caddy.sock|0444",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing owner write permission 0044",
|
|
||||||
input: "/run/caddy.sock|0044",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing owner write permission 0100",
|
|
||||||
input: "/run/caddy.sock|0100",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing owner write permission 0500",
|
|
||||||
input: "/run/caddy.sock|0500",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid octal digits",
|
|
||||||
input: "/run/caddy.sock|09ab",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid non-numeric permission",
|
|
||||||
input: "/run/caddy.sock|rwxrwxrwx",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty permission string",
|
|
||||||
input: "/run/caddy.sock|",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple pipes only splits on first",
|
|
||||||
input: "/run/caddy|sock|0222",
|
|
||||||
wantPath: "/run/caddy",
|
|
||||||
wantFileMode: 0, // "sock|0222" is not valid octal
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty path with valid permission",
|
|
||||||
input: "|0222",
|
|
||||||
wantPath: "",
|
|
||||||
wantFileMode: 0o222,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "path only with no pipe",
|
|
||||||
input: "/var/run/my-app.sock",
|
|
||||||
wantPath: "/var/run/my-app.sock",
|
|
||||||
wantFileMode: 0o200,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "permission 0300 has write bit",
|
|
||||||
input: "/run/caddy.sock|0300",
|
|
||||||
wantPath: "/run/caddy.sock",
|
|
||||||
wantFileMode: 0o300,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "permission 0422 missing owner write",
|
|
||||||
input: "/run/caddy.sock|0422",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gotPath, gotMode, err := SplitUnixSocketPermissionsBits(tt.input)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("SplitUnixSocketPermissionsBits(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if gotPath != tt.wantPath {
|
|
||||||
t.Errorf("SplitUnixSocketPermissionsBits(%q) path = %q, want %q", tt.input, gotPath, tt.wantPath)
|
|
||||||
}
|
|
||||||
if gotMode != tt.wantFileMode {
|
|
||||||
t.Errorf("SplitUnixSocketPermissionsBits(%q) mode = %04o, want %04o", tt.input, gotMode, tt.wantFileMode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,6 +30,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"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) {
|
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
|
||||||
var socketFile *os.File
|
var socketFile *os.File
|
||||||
|
|
||||||
@@ -116,8 +120,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
|
|||||||
// re-wrapped in a new fakeCloseListener each time the listener
|
// re-wrapped in a new fakeCloseListener each time the listener
|
||||||
// is reused. This type is atomic and values must not be copied.
|
// is reused. This type is atomic and values must not be copied.
|
||||||
type fakeCloseListener struct {
|
type fakeCloseListener struct {
|
||||||
closed atomic.Bool
|
closed int32 // accessed atomically; belongs to this struct only
|
||||||
*sharedListener // embedded, so we also become a net.Listener
|
*sharedListener // embedded, so we also become a net.Listener
|
||||||
keepAliveConfig net.KeepAliveConfig
|
keepAliveConfig net.KeepAliveConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +131,7 @@ type canSetKeepAliveConfig interface {
|
|||||||
|
|
||||||
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
||||||
// if the listener is already "closed", return error
|
// if the listener is already "closed", return error
|
||||||
if fcl.closed.Load() {
|
if atomic.LoadInt32(&fcl.closed) == 1 {
|
||||||
return nil, fakeClosedErr(fcl)
|
return nil, fakeClosedErr(fcl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +155,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
|||||||
// that we set when Close() was called, and return a non-temporary and
|
// 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
|
// non-timeout error value to the caller, masking the "true" error, so
|
||||||
// that server loops / goroutines won't retry, linger, and leak
|
// that server loops / goroutines won't retry, linger, and leak
|
||||||
if fcl.closed.Load() {
|
if atomic.LoadInt32(&fcl.closed) == 1 {
|
||||||
// we dereference the sharedListener explicitly even though it's embedded
|
// 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
|
// 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
|
// users of this listener, not just our own reference to it; we also don't
|
||||||
@@ -171,7 +175,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
|||||||
// underlying listener. The underlying listener is only closed
|
// underlying listener. The underlying listener is only closed
|
||||||
// if the caller is the last known user of the socket.
|
// if the caller is the last known user of the socket.
|
||||||
func (fcl *fakeCloseListener) Close() error {
|
func (fcl *fakeCloseListener) Close() error {
|
||||||
if fcl.closed.CompareAndSwap(false, true) {
|
if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) {
|
||||||
// There are two ways I know of to get an Accept()
|
// There are two ways I know of to get an Accept()
|
||||||
// function to return to the server loop that called
|
// function to return to the server loop that called
|
||||||
// it: close the listener, or set a deadline in the
|
// it: close the listener, or set a deadline in the
|
||||||
@@ -234,13 +238,13 @@ func (sl *sharedListener) Destruct() error {
|
|||||||
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
|
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
|
||||||
// or more specifically, *net.UDPConn
|
// or more specifically, *net.UDPConn
|
||||||
type fakeClosePacketConn struct {
|
type fakeClosePacketConn struct {
|
||||||
closed atomic.Bool
|
closed int32 // accessed atomically; belongs to this struct only
|
||||||
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
|
*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) {
|
func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
// if the listener is already "closed", return error
|
// if the listener is already "closed", return error
|
||||||
if fcpc.closed.Load() {
|
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||||
return 0, nil, &net.OpError{
|
return 0, nil, &net.OpError{
|
||||||
Op: "readfrom",
|
Op: "readfrom",
|
||||||
Net: fcpc.LocalAddr().Network(),
|
Net: fcpc.LocalAddr().Network(),
|
||||||
@@ -254,7 +258,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// this server was stopped, so clear the deadline and let
|
// this server was stopped, so clear the deadline and let
|
||||||
// any new server continue reading; but we will exit
|
// any new server continue reading; but we will exit
|
||||||
if fcpc.closed.Load() {
|
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
||||||
return n, addr, err
|
return n, addr, err
|
||||||
@@ -269,7 +273,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
|
|||||||
|
|
||||||
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
||||||
func (fcpc *fakeClosePacketConn) Close() error {
|
func (fcpc *fakeClosePacketConn) Close() error {
|
||||||
if fcpc.closed.CompareAndSwap(false, true) {
|
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
|
||||||
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
|
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
|
||||||
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
|
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
//go:build (!unix || solaris) && !windows
|
|
||||||
|
|
||||||
package caddy
|
|
||||||
|
|
||||||
func reuseUnixSocket(_, _ string) (any, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
//go:build windows
|
|
||||||
|
|
||||||
package caddy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errUnixSocketAlreadyInUse = errors.New("unix socket is already in use by another process")
|
|
||||||
|
|
||||||
func reuseUnixSocket(network, addr string) (any, error) {
|
|
||||||
if !IsUnixNetwork(network) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: This is here mainly for proper compatibility, because Unix sockets with abstract names are in an interesting limbo state on Windows:
|
|
||||||
// Go already translates `@` characters to `\0` for Windows: https://github.com/golang/go/blob/65d5c5f6dd8aa7b221cff6ec3f5101ea2e5f3efa/src/syscall/syscall_windows.go#L910
|
|
||||||
// ...but there still is an open issue about the fact that this is not properly supported: https://github.com/microsoft/WSL/issues/4240#issuecomment-620805115
|
|
||||||
// The main issue is that the original announcement proclaimed support for this feature, but it was (apparently) never implemented: https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/
|
|
||||||
isAbstractUnixSocket := strings.HasPrefix(addr, "@")
|
|
||||||
|
|
||||||
if isAbstractUnixSocket {
|
|
||||||
// Abstract Unix sockets do not require us to remove stale socket files.
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// On Windows, we're using the `fakeCloseListener` wrappers around a single, ever-living listener.
|
|
||||||
// So, if there's an active listener entry in the pool, we're the current owner of the Unix socket file.
|
|
||||||
_, socketBelongsToCurrentProcess := listenerPool.References(listenerKey(network, addr))
|
|
||||||
|
|
||||||
if socketBelongsToCurrentProcess {
|
|
||||||
// Reuse/cleanup is entirely handled by the refcounting mechanism in `listenerPool`.
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the socket file does not exist or has no backing server process, this will fail instantly.
|
|
||||||
connection, err := net.DialTimeout("unix", addr, 10*time.Millisecond)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
connection.Close()
|
|
||||||
return nil, fmt.Errorf("cannot reuse socket %v: %w", addr, errUnixSocketAlreadyInUse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Windows returns this error code both if the socket file does not exist and if it isn't backed by a server process anymore.
|
|
||||||
// See: https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2#wsaeconnrefused
|
|
||||||
const WSAECONNREFUSED syscall.Errno = 10061
|
|
||||||
|
|
||||||
var errno syscall.Errno
|
|
||||||
hasNoListeningServerProcess := errors.As(err, &errno) && errno == WSAECONNREFUSED
|
|
||||||
|
|
||||||
if !hasNoListeningServerProcess {
|
|
||||||
return nil, fmt.Errorf("cannot reuse socket %v: %w", addr, errUnixSocketAlreadyInUse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the socket file exists, it hasn't been created by our process, and it seemingly
|
|
||||||
// isn't backed by a server process anymore. Try to delete it so we can bind to it later.
|
|
||||||
err = os.Remove(addr)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
return nil, nil
|
|
||||||
} else if errors.Is(err, fs.ErrNotExist) {
|
|
||||||
// Either the file didn't exist in the first place, or it was deleted before we were able to.
|
|
||||||
return nil, nil
|
|
||||||
} else {
|
|
||||||
// We failed to delete the file. Likely, it belongs to another (active) process.
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-12
@@ -63,7 +63,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
unixSocket.count.Add(1)
|
atomic.AddInt32(unixSocket.count, 1)
|
||||||
unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
|
unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
|
||||||
|
|
||||||
case *unixConn:
|
case *unixConn:
|
||||||
@@ -71,7 +71,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
unixSocket.count.Add(1)
|
atomic.AddInt32(unixSocket.count, 1)
|
||||||
unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
|
unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,9 +165,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
|
|||||||
if !fd {
|
if !fd {
|
||||||
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so...
|
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so...
|
||||||
if unix, ok := ln.(*net.UnixConn); ok {
|
if unix, ok := ln.(*net.UnixConn); ok {
|
||||||
cnt := new(atomic.Int32)
|
one := int32(1)
|
||||||
cnt.Store(1)
|
ln = &unixConn{unix, lnKey, &one}
|
||||||
ln = &unixConn{unix, lnKey, cnt}
|
|
||||||
unixSockets[lnKey] = ln.(*unixConn)
|
unixSockets[lnKey] = ln.(*unixConn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,9 +181,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
|
|||||||
// (we do our own "unlink on close" -- not required, but more tidy)
|
// (we do our own "unlink on close" -- not required, but more tidy)
|
||||||
if unix, ok := ln.(*net.UnixListener); ok {
|
if unix, ok := ln.(*net.UnixListener); ok {
|
||||||
unix.SetUnlinkOnClose(false)
|
unix.SetUnlinkOnClose(false)
|
||||||
cnt := new(atomic.Int32)
|
one := int32(1)
|
||||||
cnt.Store(1)
|
ln = &unixListener{unix, lnKey, &one}
|
||||||
ln = &unixListener{unix, lnKey, cnt}
|
|
||||||
unixSockets[lnKey] = ln.(*unixListener)
|
unixSockets[lnKey] = ln.(*unixListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,11 +216,11 @@ func reusePort(network, address string, conn syscall.RawConn) error {
|
|||||||
type unixListener struct {
|
type unixListener struct {
|
||||||
*net.UnixListener
|
*net.UnixListener
|
||||||
mapKey string
|
mapKey string
|
||||||
count *atomic.Int32
|
count *int32 // accessed atomically
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uln *unixListener) Close() error {
|
func (uln *unixListener) Close() error {
|
||||||
newCount := uln.count.Add(-1)
|
newCount := atomic.AddInt32(uln.count, -1)
|
||||||
if newCount == 0 {
|
if newCount == 0 {
|
||||||
file, err := uln.File()
|
file, err := uln.File()
|
||||||
var name string
|
var name string
|
||||||
@@ -244,11 +242,11 @@ func (uln *unixListener) Close() error {
|
|||||||
type unixConn struct {
|
type unixConn struct {
|
||||||
*net.UnixConn
|
*net.UnixConn
|
||||||
mapKey string
|
mapKey string
|
||||||
count *atomic.Int32
|
count *int32 // accessed atomically
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *unixConn) Close() error {
|
func (uc *unixConn) Close() error {
|
||||||
newCount := uc.count.Add(-1)
|
newCount := atomic.AddInt32(uc.count, -1)
|
||||||
if newCount == 0 {
|
if newCount == 0 {
|
||||||
file, err := uc.File()
|
file, err := uc.File()
|
||||||
var name string
|
var name string
|
||||||
|
|||||||
+7
-20
@@ -361,7 +361,7 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
|
|||||||
if end < start {
|
if end < start {
|
||||||
return NetworkAddress{}, fmt.Errorf("end port must not be less than start port")
|
return NetworkAddress{}, fmt.Errorf("end port must not be less than start port")
|
||||||
}
|
}
|
||||||
if (end-start)+1 > maxPortSpan {
|
if (end - start) > maxPortSpan {
|
||||||
return NetworkAddress{}, fmt.Errorf("port range exceeds %d ports", maxPortSpan)
|
return NetworkAddress{}, fmt.Errorf("port range exceeds %d ports", maxPortSpan)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -462,10 +462,7 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
|
|||||||
sqs := newSharedQUICState(tlsConf)
|
sqs := newSharedQUICState(tlsConf)
|
||||||
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
|
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
quicTlsConfig := &tls.Config{
|
quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient}
|
||||||
GetConfigForClient: sqs.getConfigForClient,
|
|
||||||
GetEncryptedClientHelloKeys: sqs.getEncryptedClientHelloKeys,
|
|
||||||
}
|
|
||||||
// Require clients to verify their source address when we're handling more than 1000 handshakes per second.
|
// Require clients to verify their source address when we're handling more than 1000 handshakes per second.
|
||||||
// TODO: make tunable?
|
// TODO: make tunable?
|
||||||
limiter := rate.NewLimiter(1000, 1000)
|
limiter := rate.NewLimiter(1000, 1000)
|
||||||
@@ -543,16 +540,6 @@ func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Co
|
|||||||
return sqs.activeTlsConf.GetConfigForClient(ch)
|
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
|
// 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
|
// so that when cancelled, the active tls.Config will change
|
||||||
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) {
|
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) {
|
||||||
@@ -624,8 +611,8 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
|
|||||||
var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉")
|
var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉")
|
||||||
|
|
||||||
type fakeCloseQuicListener struct {
|
type fakeCloseQuicListener struct {
|
||||||
closed atomic.Int32
|
closed int32 // accessed atomically; belongs to this struct only
|
||||||
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
||||||
context context.Context
|
context context.Context
|
||||||
contextCancel context.CancelCauseFunc
|
contextCancel context.CancelCauseFunc
|
||||||
}
|
}
|
||||||
@@ -642,16 +629,16 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if the listener is "closed", return a fake closed error instead
|
// if the listener is "closed", return a fake closed error instead
|
||||||
if fcql.closed.Load() == 1 && errors.Is(err, context.Canceled) {
|
if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) {
|
||||||
return nil, fakeClosedErr(fcql)
|
return nil, fakeClosedErr(fcql)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fcql *fakeCloseQuicListener) Close() error {
|
func (fcql *fakeCloseQuicListener) Close() error {
|
||||||
if fcql.closed.CompareAndSwap(0, 1) {
|
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
||||||
fcql.contextCancel(errFakeClosed)
|
fcql.contextCancel(errFakeClosed)
|
||||||
} else if fcql.closed.CompareAndSwap(1, 2) {
|
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
|
||||||
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -176,63 +175,6 @@ func TestJoinNetworkAddress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSharedQUICStateGetEncryptedClientHelloKeys(t *testing.T) {
|
|
||||||
hello := &tls.ClientHelloInfo{ServerName: "example.com"}
|
|
||||||
initialKeys := []tls.EncryptedClientHelloKey{{Config: []byte("initial"), PrivateKey: []byte("initial-key")}}
|
|
||||||
updatedKeys := []tls.EncryptedClientHelloKey{{Config: []byte("updated"), PrivateKey: []byte("updated-key")}}
|
|
||||||
|
|
||||||
initialConfig := &tls.Config{
|
|
||||||
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
|
||||||
return nil, nil
|
|
||||||
},
|
|
||||||
GetEncryptedClientHelloKeys: func(*tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
|
|
||||||
return initialKeys, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
sqs := newSharedQUICState(initialConfig)
|
|
||||||
|
|
||||||
keys, err := sqs.getEncryptedClientHelloKeys(hello)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("getting initial ECH keys: %v", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(keys, initialKeys) {
|
|
||||||
t.Fatalf("unexpected initial ECH keys: got %#v, want %#v", keys, initialKeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedConfig := &tls.Config{
|
|
||||||
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
|
||||||
return nil, nil
|
|
||||||
},
|
|
||||||
GetEncryptedClientHelloKeys: func(*tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
|
|
||||||
return updatedKeys, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, cancel := sqs.addState(updatedConfig)
|
|
||||||
sqs.rmu.Lock()
|
|
||||||
sqs.activeTlsConf = updatedConfig
|
|
||||||
sqs.rmu.Unlock()
|
|
||||||
|
|
||||||
keys, err = sqs.getEncryptedClientHelloKeys(hello)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("getting updated ECH keys: %v", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(keys, updatedKeys) {
|
|
||||||
t.Fatalf("unexpected updated ECH keys: got %#v, want %#v", keys, updatedKeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel(nil)
|
|
||||||
|
|
||||||
keys, err = sqs.getEncryptedClientHelloKeys(hello)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("getting restored ECH keys: %v", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(keys, initialKeys) {
|
|
||||||
t.Fatalf("unexpected restored ECH keys: got %#v, want %#v", keys, initialKeys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseNetworkAddress(t *testing.T) {
|
func TestParseNetworkAddress(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
input string
|
input string
|
||||||
|
|||||||
-394
@@ -1,394 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package caddy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGlobalMetrics_ConfigSuccess(t *testing.T) {
|
|
||||||
// Test setting config success metric
|
|
||||||
originalValue := getMetricValue(globalMetrics.configSuccess)
|
|
||||||
|
|
||||||
// Set to success
|
|
||||||
globalMetrics.configSuccess.Set(1)
|
|
||||||
newValue := getMetricValue(globalMetrics.configSuccess)
|
|
||||||
|
|
||||||
if newValue != 1 {
|
|
||||||
t.Errorf("Expected config success metric to be 1, got %f", newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set to failure
|
|
||||||
globalMetrics.configSuccess.Set(0)
|
|
||||||
failureValue := getMetricValue(globalMetrics.configSuccess)
|
|
||||||
|
|
||||||
if failureValue != 0 {
|
|
||||||
t.Errorf("Expected config success metric to be 0, got %f", failureValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore original value if it existed
|
|
||||||
if originalValue != 0 {
|
|
||||||
globalMetrics.configSuccess.Set(originalValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGlobalMetrics_ConfigSuccessTime(t *testing.T) {
|
|
||||||
// Set success time
|
|
||||||
globalMetrics.configSuccessTime.SetToCurrentTime()
|
|
||||||
|
|
||||||
// Get the metric value
|
|
||||||
metricValue := getMetricValue(globalMetrics.configSuccessTime)
|
|
||||||
|
|
||||||
// Should be a reasonable Unix timestamp (not zero)
|
|
||||||
if metricValue == 0 {
|
|
||||||
t.Error("Config success time should not be zero")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be recent (within last minute)
|
|
||||||
now := time.Now().Unix()
|
|
||||||
if int64(metricValue) < now-60 || int64(metricValue) > now {
|
|
||||||
t.Errorf("Config success time %f should be recent (now: %d)", metricValue, now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdminMetrics_RequestCount(t *testing.T) {
|
|
||||||
// Initialize admin metrics for testing
|
|
||||||
initAdminMetrics()
|
|
||||||
|
|
||||||
labels := prometheus.Labels{
|
|
||||||
"handler": "test",
|
|
||||||
"path": "/config",
|
|
||||||
"method": "GET",
|
|
||||||
"code": "200",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get initial value
|
|
||||||
initialValue := getCounterValue(adminMetrics.requestCount, labels)
|
|
||||||
|
|
||||||
// Increment counter
|
|
||||||
adminMetrics.requestCount.With(labels).Inc()
|
|
||||||
|
|
||||||
// Verify increment
|
|
||||||
newValue := getCounterValue(adminMetrics.requestCount, labels)
|
|
||||||
if newValue != initialValue+1 {
|
|
||||||
t.Errorf("Expected counter to increment by 1, got %f -> %f", initialValue, newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdminMetrics_RequestErrors(t *testing.T) {
|
|
||||||
// Initialize admin metrics for testing
|
|
||||||
initAdminMetrics()
|
|
||||||
|
|
||||||
labels := prometheus.Labels{
|
|
||||||
"handler": "test",
|
|
||||||
"path": "/test",
|
|
||||||
"method": "POST",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get initial value
|
|
||||||
initialValue := getCounterValue(adminMetrics.requestErrors, labels)
|
|
||||||
|
|
||||||
// Increment error counter
|
|
||||||
adminMetrics.requestErrors.With(labels).Inc()
|
|
||||||
|
|
||||||
// Verify increment
|
|
||||||
newValue := getCounterValue(adminMetrics.requestErrors, labels)
|
|
||||||
if newValue != initialValue+1 {
|
|
||||||
t.Errorf("Expected error counter to increment by 1, got %f -> %f", initialValue, newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMetrics_ConcurrentAccess(t *testing.T) {
|
|
||||||
// Initialize admin metrics
|
|
||||||
initAdminMetrics()
|
|
||||||
|
|
||||||
const numGoroutines = 100
|
|
||||||
const incrementsPerGoroutine = 10
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
labels := prometheus.Labels{
|
|
||||||
"handler": "concurrent",
|
|
||||||
"path": "/concurrent",
|
|
||||||
"method": "GET",
|
|
||||||
"code": "200",
|
|
||||||
}
|
|
||||||
|
|
||||||
initialCount := getCounterValue(adminMetrics.requestCount, labels)
|
|
||||||
|
|
||||||
// Concurrent increments
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < incrementsPerGoroutine; j++ {
|
|
||||||
adminMetrics.requestCount.With(labels).Inc()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Verify final count
|
|
||||||
finalCount := getCounterValue(adminMetrics.requestCount, labels)
|
|
||||||
expectedIncrement := float64(numGoroutines * incrementsPerGoroutine)
|
|
||||||
|
|
||||||
if finalCount-initialCount != expectedIncrement {
|
|
||||||
t.Errorf("Expected counter to increase by %f, got %f",
|
|
||||||
expectedIncrement, finalCount-initialCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMetrics_LabelValidation(t *testing.T) {
|
|
||||||
// Test various label combinations
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
labels prometheus.Labels
|
|
||||||
metric string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid request count labels",
|
|
||||||
labels: prometheus.Labels{
|
|
||||||
"handler": "test",
|
|
||||||
"path": "/api/test",
|
|
||||||
"method": "GET",
|
|
||||||
"code": "200",
|
|
||||||
},
|
|
||||||
metric: "requestCount",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid error labels",
|
|
||||||
labels: prometheus.Labels{
|
|
||||||
"handler": "test",
|
|
||||||
"path": "/api/error",
|
|
||||||
"method": "POST",
|
|
||||||
},
|
|
||||||
metric: "requestErrors",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty path",
|
|
||||||
labels: prometheus.Labels{
|
|
||||||
"handler": "test",
|
|
||||||
"path": "",
|
|
||||||
"method": "GET",
|
|
||||||
"code": "404",
|
|
||||||
},
|
|
||||||
metric: "requestCount",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "special characters in path",
|
|
||||||
labels: prometheus.Labels{
|
|
||||||
"handler": "test",
|
|
||||||
"path": "/api/test%20with%20spaces",
|
|
||||||
"method": "PUT",
|
|
||||||
"code": "201",
|
|
||||||
},
|
|
||||||
metric: "requestCount",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
initAdminMetrics()
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
// This should not panic or error
|
|
||||||
switch test.metric {
|
|
||||||
case "requestCount":
|
|
||||||
adminMetrics.requestCount.With(test.labels).Inc()
|
|
||||||
case "requestErrors":
|
|
||||||
adminMetrics.requestErrors.With(test.labels).Inc()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMetrics_Initialization_Idempotent(t *testing.T) {
|
|
||||||
// Test that initializing admin metrics multiple times is safe
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
t.Errorf("Iteration %d: initAdminMetrics panicked: %v", i, r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
initAdminMetrics()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInstrumentHandlerCounter(t *testing.T) {
|
|
||||||
// Create a test counter with the expected labels
|
|
||||||
counter := prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "test_counter",
|
|
||||||
Help: "Test counter for instrumentation",
|
|
||||||
},
|
|
||||||
[]string{"code", "method"},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create instrumented handler
|
|
||||||
testHandler := instrumentHandlerCounter(
|
|
||||||
counter,
|
|
||||||
&mockHTTPHandler{statusCode: 200},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create test request
|
|
||||||
req := httptest.NewRequest("GET", "/test", nil)
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Get initial counter value
|
|
||||||
initialValue := getCounterValue(counter, prometheus.Labels{"code": "200", "method": "GET"})
|
|
||||||
|
|
||||||
// Serve request
|
|
||||||
testHandler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
// Verify counter was incremented
|
|
||||||
finalValue := getCounterValue(counter, prometheus.Labels{"code": "200", "method": "GET"})
|
|
||||||
if finalValue != initialValue+1 {
|
|
||||||
t.Errorf("Expected counter to increment by 1, got %f -> %f", initialValue, finalValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInstrumentHandlerCounter_ErrorStatus(t *testing.T) {
|
|
||||||
counter := prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "test_error_counter",
|
|
||||||
Help: "Test counter for error status",
|
|
||||||
},
|
|
||||||
[]string{"code", "method"},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test different status codes
|
|
||||||
statusCodes := []int{200, 404, 500, 301, 401}
|
|
||||||
|
|
||||||
for _, status := range statusCodes {
|
|
||||||
t.Run(fmt.Sprintf("status_%d", status), func(t *testing.T) {
|
|
||||||
handler := instrumentHandlerCounter(
|
|
||||||
counter,
|
|
||||||
&mockHTTPHandler{statusCode: status},
|
|
||||||
)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/test", nil)
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
statusLabels := prometheus.Labels{"code": fmt.Sprintf("%d", status), "method": "GET"}
|
|
||||||
initialValue := getCounterValue(counter, statusLabels)
|
|
||||||
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
finalValue := getCounterValue(counter, statusLabels)
|
|
||||||
if finalValue != initialValue+1 {
|
|
||||||
t.Errorf("Status %d: Expected counter increment", status)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
func getMetricValue(gauge prometheus.Gauge) float64 {
|
|
||||||
metric := &dto.Metric{}
|
|
||||||
gauge.Write(metric)
|
|
||||||
return metric.GetGauge().GetValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCounterValue(counter *prometheus.CounterVec, labels prometheus.Labels) float64 {
|
|
||||||
metric, err := counter.GetMetricWith(labels)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
pb := &dto.Metric{}
|
|
||||||
metric.Write(pb)
|
|
||||||
return pb.GetCounter().GetValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockHTTPHandler struct {
|
|
||||||
statusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(m.statusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMetrics_Memory_Usage(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping memory test in short mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize metrics
|
|
||||||
initAdminMetrics()
|
|
||||||
|
|
||||||
// Create many different label combinations
|
|
||||||
const numLabels = 1000
|
|
||||||
|
|
||||||
for i := 0; i < numLabels; i++ {
|
|
||||||
labels := prometheus.Labels{
|
|
||||||
"handler": fmt.Sprintf("handler_%d", i%10),
|
|
||||||
"path": fmt.Sprintf("/path_%d", i),
|
|
||||||
"method": []string{"GET", "POST", "PUT", "DELETE"}[i%4],
|
|
||||||
"code": []string{"200", "404", "500"}[i%3],
|
|
||||||
}
|
|
||||||
|
|
||||||
adminMetrics.requestCount.With(labels).Inc()
|
|
||||||
|
|
||||||
// Also increment error counter occasionally
|
|
||||||
if i%10 == 0 {
|
|
||||||
errorLabels := prometheus.Labels{
|
|
||||||
"handler": labels["handler"],
|
|
||||||
"path": labels["path"],
|
|
||||||
"method": labels["method"],
|
|
||||||
}
|
|
||||||
adminMetrics.requestErrors.With(errorLabels).Inc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test passes if we don't run out of memory or panic
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkGlobalMetrics_ConfigSuccess(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
globalMetrics.configSuccess.Set(float64(i % 2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkGlobalMetrics_ConfigSuccessTime(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
globalMetrics.configSuccessTime.SetToCurrentTime()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkAdminMetrics_RequestCount_WithLabels(b *testing.B) {
|
|
||||||
initAdminMetrics()
|
|
||||||
|
|
||||||
labels := prometheus.Labels{
|
|
||||||
"handler": "benchmark",
|
|
||||||
"path": "/benchmark",
|
|
||||||
"method": "GET",
|
|
||||||
"code": "200",
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
adminMetrics.requestCount.With(labels).Inc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+37
-17
@@ -20,6 +20,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -68,7 +69,6 @@ func init() {
|
|||||||
// `{http.request.orig_uri.path.dir}` | The request's original directory
|
// `{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.path.file}` | The request's original filename
|
||||||
// `{http.request.orig_uri.query}` | The request's original query string (without `?`)
|
// `{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.port}` | The port part of the request's Host header
|
||||||
// `{http.request.proto}` | The protocol of the request
|
// `{http.request.proto}` | The protocol of the request
|
||||||
// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
|
// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
|
||||||
@@ -98,15 +98,11 @@ func init() {
|
|||||||
// `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional)
|
// `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional)
|
||||||
// `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional)
|
// `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional)
|
||||||
// `{http.request.uri}` | The full request URI
|
// `{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}` | 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.*}` | Parts of the path, split by `/` (0-based from left)
|
||||||
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename
|
// `{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.path.file}` | The filename of the path, excluding directory
|
||||||
// `{http.request.uri.query}` | The query string (without `?`)
|
// `{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.request.uri.query.*}` | Individual query string value
|
||||||
// `{http.response.header.*}` | Specific response header field
|
// `{http.response.header.*}` | Specific response header field
|
||||||
// `{http.vars.*}` | Custom variables in the HTTP handler chain
|
// `{http.vars.*}` | Custom variables in the HTTP handler chain
|
||||||
@@ -207,9 +203,6 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
app.Metrics.httpMetrics = &httpMetrics{}
|
app.Metrics.httpMetrics = &httpMetrics{}
|
||||||
// Scan config for allowed hosts to prevent cardinality explosion
|
// Scan config for allowed hosts to prevent cardinality explosion
|
||||||
app.Metrics.scanConfigForHosts(app)
|
app.Metrics.scanConfigForHosts(app)
|
||||||
if err := app.Metrics.provisionOTLP(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// prepare each server
|
// prepare each server
|
||||||
oldContext := ctx.Context
|
oldContext := ctx.Context
|
||||||
@@ -221,6 +214,8 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
srv.ctx = ctx
|
srv.ctx = ctx
|
||||||
srv.logger = app.logger.Named("log")
|
srv.logger = app.logger.Named("log")
|
||||||
srv.errorLogger = app.logger.Named("log.error")
|
srv.errorLogger = app.logger.Named("log.error")
|
||||||
|
srv.shutdownAtMu = new(sync.RWMutex)
|
||||||
|
|
||||||
if srv.Metrics != nil {
|
if srv.Metrics != nil {
|
||||||
srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead")
|
srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead")
|
||||||
app.Metrics = cmp.Or(app.Metrics, &Metrics{
|
app.Metrics = cmp.Or(app.Metrics, &Metrics{
|
||||||
@@ -240,7 +235,12 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
|
|
||||||
// if no protocols configured explicitly, enable all except h2c
|
// if no protocols configured explicitly, enable all except h2c
|
||||||
if len(srv.Protocols) == 0 {
|
if len(srv.Protocols) == 0 {
|
||||||
srv.Protocols = srv.protocolsWithDefaults()
|
srv.Protocols = []string{"h1", "h2", "h3"}
|
||||||
|
}
|
||||||
|
|
||||||
|
srvProtocolsUnique := map[string]struct{}{}
|
||||||
|
for _, srvProtocol := range srv.Protocols {
|
||||||
|
srvProtocolsUnique[srvProtocol] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if srv.ListenProtocols != nil {
|
if srv.ListenProtocols != nil {
|
||||||
@@ -251,7 +251,31 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
|
|
||||||
for i, lnProtocols := range srv.ListenProtocols {
|
for i, lnProtocols := range srv.ListenProtocols {
|
||||||
if lnProtocols != nil {
|
if lnProtocols != nil {
|
||||||
srv.ListenProtocols[i] = srv.listenerProtocolsWithDefaults(lnProtocols)
|
// populate empty listen protocols with server protocols
|
||||||
|
lnProtocolsDefault := false
|
||||||
|
var lnProtocolsInclude []string
|
||||||
|
srvProtocolsInclude := maps.Clone(srvProtocolsUnique)
|
||||||
|
|
||||||
|
// keep existing listener protocols unless they are empty
|
||||||
|
for _, lnProtocol := range lnProtocols {
|
||||||
|
if lnProtocol == "" {
|
||||||
|
lnProtocolsDefault = true
|
||||||
|
} else {
|
||||||
|
lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
|
||||||
|
delete(srvProtocolsInclude, lnProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// append server protocols to listener protocols if any listener protocols were empty
|
||||||
|
if lnProtocolsDefault {
|
||||||
|
for _, srvProtocol := range srv.Protocols {
|
||||||
|
if _, ok := srvProtocolsInclude[srvProtocol]; ok {
|
||||||
|
lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.ListenProtocols[i] = lnProtocolsInclude
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,7 +689,9 @@ func (app *App) Stop() error {
|
|||||||
for _, addr := range na.Expand() {
|
for _, addr := range na.Expand() {
|
||||||
if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 {
|
if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 {
|
||||||
app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String()))
|
app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String()))
|
||||||
server.shutdownAt.Store(&scheduledTime)
|
server.shutdownAtMu.Lock()
|
||||||
|
server.shutdownAt = scheduledTime
|
||||||
|
server.shutdownAtMu.Unlock()
|
||||||
delay = true
|
delay = true
|
||||||
} else {
|
} else {
|
||||||
app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String()))
|
app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String()))
|
||||||
@@ -790,12 +816,6 @@ func (app *App) Stop() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// flush and shut down the OTLP metrics exporter (if configured) so any
|
|
||||||
// last data point reaches the collector before the process exits
|
|
||||||
if err := app.Metrics.shutdown(ctx); err != nil {
|
|
||||||
app.logger.Error("shutting down OTLP metrics", zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
app.stopped = true
|
app.stopped = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
|||||||
for d := range serverDomainSet {
|
for d := range serverDomainSet {
|
||||||
echDomains = append(echDomains, d)
|
echDomains = append(echDomains, d)
|
||||||
}
|
}
|
||||||
app.tlsApp.RegisterServerNames(echDomains, httpsRRALPNs(srv))
|
app.tlsApp.RegisterServerNames(echDomains)
|
||||||
|
|
||||||
// nothing more to do here if there are no domains that qualify for
|
// nothing more to do here if there are no domains that qualify for
|
||||||
// automatic HTTPS and there are no explicit TLS connection policies:
|
// automatic HTTPS and there are no explicit TLS connection policies:
|
||||||
@@ -258,13 +258,18 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
|||||||
// an empty string to indicate a catch-all, which we have to
|
// an empty string to indicate a catch-all, which we have to
|
||||||
// treat special later
|
// treat special later
|
||||||
if len(serverDomainSet) == 0 {
|
if len(serverDomainSet) == 0 {
|
||||||
app.recordAutoHTTPSRedirectAddress(redirDomains, "", addr)
|
redirDomains[""] = append(redirDomains[""], addr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...and associate it with each domain in this server
|
// ...and associate it with each domain in this server
|
||||||
for d := range serverDomainSet {
|
for d := range serverDomainSet {
|
||||||
app.recordAutoHTTPSRedirectAddress(redirDomains, d, addr)
|
// if this domain is used on more than one HTTPS-enabled
|
||||||
|
// port, we'll have to choose one, so prefer the HTTPS port
|
||||||
|
if _, ok := redirDomains[d]; !ok ||
|
||||||
|
addr.StartPort == uint(app.httpsPort()) {
|
||||||
|
redirDomains[d] = append(redirDomains[d], addr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -512,35 +517,6 @@ redirServersLoop:
|
|||||||
return nil
|
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 {
|
func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
|
||||||
redirTo := "https://{http.request.host}"
|
redirTo := "https://{http.request.host}"
|
||||||
|
|
||||||
@@ -574,20 +550,6 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpsRRALPNs(srv *Server) []string {
|
|
||||||
alpn := make(map[string]struct{}, 3)
|
|
||||||
if srv.protocol("h3") {
|
|
||||||
alpn["h3"] = struct{}{}
|
|
||||||
}
|
|
||||||
if srv.protocol("h2") {
|
|
||||||
alpn["h2"] = struct{}{}
|
|
||||||
}
|
|
||||||
if srv.protocol("h1") {
|
|
||||||
alpn["http/1.1"] = struct{}{}
|
|
||||||
}
|
|
||||||
return caddytls.OrderedHTTPSRRALPN(alpn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// createAutomationPolicies ensures that automated certificates for this
|
// createAutomationPolicies ensures that automated certificates for this
|
||||||
// app are managed properly. This adds up to two automation policies:
|
// 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
|
// one for the public names, and one for the internal names. If a catch-all
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
package caddyhttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTTPSRRALPNsDefaultProtocols(t *testing.T) {
|
|
||||||
srv := &Server{}
|
|
||||||
|
|
||||||
got := httpsRRALPNs(srv)
|
|
||||||
want := []string{"h3", "h2", "http/1.1"}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHTTPSRRALPNsListenProtocolOverrides(t *testing.T) {
|
|
||||||
srv := &Server{
|
|
||||||
Protocols: []string{"h1", "h2"},
|
|
||||||
ListenProtocols: [][]string{
|
|
||||||
{"h1"},
|
|
||||||
nil,
|
|
||||||
{},
|
|
||||||
{"h3", ""},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := httpsRRALPNs(srv)
|
|
||||||
want := []string{"h3", "h2", "http/1.1"}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHTTPSRRALPNsIgnoresH2COnly(t *testing.T) {
|
|
||||||
srv := &Server{
|
|
||||||
Protocols: []string{"h2c"},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := httpsRRALPNs(srv)
|
|
||||||
if len(got) != 0 {
|
|
||||||
t.Fatalf("unexpected ALPN values: got %v want none", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,10 +41,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
|||||||
//
|
//
|
||||||
// encode [<matcher>] <formats...> {
|
// encode [<matcher>] <formats...> {
|
||||||
// gzip [<level>]
|
// gzip [<level>]
|
||||||
// zstd [<level>] {
|
// zstd
|
||||||
// level <level>
|
|
||||||
// disable_checksum
|
|
||||||
// }
|
|
||||||
// minimum_length <length>
|
// minimum_length <length>
|
||||||
// # response matcher block
|
// # response matcher block
|
||||||
// match {
|
// match {
|
||||||
|
|||||||
@@ -33,10 +33,6 @@ type Zstd struct {
|
|||||||
// The compression level. Accepted values: fastest, better, best, default.
|
// The compression level. Accepted values: fastest, better, best, default.
|
||||||
Level string `json:"level,omitempty"`
|
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
|
// Compression level refer to type constants value from zstd.SpeedFastest to zstd.SpeedBestCompression
|
||||||
level zstd.EncoderLevel
|
level zstd.EncoderLevel
|
||||||
}
|
}
|
||||||
@@ -52,48 +48,19 @@ func (Zstd) CaddyModule() caddy.ModuleInfo {
|
|||||||
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
|
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
|
||||||
func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
d.Next() // consume option name
|
d.Next() // consume option name
|
||||||
args := d.RemainingArgs()
|
if !d.NextArg() {
|
||||||
switch len(args) {
|
return nil
|
||||||
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()
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
if ok, _ := zstd.EncoderLevelFromString(levelStr); !ok {
|
||||||
switch d.Val() {
|
return d.Errf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
|
||||||
case "level":
|
zstd.SpeedFastest,
|
||||||
args := d.RemainingArgs()
|
zstd.SpeedBetterCompression,
|
||||||
if len(args) != 1 {
|
zstd.SpeedBestCompression,
|
||||||
return d.ArgErr()
|
zstd.SpeedDefault,
|
||||||
}
|
)
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,11 +69,15 @@ func (z *Zstd) Provision(ctx caddy.Context) error {
|
|||||||
if z.Level == "" {
|
if z.Level == "" {
|
||||||
z.Level = zstd.SpeedDefault.String()
|
z.Level = zstd.SpeedDefault.String()
|
||||||
}
|
}
|
||||||
level, err := parseEncoderLevel(z.Level)
|
var ok bool
|
||||||
if err != nil {
|
if ok, z.level = zstd.EncoderLevelFromString(z.Level); !ok {
|
||||||
return err
|
return fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
|
||||||
|
zstd.SpeedFastest,
|
||||||
|
zstd.SpeedDefault,
|
||||||
|
zstd.SpeedBetterCompression,
|
||||||
|
zstd.SpeedBestCompression,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
z.level = level
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,45 +90,14 @@ func (z Zstd) NewEncoder() encode.Encoder {
|
|||||||
// The default of 8MB for the window is
|
// The default of 8MB for the window is
|
||||||
// too large for many clients, so we limit
|
// too large for many clients, so we limit
|
||||||
// it to 128K to lighten their load.
|
// it to 128K to lighten their load.
|
||||||
writer, _ := zstd.NewWriter(nil, z.writerOptions(128<<10)...)
|
writer, _ := zstd.NewWriter(
|
||||||
return writer
|
nil,
|
||||||
}
|
zstd.WithWindowSize(128<<10),
|
||||||
|
|
||||||
func (z Zstd) writerOptions(windowSize int) []zstd.EOption {
|
|
||||||
opts := []zstd.EOption{
|
|
||||||
zstd.WithWindowSize(windowSize),
|
|
||||||
zstd.WithEncoderConcurrency(1),
|
zstd.WithEncoderConcurrency(1),
|
||||||
zstd.WithZeroFrames(true),
|
zstd.WithZeroFrames(true),
|
||||||
zstd.WithEncoderLevel(z.encoderLevel()),
|
zstd.WithEncoderLevel(z.level),
|
||||||
}
|
|
||||||
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
|
// Interface guards
|
||||||
|
|||||||
@@ -85,11 +85,8 @@ func (e HandlerError) Unwrap() error { return e.Err }
|
|||||||
// randString returns a string of n random characters.
|
// randString returns a string of n random characters.
|
||||||
// It is not even remotely secure OR a proper distribution.
|
// It is not even remotely secure OR a proper distribution.
|
||||||
// But it's good enough for some things. It excludes certain
|
// But it's good enough for some things. It excludes certain
|
||||||
// confusing characters like I, l, 1, 0, O. If sameCase
|
// confusing characters like I, l, 1, 0, O, etc. If sameCase
|
||||||
// is true, then uppercase letters are excluded as well as
|
// is true, then uppercase letters are excluded.
|
||||||
// the characters l and o. If sameCase is false, both uppercase
|
|
||||||
// and lowercase letters are used, and the characters I, l, 1, 0, O
|
|
||||||
// are excluded.
|
|
||||||
func randString(n int, sameCase bool) string {
|
func randString(n int, sameCase bool) string {
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
package caddyhttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHandlerErrorError(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
err HandlerError
|
|
||||||
contains []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "full error",
|
|
||||||
err: HandlerError{
|
|
||||||
ID: "abc123",
|
|
||||||
StatusCode: 404,
|
|
||||||
Err: fmt.Errorf("not found"),
|
|
||||||
Trace: "pkg.Func (file.go:10)",
|
|
||||||
},
|
|
||||||
contains: []string{"abc123", "404", "not found", "pkg.Func"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty error",
|
|
||||||
err: HandlerError{},
|
|
||||||
contains: []string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error with only status code",
|
|
||||||
err: HandlerError{
|
|
||||||
StatusCode: 500,
|
|
||||||
},
|
|
||||||
contains: []string{"500"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error with only message",
|
|
||||||
err: HandlerError{
|
|
||||||
Err: fmt.Errorf("something broke"),
|
|
||||||
},
|
|
||||||
contains: []string{"something broke"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := tt.err.Error()
|
|
||||||
for _, needle := range tt.contains {
|
|
||||||
if !strings.Contains(result, needle) {
|
|
||||||
t.Errorf("Error() = %q, should contain %q", result, needle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlerErrorUnwrap(t *testing.T) {
|
|
||||||
originalErr := fmt.Errorf("original error")
|
|
||||||
he := HandlerError{Err: originalErr}
|
|
||||||
|
|
||||||
unwrapped := he.Unwrap()
|
|
||||||
if unwrapped != originalErr {
|
|
||||||
t.Errorf("Unwrap() = %v, want %v", unwrapped, originalErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestError(t *testing.T) {
|
|
||||||
t.Run("creates error with ID and trace", func(t *testing.T) {
|
|
||||||
err := fmt.Errorf("test error")
|
|
||||||
he := Error(500, err)
|
|
||||||
|
|
||||||
if he.StatusCode != 500 {
|
|
||||||
t.Errorf("StatusCode = %d, want 500", he.StatusCode)
|
|
||||||
}
|
|
||||||
if he.ID == "" {
|
|
||||||
t.Error("ID should not be empty")
|
|
||||||
}
|
|
||||||
if len(he.ID) != 9 {
|
|
||||||
t.Errorf("ID length = %d, want 9", len(he.ID))
|
|
||||||
}
|
|
||||||
if he.Trace == "" {
|
|
||||||
t.Error("Trace should not be empty")
|
|
||||||
}
|
|
||||||
if he.Err != err {
|
|
||||||
t.Error("Err should be the original error")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("unwraps existing HandlerError", func(t *testing.T) {
|
|
||||||
inner := HandlerError{
|
|
||||||
ID: "existing_id",
|
|
||||||
StatusCode: 404,
|
|
||||||
Err: fmt.Errorf("not found"),
|
|
||||||
Trace: "existing trace",
|
|
||||||
}
|
|
||||||
|
|
||||||
he := Error(500, inner)
|
|
||||||
|
|
||||||
// Should keep existing ID
|
|
||||||
if he.ID != "existing_id" {
|
|
||||||
t.Errorf("ID = %q, want 'existing_id'", he.ID)
|
|
||||||
}
|
|
||||||
// Should keep existing StatusCode
|
|
||||||
if he.StatusCode != 404 {
|
|
||||||
t.Errorf("StatusCode = %d, want 404 (existing)", he.StatusCode)
|
|
||||||
}
|
|
||||||
// Should keep existing Trace
|
|
||||||
if he.Trace != "existing trace" {
|
|
||||||
t.Errorf("Trace = %q, want 'existing trace'", he.Trace)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("fills missing fields in existing HandlerError", func(t *testing.T) {
|
|
||||||
inner := HandlerError{
|
|
||||||
Err: fmt.Errorf("inner error"),
|
|
||||||
// ID, StatusCode, and Trace are all empty
|
|
||||||
}
|
|
||||||
|
|
||||||
he := Error(503, inner)
|
|
||||||
|
|
||||||
if he.ID == "" {
|
|
||||||
t.Error("should fill missing ID")
|
|
||||||
}
|
|
||||||
if he.StatusCode != 503 {
|
|
||||||
t.Errorf("should fill missing StatusCode with %d, got %d", 503, he.StatusCode)
|
|
||||||
}
|
|
||||||
if he.Trace == "" {
|
|
||||||
t.Error("should fill missing Trace")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("generates unique IDs", func(t *testing.T) {
|
|
||||||
ids := make(map[string]struct{})
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
he := Error(500, fmt.Errorf("error %d", i))
|
|
||||||
if _, exists := ids[he.ID]; exists {
|
|
||||||
t.Errorf("duplicate ID generated: %s", he.ID)
|
|
||||||
}
|
|
||||||
ids[he.ID] = struct{}{}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrorAsHandlerError(t *testing.T) {
|
|
||||||
he := Error(404, fmt.Errorf("not found"))
|
|
||||||
var target HandlerError
|
|
||||||
if !errors.As(he, &target) {
|
|
||||||
t.Error("Error() result should be assertable as HandlerError via errors.As")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlerErrorWithWrappedError(t *testing.T) {
|
|
||||||
// Test that errors.As can unwrap a wrapped HandlerError
|
|
||||||
inner := HandlerError{
|
|
||||||
ID: "inner",
|
|
||||||
StatusCode: 404,
|
|
||||||
Err: fmt.Errorf("inner error"),
|
|
||||||
}
|
|
||||||
wrapped := fmt.Errorf("wrapped: %w", inner)
|
|
||||||
|
|
||||||
he := Error(500, wrapped)
|
|
||||||
// Since wrapped contains a HandlerError, it should be unwrapped
|
|
||||||
if he.ID != "inner" {
|
|
||||||
t.Errorf("should unwrap to inner ID 'inner', got %q", he.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package caddyhttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRandString(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
length int
|
|
||||||
sameCase bool
|
|
||||||
wantLen int
|
|
||||||
checkCase func(string) bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "zero length",
|
|
||||||
length: 0,
|
|
||||||
sameCase: false,
|
|
||||||
wantLen: 0,
|
|
||||||
checkCase: func(s string) bool {
|
|
||||||
return s == ""
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative length",
|
|
||||||
length: -5,
|
|
||||||
sameCase: false,
|
|
||||||
wantLen: 0,
|
|
||||||
checkCase: func(s string) bool {
|
|
||||||
return s == ""
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single character mixed case",
|
|
||||||
length: 1,
|
|
||||||
sameCase: false,
|
|
||||||
wantLen: 1,
|
|
||||||
checkCase: func(s string) bool {
|
|
||||||
// Should be alphanumeric
|
|
||||||
return len(s) == 1 && (unicode.IsLetter(rune(s[0])) || unicode.IsDigit(rune(s[0])))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single character same case",
|
|
||||||
length: 1,
|
|
||||||
sameCase: true,
|
|
||||||
wantLen: 1,
|
|
||||||
checkCase: func(s string) bool {
|
|
||||||
// Should be lowercase or digit
|
|
||||||
return len(s) == 1 && (unicode.IsLower(rune(s[0])) || unicode.IsDigit(rune(s[0])))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "short string mixed case",
|
|
||||||
length: 5,
|
|
||||||
sameCase: false,
|
|
||||||
wantLen: 5,
|
|
||||||
checkCase: func(s string) bool {
|
|
||||||
// All characters should be alphanumeric
|
|
||||||
for _, c := range s {
|
|
||||||
if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "short string same case",
|
|
||||||
length: 5,
|
|
||||||
sameCase: true,
|
|
||||||
wantLen: 5,
|
|
||||||
checkCase: func(s string) bool {
|
|
||||||
// All characters should be lowercase or digits
|
|
||||||
for _, c := range s {
|
|
||||||
if unicode.IsUpper(c) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "medium string mixed case",
|
|
||||||
length: 20,
|
|
||||||
sameCase: false,
|
|
||||||
wantLen: 20,
|
|
||||||
checkCase: func(s string) bool {
|
|
||||||
for _, c := range s {
|
|
||||||
if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "long string same case",
|
|
||||||
length: 100,
|
|
||||||
sameCase: true,
|
|
||||||
wantLen: 100,
|
|
||||||
checkCase: func(s string) bool {
|
|
||||||
for _, c := range s {
|
|
||||||
if unicode.IsUpper(c) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := randString(tt.length, tt.sameCase)
|
|
||||||
|
|
||||||
// Check length
|
|
||||||
if len(result) != tt.wantLen {
|
|
||||||
t.Errorf("randString(%d, %v) length = %d, want %d",
|
|
||||||
tt.length, tt.sameCase, len(result), tt.wantLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check case requirements
|
|
||||||
if !tt.checkCase(result) {
|
|
||||||
t.Errorf("randString(%d, %v) = %q failed case check",
|
|
||||||
tt.length, tt.sameCase, result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRandString_NoConfusingChars ensures that confusing characters
|
|
||||||
// like I, l, 1, 0, O are excluded from the generated strings
|
|
||||||
func TestRandString_NoConfusingChars(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
sameCase bool
|
|
||||||
excluded []rune
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "mixed case excludes I,l,1,0,O",
|
|
||||||
sameCase: false,
|
|
||||||
excluded: []rune{'I', 'l', '1', '0', 'O'},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "same case excludes l,0",
|
|
||||||
sameCase: true,
|
|
||||||
excluded: []rune{'l', 'o'},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Generate multiple strings to increase confidence
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
result := randString(50, tt.sameCase)
|
|
||||||
|
|
||||||
for _, char := range tt.excluded {
|
|
||||||
if strings.ContainsRune(result, char) {
|
|
||||||
t.Errorf("randString(50, %v) contains excluded character %q in %q",
|
|
||||||
tt.sameCase, char, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRandString_Uniqueness verifies that consecutive calls produce
|
|
||||||
// different strings (with high probability)
|
|
||||||
func TestRandString_Uniqueness(t *testing.T) {
|
|
||||||
const iterations = 100
|
|
||||||
const length = 16
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
sameCase bool
|
|
||||||
}{
|
|
||||||
{"mixed case", false},
|
|
||||||
{"same case", true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
duplicates := 0
|
|
||||||
|
|
||||||
for i := 0; i < iterations; i++ {
|
|
||||||
result := randString(length, tt.sameCase)
|
|
||||||
if seen[result] {
|
|
||||||
duplicates++
|
|
||||||
}
|
|
||||||
seen[result] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// With a 16-character string from a large alphabet, duplicates should be extremely rare
|
|
||||||
// Allow at most 1 duplicate in 100 iterations
|
|
||||||
if duplicates > 1 {
|
|
||||||
t.Errorf("randString(%d, %v) produced %d duplicates in %d iterations (expected ≤1)",
|
|
||||||
length, tt.sameCase, duplicates, iterations)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRandString_CharacterDistribution checks that the generated strings
|
|
||||||
// contain a reasonable mix of characters (not just one character)
|
|
||||||
func TestRandString_CharacterDistribution(t *testing.T) {
|
|
||||||
const length = 1000
|
|
||||||
const minUniqueChars = 15 // Should have at least 15 different characters in 1000 chars
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
sameCase bool
|
|
||||||
}{
|
|
||||||
{"mixed case", false},
|
|
||||||
{"same case", true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := randString(length, tt.sameCase)
|
|
||||||
|
|
||||||
uniqueChars := make(map[rune]bool)
|
|
||||||
for _, c := range result {
|
|
||||||
uniqueChars[c] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(uniqueChars) < minUniqueChars {
|
|
||||||
t.Errorf("randString(%d, %v) produced only %d unique characters (expected ≥%d)",
|
|
||||||
length, tt.sameCase, len(uniqueChars), minUniqueChars)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkRandString measures the performance of random string generation
|
|
||||||
func BenchmarkRandString(b *testing.B) {
|
|
||||||
benchmarks := []struct {
|
|
||||||
name string
|
|
||||||
length int
|
|
||||||
sameCase bool
|
|
||||||
}{
|
|
||||||
{"short_mixed", 8, false},
|
|
||||||
{"short_same", 8, true},
|
|
||||||
{"medium_mixed", 32, false},
|
|
||||||
{"medium_same", 32, true},
|
|
||||||
{"long_mixed", 128, false},
|
|
||||||
{"long_same", 128, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bm := range benchmarks {
|
|
||||||
b.Run(bm.name, func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = randString(bm.length, bm.sameCase)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -281,13 +281,7 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
|
|||||||
sortParam = sortCookie.Value
|
sortParam = sortCookie.Value
|
||||||
}
|
}
|
||||||
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
|
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
|
||||||
http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS
|
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil})
|
||||||
Name: "sort",
|
|
||||||
Value: sortParam,
|
|
||||||
Secure: r.TLS != nil,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// then figure out the order
|
// then figure out the order
|
||||||
@@ -298,13 +292,7 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
|
|||||||
orderParam = orderCookie.Value
|
orderParam = orderCookie.Value
|
||||||
}
|
}
|
||||||
case sortOrderAsc, sortOrderDesc:
|
case sortOrderAsc, sortOrderDesc:
|
||||||
http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS
|
http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil})
|
||||||
Name: "order",
|
|
||||||
Value: orderParam,
|
|
||||||
Secure: r.TLS != nil,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// finally, apply the sorting and limiting
|
// finally, apply the sorting and limiting
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -99,7 +100,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fsrv.Browse.RevealSymlinks {
|
if fsrv.Browse.RevealSymlinks {
|
||||||
symLinkTarget, err := os.Readlink(path)
|
symLinkTarget, err := filepath.EvalSymlinks(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
symlinkPath = symLinkTarget
|
symlinkPath = symLinkTarget
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/internal/filesystems"
|
"github.com/caddyserver/caddy/v2/internal/filesystems"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
@@ -189,105 +188,6 @@ func fileMatcherTest(t *testing.T, i int, tc testCase) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTryFilesRewriteEscapesMatchedPath(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
requestTarget string
|
|
||||||
filename string
|
|
||||||
extraFiles []string
|
|
||||||
wantPath string
|
|
||||||
wantRequestURI string
|
|
||||||
skipWindows bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "question mark in path",
|
|
||||||
requestTarget: "/%3F.html",
|
|
||||||
filename: "?.html",
|
|
||||||
wantPath: "/?.html",
|
|
||||||
wantRequestURI: "/%3F.html",
|
|
||||||
skipWindows: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "percent in path",
|
|
||||||
requestTarget: "/%25.html",
|
|
||||||
filename: "%.html",
|
|
||||||
wantPath: "/%.html",
|
|
||||||
wantRequestURI: "/%25.html",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "encoded question mark remains percent-encoded",
|
|
||||||
requestTarget: "/%253F.html",
|
|
||||||
filename: "%3F.html",
|
|
||||||
wantPath: "/%3F.html",
|
|
||||||
wantRequestURI: "/%253F.html",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "question mark in nested path",
|
|
||||||
requestTarget: "/nested/%3F.html",
|
|
||||||
filename: filepath.Join("nested", "?.html"),
|
|
||||||
wantPath: "/nested/?.html",
|
|
||||||
wantRequestURI: "/nested/%3F.html",
|
|
||||||
skipWindows: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "encoded slash in filename does not conflict with nesting",
|
|
||||||
requestTarget: "/nested%252Ffile.html",
|
|
||||||
filename: "nested%2Ffile.html",
|
|
||||||
extraFiles: []string{filepath.Join("nested", "file.html")},
|
|
||||||
wantPath: "/nested%2Ffile.html",
|
|
||||||
wantRequestURI: "/nested%252Ffile.html",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
if tc.skipWindows && runtime.GOOS == "windows" {
|
|
||||||
t.Skip("Windows file names cannot contain question marks")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range append([]string{tc.filename}, tc.extraFiles...) {
|
|
||||||
filename := filepath.Join(root, name)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
|
|
||||||
t.Fatalf("creating test file parent directory: %v", err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filename, []byte(name), 0o600); err != nil {
|
|
||||||
t.Fatalf("writing test file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &MatchFile{
|
|
||||||
fsmap: &filesystems.FileSystemMap{},
|
|
||||||
Root: root,
|
|
||||||
TryFiles: []string{"{http.request.uri.path}"},
|
|
||||||
}
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://example.com"+tc.requestTarget, nil)
|
|
||||||
repl := caddyhttp.NewTestReplacer(req)
|
|
||||||
|
|
||||||
matched, err := m.MatchWithError(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("matching file: %v", err)
|
|
||||||
}
|
|
||||||
if !matched {
|
|
||||||
t.Fatalf("expected request %s to match %s", tc.requestTarget, tc.filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite.Rewrite{URI: "{http.matchers.file.relative}"}.Rewrite(req, repl)
|
|
||||||
|
|
||||||
if req.URL.Path != tc.wantPath {
|
|
||||||
t.Errorf("rewritten path = %q, want %q", req.URL.Path, tc.wantPath)
|
|
||||||
}
|
|
||||||
if req.RequestURI != tc.wantRequestURI {
|
|
||||||
t.Errorf("rewritten request URI = %q, want %q", req.RequestURI, tc.wantRequestURI)
|
|
||||||
}
|
|
||||||
if req.URL.RawQuery != "" {
|
|
||||||
t.Errorf("rewritten raw query = %q, want empty", req.URL.RawQuery)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPHPFileMatcher(t *testing.T) {
|
func TestPHPFileMatcher(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
path string
|
path string
|
||||||
|
|||||||
@@ -785,7 +785,7 @@ func redirect(w http.ResponseWriter, r *http.Request, toPath string) error {
|
|||||||
if r.URL.RawQuery != "" {
|
if r.URL.RawQuery != "" {
|
||||||
toPath += "?" + r.URL.RawQuery
|
toPath += "?" + r.URL.RawQuery
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, toPath, http.StatusPermanentRedirect) //nolint:gosec // toPath is a same-origin path and leading // is stripped above
|
http.Redirect(w, r, toPath, http.StatusPermanentRedirect)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
package caddyhttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCIDRExpressionToPrefix(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
expr string
|
|
||||||
want netip.Prefix
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid CIDR IPv4",
|
|
||||||
expr: "192.168.0.0/16",
|
|
||||||
want: netip.MustParsePrefix("192.168.0.0/16"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid CIDR IPv6",
|
|
||||||
expr: "fd00::/8",
|
|
||||||
want: netip.MustParsePrefix("fd00::/8"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single IPv4 becomes /32",
|
|
||||||
expr: "192.168.1.1",
|
|
||||||
want: netip.MustParsePrefix("192.168.1.1/32"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single IPv6 becomes /128",
|
|
||||||
expr: "::1",
|
|
||||||
want: netip.MustParsePrefix("::1/128"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "loopback IPv4",
|
|
||||||
expr: "127.0.0.1",
|
|
||||||
want: netip.MustParsePrefix("127.0.0.1/32"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "full IPv6 address",
|
|
||||||
expr: "2001:db8::1",
|
|
||||||
want: netip.MustParsePrefix("2001:db8::1/128"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid CIDR",
|
|
||||||
expr: "192.168.0.0/33",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid IP",
|
|
||||||
expr: "not-an-ip",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty string",
|
|
||||||
expr: "",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "CIDR with invalid IP",
|
|
||||||
expr: "999.999.999.999/24",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "CIDR /0 matches everything",
|
|
||||||
expr: "0.0.0.0/0",
|
|
||||||
want: netip.MustParsePrefix("0.0.0.0/0"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "CIDR /32 single host",
|
|
||||||
expr: "10.0.0.1/32",
|
|
||||||
want: netip.MustParsePrefix("10.0.0.1/32"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "malformed CIDR with extra slash",
|
|
||||||
expr: "10.0.0.0/8/16",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := CIDRExpressionToPrefix(tt.expr)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("CIDRExpressionToPrefix(%q) error = %v, wantErr %v", tt.expr, err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !tt.wantErr && got != tt.want {
|
|
||||||
t.Errorf("CIDRExpressionToPrefix(%q) = %v, want %v", tt.expr, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStaticIPRangeProvision(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ranges []string
|
|
||||||
wantLen int
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid CIDR ranges",
|
|
||||||
ranges: []string{"192.168.0.0/16", "10.0.0.0/8"},
|
|
||||||
wantLen: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single IPs",
|
|
||||||
ranges: []string{"192.168.1.1", "10.0.0.1"},
|
|
||||||
wantLen: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mixed CIDR and single IP",
|
|
||||||
ranges: []string{"192.168.0.0/16", "10.0.0.1"},
|
|
||||||
wantLen: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid range",
|
|
||||||
ranges: []string{"not-valid"},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty ranges",
|
|
||||||
ranges: []string{},
|
|
||||||
wantLen: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil ranges",
|
|
||||||
ranges: nil,
|
|
||||||
wantLen: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
s := &StaticIPRange{Ranges: tt.ranges}
|
|
||||||
// We can't easily create a caddy.Context here without full module setup,
|
|
||||||
// but Provision only uses the ranges field, so we test the logic directly.
|
|
||||||
// The Provision method calls CIDRExpressionToPrefix which we test separately.
|
|
||||||
var parsedCount int
|
|
||||||
var gotErr bool
|
|
||||||
for _, r := range s.Ranges {
|
|
||||||
_, err := CIDRExpressionToPrefix(r)
|
|
||||||
if err != nil {
|
|
||||||
gotErr = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
parsedCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if gotErr != tt.wantErr {
|
|
||||||
t.Errorf("provision error = %v, wantErr %v", gotErr, tt.wantErr)
|
|
||||||
}
|
|
||||||
if !tt.wantErr && parsedCount != tt.wantLen {
|
|
||||||
t.Errorf("parsed %d ranges, want %d", parsedCount, tt.wantLen)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStaticIPRangeGetIPRanges(t *testing.T) {
|
|
||||||
s := &StaticIPRange{
|
|
||||||
ranges: []netip.Prefix{
|
|
||||||
netip.MustParsePrefix("192.168.0.0/16"),
|
|
||||||
netip.MustParsePrefix("10.0.0.0/8"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
result := s.GetIPRanges(nil) // request is unused
|
|
||||||
if len(result) != 2 {
|
|
||||||
t.Errorf("GetIPRanges() returned %d prefixes, want 2", len(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStaticIPRangeCaddyModule(t *testing.T) {
|
|
||||||
s := StaticIPRange{}
|
|
||||||
info := s.CaddyModule()
|
|
||||||
if info.ID != "http.ip_sources.static" {
|
|
||||||
t.Errorf("CaddyModule().ID = %v, want 'http.ip_sources.static'", info.ID)
|
|
||||||
}
|
|
||||||
mod := info.New()
|
|
||||||
if mod == nil {
|
|
||||||
t.Error("New() should not return nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrivateRangesCIDRWrapper(t *testing.T) {
|
|
||||||
ranges := PrivateRangesCIDR()
|
|
||||||
if len(ranges) == 0 {
|
|
||||||
t.Error("PrivateRangesCIDR() should return non-empty list")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify all ranges are valid CIDR or IP expressions
|
|
||||||
for _, r := range ranges {
|
|
||||||
_, err := CIDRExpressionToPrefix(r)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("PrivateRangesCIDR() returned invalid range %q: %v", r, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,10 +18,9 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/internal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoggableHTTPRequest makes an HTTP request loggable with zap.Object().
|
// LoggableHTTPRequest makes an HTTP request loggable with zap.Object().
|
||||||
@@ -48,12 +47,12 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
|||||||
enc.AddString("method", r.Method)
|
enc.AddString("method", r.Method)
|
||||||
enc.AddString("host", r.Host)
|
enc.AddString("host", r.Host)
|
||||||
enc.AddString("uri", r.RequestURI)
|
enc.AddString("uri", r.RequestURI)
|
||||||
enc.AddObject("headers", internal.LoggableHTTPHeader{
|
enc.AddObject("headers", LoggableHTTPHeader{
|
||||||
Header: r.Header,
|
Header: r.Header,
|
||||||
ShouldLogCredentials: r.ShouldLogCredentials,
|
ShouldLogCredentials: r.ShouldLogCredentials,
|
||||||
})
|
})
|
||||||
if r.TransferEncoding != nil {
|
if r.TransferEncoding != nil {
|
||||||
enc.AddArray("transfer_encoding", internal.LoggableStringArray(r.TransferEncoding))
|
enc.AddArray("transfer_encoding", LoggableStringArray(r.TransferEncoding))
|
||||||
}
|
}
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
|
enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
|
||||||
@@ -62,10 +61,44 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
|
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
|
||||||
type LoggableHTTPHeader = internal.LoggableHTTPHeader
|
// Headers with potentially sensitive information (Cookie, Set-Cookie,
|
||||||
|
// Authorization, and Proxy-Authorization) are logged with empty values.
|
||||||
|
type LoggableHTTPHeader struct {
|
||||||
|
http.Header
|
||||||
|
|
||||||
|
ShouldLogCredentials bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
|
||||||
|
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
||||||
|
if h.Header == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for key, val := range h.Header {
|
||||||
|
if !h.ShouldLogCredentials {
|
||||||
|
switch strings.ToLower(key) {
|
||||||
|
case "cookie", "set-cookie", "authorization", "proxy-authorization":
|
||||||
|
val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enc.AddArray(key, LoggableStringArray(val))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoggableStringArray makes a slice of strings marshalable for logging.
|
// LoggableStringArray makes a slice of strings marshalable for logging.
|
||||||
type LoggableStringArray = internal.LoggableStringArray
|
type LoggableStringArray []string
|
||||||
|
|
||||||
|
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
|
||||||
|
func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
|
||||||
|
if sa == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, s := range sa {
|
||||||
|
enc.AppendString(s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoggableTLSConnState makes a TLS connection state loggable with zap.Object().
|
// LoggableTLSConnState makes a TLS connection state loggable with zap.Object().
|
||||||
type LoggableTLSConnState tls.ConnectionState
|
type LoggableTLSConnState tls.ConnectionState
|
||||||
@@ -88,5 +121,7 @@ func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error
|
|||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil)
|
_ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil)
|
||||||
|
_ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil)
|
||||||
|
_ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil)
|
||||||
_ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil)
|
_ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,316 +0,0 @@
|
|||||||
package caddyhttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoggableHTTPRequestMarshal(t *testing.T) {
|
|
||||||
req, _ := http.NewRequest("GET", "https://example.com/path?q=1", nil)
|
|
||||||
req.RemoteAddr = "192.168.1.1:12345"
|
|
||||||
req.Header.Set("User-Agent", "test-agent")
|
|
||||||
req.Header.Set("Accept", "text/html")
|
|
||||||
|
|
||||||
ctx := context.WithValue(req.Context(), VarsCtxKey, map[string]any{
|
|
||||||
ClientIPVarKey: "192.168.1.1",
|
|
||||||
})
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
|
|
||||||
lr := LoggableHTTPRequest{Request: req}
|
|
||||||
|
|
||||||
enc := zapcore.NewMapObjectEncoder()
|
|
||||||
err := lr.MarshalLogObject(enc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MarshalLogObject() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if enc.Fields["remote_ip"] != "192.168.1.1" {
|
|
||||||
t.Errorf("remote_ip = %v, want '192.168.1.1'", enc.Fields["remote_ip"])
|
|
||||||
}
|
|
||||||
if enc.Fields["remote_port"] != "12345" {
|
|
||||||
t.Errorf("remote_port = %v, want '12345'", enc.Fields["remote_port"])
|
|
||||||
}
|
|
||||||
if enc.Fields["client_ip"] != "192.168.1.1" {
|
|
||||||
t.Errorf("client_ip = %v, want '192.168.1.1'", enc.Fields["client_ip"])
|
|
||||||
}
|
|
||||||
if enc.Fields["method"] != "GET" {
|
|
||||||
t.Errorf("method = %v, want 'GET'", enc.Fields["method"])
|
|
||||||
}
|
|
||||||
if enc.Fields["host"] != "example.com" {
|
|
||||||
t.Errorf("host = %v, want 'example.com'", enc.Fields["host"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggableHTTPRequestNoPort(t *testing.T) {
|
|
||||||
req, _ := http.NewRequest("GET", "http://example.com/", nil)
|
|
||||||
req.RemoteAddr = "192.168.1.1" // no port
|
|
||||||
|
|
||||||
ctx := context.WithValue(req.Context(), VarsCtxKey, map[string]any{})
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
|
|
||||||
lr := LoggableHTTPRequest{Request: req}
|
|
||||||
|
|
||||||
enc := zapcore.NewMapObjectEncoder()
|
|
||||||
err := lr.MarshalLogObject(enc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MarshalLogObject() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if enc.Fields["remote_ip"] != "192.168.1.1" {
|
|
||||||
t.Errorf("remote_ip = %v, want '192.168.1.1'", enc.Fields["remote_ip"])
|
|
||||||
}
|
|
||||||
if enc.Fields["remote_port"] != "" {
|
|
||||||
t.Errorf("remote_port = %v, want empty string", enc.Fields["remote_port"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggableHTTPHeaderRedaction(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
header http.Header
|
|
||||||
shouldLogCredentials bool
|
|
||||||
expectRedacted []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "redacts sensitive headers",
|
|
||||||
header: http.Header{
|
|
||||||
"Cookie": {"session=abc123"},
|
|
||||||
"Set-Cookie": {"session=xyz"},
|
|
||||||
"Authorization": {"Bearer token123"},
|
|
||||||
"Proxy-Authorization": {"Basic credentials"},
|
|
||||||
"User-Agent": {"test-agent"},
|
|
||||||
},
|
|
||||||
shouldLogCredentials: false,
|
|
||||||
expectRedacted: []string{"Cookie", "Set-Cookie", "Authorization", "Proxy-Authorization"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "logs credentials when enabled",
|
|
||||||
header: http.Header{
|
|
||||||
"Cookie": {"session=abc123"},
|
|
||||||
"Authorization": {"Bearer token123"},
|
|
||||||
},
|
|
||||||
shouldLogCredentials: true,
|
|
||||||
expectRedacted: nil, // nothing should be redacted
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil header",
|
|
||||||
header: nil,
|
|
||||||
shouldLogCredentials: false,
|
|
||||||
expectRedacted: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
h := LoggableHTTPHeader{Header: tt.header, ShouldLogCredentials: tt.shouldLogCredentials}
|
|
||||||
enc := zapcore.NewMapObjectEncoder()
|
|
||||||
err := h.MarshalLogObject(enc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MarshalLogObject() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.header == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, key := range tt.expectRedacted {
|
|
||||||
// The encoded value should be an array with ["REDACTED"]
|
|
||||||
if arr, ok := enc.Fields[key]; ok {
|
|
||||||
arrEnc, ok := arr.(zapcore.ArrayMarshaler)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Marshal the array to check its contents
|
|
||||||
testEnc := &testArrayEncoder{}
|
|
||||||
_ = arrEnc.MarshalLogArray(testEnc)
|
|
||||||
if len(testEnc.items) != 1 || testEnc.items[0] != "REDACTED" {
|
|
||||||
t.Errorf("header %q should be REDACTED, got %v", key, testEnc.items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.shouldLogCredentials && tt.header != nil {
|
|
||||||
for key, vals := range tt.header {
|
|
||||||
if arr, ok := enc.Fields[key]; ok {
|
|
||||||
arrEnc, ok := arr.(zapcore.ArrayMarshaler)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
testEnc := &testArrayEncoder{}
|
|
||||||
_ = arrEnc.MarshalLogArray(testEnc)
|
|
||||||
if len(testEnc.items) > 0 && testEnc.items[0] == "REDACTED" {
|
|
||||||
t.Errorf("header %q should NOT be redacted when credentials logging is enabled, original: %v", key, vals)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testArrayEncoder is a simple array encoder for testing
|
|
||||||
type testArrayEncoder struct {
|
|
||||||
items []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *testArrayEncoder) AppendString(s string) { e.items = append(e.items, s) }
|
|
||||||
func (e *testArrayEncoder) AppendBool(bool) {}
|
|
||||||
func (e *testArrayEncoder) AppendByteString([]byte) {}
|
|
||||||
func (e *testArrayEncoder) AppendComplex128(complex128) {}
|
|
||||||
func (e *testArrayEncoder) AppendComplex64(complex64) {}
|
|
||||||
func (e *testArrayEncoder) AppendFloat64(float64) {}
|
|
||||||
func (e *testArrayEncoder) AppendFloat32(float32) {}
|
|
||||||
func (e *testArrayEncoder) AppendInt(int) {}
|
|
||||||
func (e *testArrayEncoder) AppendInt64(int64) {}
|
|
||||||
func (e *testArrayEncoder) AppendInt32(int32) {}
|
|
||||||
func (e *testArrayEncoder) AppendInt16(int16) {}
|
|
||||||
func (e *testArrayEncoder) AppendInt8(int8) {}
|
|
||||||
func (e *testArrayEncoder) AppendUint(uint) {}
|
|
||||||
func (e *testArrayEncoder) AppendUint64(uint64) {}
|
|
||||||
func (e *testArrayEncoder) AppendUint32(uint32) {}
|
|
||||||
func (e *testArrayEncoder) AppendUint16(uint16) {}
|
|
||||||
func (e *testArrayEncoder) AppendUint8(uint8) {}
|
|
||||||
func (e *testArrayEncoder) AppendUintptr(uintptr) {}
|
|
||||||
func (e *testArrayEncoder) AppendDuration(time.Duration) {}
|
|
||||||
func (e *testArrayEncoder) AppendTime(time.Time) {}
|
|
||||||
func (e *testArrayEncoder) AppendArray(zapcore.ArrayMarshaler) error { return nil }
|
|
||||||
func (e *testArrayEncoder) AppendObject(zapcore.ObjectMarshaler) error { return nil }
|
|
||||||
func (e *testArrayEncoder) AppendReflected(any) error { return nil }
|
|
||||||
|
|
||||||
func TestLoggableStringArray(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input LoggableStringArray
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "nil array",
|
|
||||||
input: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty array",
|
|
||||||
input: LoggableStringArray{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single element",
|
|
||||||
input: LoggableStringArray{"hello"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple elements",
|
|
||||||
input: LoggableStringArray{"a", "b", "c"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
enc := &testArrayEncoder{}
|
|
||||||
err := tt.input.MarshalLogArray(enc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MarshalLogArray() error = %v", err)
|
|
||||||
}
|
|
||||||
if tt.input != nil && len(enc.items) != len(tt.input) {
|
|
||||||
t.Errorf("expected %d items, got %d", len(tt.input), len(enc.items))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggableTLSConnState(t *testing.T) {
|
|
||||||
t.Run("basic TLS state", func(t *testing.T) {
|
|
||||||
state := LoggableTLSConnState(tls.ConnectionState{
|
|
||||||
Version: tls.VersionTLS13,
|
|
||||||
CipherSuite: tls.TLS_AES_128_GCM_SHA256,
|
|
||||||
NegotiatedProtocol: "h2",
|
|
||||||
ServerName: "example.com",
|
|
||||||
})
|
|
||||||
|
|
||||||
enc := zapcore.NewMapObjectEncoder()
|
|
||||||
err := state.MarshalLogObject(enc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MarshalLogObject() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if enc.Fields["proto"] != "h2" {
|
|
||||||
t.Errorf("proto = %v, want 'h2'", enc.Fields["proto"])
|
|
||||||
}
|
|
||||||
if enc.Fields["server_name"] != "example.com" {
|
|
||||||
t.Errorf("server_name = %v, want 'example.com'", enc.Fields["server_name"])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("TLS state with peer certificates", func(t *testing.T) {
|
|
||||||
// Skipping detailed cert subject test since x509.Certificate creation
|
|
||||||
// for testing requires complex setup; covered by the no-peer-certs test
|
|
||||||
state := LoggableTLSConnState(tls.ConnectionState{
|
|
||||||
Version: tls.VersionTLS12,
|
|
||||||
CipherSuite: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
||||||
})
|
|
||||||
|
|
||||||
enc := zapcore.NewMapObjectEncoder()
|
|
||||||
err := state.MarshalLogObject(enc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MarshalLogObject() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if enc.Fields["version"] != uint16(tls.VersionTLS12) {
|
|
||||||
t.Errorf("version = %v, want TLS 1.2", enc.Fields["version"])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("TLS state without peer certificates", func(t *testing.T) {
|
|
||||||
state := LoggableTLSConnState(tls.ConnectionState{
|
|
||||||
Version: tls.VersionTLS12,
|
|
||||||
})
|
|
||||||
|
|
||||||
enc := zapcore.NewMapObjectEncoder()
|
|
||||||
err := state.MarshalLogObject(enc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MarshalLogObject() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should not contain client cert fields when no peer certs
|
|
||||||
if _, ok := enc.Fields["client_common_name"]; ok {
|
|
||||||
t.Error("should not have client_common_name without peer certificates")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggableHTTPHeaderCaseInsensitivity(t *testing.T) {
|
|
||||||
// HTTP headers should be case-insensitive for redaction
|
|
||||||
h := LoggableHTTPHeader{
|
|
||||||
Header: http.Header{
|
|
||||||
"AUTHORIZATION": {"Bearer secret"},
|
|
||||||
"cookie": {"session=abc"},
|
|
||||||
"Proxy-Authorization": {"Basic creds"},
|
|
||||||
},
|
|
||||||
ShouldLogCredentials: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := zapcore.NewMapObjectEncoder()
|
|
||||||
err := h.MarshalLogObject(enc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MarshalLogObject() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// All sensitive headers should be redacted regardless of casing
|
|
||||||
// Note: http.Header canonicalizes keys, so "cookie" becomes "Cookie"
|
|
||||||
for key := range enc.Fields {
|
|
||||||
lk := strings.ToLower(key)
|
|
||||||
if lk == "cookie" || lk == "authorization" || lk == "proxy-authorization" {
|
|
||||||
arr, ok := enc.Fields[key].(zapcore.ArrayMarshaler)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
testEnc := &testArrayEncoder{}
|
|
||||||
_ = arr.MarshalLogArray(testEnc)
|
|
||||||
if len(testEnc.items) != 1 || testEnc.items[0] != "REDACTED" {
|
|
||||||
t.Errorf("header %q should be REDACTED, got %v", key, testEnc.items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1562,14 +1562,6 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er
|
|||||||
// instances of the matcher in this set
|
// instances of the matcher in this set
|
||||||
tokensByMatcherName := make(map[string][]caddyfile.Token)
|
tokensByMatcherName := make(map[string][]caddyfile.Token)
|
||||||
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
|
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()
|
matcherName := d.Val()
|
||||||
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
|
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package caddyhttp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -11,14 +10,9 @@ import (
|
|||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"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"
|
||||||
caddymetrics "github.com/caddyserver/caddy/v2/internal/metrics"
|
"github.com/caddyserver/caddy/v2/internal/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Metrics configures metrics observations.
|
// Metrics configures metrics observations.
|
||||||
@@ -73,20 +67,10 @@ type Metrics struct {
|
|||||||
// for production environments exposed to the internet).
|
// for production environments exposed to the internet).
|
||||||
ObserveCatchallHosts bool `json:"observe_catchall_hosts,omitempty"`
|
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
|
init sync.Once
|
||||||
httpMetrics *httpMetrics
|
httpMetrics *httpMetrics
|
||||||
allowedHosts map[string]struct{}
|
allowedHosts map[string]struct{}
|
||||||
hasHTTPSServer bool
|
hasHTTPSServer bool
|
||||||
meterProvider *sdkmetric.MeterProvider
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpMetrics struct {
|
type httpMetrics struct {
|
||||||
@@ -163,70 +147,6 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
|
|||||||
}, httpLabels)
|
}, 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
|
// 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.
|
// for metrics collection, similar to how auto-HTTPS scans for domain names.
|
||||||
func (m *Metrics) scanConfigForHosts(app *App) {
|
func (m *Metrics) scanConfigForHosts(app *App) {
|
||||||
@@ -314,7 +234,7 @@ func newMetricsInstrumentedRoute(ctx caddy.Context, handler string, next Handler
|
|||||||
func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
server := serverNameFromContext(r.Context())
|
server := serverNameFromContext(r.Context())
|
||||||
labels := prometheus.Labels{"server": server, "handler": h.handler}
|
labels := prometheus.Labels{"server": server, "handler": h.handler}
|
||||||
method := caddymetrics.SanitizeMethod(r.Method)
|
method := metrics.SanitizeMethod(r.Method)
|
||||||
// the "code" value is set later, but initialized here to eliminate the possibility
|
// the "code" value is set later, but initialized here to eliminate the possibility
|
||||||
// of a panic
|
// of a panic
|
||||||
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
|
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
|
||||||
@@ -344,7 +264,7 @@ func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Requ
|
|||||||
// being called when the headers are written.
|
// being called when the headers are written.
|
||||||
// Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader.
|
// Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader.
|
||||||
writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool {
|
writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool {
|
||||||
statusLabels["code"] = caddymetrics.SanitizeCode(status)
|
statusLabels["code"] = metrics.SanitizeCode(status)
|
||||||
ttfb := time.Since(start).Seconds()
|
ttfb := time.Since(start).Seconds()
|
||||||
h.metrics.httpMetrics.responseDuration.With(statusLabels).Observe(ttfb)
|
h.metrics.httpMetrics.responseDuration.With(statusLabels).Observe(ttfb)
|
||||||
return false
|
return false
|
||||||
@@ -360,7 +280,7 @@ func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Requ
|
|||||||
if statusLabels["code"] == "" {
|
if statusLabels["code"] == "" {
|
||||||
// we still sanitize it, even though it's likely to be 0. A 200 is
|
// we still sanitize it, even though it's likely to be 0. A 200 is
|
||||||
// returned on fallthrough so we want to reflect that.
|
// returned on fallthrough so we want to reflect that.
|
||||||
statusLabels["code"] = caddymetrics.SanitizeCode(status)
|
statusLabels["code"] = metrics.SanitizeCode(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
h.metrics.httpMetrics.requestDuration.With(statusLabels).Observe(dur)
|
h.metrics.httpMetrics.requestDuration.With(statusLabels).Observe(dur)
|
||||||
|
|||||||
@@ -523,56 +523,6 @@ func TestMetricsInstrumentedRoute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetricsProvisionOTLPDisabled(t *testing.T) {
|
|
||||||
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
||||||
|
|
||||||
m := &Metrics{OTLP: false}
|
|
||||||
|
|
||||||
if err := m.provisionOTLP(ctx); err != nil {
|
|
||||||
t.Fatalf("provisionOTLP returned unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if m.meterProvider != nil {
|
|
||||||
t.Fatalf("meterProvider should remain nil when OTLP is disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// shutdown must be safe on a never-provisioned Metrics.
|
|
||||||
if err := m.shutdown(context.Background()); err != nil {
|
|
||||||
t.Fatalf("shutdown returned unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMetricsProvisionOTLPNoopExporter(t *testing.T) {
|
|
||||||
// OTEL_METRICS_EXPORTER=none makes autoexport return its built-in
|
|
||||||
// no-op reader, which avoids any network I/O while still exercising
|
|
||||||
// the full provisionOTLP -> shutdown lifecycle.
|
|
||||||
t.Setenv("OTEL_METRICS_EXPORTER", "none")
|
|
||||||
|
|
||||||
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
||||||
|
|
||||||
m := &Metrics{OTLP: true}
|
|
||||||
|
|
||||||
if err := m.provisionOTLP(ctx); err != nil {
|
|
||||||
t.Fatalf("provisionOTLP returned unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if m.meterProvider == nil {
|
|
||||||
t.Fatalf("provisionOTLP did not create a MeterProvider")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.shutdown(context.Background()); err != nil {
|
|
||||||
t.Fatalf("shutdown returned unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// shutdown on a nil receiver is a convenience so App.Stop can call it
|
|
||||||
// without guarding against app.Metrics being unset.
|
|
||||||
func TestMetricsShutdownNilReceiver(t *testing.T) {
|
|
||||||
var m *Metrics
|
|
||||||
|
|
||||||
if err := m.shutdown(context.Background()); err != nil {
|
|
||||||
t.Fatalf("shutdown on nil Metrics returned unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkMetricsInstrumentedRoute(b *testing.B) {
|
func BenchmarkMetricsInstrumentedRoute(b *testing.B) {
|
||||||
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||||
m := &Metrics{
|
m := &Metrics{
|
||||||
|
|||||||
@@ -387,14 +387,17 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
|||||||
switch key {
|
switch key {
|
||||||
case "http.shutting_down":
|
case "http.shutting_down":
|
||||||
server := req.Context().Value(ServerCtxKey).(*Server)
|
server := req.Context().Value(ServerCtxKey).(*Server)
|
||||||
return server.shutdownAt.Load() != nil, true
|
server.shutdownAtMu.RLock()
|
||||||
|
defer server.shutdownAtMu.RUnlock()
|
||||||
|
return !server.shutdownAt.IsZero(), true
|
||||||
case "http.time_until_shutdown":
|
case "http.time_until_shutdown":
|
||||||
server := req.Context().Value(ServerCtxKey).(*Server)
|
server := req.Context().Value(ServerCtxKey).(*Server)
|
||||||
t := server.shutdownAt.Load()
|
server.shutdownAtMu.RLock()
|
||||||
if t == nil {
|
defer server.shutdownAtMu.RUnlock()
|
||||||
|
if server.shutdownAt.IsZero() {
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
return time.Until(*t), true
|
return time.Until(server.shutdownAt), true
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, false
|
return nil, false
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
|||||||
// lb_retries <retries>
|
// lb_retries <retries>
|
||||||
// lb_try_duration <duration>
|
// lb_try_duration <duration>
|
||||||
// lb_try_interval <interval>
|
// lb_try_interval <interval>
|
||||||
// lb_retry_match <matcher>
|
// lb_retry_match <request-matcher>
|
||||||
//
|
//
|
||||||
// # active health checking
|
// # active health checking
|
||||||
// health_uri <uri>
|
// health_uri <uri>
|
||||||
@@ -96,7 +96,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
|||||||
// flush_interval <duration>
|
// flush_interval <duration>
|
||||||
// request_buffers <size>
|
// request_buffers <size>
|
||||||
// response_buffers <size>
|
// response_buffers <size>
|
||||||
// stream_buffer_size <size>
|
|
||||||
// stream_timeout <duration>
|
// stream_timeout <duration>
|
||||||
// stream_close_delay <duration>
|
// stream_close_delay <duration>
|
||||||
// verbose_logs
|
// verbose_logs
|
||||||
@@ -647,7 +646,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
h.FlushInterval = caddy.Duration(dur)
|
h.FlushInterval = caddy.Duration(dur)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "request_buffers", "response_buffers", "stream_buffer_size":
|
case "request_buffers", "response_buffers":
|
||||||
subdir := d.Val()
|
subdir := d.Val()
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
@@ -671,8 +670,6 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
h.RequestBuffers = size
|
h.RequestBuffers = size
|
||||||
case "response_buffers":
|
case "response_buffers":
|
||||||
h.ResponseBuffers = size
|
h.ResponseBuffers = size
|
||||||
case "stream_buffer_size":
|
|
||||||
h.StreamBufferSize = int(size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "stream_timeout":
|
case "stream_timeout":
|
||||||
@@ -728,6 +725,9 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], "", nil)
|
err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], "", nil)
|
||||||
case 2:
|
case 2:
|
||||||
// some lint checks, I guess
|
// 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}") {
|
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")
|
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up X-Forwarded-For: the reverse proxy's default behavior is to pass headers to the upstream")
|
||||||
}
|
}
|
||||||
@@ -885,14 +885,6 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
return err
|
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() {
|
if commonScheme == "http" && te.TLSEnabled() {
|
||||||
return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
|
return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"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"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
@@ -416,19 +418,14 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
|
|||||||
return env, nil
|
return env, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var splitSearchNonASCII = search.New(language.Und, search.IgnoreCase)
|
||||||
|
|
||||||
// splitPos returns the index where path should
|
// splitPos returns the index where path should
|
||||||
// be split based on t.SplitPath.
|
// be split based on t.SplitPath.
|
||||||
//
|
//
|
||||||
// example: if splitPath is [".php"]
|
// example: if splitPath is [".php"]
|
||||||
// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path")
|
// "/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)
|
// Adapted from FrankenPHP's code (copyright 2026 Kévin Dunglas, MIT license)
|
||||||
func (t Transport) splitPos(path string) int {
|
func (t Transport) splitPos(path string) int {
|
||||||
// TODO: from v1...
|
// TODO: from v1...
|
||||||
@@ -441,18 +438,31 @@ func (t Transport) splitPos(path string) int {
|
|||||||
|
|
||||||
pathLen := len(path)
|
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 {
|
for _, split := range t.SplitPath {
|
||||||
splitLen := len(split)
|
splitLen := len(split)
|
||||||
if splitLen == 0 || splitLen > pathLen {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i <= pathLen-splitLen; i++ {
|
for i := range pathLen {
|
||||||
|
if path[i] >= utf8.RuneSelf {
|
||||||
|
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
|
||||||
|
return end
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if i+splitLen > pathLen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
match := true
|
match := true
|
||||||
for j := range splitLen {
|
for j := range splitLen {
|
||||||
c := path[i+j]
|
c := path[i+j]
|
||||||
|
|
||||||
if c >= utf8.RuneSelf {
|
if c >= utf8.RuneSelf {
|
||||||
match = false
|
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
|
||||||
|
return end
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,65 +191,6 @@ func TestSplitPos(t *testing.T) {
|
|||||||
splitPath: []string{".php"},
|
splitPath: []string{".php"},
|
||||||
wantPos: 9,
|
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 {
|
for _, tt := range tests {
|
||||||
@@ -303,31 +244,3 @@ func TestSplitPosUnicodeSecurityRegression(t *testing.T) {
|
|||||||
assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php")
|
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.
|
// Mock the context variables required by Caddy.
|
||||||
// We need to inject the variable map manually since we aren't running the full server.
|
// We need to inject the variable map manually since we aren't running the full server.
|
||||||
vars := map[string]any{
|
vars := map[string]interface{}{
|
||||||
caddyhttp.TrustedProxyVarKey: false,
|
caddyhttp.TrustedProxyVarKey: false,
|
||||||
}
|
}
|
||||||
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
|
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-Proto", "https")
|
||||||
req.Header.Set("X-Forwarded-Host", "original.example.com")
|
req.Header.Set("X-Forwarded-Host", "original.example.com")
|
||||||
|
|
||||||
vars := map[string]any{
|
vars := map[string]interface{}{
|
||||||
caddyhttp.TrustedProxyVarKey: true,
|
caddyhttp.TrustedProxyVarKey: true,
|
||||||
caddyhttp.ClientIPVarKey: "1.2.3.4",
|
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-Proto", "https")
|
||||||
req.Header.Set("X-Forwarded-Host", "spoofed.example.com")
|
req.Header.Set("X-Forwarded-Host", "spoofed.example.com")
|
||||||
|
|
||||||
vars := map[string]any{
|
vars := map[string]interface{}{
|
||||||
caddyhttp.TrustedProxyVarKey: false,
|
caddyhttp.TrustedProxyVarKey: false,
|
||||||
caddyhttp.ClientIPVarKey: "",
|
caddyhttp.ClientIPVarKey: "",
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) {
|
|||||||
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||||
req.RemoteAddr = "@"
|
req.RemoteAddr = "@"
|
||||||
|
|
||||||
vars := map[string]any{
|
vars := map[string]interface{}{
|
||||||
caddyhttp.TrustedProxyVarKey: true,
|
caddyhttp.TrustedProxyVarKey: true,
|
||||||
caddyhttp.ClientIPVarKey: "5.6.7.8",
|
caddyhttp.ClientIPVarKey: "5.6.7.8",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ type Upstream struct {
|
|||||||
activeHealthCheckUpstream string
|
activeHealthCheckUpstream string
|
||||||
healthCheckPolicy *PassiveHealthChecks
|
healthCheckPolicy *PassiveHealthChecks
|
||||||
cb CircuitBreaker
|
cb CircuitBreaker
|
||||||
unhealthy atomic.Int32 // status from active health checker
|
unhealthy int32 // accessed atomically; status from active health checker
|
||||||
}
|
}
|
||||||
|
|
||||||
// (pointer receiver necessary to avoid a race condition, since
|
// (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.
|
// 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.
|
// Its fields are accessed atomically and Host values must not be copied.
|
||||||
type Host struct {
|
type Host struct {
|
||||||
numRequests atomic.Int64
|
numRequests int64 // must be 64-bit aligned on 32-bit systems (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG)
|
||||||
fails atomic.Int64
|
fails int64
|
||||||
activePasses atomic.Int64
|
activePasses int64
|
||||||
activeFails atomic.Int64
|
activeFails int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NumRequests returns the number of active requests to the upstream.
|
// NumRequests returns the number of active requests to the upstream.
|
||||||
func (h *Host) NumRequests() int {
|
func (h *Host) NumRequests() int {
|
||||||
return int(h.numRequests.Load())
|
return int(atomic.LoadInt64(&h.numRequests))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fails returns the number of recent failures with the upstream.
|
// Fails returns the number of recent failures with the upstream.
|
||||||
func (h *Host) Fails() int {
|
func (h *Host) Fails() int {
|
||||||
return int(h.fails.Load())
|
return int(atomic.LoadInt64(&h.fails))
|
||||||
}
|
}
|
||||||
|
|
||||||
// activeHealthPasses returns the number of consecutive active health check passes with the upstream.
|
// activeHealthPasses returns the number of consecutive active health check passes with the upstream.
|
||||||
func (h *Host) activeHealthPasses() int {
|
func (h *Host) activeHealthPasses() int {
|
||||||
return int(h.activePasses.Load())
|
return int(atomic.LoadInt64(&h.activePasses))
|
||||||
}
|
}
|
||||||
|
|
||||||
// activeHealthFails returns the number of consecutive active health check failures with the upstream.
|
// activeHealthFails returns the number of consecutive active health check failures with the upstream.
|
||||||
func (h *Host) activeHealthFails() int {
|
func (h *Host) activeHealthFails() int {
|
||||||
return int(h.activeFails.Load())
|
return int(atomic.LoadInt64(&h.activeFails))
|
||||||
}
|
}
|
||||||
|
|
||||||
// countRequest mutates the active request count by
|
// countRequest mutates the active request count by
|
||||||
// delta. It returns an error if the adjustment fails.
|
// delta. It returns an error if the adjustment fails.
|
||||||
func (h *Host) countRequest(delta int) error {
|
func (h *Host) countRequest(delta int) error {
|
||||||
result := h.numRequests.Add(int64(delta))
|
result := atomic.AddInt64(&h.numRequests, int64(delta))
|
||||||
if result < 0 {
|
if result < 0 {
|
||||||
return fmt.Errorf("count below 0: %d", result)
|
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
|
// countFail mutates the recent failures count by
|
||||||
// delta. It returns an error if the adjustment fails.
|
// delta. It returns an error if the adjustment fails.
|
||||||
func (h *Host) countFail(delta int) error {
|
func (h *Host) countFail(delta int) error {
|
||||||
result := h.fails.Add(int64(delta))
|
result := atomic.AddInt64(&h.fails, int64(delta))
|
||||||
if result < 0 {
|
if result < 0 {
|
||||||
return fmt.Errorf("count below 0: %d", result)
|
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
|
// countHealthPass mutates the recent passes count by
|
||||||
// delta. It returns an error if the adjustment fails.
|
// delta. It returns an error if the adjustment fails.
|
||||||
func (h *Host) countHealthPass(delta int) error {
|
func (h *Host) countHealthPass(delta int) error {
|
||||||
result := h.activePasses.Add(int64(delta))
|
result := atomic.AddInt64(&h.activePasses, int64(delta))
|
||||||
if result < 0 {
|
if result < 0 {
|
||||||
return fmt.Errorf("count below 0: %d", result)
|
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
|
// countHealthFail mutates the recent failures count by
|
||||||
// delta. It returns an error if the adjustment fails.
|
// delta. It returns an error if the adjustment fails.
|
||||||
func (h *Host) countHealthFail(delta int) error {
|
func (h *Host) countHealthFail(delta int) error {
|
||||||
result := h.activeFails.Add(int64(delta))
|
result := atomic.AddInt64(&h.activeFails, int64(delta))
|
||||||
if result < 0 {
|
if result < 0 {
|
||||||
return fmt.Errorf("count below 0: %d", result)
|
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.
|
// resetHealth resets the health check counters.
|
||||||
func (h *Host) resetHealth() {
|
func (h *Host) resetHealth() {
|
||||||
h.activePasses.Store(0)
|
atomic.StoreInt64(&h.activePasses, 0)
|
||||||
h.activeFails.Store(0)
|
atomic.StoreInt64(&h.activeFails, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// healthy returns true if the upstream is not actively marked as unhealthy.
|
// healthy returns true if the upstream is not actively marked as unhealthy.
|
||||||
// (This returns the status only from the "active" health checks.)
|
// (This returns the status only from the "active" health checks.)
|
||||||
func (u *Upstream) healthy() bool {
|
func (u *Upstream) healthy() bool {
|
||||||
return u.unhealthy.Load() == 0
|
return atomic.LoadInt32(&u.unhealthy) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetHealthy sets the upstream has healthy or unhealthy
|
// SetHealthy sets the upstream has healthy or unhealthy
|
||||||
@@ -260,7 +260,7 @@ func (u *Upstream) setHealthy(healthy bool) bool {
|
|||||||
if healthy {
|
if healthy {
|
||||||
unhealthy, compare = 0, 1
|
unhealthy, compare = 0, 1
|
||||||
}
|
}
|
||||||
return u.unhealthy.CompareAndSwap(compare, unhealthy)
|
return atomic.CompareAndSwapInt32(&u.unhealthy, compare, unhealthy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialInfo contains information needed to dial a
|
// DialInfo contains information needed to dial a
|
||||||
|
|||||||
@@ -129,11 +129,11 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
tls *TLSConfig
|
tls *TLSConfig
|
||||||
proxyProtocol string
|
proxyProtocol string
|
||||||
serverNameHasPlaceholder bool
|
serverNameHasPlaceholder bool
|
||||||
expectDialTLSContext bool
|
expectDialTLSContext bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no TLS, no proxy protocol",
|
name: "no TLS, no proxy protocol",
|
||||||
@@ -194,3 +194,4 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package reverseproxy
|
package reverseproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -9,13 +8,11 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -258,530 +255,3 @@ func TestDialErrorBodyRetry(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newExpressionMatcher provisions a MatchExpression for use in tests
|
|
||||||
func newExpressionMatcher(t *testing.T, expr string) *caddyhttp.MatchExpression {
|
|
||||||
t.Helper()
|
|
||||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
||||||
t.Cleanup(cancel)
|
|
||||||
m := &caddyhttp.MatchExpression{Expr: expr}
|
|
||||||
if err := m.Provision(ctx); err != nil {
|
|
||||||
t.Fatalf("failed to provision expression %q: %v", expr, err)
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// minimalHandlerWithRetryMatch is like minimalHandler but also configures
|
|
||||||
// RetryMatch so that response-based retry can be tested
|
|
||||||
func minimalHandlerWithRetryMatch(retries int, retryMatch caddyhttp.MatcherSets, upstreams ...*Upstream) *Handler {
|
|
||||||
h := minimalHandler(retries, upstreams...)
|
|
||||||
h.LoadBalancing.RetryMatch = retryMatch
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestResponseRetryStatusCode verifies that when an upstream returns a status
|
|
||||||
// code matching a retry_match expression, the request is retried on the next
|
|
||||||
// upstream
|
|
||||||
func TestResponseRetryStatusCode(t *testing.T) {
|
|
||||||
// Bad upstream: returns 502
|
|
||||||
badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
|
||||||
}))
|
|
||||||
t.Cleanup(badServer.Close)
|
|
||||||
|
|
||||||
// Good upstream: returns 200
|
|
||||||
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}))
|
|
||||||
t.Cleanup(goodServer.Close)
|
|
||||||
|
|
||||||
retryMatch := caddyhttp.MatcherSets{
|
|
||||||
caddyhttp.MatcherSet{
|
|
||||||
newExpressionMatcher(t, "{http.reverse_proxy.status_code} in [502, 503]"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundRobin picks index 1 first, then 0
|
|
||||||
upstreams := []*Upstream{
|
|
||||||
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
|
|
||||||
{Host: new(Host), Dial: badServer.Listener.Addr().String()},
|
|
||||||
}
|
|
||||||
|
|
||||||
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
|
|
||||||
req = prepareTestRequest(req)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
|
|
||||||
gotStatus := rec.Code
|
|
||||||
if err != nil {
|
|
||||||
if herr, ok := err.(caddyhttp.HandlerError); ok {
|
|
||||||
gotStatus = herr.StatusCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if gotStatus != http.StatusOK {
|
|
||||||
t.Errorf("status: got %d, want %d (err=%v)", gotStatus, http.StatusOK, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestResponseRetryHeader verifies that response header matching triggers
|
|
||||||
// retries via a CEL expression checking {rp.header.*}
|
|
||||||
func TestResponseRetryHeader(t *testing.T) {
|
|
||||||
// Bad upstream: returns 200 but with X-Upstream-Retry header
|
|
||||||
badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("X-Upstream-Retry", "true")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("bad"))
|
|
||||||
}))
|
|
||||||
t.Cleanup(badServer.Close)
|
|
||||||
|
|
||||||
// Good upstream: returns 200 without retry header
|
|
||||||
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("good"))
|
|
||||||
}))
|
|
||||||
t.Cleanup(goodServer.Close)
|
|
||||||
|
|
||||||
retryMatch := caddyhttp.MatcherSets{
|
|
||||||
caddyhttp.MatcherSet{
|
|
||||||
newExpressionMatcher(t, `{http.reverse_proxy.header.X-Upstream-Retry} == "true"`),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundRobin picks index 1 first, then 0
|
|
||||||
upstreams := []*Upstream{
|
|
||||||
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
|
|
||||||
{Host: new(Host), Dial: badServer.Listener.Addr().String()},
|
|
||||||
}
|
|
||||||
|
|
||||||
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
|
|
||||||
req = prepareTestRequest(req)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
|
|
||||||
}
|
|
||||||
if rec.Body.String() != "good" {
|
|
||||||
t.Errorf("body: got %q, want %q (retried to wrong upstream)", rec.Body.String(), "good")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestResponseRetryNoMatchNoRetry verifies that when no retry_match entries
|
|
||||||
// match the response, the original response is returned without retrying
|
|
||||||
func TestResponseRetryNoMatchNoRetry(t *testing.T) {
|
|
||||||
var hits atomic.Int32
|
|
||||||
|
|
||||||
// Server that returns 500 - but retry_match only matches 502/503
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
hits.Add(1)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
}))
|
|
||||||
t.Cleanup(server.Close)
|
|
||||||
|
|
||||||
retryMatch := caddyhttp.MatcherSets{
|
|
||||||
caddyhttp.MatcherSet{
|
|
||||||
newExpressionMatcher(t, "{http.reverse_proxy.status_code} in [502, 503]"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
upstreams := []*Upstream{
|
|
||||||
{Host: new(Host), Dial: server.Listener.Addr().String()},
|
|
||||||
{Host: new(Host), Dial: server.Listener.Addr().String()},
|
|
||||||
}
|
|
||||||
|
|
||||||
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
|
|
||||||
req = prepareTestRequest(req)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
_ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Only one hit - no retry since 500 doesn't match [502, 503]
|
|
||||||
if hits.Load() != 1 {
|
|
||||||
t.Errorf("upstream hits: got %d, want 1 (should not have retried)", hits.Load())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestResponseRetryExhaustedPreservesStatusCode verifies that when retries
|
|
||||||
// are exhausted, the actual upstream status code (e.g. 503) is reported
|
|
||||||
// to the client, not a generic 502
|
|
||||||
func TestResponseRetryExhaustedPreservesStatusCode(t *testing.T) {
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusServiceUnavailable) // 503
|
|
||||||
}))
|
|
||||||
t.Cleanup(server.Close)
|
|
||||||
|
|
||||||
retryMatch := caddyhttp.MatcherSets{
|
|
||||||
caddyhttp.MatcherSet{
|
|
||||||
newExpressionMatcher(t, "{http.reverse_proxy.status_code} == 503"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
upstreams := []*Upstream{
|
|
||||||
{Host: new(Host), Dial: server.Listener.Addr().String()},
|
|
||||||
{Host: new(Host), Dial: server.Listener.Addr().String()},
|
|
||||||
}
|
|
||||||
|
|
||||||
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
|
|
||||||
req = prepareTestRequest(req)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
|
|
||||||
gotStatus := rec.Code
|
|
||||||
if err != nil {
|
|
||||||
if herr, ok := err.(caddyhttp.HandlerError); ok {
|
|
||||||
gotStatus = herr.StatusCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must return 503 (actual upstream status), not 502 (generic proxy error)
|
|
||||||
if gotStatus != http.StatusServiceUnavailable {
|
|
||||||
t.Errorf("status: got %d, want %d (status code not preserved)", gotStatus, http.StatusServiceUnavailable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestResponseRetryHeaderCleanup verifies that stale response header
|
|
||||||
// placeholders from a previous upstream attempt are cleaned up before the
|
|
||||||
// next retry evaluation. Without cleanup, a header like X-Retry: true from
|
|
||||||
// upstream A would leak into the retry match for upstream B even if B does
|
|
||||||
// not set that header
|
|
||||||
func TestResponseRetryHeaderCleanup(t *testing.T) {
|
|
||||||
// First upstream: returns 200 with X-Retry header (triggers retry)
|
|
||||||
firstServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("X-Retry", "true")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("first"))
|
|
||||||
}))
|
|
||||||
t.Cleanup(firstServer.Close)
|
|
||||||
|
|
||||||
// Second upstream: returns 200 WITHOUT X-Retry header (should NOT retry)
|
|
||||||
secondServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("second"))
|
|
||||||
}))
|
|
||||||
t.Cleanup(secondServer.Close)
|
|
||||||
|
|
||||||
retryMatch := caddyhttp.MatcherSets{
|
|
||||||
caddyhttp.MatcherSet{
|
|
||||||
newExpressionMatcher(t, `{http.reverse_proxy.header.X-Retry} == "true"`),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundRobin picks index 1 first, then 0
|
|
||||||
upstreams := []*Upstream{
|
|
||||||
{Host: new(Host), Dial: secondServer.Listener.Addr().String()},
|
|
||||||
{Host: new(Host), Dial: firstServer.Listener.Addr().String()},
|
|
||||||
}
|
|
||||||
|
|
||||||
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
|
|
||||||
req = prepareTestRequest(req)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should get "second" - the first upstream's X-Retry header must not
|
|
||||||
// leak into the second upstream's retry evaluation
|
|
||||||
if rec.Body.String() != "second" {
|
|
||||||
t.Errorf("body: got %q, want %q (stale header leaked between retries)", rec.Body.String(), "second")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRequestOnlyMatcherDoesNotRetryResponses verifies that a pure request
|
|
||||||
// matcher like method PUT in lb_retry_match does NOT trigger response-based
|
|
||||||
// retries. Only expression matchers (which can reference response data)
|
|
||||||
// should trigger response retries
|
|
||||||
func TestRequestOnlyMatcherDoesNotRetryResponses(t *testing.T) {
|
|
||||||
var hits atomic.Int32
|
|
||||||
|
|
||||||
// Server returns 200 OK for all requests
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
hits.Add(1)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}))
|
|
||||||
t.Cleanup(server.Close)
|
|
||||||
|
|
||||||
// method PUT matcher - should NOT trigger response retries
|
|
||||||
retryMatch := caddyhttp.MatcherSets{
|
|
||||||
caddyhttp.MatcherSet{
|
|
||||||
caddyhttp.MatchMethod{"PUT"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
upstreams := []*Upstream{
|
|
||||||
{Host: new(Host), Dial: server.Listener.Addr().String()},
|
|
||||||
{Host: new(Host), Dial: server.Listener.Addr().String()},
|
|
||||||
}
|
|
||||||
|
|
||||||
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPut, "http://example.com/", nil)
|
|
||||||
req = prepareTestRequest(req)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should hit only once - no retry for 200 OK even though method matches
|
|
||||||
if hits.Load() != 1 {
|
|
||||||
t.Errorf("upstream hits: got %d, want 1 (should not retry successful responses)", hits.Load())
|
|
||||||
}
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// brokenUpstreamAddr returns the address of a TCP listener that accepts
|
|
||||||
// connections but immediately closes them, causing a transport error (not
|
|
||||||
// a dial error). This simulates an upstream that is reachable but broken
|
|
||||||
func brokenUpstreamAddr(t *testing.T) string {
|
|
||||||
t.Helper()
|
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { ln.Close() })
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
conn, err := ln.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return ln.Addr().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTransportErrorPlaceholder verifies that the is_transport_error
|
|
||||||
// placeholder is set to true during transport error evaluation in tryAgain()
|
|
||||||
// and that expression matchers using {rp.is_transport_error} can match it
|
|
||||||
func TestTransportErrorPlaceholder(t *testing.T) {
|
|
||||||
broken := brokenUpstreamAddr(t)
|
|
||||||
|
|
||||||
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}))
|
|
||||||
t.Cleanup(goodServer.Close)
|
|
||||||
|
|
||||||
retryMatch := caddyhttp.MatcherSets{
|
|
||||||
caddyhttp.MatcherSet{
|
|
||||||
newExpressionMatcher(t, "{http.reverse_proxy.is_transport_error} == true"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundRobin picks index 1 first (broken), then 0 (good)
|
|
||||||
upstreams := []*Upstream{
|
|
||||||
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
|
|
||||||
{Host: new(Host), Dial: broken},
|
|
||||||
}
|
|
||||||
|
|
||||||
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "http://example.com/", nil)
|
|
||||||
req = prepareTestRequest(req)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
|
|
||||||
gotStatus := rec.Code
|
|
||||||
if err != nil {
|
|
||||||
if herr, ok := err.(caddyhttp.HandlerError); ok {
|
|
||||||
gotStatus = herr.StatusCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST transport error should be retried because is_transport_error matched
|
|
||||||
if gotStatus != http.StatusOK {
|
|
||||||
t.Errorf("status: got %d, want %d (transport error should have been retried)", gotStatus, http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTransportErrorPlaceholderNotSetForResponses verifies that the
|
|
||||||
// is_transport_error placeholder is NOT set when evaluating response
|
|
||||||
// matchers, so {rp.is_transport_error} is false for response retries
|
|
||||||
func TestTransportErrorPlaceholderNotSetForResponses(t *testing.T) {
|
|
||||||
var hits atomic.Int32
|
|
||||||
|
|
||||||
// Server returns 502 - but the matcher only checks is_transport_error
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
hits.Add(1)
|
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
|
||||||
}))
|
|
||||||
t.Cleanup(server.Close)
|
|
||||||
|
|
||||||
// Only matches transport errors, not response errors
|
|
||||||
retryMatch := caddyhttp.MatcherSets{
|
|
||||||
caddyhttp.MatcherSet{
|
|
||||||
newExpressionMatcher(t, "{http.reverse_proxy.is_transport_error} == true"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
upstreams := []*Upstream{
|
|
||||||
{Host: new(Host), Dial: server.Listener.Addr().String()},
|
|
||||||
{Host: new(Host), Dial: server.Listener.Addr().String()},
|
|
||||||
}
|
|
||||||
|
|
||||||
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
|
|
||||||
req = prepareTestRequest(req)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
_ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Should hit only once - is_transport_error is false during response
|
|
||||||
// evaluation so the 502 is NOT retried
|
|
||||||
if hits.Load() != 1 {
|
|
||||||
t.Errorf("upstream hits: got %d, want 1 (is_transport_error should be false for responses)", hits.Load())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRetryMatchAllowsExpressionMixedWithOtherMatchers verifies that
|
|
||||||
// lb_retry_match accepts a block mixing expression with other matchers
|
|
||||||
func TestRetryMatchAllowsExpressionMixedWithOtherMatchers(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "expression alone",
|
|
||||||
input: `reverse_proxy localhost:9080 {
|
|
||||||
lb_retry_match {
|
|
||||||
expression ` + "`{rp.status_code} in [502, 503]`" + `
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "method alone",
|
|
||||||
input: `reverse_proxy localhost:9080 {
|
|
||||||
lb_retry_match {
|
|
||||||
method PUT
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "expression mixed with method",
|
|
||||||
input: `reverse_proxy localhost:9080 {
|
|
||||||
lb_retry_match {
|
|
||||||
method POST
|
|
||||||
expression ` + "`{rp.status_code} in [502, 503]`" + `
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "expression mixed with path",
|
|
||||||
input: `reverse_proxy localhost:9080 {
|
|
||||||
lb_retry_match {
|
|
||||||
path /api*
|
|
||||||
expression ` + "`{rp.status_code} == 502`" + `
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
h := &Handler{}
|
|
||||||
d := caddyfile.NewTestDispenser(tc.input)
|
|
||||||
err := h.UnmarshalCaddyfile(d)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSubrouteErrorFallbackWithBody is similar to TestDialErrorBodyRetry but
|
|
||||||
// mimics Subroute's Error handler rather than testing retries specifically
|
|
||||||
func TestSubrouteErrorFallbackWithBody(t *testing.T) {
|
|
||||||
// Good upstream: echoes the request body with 200 OK.
|
|
||||||
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "read body: "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, err = w.Write(body)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error writing in good server: %v", err)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
t.Cleanup(goodServer.Close)
|
|
||||||
|
|
||||||
// Handler which will dial error
|
|
||||||
badProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: deadUpstreamAddr(t)})
|
|
||||||
|
|
||||||
bodyReader := newCloseOnCloseReader("hello world")
|
|
||||||
req := httptest.NewRequest("POST", "http://localhost/", bodyReader)
|
|
||||||
// httptest.NewRequest wraps the reader in NopCloser; replace
|
|
||||||
// it with our close-aware reader so Close() is propagated.
|
|
||||||
req.Body = bodyReader
|
|
||||||
|
|
||||||
req = prepareTestRequest(req)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
err := badProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error from badProxy.ServeHTTP")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate the Subroute's Error handler by calling another handler with the
|
|
||||||
// same request and recorder
|
|
||||||
goodProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: goodServer.Listener.Addr().String()})
|
|
||||||
err = goodProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Expected no error from goodProxy.ServeHTTP, got: %v", err)
|
|
||||||
}
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
|
|
||||||
}
|
|
||||||
expectedBody := "hello world"
|
|
||||||
if rec.Body.String() != expectedBody {
|
|
||||||
t.Errorf("body: got %q, want %q", rec.Body.String(), expectedBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -171,12 +171,6 @@ type Handler struct {
|
|||||||
// forcibly closed at the end of the timeout. Default: no timeout.
|
// forcibly closed at the end of the timeout. Default: no timeout.
|
||||||
StreamTimeout caddy.Duration `json:"stream_timeout,omitempty"`
|
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
|
// If nonzero, streaming requests such as WebSockets will not be
|
||||||
// closed when the proxy config is unloaded, and instead the stream
|
// closed when the proxy config is unloaded, and instead the stream
|
||||||
// will remain open until the delay is complete. In other words,
|
// will remain open until the delay is complete. In other words,
|
||||||
@@ -488,19 +482,20 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
|||||||
reqHost := clonedReq.Host
|
reqHost := clonedReq.Host
|
||||||
reqHeader := clonedReq.Header
|
reqHeader := clonedReq.Header
|
||||||
|
|
||||||
// If the request contained a body, wrap it in io.NopCloser
|
// When retries are configured and there is a body, wrap it in
|
||||||
// to prevent Go's transport from closing it on dial errors.
|
// io.NopCloser to prevent Go's transport from closing it on dial
|
||||||
// cloneRequest does a shallow copy, so clonedReq.Body and
|
// errors. cloneRequest does a shallow copy, so clonedReq.Body and
|
||||||
// r.Body share the same io.ReadCloser — a dial-failure Close()
|
// r.Body share the same io.ReadCloser — a dial-failure Close()
|
||||||
// would kill the original body for all subsequent retry
|
// would kill the original body for all subsequent retry attempts.
|
||||||
// attempts or subsequent handlers. The real body is closed by
|
// The real body is closed by the HTTP server when the handler
|
||||||
// the HTTP server when the handler returns.
|
// returns.
|
||||||
//
|
//
|
||||||
// If the body was already fully buffered (via request_buffers),
|
// If the body was already fully buffered (via request_buffers),
|
||||||
// we also extract the buffer so the retry loop can replay it
|
// we also extract the buffer so the retry loop can replay it
|
||||||
// from the beginning on each attempt. (see #6259, #7546, #7713)
|
// from the beginning on each attempt. (see #6259, #7546)
|
||||||
var bufferedReqBody *bytes.Buffer
|
var bufferedReqBody *bytes.Buffer
|
||||||
if clonedReq.Body != nil {
|
if clonedReq.Body != nil && h.LoadBalancing != nil &&
|
||||||
|
(h.LoadBalancing.Retries > 0 || h.LoadBalancing.TryDuration > 0) {
|
||||||
if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
|
if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
|
||||||
bufferedReqBody = reqBodyBuf.buf
|
bufferedReqBody = reqBodyBuf.buf
|
||||||
reqBodyBuf.buf = nil
|
reqBodyBuf.buf = nil
|
||||||
@@ -573,17 +568,6 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
|
|||||||
// get the updated list of upstreams
|
// get the updated list of upstreams
|
||||||
upstreams := h.Upstreams
|
upstreams := h.Upstreams
|
||||||
if h.DynamicUpstreams != nil {
|
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)
|
dUpstreams, err := h.DynamicUpstreams.GetUpstreams(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if c := h.logger.Check(zapcore.ErrorLevel, "failed getting dynamic upstreams; falling back to static upstreams"); c != nil {
|
if c := h.logger.Check(zapcore.ErrorLevel, "failed getting dynamic upstreams; falling back to static upstreams"); c != nil {
|
||||||
@@ -680,12 +664,8 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
|
|||||||
return true, succ.error
|
return true, succ.error
|
||||||
}
|
}
|
||||||
|
|
||||||
// remember this failure (if enabled); response-based retries
|
// remember this failure (if enabled)
|
||||||
// are not counted as failures since the upstream did respond
|
h.countFailure(upstream)
|
||||||
// successfully - only the response content triggered a retry
|
|
||||||
if _, isRetryableResponse := proxyErr.(retryableResponseError); !isRetryableResponse {
|
|
||||||
h.countFailure(upstream)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we've tried long enough, break
|
// if we've tried long enough, break
|
||||||
if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
|
if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
|
||||||
@@ -1069,45 +1049,6 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
|
|||||||
res.Body, _ = h.bufferedBody(res.Body, h.ResponseBuffers)
|
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
|
// see if any response handler is configured for this response from the backend
|
||||||
for i, rh := range h.HandleResponse {
|
for i, rh := range h.HandleResponse {
|
||||||
if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) {
|
if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) {
|
||||||
@@ -1127,6 +1068,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
|
|||||||
break
|
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 {
|
if c := logger.Check(zapcore.DebugLevel, "handling response"); c != nil {
|
||||||
c.Write(zap.Int("handler", i))
|
c.Write(zap.Int("handler", i))
|
||||||
}
|
}
|
||||||
@@ -1311,29 +1260,18 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int
|
|||||||
// specifically a dialer error, we need to be careful
|
// specifically a dialer error, we need to be careful
|
||||||
if proxyErr != nil {
|
if proxyErr != nil {
|
||||||
_, isDialError := proxyErr.(DialError)
|
_, isDialError := proxyErr.(DialError)
|
||||||
_, isRetryableResponse := proxyErr.(retryableResponseError)
|
|
||||||
herr, isHandlerError := proxyErr.(caddyhttp.HandlerError)
|
herr, isHandlerError := proxyErr.(caddyhttp.HandlerError)
|
||||||
|
|
||||||
// if the error occurred after a connection was established,
|
// if the error occurred after a connection was established,
|
||||||
// we have to assume the upstream received the request, and
|
// we have to assume the upstream received the request, and
|
||||||
// retries need to be carefully decided, because some requests
|
// retries need to be carefully decided, because some requests
|
||||||
// are not idempotent; retryableResponseError is excluded here
|
// are not idempotent
|
||||||
// because its retry decision was already made in reverseProxy()
|
if !isDialError && (!isHandlerError || !errors.Is(herr, errNoUpstream)) {
|
||||||
// when the response matchers were evaluated
|
|
||||||
if !isDialError && !isRetryableResponse && (!isHandlerError || !errors.Is(herr, errNoUpstream)) {
|
|
||||||
if lb.RetryMatch == nil && req.Method != "GET" {
|
if lb.RetryMatch == nil && req.Method != "GET" {
|
||||||
// by default, don't retry requests if they aren't GET
|
// by default, don't retry requests if they aren't GET
|
||||||
return false
|
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)
|
match, err := lb.RetryMatch.AnyMatchWithError(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("error matching request for retry", zap.Error(err))
|
logger.Error("error matching request for retry", zap.Error(err))
|
||||||
@@ -1563,12 +1501,6 @@ func removeConnectionHeaders(h http.Header) {
|
|||||||
|
|
||||||
// statusError returns an error value that has a status code.
|
// statusError returns an error value that has a status code.
|
||||||
func statusError(err error) error {
|
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)
|
// errors proxying usually mean there is a problem with the upstream(s)
|
||||||
statusCode := http.StatusBadGateway
|
statusCode := http.StatusBadGateway
|
||||||
|
|
||||||
@@ -1620,15 +1552,13 @@ type LoadBalancing struct {
|
|||||||
// to spin if all backends are down and latency is very low.
|
// to spin if all backends are down and latency is very low.
|
||||||
TryInterval caddy.Duration `json:"try_interval,omitempty"`
|
TryInterval caddy.Duration `json:"try_interval,omitempty"`
|
||||||
|
|
||||||
// A list of matcher sets that controls retry behavior. Matcher sets
|
// A list of matcher sets that restricts with which requests retries are
|
||||||
// without expression matchers (e.g. method, path) restrict which
|
// allowed. A request must match any of the given matcher sets in order
|
||||||
// requests are retried on transport errors - if unspecified, only
|
// to be retried if the connection to the upstream succeeded but the
|
||||||
// GET requests will be retried. Matcher sets with CEL expression
|
// subsequent round-trip failed. If the connection to the upstream failed,
|
||||||
// matchers are evaluated against upstream responses and can
|
// a retry is always allowed. If unspecified, only GET requests will be
|
||||||
// reference {rp.status_code}, {rp.header.*}, and
|
// allowed to be retried. Note that a retry is done with the next available
|
||||||
// {rp.is_transport_error}. Dial errors are always retried
|
// host according to the load balancing policy.
|
||||||
// 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"`
|
RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty" caddy:"namespace=http.matchers"`
|
||||||
|
|
||||||
SelectionPolicy Selector `json:"-"`
|
SelectionPolicy Selector `json:"-"`
|
||||||
@@ -1650,28 +1580,10 @@ type Selector interface {
|
|||||||
// may be called during each retry, multiple times per request, and as
|
// may be called during each retry, multiple times per request, and as
|
||||||
// such, needs to be instantaneous. The returned slice will not be
|
// such, needs to be instantaneous. The returned slice will not be
|
||||||
// modified.
|
// modified.
|
||||||
//
|
|
||||||
// For upstream sources that cache results, implement the
|
|
||||||
// [CachingUpstreamSource] interface for optimal performance.
|
|
||||||
type UpstreamSource interface {
|
type UpstreamSource interface {
|
||||||
GetUpstreams(*http.Request) ([]*Upstream, error)
|
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.
|
// 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
|
// As of RFC 7230, hop-by-hop headers are required to appear in the
|
||||||
// Connection header field. These are the headers defined by the
|
// Connection header field. These are the headers defined by the
|
||||||
@@ -1744,34 +1656,10 @@ type RequestHeaderOpsTransport interface {
|
|||||||
RequestHeaderOps() *headers.HeaderOps
|
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
|
// roundtripSucceededError is an error type that is returned if the
|
||||||
// roundtrip succeeded, but an error occurred after-the-fact.
|
// roundtrip succeeded, but an error occurred after-the-fact.
|
||||||
type roundtripSucceededError struct{ error }
|
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
|
// bodyReadCloser is a reader that, upon closing, will return
|
||||||
// its buffer to the pool and close the underlying body reader.
|
// its buffer to the pool and close the underlying body reader.
|
||||||
type bodyReadCloser struct {
|
type bodyReadCloser struct {
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ func init() {
|
|||||||
caddy.RegisterModule(RandomSelection{})
|
caddy.RegisterModule(RandomSelection{})
|
||||||
caddy.RegisterModule(RandomChoiceSelection{})
|
caddy.RegisterModule(RandomChoiceSelection{})
|
||||||
caddy.RegisterModule(LeastConnSelection{})
|
caddy.RegisterModule(LeastConnSelection{})
|
||||||
caddy.RegisterModule(new(RoundRobinSelection))
|
caddy.RegisterModule(RoundRobinSelection{})
|
||||||
caddy.RegisterModule(new(WeightedRoundRobinSelection))
|
caddy.RegisterModule(WeightedRoundRobinSelection{})
|
||||||
caddy.RegisterModule(FirstSelection{})
|
caddy.RegisterModule(FirstSelection{})
|
||||||
caddy.RegisterModule(IPHashSelection{})
|
caddy.RegisterModule(IPHashSelection{})
|
||||||
caddy.RegisterModule(ClientIPHashSelection{})
|
caddy.RegisterModule(ClientIPHashSelection{})
|
||||||
@@ -83,12 +83,12 @@ type WeightedRoundRobinSelection struct {
|
|||||||
// The weight of each upstream in order,
|
// The weight of each upstream in order,
|
||||||
// corresponding with the list of upstreams configured.
|
// corresponding with the list of upstreams configured.
|
||||||
Weights []int `json:"weights,omitempty"`
|
Weights []int `json:"weights,omitempty"`
|
||||||
index atomic.Uint32
|
index uint32
|
||||||
totalWeight int
|
totalWeight int
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaddyModule returns the Caddy module information.
|
// CaddyModule returns the Caddy module information.
|
||||||
func (*WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo {
|
func (WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo {
|
||||||
return caddy.ModuleInfo{
|
return caddy.ModuleInfo{
|
||||||
ID: "http.reverse_proxy.selection_policies.weighted_round_robin",
|
ID: "http.reverse_proxy.selection_policies.weighted_round_robin",
|
||||||
New: func() caddy.Module {
|
New: func() caddy.Module {
|
||||||
@@ -143,7 +143,7 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request,
|
|||||||
weights = append(weights, w)
|
weights = append(weights, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentWeight := int(r.index.Add(1)) % r.totalWeight
|
currentWeight := int(atomic.AddUint32(&r.index, 1)) % r.totalWeight
|
||||||
for i, weight := range weights {
|
for i, weight := range weights {
|
||||||
totalWeight += weight
|
totalWeight += weight
|
||||||
if currentWeight < totalWeight {
|
if currentWeight < totalWeight {
|
||||||
@@ -295,11 +295,11 @@ func (r *LeastConnSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
// RoundRobinSelection is a policy that selects
|
// RoundRobinSelection is a policy that selects
|
||||||
// a host based on round-robin ordering.
|
// a host based on round-robin ordering.
|
||||||
type RoundRobinSelection struct {
|
type RoundRobinSelection struct {
|
||||||
robin atomic.Uint32
|
robin uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaddyModule returns the Caddy module information.
|
// CaddyModule returns the Caddy module information.
|
||||||
func (*RoundRobinSelection) CaddyModule() caddy.ModuleInfo {
|
func (RoundRobinSelection) CaddyModule() caddy.ModuleInfo {
|
||||||
return caddy.ModuleInfo{
|
return caddy.ModuleInfo{
|
||||||
ID: "http.reverse_proxy.selection_policies.round_robin",
|
ID: "http.reverse_proxy.selection_policies.round_robin",
|
||||||
New: func() caddy.Module { return new(RoundRobinSelection) },
|
New: func() caddy.Module { return new(RoundRobinSelection) },
|
||||||
@@ -313,7 +313,7 @@ func (r *RoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for range n {
|
for range n {
|
||||||
robin := r.robin.Add(1)
|
robin := atomic.AddUint32(&r.robin, 1)
|
||||||
host := pool[robin%n]
|
host := pool[robin%n]
|
||||||
if host.Available() {
|
if host.Available() {
|
||||||
return host
|
return host
|
||||||
@@ -664,12 +664,10 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
|
|||||||
return upstream
|
return upstream
|
||||||
}
|
}
|
||||||
cookie := &http.Cookie{
|
cookie := &http.Cookie{
|
||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
Value: sha,
|
Value: sha,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Secure: false,
|
Secure: false,
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
}
|
}
|
||||||
isProxyHttps := false
|
isProxyHttps := false
|
||||||
if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted {
|
if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted {
|
||||||
|
|||||||
@@ -204,12 +204,7 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
|
|||||||
defer deleteFrontConn()
|
defer deleteFrontConn()
|
||||||
defer deleteBackConn()
|
defer deleteBackConn()
|
||||||
|
|
||||||
spc := switchProtocolCopier{
|
spc := switchProtocolCopier{user: conn, backend: backConn, wg: wg}
|
||||||
user: conn,
|
|
||||||
backend: backConn,
|
|
||||||
wg: wg,
|
|
||||||
bufferSize: h.StreamBufferSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup the timeout if requested
|
// setup the timeout if requested
|
||||||
var timeoutc <-chan time.Time
|
var timeoutc <-chan time.Time
|
||||||
@@ -641,29 +636,20 @@ func (m *maxLatencyWriter) stop() {
|
|||||||
type switchProtocolCopier struct {
|
type switchProtocolCopier struct {
|
||||||
user, backend io.ReadWriteCloser
|
user, backend io.ReadWriteCloser
|
||||||
wg *sync.WaitGroup
|
wg *sync.WaitGroup
|
||||||
bufferSize int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
|
func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
|
||||||
_, err := io.CopyBuffer(c.user, c.backend, c.buffer())
|
_, err := io.Copy(c.user, c.backend)
|
||||||
errc <- err
|
errc <- err
|
||||||
c.wg.Done()
|
c.wg.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
|
func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
|
||||||
_, err := io.CopyBuffer(c.backend, c.user, c.buffer())
|
_, err := io.Copy(c.backend, c.user)
|
||||||
errc <- err
|
errc <- err
|
||||||
c.wg.Done()
|
c.wg.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c switchProtocolCopier) buffer() []byte {
|
|
||||||
size := c.bufferSize
|
|
||||||
if size <= 0 {
|
|
||||||
size = defaultBufferSize
|
|
||||||
}
|
|
||||||
return make([]byte, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
var streamingBufPool = sync.Pool{
|
var streamingBufPool = sync.Pool{
|
||||||
New: func() any {
|
New: func() any {
|
||||||
// The Pool's New function should generally only return pointer
|
// The Pool's New function should generally only return pointer
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package reverseproxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
@@ -36,47 +34,3 @@ func TestHandlerCopyResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSwitchProtocolCopierBufferSize(t *testing.T) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var errc = make(chan error, 1)
|
|
||||||
var dst bytes.Buffer
|
|
||||||
|
|
||||||
copier := switchProtocolCopier{
|
|
||||||
user: nopReadWriteCloser{Reader: strings.NewReader("hello")},
|
|
||||||
backend: nopReadWriteCloser{Writer: &dst},
|
|
||||||
wg: &wg,
|
|
||||||
bufferSize: 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := copier.buffer()
|
|
||||||
if got := len(buf); got != 7 {
|
|
||||||
t.Fatalf("buffer len = %d, want 7", got)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go copier.copyToBackend(errc)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if err := <-errc; err != nil {
|
|
||||||
t.Fatalf("copyToBackend() error = %v", err)
|
|
||||||
}
|
|
||||||
if got := dst.String(); got != "hello" {
|
|
||||||
t.Fatalf("copied data = %q, want %q", got, "hello")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSwitchProtocolCopierDefaultBufferSize(t *testing.T) {
|
|
||||||
copier := switchProtocolCopier{}
|
|
||||||
buf := copier.buffer()
|
|
||||||
if got := len(buf); got != defaultBufferSize {
|
|
||||||
t.Fatalf("buffer len = %d, want %d", got, defaultBufferSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type nopReadWriteCloser struct {
|
|
||||||
io.Reader
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nopReadWriteCloser) Close() error { return nil }
|
|
||||||
|
|||||||
@@ -119,18 +119,6 @@ func (su *SRVUpstreams) Provision(ctx caddy.Context) error {
|
|||||||
return nil
|
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) {
|
func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
|
||||||
suAddr, service, proto, name := su.expandedAddr(r)
|
suAddr, service, proto, name := su.expandedAddr(r)
|
||||||
|
|
||||||
@@ -566,9 +554,8 @@ var (
|
|||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ caddy.Provisioner = (*SRVUpstreams)(nil)
|
_ caddy.Provisioner = (*SRVUpstreams)(nil)
|
||||||
_ UpstreamSource = (*SRVUpstreams)(nil)
|
_ UpstreamSource = (*SRVUpstreams)(nil)
|
||||||
_ CachingUpstreamSource = (*SRVUpstreams)(nil)
|
_ caddy.Provisioner = (*AUpstreams)(nil)
|
||||||
_ caddy.Provisioner = (*AUpstreams)(nil)
|
_ UpstreamSource = (*AUpstreams)(nil)
|
||||||
_ UpstreamSource = (*AUpstreams)(nil)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -211,7 +211,12 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
|
|||||||
var newPath, newQuery, newFrag string
|
var newPath, newQuery, newFrag string
|
||||||
|
|
||||||
if path != "" {
|
if path != "" {
|
||||||
path = escapePathPlaceholders(path, r, repl)
|
// replace the `path` placeholder to escaped path
|
||||||
|
pathPlaceholder := "{http.request.uri.path}"
|
||||||
|
if strings.Contains(path, pathPlaceholder) {
|
||||||
|
path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
|
||||||
|
}
|
||||||
|
|
||||||
newPath = repl.ReplaceAll(path, "")
|
newPath = repl.ReplaceAll(path, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,31 +300,6 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
|
|||||||
return r.Method != oldMethod || r.RequestURI != oldURI
|
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
|
// buildQueryString takes an input query string and
|
||||||
// performs replacements on each component, returning
|
// performs replacements on each component, returning
|
||||||
// the resulting query string. This function appends
|
// the resulting query string. This function appends
|
||||||
@@ -549,14 +529,7 @@ func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) {
|
|||||||
if key == "" || val == "" {
|
if key == "" || val == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if key == val {
|
query[val] = query[key]
|
||||||
continue
|
|
||||||
}
|
|
||||||
originalValues, ok := query[key]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
query[val] = originalValues
|
|
||||||
delete(query, key)
|
delete(query, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ package rewrite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -398,55 +397,6 @@ func TestRewrite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryOpsRenameNoOpCases(t *testing.T) {
|
|
||||||
repl := caddy.NewReplacer()
|
|
||||||
|
|
||||||
for i, tc := range []struct {
|
|
||||||
input *http.Request
|
|
||||||
expect map[string][]string
|
|
||||||
ops *queryOps
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
ops: &queryOps{
|
|
||||||
Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
|
|
||||||
},
|
|
||||||
input: newRequest(t, "GET", "/?page=test&id=5&test=100"),
|
|
||||||
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ops: &queryOps{
|
|
||||||
Rename: []queryOpsArguments{{Key: "id", Val: "id"}},
|
|
||||||
},
|
|
||||||
input: newRequest(t, "GET", "/?page=test&id=5&test=100"),
|
|
||||||
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ops: &queryOps{
|
|
||||||
Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
|
|
||||||
},
|
|
||||||
input: newRequest(t, "GET", "/?page=test&ID=5&test=100"),
|
|
||||||
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ops: &queryOps{
|
|
||||||
Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
|
|
||||||
},
|
|
||||||
input: newRequest(t, "GET", "/?page=test&ID=5&id=7&test=100"),
|
|
||||||
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
repl.Set("http.request.uri", tc.input.RequestURI)
|
|
||||||
repl.Set("http.request.uri.path", tc.input.URL.Path)
|
|
||||||
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
|
|
||||||
|
|
||||||
tc.ops.do(tc.input, repl)
|
|
||||||
|
|
||||||
if actual := tc.input.URL.Query(); !reflect.DeepEqual(tc.expect, map[string][]string(actual)) {
|
|
||||||
t.Errorf("Test %d: Expected query=%v but got %v", i, tc.expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRequest(t *testing.T, method, uri string) *http.Request {
|
func newRequest(t *testing.T, method, uri string) *http.Request {
|
||||||
req, err := http.NewRequest(method, uri, nil)
|
req, err := http.NewRequest(method, uri, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package rewrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReverse(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple string",
|
|
||||||
input: "hello",
|
|
||||||
expected: "olleh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty string",
|
|
||||||
input: "",
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single character",
|
|
||||||
input: "a",
|
|
||||||
expected: "a",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "two characters",
|
|
||||||
input: "ab",
|
|
||||||
expected: "ba",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "palindrome",
|
|
||||||
input: "racecar",
|
|
||||||
expected: "racecar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with spaces",
|
|
||||||
input: "hello world",
|
|
||||||
expected: "dlrow olleh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with numbers",
|
|
||||||
input: "abc123",
|
|
||||||
expected: "321cba",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unicode characters",
|
|
||||||
input: "hello世界",
|
|
||||||
expected: "界世olleh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "emoji",
|
|
||||||
input: "🎉🎊🎈",
|
|
||||||
expected: "🎈🎊🎉",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mixed unicode and ascii",
|
|
||||||
input: "café☕",
|
|
||||||
expected: "☕éfac",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "special characters",
|
|
||||||
input: "a!b@c#d$",
|
|
||||||
expected: "$d#c@b!a",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "path-like string",
|
|
||||||
input: "/path/to/file",
|
|
||||||
expected: "elif/ot/htap/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "url-like string",
|
|
||||||
input: "https://example.com",
|
|
||||||
expected: "moc.elpmaxe//:sptth",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "long string",
|
|
||||||
input: "The quick brown fox jumps over the lazy dog",
|
|
||||||
expected: "god yzal eht revo spmuj xof nworb kciuq ehT",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "newlines",
|
|
||||||
input: "line1\nline2\nline3",
|
|
||||||
expected: "3enil\n2enil\n1enil",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tabs",
|
|
||||||
input: "a\tb\tc",
|
|
||||||
expected: "c\tb\ta",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := reverse(tt.input)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("reverse(%q) = %q; want %q", tt.input, result, tt.expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that reversing twice gives the original string
|
|
||||||
if tt.input != "" {
|
|
||||||
doubleReverse := reverse(reverse(tt.input))
|
|
||||||
if doubleReverse != tt.input {
|
|
||||||
t.Errorf("reverse(reverse(%q)) = %q; want %q", tt.input, doubleReverse, tt.input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReverse_LengthPreservation(t *testing.T) {
|
|
||||||
// Test that reverse preserves string length
|
|
||||||
testStrings := []string{
|
|
||||||
"",
|
|
||||||
"a",
|
|
||||||
"ab",
|
|
||||||
"abc",
|
|
||||||
"hello world",
|
|
||||||
"🎉🎊🎈",
|
|
||||||
"café☕",
|
|
||||||
"The quick brown fox jumps over the lazy dog",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range testStrings {
|
|
||||||
reversed := reverse(s)
|
|
||||||
if len([]rune(s)) != len([]rune(reversed)) {
|
|
||||||
t.Errorf("reverse(%q) changed length: original %d, reversed %d", s, len([]rune(s)), len([]rune(reversed)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkReverse benchmarks the reverse function
|
|
||||||
func BenchmarkReverse(b *testing.B) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
}{
|
|
||||||
{"empty", ""},
|
|
||||||
{"short", "hello"},
|
|
||||||
{"medium", "The quick brown fox jumps over the lazy dog"},
|
|
||||||
{"long", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."},
|
|
||||||
{"unicode", "hello世界🎉"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
b.Run(tc.name, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
reverse(tc.input)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReverse_EdgeCases(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
}{
|
|
||||||
{"null byte", "\x00"},
|
|
||||||
{"multiple null bytes", "\x00\x00\x00"},
|
|
||||||
{"control characters", "\t\n\r"},
|
|
||||||
{"high unicode", "𝕳𝖊𝖑𝖑𝖔"},
|
|
||||||
{"zero-width characters", "a\u200Bb\u200Cc"},
|
|
||||||
{"combining characters", "é"}, // e + combining acute
|
|
||||||
{"rtl text", "مرحبا"},
|
|
||||||
{"mixed rtl/ltr", "Hello مرحبا World"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := reverse(tt.input)
|
|
||||||
// Just ensure it doesn't panic and returns something
|
|
||||||
if result == "" && tt.input != "" {
|
|
||||||
t.Errorf("reverse(%q) returned empty string", tt.input)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
@@ -242,8 +241,8 @@ func (routes RouteList) Compile(next Handler) Handler {
|
|||||||
mid = append(mid, wrapRoute(route))
|
mid = append(mid, wrapRoute(route))
|
||||||
}
|
}
|
||||||
stack := next
|
stack := next
|
||||||
for _, middleware := range slices.Backward(mid) {
|
for i := len(mid) - 1; i >= 0; i-- {
|
||||||
stack = middleware(stack)
|
stack = mid[i](stack)
|
||||||
}
|
}
|
||||||
return stack
|
return stack
|
||||||
}
|
}
|
||||||
@@ -306,8 +305,8 @@ func wrapRoute(route Route) Middleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// compile this route's handler stack
|
// compile this route's handler stack
|
||||||
for _, middleware := range slices.Backward(route.middleware) {
|
for i := len(route.middleware) - 1; i >= 0; i-- {
|
||||||
nextCopy = middleware(nextCopy)
|
nextCopy = route.middleware[i](nextCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply metrics instrumentation once for the entire route,
|
// Apply metrics instrumentation once for the entire route,
|
||||||
|
|||||||
+14
-51
@@ -28,7 +28,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
@@ -291,7 +291,8 @@ type Server struct {
|
|||||||
|
|
||||||
trustedProxies IPRangeSource
|
trustedProxies IPRangeSource
|
||||||
|
|
||||||
shutdownAt atomic.Pointer[time.Time]
|
shutdownAt time.Time
|
||||||
|
shutdownAtMu *sync.RWMutex
|
||||||
|
|
||||||
// registered callback functions
|
// registered callback functions
|
||||||
connStateFuncs []func(net.Conn, http.ConnState)
|
connStateFuncs []func(net.Conn, http.ConnState)
|
||||||
@@ -300,8 +301,6 @@ type Server struct {
|
|||||||
onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023)
|
onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023)
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultProtocols = []string{"h1", "h2", "h3"}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ServerHeader = "Caddy"
|
ServerHeader = "Caddy"
|
||||||
serverHeader = []string{ServerHeader}
|
serverHeader = []string{ServerHeader}
|
||||||
@@ -901,56 +900,20 @@ func (s *Server) logRequest(
|
|||||||
// protocol returns true if the protocol proto is configured/enabled.
|
// protocol returns true if the protocol proto is configured/enabled.
|
||||||
func (s *Server) protocol(proto string) bool {
|
func (s *Server) protocol(proto string) bool {
|
||||||
if s.ListenProtocols == nil {
|
if s.ListenProtocols == nil {
|
||||||
return slices.Contains(s.protocolsWithDefaults(), proto)
|
if slices.Contains(s.Protocols, proto) {
|
||||||
}
|
|
||||||
|
|
||||||
for _, lnProtocols := range s.ListenProtocols {
|
|
||||||
if slices.Contains(s.listenerProtocolsWithDefaults(lnProtocols), proto) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
for _, lnProtocols := range s.ListenProtocols {
|
||||||
return false
|
for _, lnProtocol := range lnProtocols {
|
||||||
}
|
if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto {
|
||||||
|
return true
|
||||||
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listeners returns the server's listeners. These are active listeners,
|
// Listeners returns the server's listeners. These are active listeners,
|
||||||
@@ -1123,11 +1086,11 @@ func strictUntrustedClientIp(r *http.Request, headers []string, trusted []netip.
|
|||||||
for _, headerName := range headers {
|
for _, headerName := range headers {
|
||||||
parts := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",")
|
parts := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",")
|
||||||
|
|
||||||
for _, part := range slices.Backward(parts) {
|
for i := len(parts) - 1; i >= 0; i-- {
|
||||||
// Some proxies may retain the port number, so split if possible
|
// Some proxies may retain the port number, so split if possible
|
||||||
host, _, err := net.SplitHostPort(part)
|
host, _, err := net.SplitHostPort(parts[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host = part
|
host = parts[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any zone identifier from the IP address
|
// Remove any zone identifier from the IP address
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
package caddyhttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStaticErrorCaddyModule(t *testing.T) {
|
|
||||||
se := StaticError{}
|
|
||||||
info := se.CaddyModule()
|
|
||||||
if info.ID != "http.handlers.error" {
|
|
||||||
t.Errorf("CaddyModule().ID = %q, want 'http.handlers.error'", info.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStaticErrorServeHTTP(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
staticErr StaticError
|
|
||||||
wantStatusCode int
|
|
||||||
wantMessage string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "default status code 500",
|
|
||||||
staticErr: StaticError{},
|
|
||||||
wantStatusCode: 500,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom status code",
|
|
||||||
staticErr: StaticError{StatusCode: "404"},
|
|
||||||
wantStatusCode: 404,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom error message",
|
|
||||||
staticErr: StaticError{Error: "custom error", StatusCode: "503"},
|
|
||||||
wantStatusCode: 503,
|
|
||||||
wantMessage: "custom error",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "status code only",
|
|
||||||
staticErr: StaticError{StatusCode: "403"},
|
|
||||||
wantStatusCode: 403,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
repl := caddy.NewReplacer()
|
|
||||||
ctx := context.WithValue(context.Background(), caddy.ReplacerCtxKey, repl)
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", "http://example.com/", nil)
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
err := tt.staticErr.ServeHTTP(w, req, nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("ServeHTTP() should return an error")
|
|
||||||
}
|
|
||||||
|
|
||||||
var he HandlerError
|
|
||||||
if !errors.As(err, &he) {
|
|
||||||
t.Fatal("ServeHTTP() error should be HandlerError")
|
|
||||||
}
|
|
||||||
|
|
||||||
if he.StatusCode != tt.wantStatusCode {
|
|
||||||
t.Errorf("StatusCode = %d, want %d", he.StatusCode, tt.wantStatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.wantMessage != "" && he.Err != nil {
|
|
||||||
if he.Err.Error() != tt.wantMessage {
|
|
||||||
t.Errorf("Err.Error() = %q, want %q", he.Err.Error(), tt.wantMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStaticErrorServeHTTPInvalidStatusCode(t *testing.T) {
|
|
||||||
repl := caddy.NewReplacer()
|
|
||||||
ctx := context.WithValue(context.Background(), caddy.ReplacerCtxKey, repl)
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", "http://example.com/", nil)
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
se := StaticError{StatusCode: "not_a_number"}
|
|
||||||
err := se.ServeHTTP(w, req, nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("ServeHTTP() should return error for invalid status code")
|
|
||||||
}
|
|
||||||
|
|
||||||
var he HandlerError
|
|
||||||
if !errors.As(err, &he) {
|
|
||||||
t.Fatal("error should be HandlerError")
|
|
||||||
}
|
|
||||||
// Invalid status code should return 500
|
|
||||||
if he.StatusCode != 500 {
|
|
||||||
t.Errorf("StatusCode = %d, want 500 for invalid status code", he.StatusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStaticErrorUnmarshalCaddyfile(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
wantErr bool
|
|
||||||
wantStatus string
|
|
||||||
wantMsg string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "status code only",
|
|
||||||
input: `error 404`,
|
|
||||||
wantStatus: "404",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "message only (non-3-digit)",
|
|
||||||
input: `error "Page not found"`,
|
|
||||||
wantMsg: "Page not found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "message and status code",
|
|
||||||
input: `error "Page not found" 404`,
|
|
||||||
wantStatus: "404",
|
|
||||||
wantMsg: "Page not found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no args",
|
|
||||||
input: `error`,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "too many args",
|
|
||||||
input: `error "msg" 404 extra`,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "status in block",
|
|
||||||
input: "error 500 {\n message \"server error\"\n}",
|
|
||||||
wantStatus: "500",
|
|
||||||
wantMsg: "server error",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "two-digit number is treated as message",
|
|
||||||
input: `error 42`,
|
|
||||||
wantMsg: "42",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "four-digit number is treated as message",
|
|
||||||
input: `error 1234`,
|
|
||||||
wantMsg: "1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "three-digit is status code",
|
|
||||||
input: `error 503`,
|
|
||||||
wantStatus: "503",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
d := caddyfile.NewTestDispenser(tt.input)
|
|
||||||
se := &StaticError{}
|
|
||||||
err := se.UnmarshalCaddyfile(d)
|
|
||||||
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.wantStatus != "" && string(se.StatusCode) != tt.wantStatus {
|
|
||||||
t.Errorf("StatusCode = %q, want %q", se.StatusCode, tt.wantStatus)
|
|
||||||
}
|
|
||||||
if tt.wantMsg != "" && se.Error != tt.wantMsg {
|
|
||||||
t.Errorf("Error = %q, want %q", se.Error, tt.wantMsg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStaticErrorUnmarshalCaddyfileDuplicateMessage(t *testing.T) {
|
|
||||||
input := "error \"first message\" 500 {\n message \"second message\"\n}"
|
|
||||||
d := caddyfile.NewTestDispenser(input)
|
|
||||||
se := &StaticError{}
|
|
||||||
err := se.UnmarshalCaddyfile(d)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error when message is specified both inline and in block")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,22 +36,13 @@ func init() {
|
|||||||
// Templates is a middleware which executes response bodies as Go templates.
|
// Templates is a middleware which executes response bodies as Go templates.
|
||||||
// The syntax is documented in the Go standard library's
|
// The syntax is documented in the Go standard library's
|
||||||
// [text/template package](https://golang.org/pkg/text/template/).
|
// [text/template package](https://golang.org/pkg/text/template/).
|
||||||
// Note that ANY response body that matches and qualifies may be evaluated,
|
|
||||||
// even if it comes from a proxied backend.
|
|
||||||
//
|
//
|
||||||
// ⚠️ Template functions/actions can access the environment, files on disk,
|
// ⚠️ Template functions/actions are still experimental, so they are subject to change.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// ⚠️ Templates are still experimental, so they are subject to change.
|
// Custom template functions can be registered by creating a plugin module under the `http.handlers.templates.functions.*` namespace that implements the `CustomFunctions` interface.
|
||||||
//
|
//
|
||||||
// [All Sprig functions](https://masterminds.github.io/sprig/) are supported.
|
// [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
|
// In addition to the standard functions and the Sprig library, Caddy adds
|
||||||
// extra functions and data that are available to a template:
|
// extra functions and data that are available to a template:
|
||||||
//
|
//
|
||||||
@@ -171,25 +162,6 @@ func init() {
|
|||||||
// {{listFiles "/mydir"}}
|
// {{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`
|
// ##### `markdown`
|
||||||
//
|
//
|
||||||
// Renders the given Markdown text as HTML and returns it. This uses the
|
// Renders the given Markdown text as HTML and returns it. This uses the
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
webEngineName = "Caddy"
|
||||||
defaultSpanName = "handler"
|
defaultSpanName = "handler"
|
||||||
nextCallCtxKey caddy.CtxKey = "nextCall"
|
nextCallCtxKey caddy.CtxKey = "nextCall"
|
||||||
)
|
)
|
||||||
@@ -57,7 +58,7 @@ func newOpenTelemetryWrapper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
version, _ := caddy.Version()
|
version, _ := caddy.Version()
|
||||||
res, err := ot.newResource(caddyhttp.ServerHeader, version)
|
res, err := ot.newResource(webEngineName, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ot, fmt.Errorf("creating resource error: %w", err)
|
return ot, fmt.Errorf("creating resource error: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user