Compare commits

..

No commits in common. "master" and "v2.9.0-beta.3" have entirely different histories.

119 changed files with 1297 additions and 5124 deletions

10
.github/SECURITY.md vendored
View File

@ -5,11 +5,11 @@ The Caddy project would like to make sure that it stays on top of all practicall
## Supported Versions
| Version | Supported |
| -------- | ----------|
| 2.latest | ✔️ |
| 1.x | :x: |
| < 1.x | :x: |
| Version | Supported |
| ------- | ------------------ |
| 2.x | ✔️ |
| 1.x | :x: |
| < 1.x | :x: |
## Acceptable Scope

View File

@ -12,28 +12,28 @@ on:
- master
- 2.*
env:
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
jobs:
test:
strategy:
# Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false
matrix:
os:
os:
- linux
- mac
- windows
go:
- '1.24'
go:
- '1.22'
- '1.23'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.24'
GO_SEMVER: '~1.24.1'
- go: '1.22'
GO_SEMVER: '~1.22.3'
- go: '1.23'
GO_SEMVER: '~1.23.0'
# Set some variables per OS, usable via ${{ matrix.VAR }}
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
@ -99,7 +99,7 @@ jobs:
env:
CGO_ENABLED: 0
run: |
go build -tags nobadger,nomysql,nopgx -trimpath -ldflags="-w -s" -v
go build -tags nobadger -trimpath -ldflags="-w -s" -v
- name: Smoke test Caddy
working-directory: ./cmd/caddy
@ -122,7 +122,7 @@ jobs:
# continue-on-error: true
run: |
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
go test -tags nobadger,nomysql,nopgx -v -coverprofile="cover-profile.out" -short -race ./...
go test -tags nobadger -v -coverprofile="cover-profile.out" -short -race ./...
# echo "status=$?" >> $GITHUB_OUTPUT
# Relevant step if we reinvestigate publishing test/coverage reports
@ -143,7 +143,7 @@ jobs:
s390x-test:
name: test (s390x on IBM Z)
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps:
- name: Checkout code
@ -170,7 +170,7 @@ jobs:
retries=3
exit_code=0
while ((retries > 0)); do
CGO_ENABLED=0 go test -p 1 -tags nobadger,nomysql,nopgx -v ./...
CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./...
exit_code=$?
if ((exit_code == 0)); then
break
@ -194,7 +194,6 @@ jobs:
goreleaser-check:
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
steps:
- name: Checkout code
uses: actions/checkout@v4
@ -206,7 +205,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: "~1.24"
go-version: "~1.23"
check-latest: true
- name: Install xcaddy
run: |
@ -217,4 +216,4 @@ jobs:
version: latest
args: build --single-target --snapshot
env:
TAG: ${{ github.head_ref || github.ref_name }}
TAG: "master"

View File

@ -10,16 +10,12 @@ on:
- master
- 2.*
env:
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
jobs:
build:
strategy:
fail-fast: false
matrix:
goos:
goos:
- 'aix'
- 'linux'
- 'solaris'
@ -30,14 +26,18 @@ jobs:
- 'windows'
- 'darwin'
- 'netbsd'
go:
- '1.24'
go:
- '1.22'
- '1.23'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.24'
GO_SEMVER: '~1.24.1'
- go: '1.22'
GO_SEMVER: '~1.22.3'
- go: '1.23'
GO_SEMVER: '~1.23.0'
runs-on: ubuntu-latest
continue-on-error: true

View File

@ -13,10 +13,6 @@ on:
permissions:
contents: read
env:
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
jobs:
# From https://github.com/golangci/golangci-lint-action
golangci:
@ -47,7 +43,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '~1.24'
go-version: '~1.23'
check-latest: true
- name: golangci-lint
@ -67,5 +63,5 @@ jobs:
- name: govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-input: '~1.24.1'
go-version-input: '~1.23.0'
check-latest: true

View File

@ -5,10 +5,6 @@ on:
tags:
- 'v*.*.*'
env:
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
jobs:
release:
name: Release
@ -17,13 +13,13 @@ jobs:
os:
- ubuntu-latest
go:
- '1.24'
- '1.23'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.24'
GO_SEMVER: '~1.24.1'
- go: '1.23'
GO_SEMVER: '~1.23.0'
runs-on: ${{ matrix.os }}
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233

View File

@ -111,7 +111,7 @@ archives:
- id: default
format_overrides:
- goos: windows
formats: zip
format: zip
name_template: >-
{{ .ProjectName }}_
{{- .Version }}_
@ -192,9 +192,6 @@ nfpms:
preremove: ./caddy-dist/scripts/preremove.sh
postremove: ./caddy-dist/scripts/postremove.sh
provides:
- httpd
release:
github:
owner: caddyserver

View File

@ -16,7 +16,7 @@
<a href="https://github.com/caddyserver/caddy/actions/workflows/ci.yml"><img src="https://github.com/caddyserver/caddy/actions/workflows/ci.yml/badge.svg"></a>
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
<br>
<a href="https://x.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/twitter/follow/caddyserver" alt="@caddyserver on Twitter"></a>
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
<br>
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
@ -67,7 +67,6 @@
- Fully-managed local CA for internal names & IPs
- Can coordinate with other Caddy instances in a cluster
- Multi-issuer fallback
- Encrypted ClientHello (ECH) support
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates
- **Scales to hundreds of thousands of sites** as proven in production
@ -88,7 +87,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
Requirements:
- [Go 1.24.0 or newer](https://golang.org/dl/)
- [Go 1.22.3 or newer](https://golang.org/dl/)
### For development
@ -177,7 +176,7 @@ The docs are also open source. You can contribute to them here: https://github.c
## Getting help
- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com) before help is needed.
- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers!
@ -193,8 +192,8 @@ Matthew Holt began developing Caddy in 2014 while studying computer science at B
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
- _Project on X: [@caddyserver](https://x.com/caddyserver)_
- _Author on X: [@mholt6](https://x.com/mholt6)_
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.

View File

@ -221,8 +221,7 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co
if remote {
muxWrap.remoteControl = admin.Remote
} else {
// see comment in allowedOrigins() as to why we disable the host check for unix/fd networks
muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFdNetwork()
muxWrap.enforceHost = !addr.isWildcardInterface()
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
muxWrap.enforceOrigin = admin.EnforceOrigin
}
@ -311,43 +310,47 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
for _, o := range admin.Origins {
uniqueOrigins[o] = struct{}{}
}
// RFC 2616, Section 14.26:
// "A client MUST include a Host header field in all HTTP/1.1 request
// messages. If the requested URI does not include an Internet host
// name for the service being requested, then the Host header field MUST
// be given with an empty value."
//
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
// Understandable, but frustrating. See:
// https://github.com/golang/go/issues/60374
// See also the discussion here:
// https://github.com/golang/go/issues/61431
//
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
// security checks, the infosec community assures me that it is secure to do
// so, because:
//
// 1) Browsers do not allow access to unix sockets
// 2) DNS is irrelevant to unix sockets
//
// If either of those two statements ever fail to hold true, it is not the
// fault of Caddy.
//
// Thus, we do not fill out allowed origins and do not enforce Host
// requirements for unix sockets. Enforcing it leads to confusion and
// frustration, when UDS have their own permissions from the OS.
// Enforcing host requirements here is effectively security theater,
// and a false sense of security.
//
// See also the discussion in #6832.
if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
if admin.Origins == nil {
if addr.isLoopback() {
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
} else {
if addr.IsUnixNetwork() || addr.IsFdNetwork() {
// RFC 2616, Section 14.26:
// "A client MUST include a Host header field in all HTTP/1.1 request
// messages. If the requested URI does not include an Internet host
// name for the service being requested, then the Host header field MUST
// be given with an empty value."
//
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
// Understandable, but frustrating. See:
// https://github.com/golang/go/issues/60374
// See also the discussion here:
// https://github.com/golang/go/issues/61431
//
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
// security checks, the infosec community assures me that it is secure to do
// so, because:
// 1) Browsers do not allow access to unix sockets
// 2) DNS is irrelevant to unix sockets
//
// I am not quite ready to trust either of those external factors, so instead
// of disabling Host/Origin checks, we now allow specific Host values when
// accessing the admin endpoint over unix sockets. I definitely don't trust
// DNS (e.g. I don't trust 'localhost' to always resolve to the local host),
// and IP shouldn't even be used, but if it is for some reason, I think we can
// at least be reasonably assured that 127.0.0.1 and ::1 route to the local
// machine, meaning that a hypothetical browser origin would have to be on the
// local machine as well.
uniqueOrigins[""] = struct{}{}
uniqueOrigins["127.0.0.1"] = struct{}{}
uniqueOrigins["::1"] = struct{}{}
} else {
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
}
}
if !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
}
}
@ -424,13 +427,6 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
handler := cfg.Admin.newAdminHandler(addr, false, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return err
}
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
if err != nil {
return err
@ -552,13 +548,6 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
// because we are using TLS authentication instead
handler := cfg.Admin.newAdminHandler(addr, true, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return err
}
// create client certificate pool for TLS mutual auth, and extract public keys
// so that we can enforce access controls at the application layer
clientCertPool := x509.NewCertPool()
@ -1150,7 +1139,7 @@ traverseLoop:
return fmt.Errorf("[%s] invalid array index '%s': %v",
path, idxStr, err)
}
if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
if idx < 0 || idx >= len(arr) {
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
}
}

View File

@ -15,20 +15,12 @@
package caddy
import (
"context"
"crypto/x509"
"encoding/json"
"fmt"
"maps"
"net/http"
"net/http/httptest"
"reflect"
"sync"
"testing"
"github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
var testCfg = []byte(`{
@ -211,719 +203,3 @@ func BenchmarkLoad(b *testing.B) {
Load(testCfg, true)
}
}
func TestAdminHandlerErrorHandling(t *testing.T) {
initAdminMetrics()
handler := adminHandler{
mux: http.NewServeMux(),
}
handler.mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := fmt.Errorf("test error")
handler.handleError(w, r, err)
}))
req := httptest.NewRequest(http.MethodGet, "/error", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code == http.StatusOK {
t.Error("expected error response, got success")
}
var apiErr APIError
if err := json.NewDecoder(rr.Body).Decode(&apiErr); err != nil {
t.Fatalf("decoding response: %v", err)
}
if apiErr.Message != "test error" {
t.Errorf("expected error message 'test error', got '%s'", apiErr.Message)
}
}
func initAdminMetrics() {
if adminMetrics.requestErrors != nil {
prometheus.Unregister(adminMetrics.requestErrors)
}
if adminMetrics.requestCount != nil {
prometheus.Unregister(adminMetrics.requestCount)
}
adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "caddy",
Subsystem: "admin_http",
Name: "request_errors_total",
Help: "Number of errors that occurred handling admin endpoint requests",
}, []string{"handler", "path", "method"})
adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "caddy",
Subsystem: "admin_http",
Name: "requests_total",
Help: "Count of requests to the admin endpoint",
}, []string{"handler", "path", "code", "method"}) // Added code and method labels
prometheus.MustRegister(adminMetrics.requestErrors)
prometheus.MustRegister(adminMetrics.requestCount)
}
func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
initAdminMetrics()
cfg := &Config{
Admin: &AdminConfig{
Listen: "localhost:2019",
},
}
err := replaceLocalAdminServer(cfg, Context{})
if err != nil {
t.Fatalf("setting up admin server: %v", err)
}
defer func() {
stopAdminServer(localAdminServer)
}()
tests := []struct {
name string
path string
method string
expectedStatus int
}{
{
name: "stop endpoint wrong method",
path: "/stop",
method: http.MethodGet,
expectedStatus: http.StatusMethodNotAllowed,
},
{
name: "config endpoint wrong content-type",
path: "/config/",
method: http.MethodPost,
expectedStatus: http.StatusBadRequest,
},
{
name: "config ID missing ID",
path: "/id/",
method: http.MethodGet,
expectedStatus: http.StatusBadRequest,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
rr := httptest.NewRecorder()
localAdminServer.Handler.ServeHTTP(rr, req)
if rr.Code != test.expectedStatus {
t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
}
metricValue := testGetMetricValue(map[string]string{
"path": test.path,
"handler": "admin",
"method": test.method,
})
if metricValue != 1 {
t.Errorf("expected error metric to be incremented once, got %v", metricValue)
}
})
}
}
func testGetMetricValue(labels map[string]string) float64 {
promLabels := prometheus.Labels{}
maps.Copy(promLabels, labels)
metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels)
if err != nil {
return 0
}
pb := &dto.Metric{}
metric.Write(pb)
return pb.GetCounter().GetValue()
}
type mockRouter struct {
routes []AdminRoute
}
func (m mockRouter) Routes() []AdminRoute {
return m.routes
}
type mockModule struct {
mockRouter
}
func (m *mockModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "admin.api.mock",
New: func() Module {
mm := &mockModule{
mockRouter: mockRouter{
routes: m.routes,
},
}
return mm
},
}
}
func TestNewAdminHandlerRouterRegistration(t *testing.T) {
originalModules := make(map[string]ModuleInfo)
maps.Copy(originalModules, modules)
defer func() {
modules = originalModules
}()
mockRoute := AdminRoute{
Pattern: "/mock",
Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(http.StatusOK)
return nil
}),
}
mock := &mockModule{
mockRouter: mockRouter{
routes: []AdminRoute{mockRoute},
},
}
RegisterModule(mock)
addr, err := ParseNetworkAddress("localhost:2019")
if err != nil {
t.Fatalf("Failed to parse address: %v", err)
}
admin := &AdminConfig{
EnforceOrigin: false,
}
handler := admin.newAdminHandler(addr, false, Context{})
req := httptest.NewRequest("GET", "/mock", nil)
req.Host = "localhost:2019"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
t.Logf("Response body: %s", rr.Body.String())
}
if len(admin.routers) != 1 {
t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
}
}
type mockProvisionableRouter struct {
mockRouter
provisionErr error
provisioned bool
}
func (m *mockProvisionableRouter) Provision(Context) error {
m.provisioned = true
return m.provisionErr
}
type mockProvisionableModule struct {
*mockProvisionableRouter
}
func (m *mockProvisionableModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "admin.api.mock_provision",
New: func() Module {
mm := &mockProvisionableModule{
mockProvisionableRouter: &mockProvisionableRouter{
mockRouter: m.mockRouter,
provisionErr: m.provisionErr,
},
}
return mm
},
}
}
func TestAdminRouterProvisioning(t *testing.T) {
tests := []struct {
name string
provisionErr error
wantErr bool
routersAfter int // expected number of routers after provisioning
}{
{
name: "successful provisioning",
provisionErr: nil,
wantErr: false,
routersAfter: 0,
},
{
name: "provisioning error",
provisionErr: fmt.Errorf("provision failed"),
wantErr: true,
routersAfter: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
originalModules := make(map[string]ModuleInfo)
maps.Copy(originalModules, modules)
defer func() {
modules = originalModules
}()
mockRoute := AdminRoute{
Pattern: "/mock",
Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}),
}
// Create provisionable module
mock := &mockProvisionableModule{
mockProvisionableRouter: &mockProvisionableRouter{
mockRouter: mockRouter{
routes: []AdminRoute{mockRoute},
},
provisionErr: test.provisionErr,
},
}
RegisterModule(mock)
admin := &AdminConfig{}
addr, err := ParseNetworkAddress("localhost:2019")
if err != nil {
t.Fatalf("Failed to parse address: %v", err)
}
_ = admin.newAdminHandler(addr, false, Context{})
err = admin.provisionAdminRouters(Context{})
if test.wantErr {
if err == nil {
t.Error("Expected error but got nil")
}
} else {
if err != nil {
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))
}
})
}
}
func TestAllowedOriginsUnixSocket(t *testing.T) {
// see comment in allowedOrigins() as to why we do not fill out allowed origins for UDS
tests := []struct {
name string
addr NetworkAddress
origins []string
expectOrigins []string
}{
{
name: "unix socket with default origins",
addr: NetworkAddress{
Network: "unix",
Host: "/tmp/caddy.sock",
},
origins: nil, // default origins
expectOrigins: []string{},
},
{
name: "unix socket with custom origins",
addr: NetworkAddress{
Network: "unix",
Host: "/tmp/caddy.sock",
},
origins: []string{"example.com"},
expectOrigins: []string{
"example.com",
},
},
{
name: "tcp socket on localhost gets all loopback addresses",
addr: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 2019,
EndPort: 2019,
},
origins: nil,
expectOrigins: []string{
"localhost:2019",
"[::1]:2019",
"127.0.0.1:2019",
},
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
admin := AdminConfig{
Origins: test.origins,
}
got := admin.allowedOrigins(test.addr)
var gotOrigins []string
for _, u := range got {
gotOrigins = append(gotOrigins, u.Host)
}
if len(gotOrigins) != len(test.expectOrigins) {
t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins))
return
}
expectMap := make(map[string]struct{})
for _, origin := range test.expectOrigins {
expectMap[origin] = struct{}{}
}
gotMap := make(map[string]struct{})
for _, origin := range gotOrigins {
gotMap[origin] = struct{}{}
}
if !reflect.DeepEqual(expectMap, gotMap) {
t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins)
}
})
}
}
func TestReplaceRemoteAdminServer(t *testing.T) {
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
MTAwMDAwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA4O4S6BSoYcoxvRqI+h7yPOjF6KjntjzVVm9M+uHK4lzX
F1L3pSxJ2nDD4wZEV3FJ5yFOHVFqkG2vXG3BIczOlYG7UeNmKbQnKc5kZj3HGUrS
VGEktA4OJbeZhhWP15gcXN5eDM2eH3g9BFXVX6AURxLiUXzhNBUEZuj/OEyH9yEF
/qPCE+EjzVvWxvBXwgz/io4r4yok/Vq/bxJ6FlV6R7DX5oJSXyO0VEHZPi9DIyNU
kK3F/r4U1sWiJGWOs8i3YQWZ2ejh1C0aLFZpPcCGGgMNpoF31gyYP6ZuPDUyCXsE
g36UUw1JHNtIXYcLhnXuqj4A8TybTDpgXLqvwA9DBQIDAQABo1MwUTAdBgNVHQ4E
FgQUc13z30pFC63rr/HGKOE7E82vjXwwHwYDVR0jBBgwFoAUc13z30pFC63rr/HG
KOE7E82vjXwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHO3j
oeiUXXJ7xD4P8Wj5t9d+E8lE1Xv1Dk3Z+EdG5+dan+RcToE42JJp9zB7FIh5Qz8g
W77LAjqh5oyqz3A2VJcyVgfE3uJP1R1mJM7JfGHf84QH4TZF2Q1RZY4SZs0VQ6+q
5wSlIZ4NXDy4Q4XkIJBGS61wT8IzYFXYBpx4PCP1Qj0PIE4sevEGwjsBIgxK307o
BxF8AWe6N6e4YZmQLGjQ+SeH0iwZb6vpkHyAY8Kj2hvK+cq2P7vU3VGi0t3r1F8L
IvrXHCvO2BMNJ/1UK1M4YNX8LYJqQhg9hEsIROe1OE/m3VhxIYMJI+qZXk9yHfgJ
vq+SH04xKhtFudVBAQ==`
tests := []struct {
name string
cfg *Config
wantErr bool
}{
{
name: "nil config",
cfg: nil,
wantErr: false,
},
{
name: "nil admin config",
cfg: &Config{
Admin: nil,
},
wantErr: false,
},
{
name: "nil remote config",
cfg: &Config{
Admin: &AdminConfig{},
},
wantErr: false,
},
{
name: "invalid listen address",
cfg: &Config{
Admin: &AdminConfig{
Remote: &RemoteAdmin{
Listen: "invalid:address",
},
},
},
wantErr: true,
},
{
name: "valid config",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{},
Remote: &RemoteAdmin{
Listen: "localhost:2021",
AccessControl: []*AdminAccess{
{
PublicKeys: []string{testCert},
Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
},
},
},
},
},
wantErr: false,
},
{
name: "invalid certificate",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{},
Remote: &RemoteAdmin{
Listen: "localhost:2021",
AccessControl: []*AdminAccess{
{
PublicKeys: []string{"invalid-cert-data"},
Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
},
},
},
},
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := Context{
Context: context.Background(),
cfg: test.cfg,
}
if test.cfg != nil {
test.cfg.storage = &certmagic.FileStorage{Path: t.TempDir()}
}
if test.cfg != nil && test.cfg.Admin != nil && test.cfg.Admin.Identity != nil {
identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
return &certmagic.Config{}, nil
},
})
}
err := replaceRemoteAdminServer(ctx, test.cfg)
if test.wantErr {
if err == nil {
t.Error("Expected error but got nil")
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
}
// Clean up
if remoteAdminServer != nil {
_ = stopAdminServer(remoteAdminServer)
}
})
}
}
type mockIssuer struct {
configSet *certmagic.Config
}
func (m *mockIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
return &certmagic.IssuedCertificate{
Certificate: []byte(csr.Raw),
}, nil
}
func (m *mockIssuer) SetConfig(cfg *certmagic.Config) {
m.configSet = cfg
}
func (m *mockIssuer) IssuerKey() string {
return "mock"
}
type mockIssuerModule struct {
*mockIssuer
}
func (m *mockIssuerModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "tls.issuance.acme",
New: func() Module {
return &mockIssuerModule{mockIssuer: new(mockIssuer)}
},
}
}
func TestManageIdentity(t *testing.T) {
originalModules := make(map[string]ModuleInfo)
maps.Copy(originalModules, modules)
defer func() {
modules = originalModules
}()
RegisterModule(&mockIssuerModule{})
certPEM := []byte(`-----BEGIN CERTIFICATE-----
MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3lcub2pUwkjC
5GJQA2ZZfJJi6d1QHhEmkX9VxKYGp6gagZuRqJWy9TXP6++1ZzQQxqZLD0TkuxZ9
8i9Nz00000CCBjCCAQQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgG
CCsGAQUFBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29t
L0dJQUcyLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5j
b20vb2NzcDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/
BAIwADAfBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHREEEDAO
ggxtYWlsLmdvb2dsZTANBgkqhkiG9w0BAQUFAAOCAQEAMP6IWgNGZE8wP9TjFjSZ
3mmW3A1eIr0CuPwNZ2LJ5ZD1i70ojzcj4I9IdP5yPg9CAEV4hNASbM1LzfC7GmJE
tPzW5tRmpKVWZGRgTgZI8Hp/xZXMwLh9ZmXV4kESFAGj5G5FNvJyUV7R5Eh+7OZX
7G4jJ4ZGJh+5jzN9HdJJHQHGYNIYOzC7+HH9UMwCjX9vhQ4RjwFZJThS2Yb+y7pb
9yxTJZoXC6J0H5JpnZb7kZEJ+Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END CERTIFICATE-----`)
keyPEM := []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
...
-----END PRIVATE KEY-----`)
testStorage := certmagic.FileStorage{Path: t.TempDir()}
err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
if err != nil {
t.Fatal(err)
}
err = testStorage.Store(context.Background(), "localhost/localhost.key", keyPEM)
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
cfg *Config
wantErr bool
checkState func(*testing.T, *Config)
}{
{
name: "nil config",
cfg: nil,
},
{
name: "nil admin config",
cfg: &Config{
Admin: nil,
},
},
{
name: "nil identity config",
cfg: &Config{
Admin: &AdminConfig{},
},
},
{
name: "default issuer when none specified",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{
Identifiers: []string{"localhost"},
},
},
storage: &testStorage,
},
checkState: func(t *testing.T, cfg *Config) {
if len(cfg.Admin.Identity.issuers) == 0 {
t.Error("Expected at least 1 issuer to be configured")
return
}
if _, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule); !ok {
t.Error("Expected mock issuer to be configured")
}
},
},
{
name: "custom issuer",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{
Identifiers: []string{"localhost"},
IssuersRaw: []json.RawMessage{
json.RawMessage(`{"module": "acme"}`),
},
},
},
storage: &certmagic.FileStorage{Path: "testdata"},
},
checkState: func(t *testing.T, cfg *Config) {
if len(cfg.Admin.Identity.issuers) != 1 {
t.Fatalf("Expected 1 issuer, got %d", len(cfg.Admin.Identity.issuers))
}
mockIss, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule)
if !ok {
t.Fatal("Expected mock issuer")
}
if mockIss.configSet == nil {
t.Error("Issuer config was not set")
}
},
},
{
name: "invalid issuer module",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{
Identifiers: []string{"localhost"},
IssuersRaw: []json.RawMessage{
json.RawMessage(`{"module": "doesnt_exist"}`),
},
},
},
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if identityCertCache != nil {
// Reset the cert cache before each test
identityCertCache.Stop()
identityCertCache = nil
}
ctx := Context{
Context: context.Background(),
cfg: test.cfg,
moduleInstances: make(map[string][]Module),
}
err := manageIdentity(ctx, test.cfg)
if test.wantErr {
if err == nil {
t.Error("Expected error but got nil")
}
return
}
if err != nil {
t.Fatalf("Expected no error but got: %v", err)
}
if test.checkState != nil {
test.checkState(t, test.cfg)
}
})
}
}

126
caddy.go
View File

@ -81,14 +81,13 @@ type Config struct {
// associated value.
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
apps map[string]App
storage certmagic.Storage
eventEmitter eventEmitter
apps map[string]App
storage certmagic.Storage
cancelFunc context.CancelFunc
// fileSystems is a dict of fileSystems that will later be loaded from and added to.
fileSystems FileSystems
// filesystems is a dict of filesystems that will later be loaded from and added to.
filesystems FileSystems
}
// App is a thing that Caddy runs.
@ -443,10 +442,6 @@ func run(newCfg *Config, start bool) (Context, error) {
}
globalMetrics.configSuccess.Set(1)
globalMetrics.configSuccessTime.SetToCurrentTime()
// TODO: This event is experimental and subject to change.
ctx.emitEvent("started", nil)
// now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc.
return ctx, finishSettingUp(ctx, ctx.cfg)
@ -505,8 +500,16 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
return ctx, err
}
// start the admin endpoint (and stop any prior one)
if replaceAdminServer {
err = replaceLocalAdminServer(newCfg, ctx)
if err != nil {
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
}
}
// create the new filesystem map
newCfg.fileSystems = &filesystems.FileSystemMap{}
newCfg.filesystems = &filesystems.FilesystemMap{}
// prepare the new config for use
newCfg.apps = make(map[string]App)
@ -536,14 +539,6 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
return ctx, err
}
// start the admin endpoint (and stop any prior one)
if replaceAdminServer {
err = replaceLocalAdminServer(newCfg, ctx)
if err != nil {
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
}
}
// Load and Provision each app and their submodules
err = func() error {
for appName := range newCfg.AppsRaw {
@ -701,9 +696,6 @@ func unsyncedStop(ctx Context) {
return
}
// TODO: This event is experimental and subject to change.
ctx.emitEvent("stopping", nil)
// stop each app
for name, a := range ctx.cfg.apps {
err := a.Stop()
@ -733,10 +725,8 @@ func Validate(cfg *Config) error {
// Errors are logged along the way, and an appropriate exit
// code is emitted.
func exitProcess(ctx context.Context, logger *zap.Logger) {
// let the rest of the program know we're quitting; only do it once
if !atomic.CompareAndSwapInt32(exiting, 0, 1) {
return
}
// let the rest of the program know we're quitting
atomic.StoreInt32(exiting, 1)
// give the OS or service/process manager our 2 weeks' notice: we quit
if err := notify.Stopping(); err != nil {
@ -1046,92 +1036,6 @@ func Version() (simple, full string) {
return
}
// Event represents something that has happened or is happening.
// An Event value is not synchronized, so it should be copied if
// being used in goroutines.
//
// EXPERIMENTAL: Events are subject to change.
type Event struct {
// If non-nil, the event has been aborted, meaning
// propagation has stopped to other handlers and
// the code should stop what it was doing. Emitters
// may choose to use this as a signal to adjust their
// code path appropriately.
Aborted error
// The data associated with the event. Usually the
// original emitter will be the only one to set or
// change these values, but the field is exported
// so handlers can have full access if needed.
// However, this map is not synchronized, so
// handlers must not use this map directly in new
// goroutines; instead, copy the map to use it in a
// goroutine. Data may be nil.
Data map[string]any
id uuid.UUID
ts time.Time
name string
origin Module
}
// NewEvent creates a new event, but does not emit the event. To emit an
// event, call Emit() on the current instance of the caddyevents app insteaad.
//
// EXPERIMENTAL: Subject to change.
func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
id, err := uuid.NewRandom()
if err != nil {
return Event{}, fmt.Errorf("generating new event ID: %v", err)
}
name = strings.ToLower(name)
return Event{
Data: data,
id: id,
ts: time.Now(),
name: name,
origin: ctx.Module(),
}, nil
}
func (e Event) ID() uuid.UUID { return e.id }
func (e Event) Timestamp() time.Time { return e.ts }
func (e Event) Name() string { return e.name }
func (e Event) Origin() Module { return e.origin } // Returns the module that originated the event. May be nil, usually if caddy core emits the event.
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
func (e Event) CloudEvent() CloudEvent {
dataJSON, _ := json.Marshal(e.Data)
return CloudEvent{
ID: e.id.String(),
Source: e.origin.CaddyModule().String(),
SpecVersion: "1.0",
Type: e.name,
Time: e.ts,
DataContentType: "application/json",
Data: dataJSON,
}
}
// CloudEvent is a JSON-serializable structure that
// is compatible with the CloudEvents specification.
// See https://cloudevents.io.
// EXPERIMENTAL: Subject to change.
type CloudEvent struct {
ID string `json:"id"`
Source string `json:"source"`
SpecVersion string `json:"specversion"`
Type string `json:"type"`
Time time.Time `json:"time"`
DataContentType string `json:"datacontenttype,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// ErrEventAborted cancels an event.
var ErrEventAborted = errors.New("event aborted")
// ActiveContext returns the currently-active context.
// This function is experimental and might be changed
// or removed in the future.

View File

@ -61,8 +61,7 @@ func Format(input []byte) []byte {
heredocMarker []rune
heredocClosingMarker []rune
nesting int // indentation level
withinBackquote bool
nesting int // indentation level
)
write := func(ch rune) {
@ -89,9 +88,6 @@ func Format(input []byte) []byte {
}
panic(err)
}
if ch == '`' {
withinBackquote = !withinBackquote
}
// detect whether we have the start of a heredoc
if !quoted && !(heredoc != heredocClosed || heredocEscaped) &&
@ -240,23 +236,14 @@ func Format(input []byte) []byte {
switch {
case ch == '{':
openBrace = true
openBraceWritten = false
openBraceSpace = spacePrior && !beginningOfLine
if openBraceSpace {
write(' ')
}
openBraceWritten = false
if withinBackquote {
write('{')
openBraceWritten = true
continue
}
continue
case ch == '}' && (spacePrior || !openBrace):
if withinBackquote {
write('}')
continue
}
if last != '\n' {
nextLine()
}

View File

@ -434,16 +434,6 @@ block2 {
}
`,
},
{
description: "Preserve braces wrapped by backquotes",
input: "block {respond `All braces should remain: {{now | date \"2006\"}}`}",
expect: "block {respond `All braces should remain: {{now | date \"2006\"}}`}",
},
{
description: "Preserve braces wrapped by quotes",
input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
expect: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
},
} {
// the formatter should output a trailing newline,
// even if the tests aren't written to expect that

View File

@ -423,7 +423,7 @@ func (p *parser) doImport(nesting int) error {
// make path relative to the file of the _token_ being processed rather
// than current working directory (issue #867) and then use glob to get
// list of matching filenames
absFile, err := caddy.FastAbs(p.Dispenser.File())
absFile, err := filepath.Abs(p.Dispenser.File())
if err != nil {
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.File(), err)
}
@ -622,7 +622,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
// Tack the file path onto these tokens so errors show the imported file's name
// (we use full, absolute path to avoid bugs: issue #1892)
filename, err := caddy.FastAbs(importFile)
filename, err := filepath.Abs(importFile)
if err != nil {
return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err)
}

View File

@ -24,7 +24,7 @@ import (
"time"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/v3/acme"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2"
@ -84,7 +84,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// parseTLS parses the tls directive. Syntax:
//
// tls [<email>|internal|force_automate]|[<cert_file> <key_file>] {
// tls [<email>|internal]|[<cert_file> <key_file>] {
// protocols <min> [<max>]
// ciphers <cipher_suites...>
// curves <curves...>
@ -99,7 +99,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// ca <acme_ca_endpoint>
// ca_root <pem_file>
// key_type [ed25519|p256|p384|rsa2048|rsa4096]
// dns [<provider_name> [...]] (required, though, if DNS is not configured as global option)
// dns <provider_name> [...]
// propagation_delay <duration>
// propagation_timeout <duration>
// resolvers <dns_servers...>
@ -107,7 +107,6 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// dns_challenge_override_domain <domain>
// on_demand
// reuse_private_keys
// force_automate
// eab <key_id> <mac_key>
// issuer <module_name> [...]
// get_certificate <module_name> [...]
@ -127,7 +126,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
var certManagers []certmagic.Manager
var onDemand bool
var reusePrivateKeys bool
var forceAutomate bool
firstLine := h.RemainingArgs()
switch len(firstLine) {
@ -135,10 +133,8 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
case 1:
if firstLine[0] == "internal" {
internalIssuer = new(caddytls.InternalIssuer)
} else if firstLine[0] == "force_automate" {
forceAutomate = true
} else if !strings.Contains(firstLine[0], "@") {
return nil, h.Err("single argument must either be 'internal', 'force_automate', or an email address")
return nil, h.Err("single argument must either be 'internal' or an email address")
} else {
acmeIssuer = &caddytls.ACMEIssuer{
Email: firstLine[0],
@ -312,6 +308,10 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
certManagers = append(certManagers, certManager)
case "dns":
if !h.NextArg() {
return nil, h.ArgErr()
}
provName := h.Val()
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
@ -321,19 +321,12 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
// DNS provider configuration optional, since it may be configured globally via the TLS app with global options
if h.NextArg() {
provName := h.Val()
modID := "dns.providers." + provName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings)
} else if h.Option("dns") == nil {
// if DNS is omitted locally, it needs to be configured globally
return nil, h.ArgErr()
modID := "dns.providers." + provName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings)
case "resolvers":
args := h.RemainingArgs()
@ -576,15 +569,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
})
}
// if enabled, the names in the site addresses will be
// added to the automation policies
if forceAutomate {
configVals = append(configVals, ConfigValue{
Class: "tls.force_automate",
Value: true,
})
}
// custom certificate selection
if len(certSelector.AnyTag) > 0 {
cp.CertSelection = &certSelector
@ -997,50 +981,6 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
}
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
case "sampling":
d := h.Dispenser.NewFromNextSegment()
for d.NextArg() {
// consume any tokens on the same line, if any.
}
sampling := &caddy.LogSampling{}
for nesting := d.Nesting(); d.NextBlock(nesting); {
subdir := d.Val()
switch subdir {
case "interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
interval, err := time.ParseDuration(d.Val() + "ns")
if err != nil {
return nil, d.Errf("failed to parse interval: %v", err)
}
sampling.Interval = interval
case "first":
if !d.NextArg() {
return nil, d.ArgErr()
}
first, err := strconv.Atoi(d.Val())
if err != nil {
return nil, d.Errf("failed to parse first: %v", err)
}
sampling.First = first
case "thereafter":
if !d.NextArg() {
return nil, d.ArgErr()
}
thereafter, err := strconv.Atoi(d.Val())
if err != nil {
return nil, d.Errf("failed to parse thereafter: %v", err)
}
sampling.Thereafter = thereafter
default:
return nil, d.Errf("unrecognized subdirective: %s", subdir)
}
}
cl.Sampling = sampling
case "core":
if !h.NextArg() {
return nil, h.ArgErr()

View File

@ -62,20 +62,6 @@ func TestLogDirectiveSyntax(t *testing.T) {
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
expectError: false,
},
{
input: `:8080 {
log {
sampling {
interval 2
first 3
thereafter 4
}
}
}
`,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false,
},
} {
adapter := caddyfile.Adapter{

View File

@ -16,7 +16,6 @@ package httpcaddyfile
import (
"encoding/json"
"maps"
"net"
"slices"
"sort"
@ -366,7 +365,9 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
// copy existing matcher definitions so we can augment
// new ones that are defined only in this scope
matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
maps.Copy(matcherDefs, h.matcherDefs)
for key, val := range h.matcherDefs {
matcherDefs[key] = val
}
// find and extract any embedded matcher definitions in this scope
for i := 0; i < len(segments); i++ {
@ -482,29 +483,12 @@ func sortRoutes(routes []ConfigValue) {
// we can only confidently compare path lengths if both
// directives have a single path to match (issue #5037)
if iPathLen > 0 && jPathLen > 0 {
// trim the trailing wildcard if there is one
iPathTrimmed := strings.TrimSuffix(iPM[0], "*")
jPathTrimmed := strings.TrimSuffix(jPM[0], "*")
// if both paths are the same except for a trailing wildcard,
// sort by the shorter path first (which is more specific)
if iPathTrimmed == jPathTrimmed {
if strings.TrimSuffix(iPM[0], "*") == strings.TrimSuffix(jPM[0], "*") {
return iPathLen < jPathLen
}
// we use the trimmed length to compare the paths
// https://github.com/caddyserver/caddy/issues/7012#issuecomment-2870142195
// credit to https://github.com/Hellio404
// for sorts with many items, mixing matchers w/ and w/o wildcards will confuse the sort and result in incorrect orders
iPathLen = len(iPathTrimmed)
jPathLen = len(jPathTrimmed)
// if both paths have the same length, sort lexically
// https://github.com/caddyserver/caddy/pull/7015#issuecomment-2871993588
if iPathLen == jPathLen {
return iPathTrimmed < jPathTrimmed
}
// sort most-specific (longest) path first
return iPathLen > jPathLen
}

View File

@ -191,7 +191,7 @@ func (st ServerType) Setup(
metrics, _ := options["metrics"].(*caddyhttp.Metrics)
for _, s := range servers {
if s.Metrics != nil {
metrics = cmp.Or(metrics, &caddyhttp.Metrics{})
metrics = cmp.Or[*caddyhttp.Metrics](metrics, &caddyhttp.Metrics{})
metrics = &caddyhttp.Metrics{
PerHost: metrics.PerHost || s.Metrics.PerHost,
}
@ -350,7 +350,7 @@ func (st ServerType) Setup(
// avoid duplicates by sorting + compacting
sort.Strings(defaultLog.Exclude)
defaultLog.Exclude = slices.Compact(defaultLog.Exclude)
defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude)
}
}
// we may have not actually added anything, so remove if empty
@ -633,6 +633,12 @@ func (st *ServerType) serversFromPairings(
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
}
srv.AutoHTTPS.IgnoreLoadedCerts = true
case "prefer_wildcard":
if srv.AutoHTTPS == nil {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
}
srv.AutoHTTPS.PreferWildcard = true
}
}
@ -700,6 +706,16 @@ func (st *ServerType) serversFromPairings(
return specificity(iLongestHost) > specificity(jLongestHost)
})
// collect all hosts that have a wildcard in them
wildcardHosts := []string{}
for _, sblock := range p.serverBlocks {
for _, addr := range sblock.parsedKeys {
if strings.HasPrefix(addr.Host, "*.") {
wildcardHosts = append(wildcardHosts, addr.Host[2:])
}
}
}
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
@ -747,14 +763,6 @@ func (st *ServerType) serversFromPairings(
}
}
// collect hosts that are forced to be automated
forceAutomatedNames := make(map[string]struct{})
if _, ok := sblock.pile["tls.force_automate"]; ok {
for _, host := range hosts {
forceAutomatedNames[host] = struct{}{}
}
}
// tls: connection policies
if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
// tls connection policies
@ -785,14 +793,8 @@ func (st *ServerType) serversFromPairings(
cp.FallbackSNI = fallbackSNI
}
// only append this policy if it actually changes something,
// or if the configuration explicitly automates certs for
// these names (this is necessary to hoist a connection policy
// above one that may manually load a wildcard cert that would
// otherwise clobber the automated one; the code that appends
// policies that manually load certs comes later, so they're
// lower in the list)
if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) {
// only append this policy if it actually changes something
if !cp.SettingsEmpty() {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
hasCatchAllTLSConnPolicy = len(hosts) == 0
}
@ -831,6 +833,18 @@ func (st *ServerType) serversFromPairings(
addressQualifiesForTLS = true
}
// If prefer wildcard is enabled, then we add hosts that are
// already covered by the wildcard to the skip list
if addressQualifiesForTLS && srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard {
baseDomain := addr.Host
if idx := strings.Index(baseDomain, "."); idx != -1 {
baseDomain = baseDomain[idx+1:]
}
if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) {
srv.AutoHTTPS.SkipCerts = append(srv.AutoHTTPS.SkipCerts, addr.Host)
}
}
// predict whether auto-HTTPS will add the conn policy for us; if so, we
// may not need to add one for this server
autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
@ -1061,40 +1075,11 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
// if they're exactly equal in every way, just keep one of them
if reflect.DeepEqual(cps[i], cps[j]) {
cps = slices.Delete(cps, j, j+1)
cps = append(cps[:j], cps[j+1:]...)
i--
break
}
// as a special case, if there are adjacent TLS conn policies that are identical except
// by their matchers, and the matchers are specifically just ServerName ("sni") matchers
// (by far the most common), we can combine them into a single policy
if i == j-1 && len(cps[i].MatchersRaw) == 1 && len(cps[j].MatchersRaw) == 1 {
if iSNIMatcherJSON, ok := cps[i].MatchersRaw["sni"]; ok {
if jSNIMatcherJSON, ok := cps[j].MatchersRaw["sni"]; ok {
// position of policies and the matcher criteria check out; if settings are
// the same, then we can combine the policies; we have to unmarshal and
// remarshal the matchers though
if cps[i].SettingsEqual(*cps[j]) {
var iSNIMatcher caddytls.MatchServerName
if err := json.Unmarshal(iSNIMatcherJSON, &iSNIMatcher); err == nil {
var jSNIMatcher caddytls.MatchServerName
if err := json.Unmarshal(jSNIMatcherJSON, &jSNIMatcher); err == nil {
iSNIMatcher = append(iSNIMatcher, jSNIMatcher...)
cps[i].MatchersRaw["sni"], err = json.Marshal(iSNIMatcher)
if err != nil {
return nil, fmt.Errorf("recombining SNI matchers: %v", err)
}
cps = slices.Delete(cps, j, j+1)
i--
break
}
}
}
}
}
}
// if they have the same matcher, try to reconcile each field: either they must
// be identical, or we have to be able to combine them safely
if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) {
@ -1128,12 +1113,6 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
return nil, fmt.Errorf("two policies with same match criteria have conflicting default SNI: %s vs. %s",
cps[i].DefaultSNI, cps[j].DefaultSNI)
}
if cps[i].FallbackSNI != "" &&
cps[j].FallbackSNI != "" &&
cps[i].FallbackSNI != cps[j].FallbackSNI {
return nil, fmt.Errorf("two policies with same match criteria have conflicting fallback SNI: %s vs. %s",
cps[i].FallbackSNI, cps[j].FallbackSNI)
}
if cps[i].ProtocolMin != "" &&
cps[j].ProtocolMin != "" &&
cps[i].ProtocolMin != cps[j].ProtocolMin {
@ -1174,9 +1153,6 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
if cps[i].DefaultSNI == "" && cps[j].DefaultSNI != "" {
cps[i].DefaultSNI = cps[j].DefaultSNI
}
if cps[i].FallbackSNI == "" && cps[j].FallbackSNI != "" {
cps[i].FallbackSNI = cps[j].FallbackSNI
}
if cps[i].ProtocolMin == "" && cps[j].ProtocolMin != "" {
cps[i].ProtocolMin = cps[j].ProtocolMin
}
@ -1196,13 +1172,12 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
}
}
cps = slices.Delete(cps, j, j+1)
cps = append(cps[:j], cps[j+1:]...)
i--
break
}
}
}
return cps, nil
}
@ -1686,18 +1661,6 @@ func listenersUseAnyPortOtherThan(addresses []string, otherPort string) bool {
return false
}
func mapContains[K comparable, V any](m map[K]V, keys []K) bool {
if len(m) == 0 || len(keys) == 0 {
return false
}
for _, key := range keys {
if _, ok := m[key]; ok {
return true
}
}
return false
}
// specificity returns len(s) minus any wildcards (*) and
// placeholders ({...}). Basically, it's a length count
// that penalizes the use of wildcards and placeholders.

View File

@ -19,8 +19,7 @@ import (
"strconv"
"github.com/caddyserver/certmagic"
"github.com/libdns/libdns"
"github.com/mholt/acmez/v3/acme"
"github.com/mholt/acmez/v2/acme"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@ -46,7 +45,7 @@ func init() {
RegisterGlobalOption("ocsp_interval", parseOptDuration)
RegisterGlobalOption("acme_ca", parseOptSingleString)
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptDNS)
RegisterGlobalOption("acme_dns", parseOptACMEDNS)
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
RegisterGlobalOption("skip_install_trust", parseOptTrue)
@ -63,8 +62,6 @@ func init() {
RegisterGlobalOption("log", parseLogOptions)
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
RegisterGlobalOption("persist_config", parseOptPersistConfig)
RegisterGlobalOption("dns", parseOptDNS)
RegisterGlobalOption("ech", parseOptECH)
}
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
@ -241,6 +238,25 @@ func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
return caddy.Duration(dur), nil
}
func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get DNS module name
return nil, d.ArgErr()
}
modID := "dns.providers." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
prov, ok := unm.(certmagic.DNSProvider)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.DNSProvider", modID, unm)
}
return prov, nil
}
func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
eab := new(acme.EAB)
d.Next() // consume option name
@ -554,68 +570,3 @@ func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next()
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
}
func parseOptDNS(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
if !d.Next() { // get DNS module name
return nil, d.ArgErr()
}
modID := "dns.providers." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
switch unm.(type) {
case libdns.RecordGetter,
libdns.RecordSetter,
libdns.RecordAppender,
libdns.RecordDeleter:
default:
return nil, d.Errf("module %s (%T) is not a libdns provider", modID, unm)
}
return unm, nil
}
func parseOptECH(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
ech := new(caddytls.ECH)
publicNames := d.RemainingArgs()
for _, publicName := range publicNames {
ech.Configs = append(ech.Configs, caddytls.ECHConfiguration{
PublicName: publicName,
})
}
if len(ech.Configs) == 0 {
return nil, d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "dns":
if !d.Next() {
return nil, d.ArgErr()
}
providerName := d.Val()
modID := "dns.providers." + providerName
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
ech.Publication = append(ech.Publication, &caddytls.ECHPublication{
Configs: publicNames,
PublishersRaw: caddy.ModuleMap{
"dns": caddyconfig.JSON(caddytls.ECHDNSPublisher{
ProviderRaw: caddyconfig.JSONModuleObject(unm, "name", providerName, nil),
}, nil),
},
})
default:
return nil, d.Errf("ech: unrecognized subdirective '%s'", d.Val())
}
}
return ech, nil
}

View File

@ -246,8 +246,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
switch d.Val() {
case "per_host":
serverOpts.Metrics.PerHost = true
default:
return nil, d.Errf("unrecognized metrics option '%s'", d.Val())
}
}

View File

@ -52,27 +52,19 @@ func NewShorthandReplacer() ShorthandReplacer {
// be used in the Caddyfile, and the right is the replacement.
func placeholderShorthands() []string {
return []string{
"{dir}", "{http.request.uri.path.dir}",
"{file}", "{http.request.uri.path.file}",
"{host}", "{http.request.host}",
"{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
"{orig_method}", "{http.request.orig_method}",
"{orig_uri}", "{http.request.orig_uri}",
"{orig_path}", "{http.request.orig_uri.path}",
"{orig_dir}", "{http.request.orig_uri.path.dir}",
"{orig_file}", "{http.request.orig_uri.path.file}",
"{orig_query}", "{http.request.orig_uri.query}",
"{orig_?query}", "{http.request.orig_uri.prefixed_query}",
"{method}", "{http.request.method}",
"{uri}", "{http.request.uri}",
"{path}", "{http.request.uri.path}",
"{dir}", "{http.request.uri.path.dir}",
"{file}", "{http.request.uri.path.file}",
"{query}", "{http.request.uri.query}",
"{?query}", "{http.request.uri.prefixed_query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
"{uri}", "{http.request.uri}",
"{uuid}", "{http.request.uuid}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",

View File

@ -25,7 +25,7 @@ import (
"strings"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/v3/acme"
"github.com/mholt/acmez/v2/acme"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@ -92,9 +92,8 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
}
var wildcardHosts []string // collect all hosts that have a wildcard in them, and aren't HTTP
forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard
// collect all hosts that have a wildcard in them, and arent HTTP
wildcardHosts := []string{}
for _, p := range pairings {
var addresses []string
for _, addressWithProtocols := range p.addressesWithProtocols {
@ -151,13 +150,6 @@ func (st ServerType) buildTLSApp(
ap.OnDemand = true
}
// collect hosts that are forced to have certs automated for their specific name
if _, ok := sblock.pile["tls.force_automate"]; ok {
for _, host := range sblockHosts {
forcedAutomatedNames[host] = struct{}{}
}
}
// reuse private keys tls
if _, ok := sblock.pile["tls.reuse_private_keys"]; ok {
ap.ReusePrivateKeys = true
@ -338,7 +330,7 @@ func (st ServerType) buildTLSApp(
combined = reflect.New(reflect.TypeOf(cl)).Elem()
}
clVal := reflect.ValueOf(cl)
for i := range clVal.Len() {
for i := 0; i < clVal.Len(); i++ {
combined = reflect.Append(combined, clVal.Index(i))
}
loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
@ -357,32 +349,6 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.OnDemand = onDemand
}
// set up "global" (to the TLS app) DNS provider config
if globalDNS, ok := options["dns"]; ok && globalDNS != nil {
tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
}
// set up ECH from Caddyfile options
if ech, ok := options["ech"].(*caddytls.ECH); ok {
tlsApp.EncryptedClientHello = ech
// outer server names will need certificates, so make sure they're included
// in an automation policy for them that applies any global options
ap, err := newBaseAutomationPolicy(options, warnings, true)
if err != nil {
return nil, warnings, err
}
for _, cfg := range ech.Configs {
if cfg.PublicName != "" {
ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName)
}
}
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
}
// if the storage clean interval is a boolean, then it's "off" to disable cleaning
if sc, ok := options["storage_check"].(string); ok && sc == "off" {
tlsApp.DisableStorageCheck = true
@ -441,13 +407,6 @@ func (st ServerType) buildTLSApp(
}
}
}
for name := range forcedAutomatedNames {
if slices.Contains(al, name) {
continue
}
al = append(al, name)
}
slices.Sort(al) // to stabilize the adapt output
if len(al) > 0 {
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
}
@ -469,7 +428,7 @@ func (st ServerType) buildTLSApp(
globalPreferredChains := options["preferred_chains"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
if hasGlobalACMEDefaults {
for i := range tlsApp.Automation.Policies {
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
ap := tlsApp.Automation.Policies[i]
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
// for public names, create default issuers which will later be filled in with configured global defaults
@ -577,8 +536,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
}
// only configure alt HTTP and TLS-ALPN ports if the DNS challenge is not enabled (wouldn't hurt, but isn't necessary since the DNS challenge is exclusive of others)
if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) {
if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) {
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
@ -587,7 +545,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
}
acmeIssuer.Challenges.HTTP.AlternatePort = globalHTTPPort.(int)
}
if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) {
if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) {
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}

View File

@ -35,7 +35,7 @@ func init() {
// If the response is not a JSON config, a config adapter must be specified
// either in the loader config (`adapter`), or in the Content-Type HTTP header
// returned in the HTTP response from the server. The Content-Type header is
// read just like the admin API's `/load` endpoint. If you don't have control
// read just like the admin API's `/load` endpoint. Uf you don't have control
// over the HTTP server (but can still trust its response), you can override
// the Content-Type header by setting the `adapter` property in this config.
type HTTPLoader struct {

View File

@ -31,8 +31,8 @@ import (
_ "github.com/caddyserver/caddy/v2/modules/standard"
)
// Config store any configuration required to make the tests run
type Config struct {
// Defaults store any configuration required to make the tests run
type Defaults struct {
// Port we expect caddy to listening on
AdminPort int
// Certificates we expect to be loaded before attempting to run the tests
@ -44,7 +44,7 @@ type Config struct {
}
// Default testing values
var Default = Config{
var Default = Defaults{
AdminPort: 2999, // different from what a real server also running on a developer's machine might be
Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second,
@ -61,7 +61,6 @@ type Tester struct {
Client *http.Client
configLoaded bool
t testing.TB
config Config
}
// NewTester will create a new testing client with an attached cookie jar
@ -79,29 +78,9 @@ func NewTester(t testing.TB) *Tester {
},
configLoaded: false,
t: t,
config: Default,
}
}
// WithDefaultOverrides this will override the default test configuration with the provided values.
func (tc *Tester) WithDefaultOverrides(overrides Config) *Tester {
if overrides.AdminPort != 0 {
tc.config.AdminPort = overrides.AdminPort
}
if len(overrides.Certificates) > 0 {
tc.config.Certificates = overrides.Certificates
}
if overrides.TestRequestTimeout != 0 {
tc.config.TestRequestTimeout = overrides.TestRequestTimeout
tc.Client.Timeout = overrides.TestRequestTimeout
}
if overrides.LoadRequestTimeout != 0 {
tc.config.LoadRequestTimeout = overrides.LoadRequestTimeout
}
return tc
}
type configLoadError struct {
Response string
}
@ -134,7 +113,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return nil
}
err := validateTestPrerequisites(tc)
err := validateTestPrerequisites(tc.t)
if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil
@ -142,7 +121,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
tc.t.Cleanup(func() {
if tc.t.Failed() && tc.configLoaded {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
tc.t.Log("unable to read the current config")
return
@ -172,10 +151,10 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
tc.t.Logf("After: %s", rawConfig)
}
client := &http.Client{
Timeout: tc.config.LoadRequestTimeout,
Timeout: Default.LoadRequestTimeout,
}
start := time.Now()
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.config.AdminPort), strings.NewReader(rawConfig))
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig))
if err != nil {
tc.t.Errorf("failed to create request. %s", err)
return err
@ -226,11 +205,11 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
}
client := &http.Client{
Timeout: tc.config.LoadRequestTimeout,
Timeout: Default.LoadRequestTimeout,
}
fetchConfig := func(client *http.Client) any {
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
return nil
}
@ -258,30 +237,30 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
}
const initConfig = `{
admin localhost:%d
admin localhost:2999
}
`
// validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running.
func validateTestPrerequisites(tc *Tester) error {
func validateTestPrerequisites(t testing.TB) error {
// check certificates are found
for _, certName := range tc.config.Certificates {
for _, certName := range Default.Certificates {
if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
}
}
if isCaddyAdminRunning(tc) != nil {
if isCaddyAdminRunning() != nil {
// setup the init config file, and set the cleanup afterwards
f, err := os.CreateTemp("", "")
if err != nil {
return err
}
tc.t.Cleanup(func() {
t.Cleanup(func() {
os.Remove(f.Name())
})
if _, err := f.WriteString(fmt.Sprintf(initConfig, tc.config.AdminPort)); err != nil {
if _, err := f.WriteString(initConfig); err != nil {
return err
}
@ -292,23 +271,23 @@ func validateTestPrerequisites(tc *Tester) error {
}()
// wait for caddy to start serving the initial config
for retries := 10; retries > 0 && isCaddyAdminRunning(tc) != nil; retries-- {
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(1 * time.Second)
}
}
// one more time to return the error
return isCaddyAdminRunning(tc)
return isCaddyAdminRunning()
}
func isCaddyAdminRunning(tc *Tester) error {
func isCaddyAdminRunning() error {
// assert that caddy is running
client := &http.Client{
Timeout: tc.config.LoadRequestTimeout,
Timeout: Default.LoadRequestTimeout,
}
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.config.AdminPort)
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort)
}
resp.Body.Close()

View File

@ -6,7 +6,6 @@ import (
"crypto/elliptic"
"crypto/rand"
"fmt"
"log/slog"
"net"
"net/http"
"strings"
@ -14,11 +13,10 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/mholt/acmez/v3"
"github.com/mholt/acmez/v3/acme"
"github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme"
smallstepacme "github.com/smallstep/certificates/acme"
"go.uber.org/zap"
"go.uber.org/zap/exp/zapslog"
)
const acmeChallengePort = 9081
@ -50,7 +48,7 @@ func TestACMEServerWithDefaults(t *testing.T) {
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: slog.New(zapslog.NewHandler(logger.Core())),
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
@ -119,7 +117,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: slog.New(zapslog.NewHandler(logger.Core())),
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},

View File

@ -5,15 +5,13 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"log/slog"
"strings"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/mholt/acmez/v3"
"github.com/mholt/acmez/v3/acme"
"github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
"go.uber.org/zap/exp/zapslog"
)
func TestACMEServerDirectory(t *testing.T) {
@ -78,7 +76,7 @@ func TestACMEServerAllowPolicy(t *testing.T) {
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: slog.New(zapslog.NewHandler(logger.Core())),
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
@ -167,7 +165,7 @@ func TestACMEServerDenyPolicy(t *testing.T) {
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: slog.New(zapslog.NewHandler(logger.Core())),
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},

View File

@ -1,72 +0,0 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
allow {
domains host-1.internal.example.com host-2.internal.example.com
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"handler": "acme_server",
"policy": {
"allow": {
"domains": [
"host-1.internal.example.com",
"host-2.internal.example.com"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}

View File

@ -1,80 +0,0 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
allow {
domains host-1.internal.example.com host-2.internal.example.com
}
deny {
domains dc.internal.example.com
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"handler": "acme_server",
"policy": {
"allow": {
"domains": [
"host-1.internal.example.com",
"host-2.internal.example.com"
]
},
"deny": {
"domains": [
"dc.internal.example.com"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}

View File

@ -1,71 +0,0 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
deny {
domains dc.internal.example.com
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"handler": "acme_server",
"policy": {
"deny": {
"domains": [
"dc.internal.example.com"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}

View File

@ -1,10 +1,16 @@
subdomain.example.com {
respond "Subdomain!"
{
auto_https prefer_wildcard
}
*.example.com {
tls cert.pem key.pem
respond "Wildcard!"
tls {
dns mock
}
respond "fallback"
}
foo.example.com {
respond "foo"
}
----------
{
@ -20,7 +26,7 @@ subdomain.example.com {
"match": [
{
"host": [
"subdomain.example.com"
"foo.example.com"
]
}
],
@ -31,7 +37,7 @@ subdomain.example.com {
{
"handle": [
{
"body": "Subdomain!",
"body": "foo",
"handler": "static_response"
}
]
@ -56,7 +62,7 @@ subdomain.example.com {
{
"handle": [
{
"body": "Wildcard!",
"body": "fallback",
"handler": "static_response"
}
]
@ -67,32 +73,33 @@ subdomain.example.com {
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"*.example.com"
]
},
"certificate_selection": {
"any_tag": [
"cert0"
]
}
},
{}
]
"automatic_https": {
"skip_certificates": [
"foo.example.com"
],
"prefer_wildcard": true
}
}
}
},
"tls": {
"certificates": {
"load_files": [
"automation": {
"policies": [
{
"certificate": "cert.pem",
"key": "key.pem",
"tags": [
"cert0"
"subjects": [
"*.example.com"
],
"issuers": [
{
"challenges": {
"dns": {
"provider": {
"name": "mock"
}
}
},
"module": "acme"
}
]
}
]

View File

@ -0,0 +1,268 @@
{
auto_https prefer_wildcard
}
# Covers two domains
*.one.example.com {
tls {
dns mock
}
respond "one fallback"
}
# Is covered, should not get its own AP
foo.one.example.com {
respond "foo one"
}
# This one has its own tls config so it doesn't get covered (escape hatch)
bar.one.example.com {
respond "bar one"
tls bar@bar.com
}
# Covers nothing but AP gets consolidated with the first
*.two.example.com {
tls {
dns mock
}
respond "two fallback"
}
# Is HTTP so it should not cover
http://*.three.example.com {
respond "three fallback"
}
# Has no wildcard coverage so it gets an AP
foo.three.example.com {
respond "foo three"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"foo.three.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "foo three",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"foo.one.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "foo one",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"bar.one.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "bar one",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.one.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "one fallback",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.two.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "two fallback",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip_certificates": [
"foo.one.example.com",
"bar.one.example.com"
],
"prefer_wildcard": true
}
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"*.three.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "three fallback",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"prefer_wildcard": true
}
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"foo.three.example.com"
]
},
{
"subjects": [
"bar.one.example.com"
],
"issuers": [
{
"email": "bar@bar.com",
"module": "acme"
},
{
"ca": "https://acme.zerossl.com/v2/DV90",
"email": "bar@bar.com",
"module": "acme"
}
]
},
{
"subjects": [
"*.one.example.com",
"*.two.example.com"
],
"issuers": [
{
"challenges": {
"dns": {
"provider": {
"name": "mock"
}
}
},
"module": "acme"
}
]
}
]
}
}
}
}

View File

@ -21,8 +21,6 @@ encode {
zstd
gzip 5
}
encode
----------
{
"apps": {
@ -78,17 +76,6 @@ encode
"zstd",
"gzip"
]
},
{
"encodings": {
"gzip": {},
"zstd": {}
},
"handler": "encode",
"prefer": [
"zstd",
"gzip"
]
}
]
}

View File

@ -3,10 +3,6 @@
file_server {
precompressed zstd br gzip
}
file_server {
precompressed
}
----------
{
"apps": {
@ -34,22 +30,6 @@ file_server {
"br",
"gzip"
]
},
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"precompressed": {
"br": {},
"gzip": {},
"zstd": {}
},
"precompressed_order": [
"br",
"zstd",
"gzip"
]
}
]
}

View File

@ -1,23 +0,0 @@
{
log {
sampling {
interval 300
first 50
thereafter 40
}
}
}
----------
{
"logging": {
"logs": {
"default": {
"sampling": {
"interval": 300,
"first": 50,
"thereafter": 40
}
}
}
}
}

View File

@ -12,14 +12,10 @@
@images path /images/*
header @images {
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
match {
status 200
}
}
header {
+Link "Foo"
+Link "Bar"
match status 200
}
header >Set Defer
header >Replace Deferred Replacement
@ -46,11 +42,6 @@
{
"handler": "headers",
"response": {
"require": {
"status_code": [
200
]
},
"set": {
"Cache-Control": [
"public, max-age=3600, stale-while-revalidate=86400"
@ -145,11 +136,6 @@
"Foo",
"Bar"
]
},
"require": {
"status_code": [
200
]
}
}
},

View File

@ -1,45 +0,0 @@
:80 {
log {
sampling {
interval 300
first 50
thereafter 40
}
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"sampling": {
"interval": 300,
"first": 50,
"thereafter": 40
},
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}

View File

@ -8,7 +8,7 @@ route {
}
not path */
}
redir @canonicalPath {orig_path}/{orig_?query} 308
redir @canonicalPath {http.request.orig_uri.path}/ 308
# If the requested file does not exist, try index files
@indexFiles {
@ -17,7 +17,7 @@ route {
split_path .php
}
}
rewrite @indexFiles {file_match.relative}
rewrite @indexFiles {http.matchers.file.relative}
# Proxy PHP files to the FastCGI responder
@phpFiles {
@ -50,7 +50,7 @@ route {
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"
"{http.request.orig_uri.path}/"
]
},
"status_code": 308

View File

@ -42,7 +42,7 @@
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
@ -58,7 +58,6 @@
"{http.request.uri.path}/index.php",
"index.php"
],
"try_policy": "first_exist_fallback",
"split_path": [
".php"
]

View File

@ -33,7 +33,7 @@ php_fastcgi @test localhost:9000
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
@ -73,8 +73,7 @@ php_fastcgi @test localhost:9000
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
],
"try_policy": "first_exist_fallback"
]
}
}
]

View File

@ -43,7 +43,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
@ -59,7 +59,6 @@ php_fastcgi localhost:9000 {
"{http.request.uri.path}/index.php5",
"index.php5"
],
"try_policy": "first_exist_fallback",
"split_path": [
".php",
".php5"

View File

@ -46,7 +46,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"
"{http.request.orig_uri.path}/"
]
},
"status_code": 308

View File

@ -1,95 +0,0 @@
:8884
php_fastcgi localhost:9000 {
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
try_files {path} index.php
dial_timeout 3s
read_timeout 10s
write_timeout 20s
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}",
"index.php"
],
"try_policy": "first_exist_fallback",
"split_path": [
".php",
".php5"
]
}
}
],
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
]
},
{
"match": [
{
"path": [
"*.php",
"*.php5"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"selection_policy": {
"policy": "random"
}
},
"transport": {
"dial_timeout": 3000000000,
"env": {
"VAR1": "value1",
"VAR2": "value2"
},
"protocol": "fastcgi",
"read_timeout": 10000000000,
"root": "/var/www",
"split_path": [
".php",
".php5"
],
"write_timeout": 20000000000
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
}

View File

@ -1,41 +0,0 @@
:8884
reverse_proxy 127.0.0.1:65535 {
transport http {
forward_proxy_url http://localhost:8080
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"network_proxy": {
"from": "url",
"url": "http://localhost:8080"
},
"protocol": "http"
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}

View File

@ -1,40 +0,0 @@
:8884
reverse_proxy 127.0.0.1:65535 {
transport http {
network_proxy none
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"network_proxy": {
"from": "none"
},
"protocol": "http"
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}

View File

@ -1,41 +0,0 @@
:8884
reverse_proxy 127.0.0.1:65535 {
transport http {
network_proxy url http://localhost:8080
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"network_proxy": {
"from": "url",
"url": "http://localhost:8080"
},
"protocol": "http"
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}

View File

@ -1,174 +0,0 @@
automated1.example.com {
tls force_automate
respond "Automated!"
}
automated2.example.com {
tls force_automate
respond "Automated!"
}
shadowed.example.com {
respond "Shadowed!"
}
*.example.com {
tls cert.pem key.pem
respond "Wildcard!"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"automated1.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Automated!",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"automated2.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Automated!",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"shadowed.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Shadowed!",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Wildcard!",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"automated1.example.com",
"automated2.example.com"
]
}
},
{
"match": {
"sni": [
"*.example.com"
]
},
"certificate_selection": {
"any_tag": [
"cert0"
]
}
},
{}
]
}
}
},
"tls": {
"certificates": {
"automate": [
"automated1.example.com",
"automated2.example.com"
],
"load_files": [
{
"certificate": "cert.pem",
"key": "key.pem",
"tags": [
"cert0"
]
}
]
}
}
}
}

View File

@ -1,3 +1,8 @@
// The below line is required to enable post-quantum key agreement in Go 1.23
// by default without insisting on setting a minimum version of 1.23 in go.mod.
// See https://github.com/caddyserver/caddy/issues/6540#issuecomment-2313094905
//go:debug tlskyber=1
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -24,7 +24,6 @@ import (
"io"
"io/fs"
"log"
"maps"
"net"
"net/http"
"os"
@ -172,10 +171,6 @@ func cmdStart(fl Flags) (int, error) {
func cmdRun(fl Flags) (int, error) {
caddy.TrapSignals()
logger := caddy.Log()
undoMaxProcs := setResourceLimits(logger)
defer undoMaxProcs()
configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter")
resumeFlag := fl.Bool("resume")
@ -201,18 +196,18 @@ func cmdRun(fl Flags) (int, error) {
config, err = os.ReadFile(caddy.ConfigAutosavePath)
if errors.Is(err, fs.ErrNotExist) {
// not a bad error; just can't resume if autosave file doesn't exist
logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
resumeFlag = false
} else if err != nil {
return caddy.ExitCodeFailedStartup, err
} else {
if configFlag == "" {
logger.Info("resuming from last configuration",
caddy.Log().Info("resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath))
} else {
// if they also specified a config file, user should be aware that we're not
// using it (doing so could lead to data/config loss by overwriting!)
logger.Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration",
caddy.Log().Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath))
}
}
@ -230,7 +225,7 @@ func cmdRun(fl Flags) (int, error) {
if pidfileFlag != "" {
err := caddy.PIDFile(pidfileFlag)
if err != nil {
logger.Error("unable to write PID file",
caddy.Log().Error("unable to write PID file",
zap.String("pidfile", pidfileFlag),
zap.Error(err))
}
@ -241,7 +236,7 @@ func cmdRun(fl Flags) (int, error) {
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
}
logger.Info("serving initial configuration")
caddy.Log().Info("serving initial configuration")
// if we are to report to another process the successful start
// of the server, do so now by echoing back contents of stdin
@ -277,15 +272,15 @@ func cmdRun(fl Flags) (int, error) {
switch runtime.GOOS {
case "windows":
if os.Getenv("HOME") == "" && os.Getenv("USERPROFILE") == "" && !hasXDG {
logger.Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy")
caddy.Log().Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy")
}
case "plan9":
if os.Getenv("home") == "" && !hasXDG {
logger.Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy")
caddy.Log().Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy")
}
default:
if os.Getenv("HOME") == "" && !hasXDG {
logger.Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy")
caddy.Log().Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy")
}
}
@ -565,15 +560,10 @@ func cmdValidateConfig(fl Flags) (int, error) {
func cmdFmt(fl Flags) (int, error) {
configFile := fl.Arg(0)
configFlag := fl.String("config")
if (len(fl.Args()) > 1) || (configFlag != "" && configFile != "") {
return caddy.ExitCodeFailedStartup, fmt.Errorf("fmt does not support multiple files %s %s", configFlag, strings.Join(fl.Args(), " "))
}
if configFile == "" && configFlag == "" {
if configFile == "" {
configFile = "Caddyfile"
} else if configFile == "" {
configFile = configFlag
}
// as a special case, read from stdin if the file name is "-"
if configFile == "-" {
input, err := io.ReadAll(os.Stdin)
@ -704,7 +694,9 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
maps.Copy(req.Header, headers)
for k, v := range headers {
req.Header[k] = v
}
// make an HTTP client that dials our network type, since admin
// endpoints aren't always TCP, which is what the default transport

View File

@ -388,7 +388,6 @@ When reading from stdin, the --overwrite flag has no effect: the result
is always printed to stdout.
`,
CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("config", "c", "", "Configuration file")
cmd.Flags().BoolP("overwrite", "w", false, "Overwrite the input file with the results")
cmd.Flags().BoolP("diff", "d", false, "Print the differences between the input file and the formatted output")
cmd.RunE = WrapCommandFuncForCobra(cmdFmt)

View File

@ -24,7 +24,6 @@ import (
"io"
"io/fs"
"log"
"log/slog"
"net"
"os"
"path/filepath"
@ -34,12 +33,10 @@ import (
"strings"
"time"
"github.com/KimMachineGun/automemlimit/memlimit"
"github.com/caddyserver/certmagic"
"github.com/spf13/pflag"
"go.uber.org/automaxprocs/maxprocs"
"go.uber.org/zap"
"go.uber.org/zap/exp/zapslog"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@ -69,6 +66,12 @@ func Main() {
os.Exit(caddy.ExitCodeFailedStartup)
}
undo, err := maxprocs.Set()
defer undo()
if err != nil {
caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
}
if err := defaultFactory.Build().Execute(); err != nil {
var exitError *exitError
if errors.As(err, &exitError) {
@ -464,31 +467,6 @@ func printEnvironment() {
}
}
func setResourceLimits(logger *zap.Logger) func() {
// Configure the maximum number of CPUs to use to match the Linux container quota (if any)
// See https://pkg.go.dev/runtime#GOMAXPROCS
undo, err := maxprocs.Set(maxprocs.Logger(logger.Sugar().Infof))
if err != nil {
logger.Warn("failed to set GOMAXPROCS", zap.Error(err))
}
// Configure the maximum memory to use to match the Linux container quota (if any) or system memory
// See https://pkg.go.dev/runtime/debug#SetMemoryLimit
_, _ = memlimit.SetGoMemLimitWithOpts(
memlimit.WithLogger(
slog.New(zapslog.NewHandler(logger.Core())),
),
memlimit.WithProvider(
memlimit.ApplyFallback(
memlimit.FromCgroup,
memlimit.FromSystem,
),
),
)
return undo
}
// StringSlice is a flag.Value that enables repeated use of a string flag.
type StringSlice []string

View File

@ -21,7 +21,6 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"github.com/caddyserver/certmagic"
@ -191,20 +190,12 @@ func cmdExportStorage(fl Flags) (int, error) {
for _, k := range keys {
info, err := stor.Stat(ctx, k)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
caddy.Log().Warn(fmt.Sprintf("key: %s removed while export is in-progress", k))
continue
}
return caddy.ExitCodeFailedQuit, err
}
if info.IsTerminal {
v, err := stor.Load(ctx, k)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
caddy.Log().Warn(fmt.Sprintf("key: %s removed while export is in-progress", k))
continue
}
return caddy.ExitCodeFailedQuit, err
}

View File

@ -91,14 +91,14 @@ func (ctx *Context) OnCancel(f func()) {
ctx.cleanupFuncs = append(ctx.cleanupFuncs, f)
}
// FileSystems returns a ref to the FilesystemMap.
// Filesystems returns a ref to the FilesystemMap.
// EXPERIMENTAL: This API is subject to change.
func (ctx *Context) FileSystems() FileSystems {
func (ctx *Context) Filesystems() FileSystems {
// if no config is loaded, we use a default filesystemmap, which includes the osfs
if ctx.cfg == nil {
return &filesystems.FileSystemMap{}
return &filesystems.FilesystemMap{}
}
return ctx.cfg.fileSystems
return ctx.cfg.filesystems
}
// Returns the active metrics registry for the context
@ -110,8 +110,6 @@ func (ctx *Context) GetMetricsRegistry() *prometheus.Registry {
func (ctx *Context) initMetrics() {
ctx.metricsRegistry.MustRegister(
collectors.NewBuildInfoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
collectors.NewGoCollector(),
adminMetrics.requestCount,
adminMetrics.requestErrors,
globalMetrics.configSuccess,
@ -277,14 +275,6 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
return result, nil
}
// emitEvent is a small convenience method so the caddy core can emit events, if the event app is configured.
func (ctx Context) emitEvent(name string, data map[string]any) Event {
if ctx.cfg == nil || ctx.cfg.eventEmitter == nil {
return Event{}
}
return ctx.cfg.eventEmitter.Emit(ctx, name, data)
}
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any.
// Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module
// name) or as a regular map (key is not the module name, and module name is defined inline).
@ -393,17 +383,6 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
return nil, fmt.Errorf("module value cannot be null")
}
// if this is an app module, keep a reference to it,
// since submodules may need to reference it during
// provisioning (even though the parent app module
// may not be fully provisioned yet; this is the case
// with the tls app's automation policies, which may
// refer to the tls app to check if a global DNS
// module has been configured for DNS challenges)
if appModule, ok := val.(App); ok {
ctx.cfg.apps[id] = appModule
}
ctx.ancestry = append(ctx.ancestry, val)
if prov, ok := val.(Provisioner); ok {
@ -437,14 +416,6 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val)
// if the loaded module happens to be an app that can emit events, store it so the
// core can have access to emit events without an import cycle
if ee, ok := val.(eventEmitter); ok {
if _, ok := ee.(App); ok {
ctx.cfg.eventEmitter = ee
}
}
return val, nil
}
@ -498,6 +469,7 @@ func (ctx Context) App(name string) (any, error) {
if appRaw != nil {
ctx.cfg.AppsRaw[name] = nil // allow GC to deallocate
}
ctx.cfg.apps[name] = modVal.(App)
return modVal, nil
}
@ -577,11 +549,11 @@ func (ctx Context) Slogger() *slog.Logger {
if err != nil {
panic("config missing, unable to create dev logger: " + err.Error())
}
return slog.New(zapslog.NewHandler(l.Core()))
return slog.New(zapslog.NewHandler(l.Core(), nil))
}
mod := ctx.Module()
if mod == nil {
return slog.New(zapslog.NewHandler(Log().Core()))
return slog.New(zapslog.NewHandler(Log().Core(), nil))
}
return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(),
zapslog.WithName(string(mod.CaddyModule().ID)),
@ -616,11 +588,3 @@ func (ctx *Context) WithValue(key, value any) Context {
exitFuncs: ctx.exitFuncs,
}
}
// eventEmitter is a small interface that inverts dependencies for
// the caddyevents package, so the core can emit events without an
// import cycle (i.e. the caddy package doesn't have to import
// the caddyevents package, which imports the caddy package).
type eventEmitter interface {
Emit(ctx Context, eventName string, data map[string]any) Event
}

View File

@ -1,39 +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 (
"os"
"path/filepath"
)
// FastAbs is an optimized version of filepath.Abs for Unix systems,
// since we don't expect the working directory to ever change once
// Caddy is running. Avoid the os.Getwd() syscall overhead.
// It's overall the same as stdlib's implementation, the difference
// being cached working directory.
func FastAbs(path string) (string, error) {
if filepath.IsAbs(path) {
return filepath.Clean(path), nil
}
if wderr != nil {
return "", wderr
}
return filepath.Join(wd, path), nil
}
var wd, wderr = os.Getwd()

View File

@ -1,27 +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 (
"path/filepath"
)
// FastAbs can't be optimized on Windows because there
// are special file paths that require the use of syscall.FullPath
// to handle correctly.
// Just call stdlib's implementation which uses that function.
func FastAbs(path string) (string, error) {
return filepath.Abs(path)
}

View File

@ -1,17 +1,3 @@
// 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 "io/fs"

67
go.mod
View File

@ -1,31 +1,31 @@
module github.com/caddyserver/caddy/v2
go 1.24
go 1.22.3
toolchain go1.23.0
require (
github.com/BurntSushi/toml v1.4.0
github.com/KimMachineGun/automemlimit v0.7.1
github.com/Masterminds/sprig/v3 v3.3.0
github.com/alecthomas/chroma/v2 v2.15.0
github.com/alecthomas/chroma/v2 v2.14.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.23.0
github.com/caddyserver/certmagic v0.21.5-0.20241105180249-4293198e094d
github.com/caddyserver/zerossl v0.1.3
github.com/cloudflare/circl v1.6.0
github.com/dustin/go-humanize v1.0.1
github.com/go-chi/chi/v5 v5.2.1
github.com/google/cel-go v0.24.1
github.com/go-chi/chi/v5 v5.0.12
github.com/google/cel-go v0.21.0
github.com/google/uuid v1.6.0
github.com/klauspost/compress v1.18.0
github.com/klauspost/cpuid/v2 v2.2.10
github.com/mholt/acmez/v3 v3.1.2
github.com/klauspost/compress v1.17.11
github.com/klauspost/cpuid/v2 v2.2.8
github.com/mholt/acmez/v2 v2.0.3
github.com/prometheus/client_golang v1.19.1
github.com/quic-go/quic-go v0.51.0
github.com/quic-go/quic-go v0.48.1
github.com/smallstep/certificates v0.26.1
github.com/smallstep/nosql v0.6.1
github.com/smallstep/truststore v0.13.0
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
github.com/yuin/goldmark v1.7.8
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
@ -37,33 +37,32 @@ require (
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
go.uber.org/zap/exp v0.3.0
golang.org/x/crypto v0.36.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810
golang.org/x/net v0.38.0
golang.org/x/sync v0.12.0
golang.org/x/term v0.30.0
golang.org/x/time v0.11.0
golang.org/x/crypto v0.28.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9
golang.org/x/net v0.30.0
golang.org/x/sync v0.8.0
golang.org/x/term v0.25.0
golang.org/x/time v0.7.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
)
require (
cel.dev/expr v0.19.1 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/golang/glog v1.2.2 // indirect
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
github.com/google/go-tpm v0.9.0 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/onsi/ginkgo/v2 v2.13.2 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect
@ -75,7 +74,7 @@ require (
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
@ -91,12 +90,12 @@ require (
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
@ -116,18 +115,18 @@ require (
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.3 // indirect
github.com/libdns/libdns v1.0.0-beta.1
github.com/libdns/libdns v0.2.2
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.5.0
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rs/xid v1.5.0 // indirect
@ -148,10 +147,10 @@ require (
go.step.sm/crypto v0.45.0
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sys v0.26.0
golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
howett.net/plist v1.0.0 // indirect

128
go.sum
View File

@ -1,5 +1,3 @@
cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@ -33,8 +31,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/KimMachineGun/automemlimit v0.7.1 h1:QcG/0iCOLChjfUweIMC3YL5Xy9C3VBeNmCZHrZfJMBw=
github.com/KimMachineGun/automemlimit v0.7.1/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
@ -46,11 +42,11 @@ github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2y
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@ -93,14 +89,15 @@ 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/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4=
github.com/caddyserver/certmagic v0.21.5-0.20241105180249-4293198e094d h1:+zOduGxxC4WBAnlDf5Uf0TXbWXRqjUXkJKevDZZa79A=
github.com/caddyserver/certmagic v0.21.5-0.20241105180249-4293198e094d/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@ -113,8 +110,6 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@ -125,8 +120,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -137,15 +132,15 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
@ -161,11 +156,11 @@ github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1t
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
@ -191,6 +186,8 @@ github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPh
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
@ -206,8 +203,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
@ -305,10 +302,10 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -327,8 +324,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ=
github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -347,11 +344,11 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -369,8 +366,6 @@ github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
@ -397,8 +392,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/quic-go/quic-go v0.48.1 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA=
github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
@ -473,12 +468,12 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
@ -496,8 +491,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
@ -569,8 +564,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@ -600,10 +595,10 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 h1:V5+zy0jmgNYmK1uW/sPpBw8ioFvalrhaUrYWmu1Fpe4=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 h1:4cEcP5+OjGppY79LCQ5Go2B1Boix2x0v6pvA01P3FoA=
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
@ -615,8 +610,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -633,8 +628,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -649,8 +644,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -668,6 +663,7 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -679,16 +675,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -699,12 +695,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/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-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -720,8 +716,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
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.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -7,10 +7,10 @@ import (
)
const (
DefaultFileSystemKey = "default"
DefaultFilesystemKey = "default"
)
var DefaultFileSystem = &wrapperFs{key: DefaultFileSystemKey, FS: OsFS{}}
var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}}
// wrapperFs exists so can easily add to wrapperFs down the line
type wrapperFs struct {
@ -18,24 +18,24 @@ type wrapperFs struct {
fs.FS
}
// FileSystemMap stores a map of filesystems
// FilesystemMap stores a map of filesystems
// the empty key will be overwritten to be the default key
// it includes a default filesystem, based off the os fs
type FileSystemMap struct {
type FilesystemMap struct {
m sync.Map
}
// note that the first invocation of key cannot be called in a racy context.
func (f *FileSystemMap) key(k string) string {
func (f *FilesystemMap) key(k string) string {
if k == "" {
k = DefaultFileSystemKey
k = DefaultFilesystemKey
}
return k
}
// Register will add the filesystem with key to later be retrieved
// A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil
func (f *FileSystemMap) Register(k string, v fs.FS) {
func (f *FilesystemMap) Register(k string, v fs.FS) {
k = f.key(k)
if v == nil {
f.Unregister(k)
@ -47,23 +47,23 @@ func (f *FileSystemMap) Register(k string, v fs.FS) {
// Unregister will remove the filesystem with key from the filesystem map
// if the key is the default key, it will set the default to the osFS instead of deleting it
// modules should call this on cleanup to be safe
func (f *FileSystemMap) Unregister(k string) {
func (f *FilesystemMap) Unregister(k string) {
k = f.key(k)
if k == DefaultFileSystemKey {
f.m.Store(k, DefaultFileSystem)
if k == DefaultFilesystemKey {
f.m.Store(k, DefaultFilesystem)
} else {
f.m.Delete(k)
}
}
// Get will get a filesystem with a given key
func (f *FileSystemMap) Get(k string) (v fs.FS, ok bool) {
func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
k = f.key(k)
c, ok := f.m.Load(strings.TrimSpace(k))
if !ok {
if k == DefaultFileSystemKey {
f.m.Store(k, DefaultFileSystem)
return DefaultFileSystem, true
if k == DefaultFilesystemKey {
f.m.Store(k, DefaultFilesystem)
return DefaultFilesystem, true
}
return nil, ok
}
@ -71,7 +71,7 @@ func (f *FileSystemMap) Get(k string) (v fs.FS, ok bool) {
}
// Default will get the default filesystem in the filesystem map
func (f *FileSystemMap) Default() fs.FS {
val, _ := f.Get(DefaultFileSystemKey)
func (f *FilesystemMap) Default() fs.FS {
val, _ := f.Get(DefaultFilesystemKey)
return val
}

View File

@ -1,22 +0,0 @@
package internal
import "fmt"
// MaxSizeSubjectsListForLog returns the keys in the map as a slice of maximum length
// maxToDisplay. It is useful for logging domains being managed, for example, since a
// map is typically needed for quick lookup, but a slice is needed for logging, and this
// can be quite a doozy since there may be a huge amount (hundreds of thousands).
func MaxSizeSubjectsListForLog(subjects map[string]struct{}, maxToDisplay int) []string {
numberOfNamesToDisplay := min(len(subjects), maxToDisplay)
domainsToDisplay := make([]string, 0, numberOfNamesToDisplay)
for domain := range subjects {
domainsToDisplay = append(domainsToDisplay, domain)
if len(domainsToDisplay) >= numberOfNamesToDisplay {
break
}
}
if len(subjects) > maxToDisplay {
domainsToDisplay = append(domainsToDisplay, fmt.Sprintf("(and %d more...)", len(subjects)-maxToDisplay))
}
return domainsToDisplay
}

View File

@ -30,7 +30,7 @@ import (
"go.uber.org/zap"
)
func reuseUnixSocket(_, _ string) (any, error) {
func reuseUnixSocket(network, addr string) (any, error) {
return nil, nil
}

View File

@ -139,7 +139,7 @@ func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net
}
// check to see if plugin provides listener
if ln, err := getListenerFromPlugin(ctx, na.Network, na.Host, na.port(), portOffset, config); ln != nil || err != nil {
if ln, err := getListenerFromPlugin(ctx, na.Network, na.JoinHostPort(portOffset), config); ln != nil || err != nil {
return ln, err
}
@ -210,7 +210,7 @@ func (na NetworkAddress) IsUnixNetwork() bool {
return IsUnixNetwork(na.Network)
}
// IsFdNetwork returns true if na.Network is
// IsUnixNetwork returns true if na.Network is
// fd or fdgram.
func (na NetworkAddress) IsFdNetwork() bool {
return IsFdNetwork(na.Network)
@ -641,7 +641,7 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
if network == "tcp" || network == "tcp4" || network == "tcp6" ||
network == "udp" || network == "udp4" || network == "udp6" ||
network == "unix" || network == "unixpacket" || network == "unixgram" ||
strings.HasPrefix(network, "ip:") || strings.HasPrefix(network, "ip4:") || strings.HasPrefix(network, "ip6:") ||
strings.HasPrefix("ip:", network) || strings.HasPrefix("ip4:", network) || strings.HasPrefix("ip6:", network) ||
network == "fd" || network == "fdgram" {
panic("network type " + network + " is reserved")
}
@ -658,11 +658,11 @@ var unixSocketsMu sync.Mutex
// getListenerFromPlugin returns a listener on the given network and address
// if a plugin has registered the network name. It may return (nil, nil) if
// no plugin can provide a listener.
func getListenerFromPlugin(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) {
func getListenerFromPlugin(ctx context.Context, network, addr string, config net.ListenConfig) (any, error) {
// get listener from plugin if network type is registered
if getListener, ok := networkTypes[network]; ok {
Log().Debug("getting listener from plugin", zap.String("network", network))
return getListener(ctx, network, host, port, portOffset, config)
return getListener(ctx, network, addr, config)
}
return nil, nil
@ -676,7 +676,7 @@ func listenerKey(network, addr string) string {
// The listeners must be capable of overlapping: with Caddy, new configs are loaded
// before old ones are unloaded, so listeners may overlap briefly if the configs
// both need the same listener. EXPERIMENTAL and subject to change.
type ListenerFunc func(ctx context.Context, network, host, portRange string, portOffset uint, cfg net.ListenConfig) (any, error)
type ListenerFunc func(ctx context.Context, network, addr string, cfg net.ListenConfig) (any, error)
var networkTypes = map[string]ListenerFunc{}

View File

@ -20,7 +20,6 @@ import (
"io"
"log"
"os"
"slices"
"strings"
"sync"
"time"
@ -162,9 +161,7 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
if err != nil {
return fmt.Errorf("setting up default log: %v", err)
}
filteringCore := &filteringCore{newDefault.CustomLog.core, newDefault.CustomLog}
newDefault.logger = zap.New(filteringCore, options...)
newDefault.logger = zap.New(newDefault.CustomLog.core, options...)
// redirect the default caddy logs
defaultLoggerMu.Lock()
@ -493,8 +490,10 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error {
if len(cl.Include) > 0 && len(cl.Exclude) > 0 {
// prevent intersections
for _, allow := range cl.Include {
if slices.Contains(cl.Exclude, allow) {
return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow)
for _, deny := range cl.Exclude {
if allow == deny {
return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow)
}
}
}

View File

@ -1,106 +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 "testing"
func TestCustomLog_loggerAllowed(t *testing.T) {
type fields struct {
BaseLog BaseLog
Include []string
Exclude []string
}
type args struct {
name string
isModule bool
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "include",
fields: fields{
Include: []string{"foo"},
},
args: args{
name: "foo",
isModule: true,
},
want: true,
},
{
name: "exclude",
fields: fields{
Exclude: []string{"foo"},
},
args: args{
name: "foo",
isModule: true,
},
want: false,
},
{
name: "include and exclude",
fields: fields{
Include: []string{"foo"},
Exclude: []string{"foo"},
},
args: args{
name: "foo",
isModule: true,
},
want: false,
},
{
name: "include and exclude (longer namespace)",
fields: fields{
Include: []string{"foo.bar"},
Exclude: []string{"foo"},
},
args: args{
name: "foo.bar",
isModule: true,
},
want: true,
},
{
name: "excluded module is not printed",
fields: fields{
Include: []string{"admin.api.load"},
Exclude: []string{"admin.api"},
},
args: args{
name: "admin.api",
isModule: false,
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cl := &CustomLog{
BaseLog: tt.fields.BaseLog,
Include: tt.fields.Include,
Exclude: tt.fields.Exclude,
}
if got := cl.loggerAllowed(tt.args.name, tt.args.isModule); got != tt.want {
t.Errorf("CustomLog.loggerAllowed() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -18,8 +18,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"reflect"
"sort"
"strings"
@ -362,14 +360,6 @@ func isModuleMapType(typ reflect.Type) bool {
isJSONRawMessage(typ.Elem())
}
// ProxyFuncProducer is implemented by modules which produce a
// function that returns a URL to use as network proxy. Modules
// in the namespace `caddy.network_proxy` must implement this
// interface.
type ProxyFuncProducer interface {
ProxyFunc() func(*http.Request) (*url.URL, error)
}
var (
modules = make(map[string]ModuleInfo)
modulesMu sync.RWMutex

View File

@ -20,7 +20,9 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
@ -204,26 +206,27 @@ func (app *App) On(eventName string, handler Handler) error {
//
// Note that the data map is not copied, for efficiency. After Emit() is called, the
// data passed in should not be changed in other goroutines.
func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) caddy.Event {
func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event {
logger := app.logger.With(zap.String("name", eventName))
e, err := caddy.NewEvent(ctx, eventName, data)
id, err := uuid.NewRandom()
if err != nil {
logger.Error("failed to create event", zap.Error(err))
logger.Error("failed generating new event ID", zap.Error(err))
}
var originModule caddy.ModuleInfo
var originModuleID caddy.ModuleID
var originModuleName string
if origin := e.Origin(); origin != nil {
originModule = origin.CaddyModule()
originModuleID = originModule.ID
originModuleName = originModule.String()
eventName = strings.ToLower(eventName)
e := Event{
Data: data,
id: id,
ts: time.Now(),
name: eventName,
origin: ctx.Module(),
}
logger = logger.With(
zap.String("id", e.ID().String()),
zap.String("origin", originModuleName))
zap.String("id", e.id.String()),
zap.String("origin", e.origin.CaddyModule().String()))
// add event info to replacer, make sure it's in the context
repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@ -236,15 +239,15 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) c
case "event":
return e, true
case "event.id":
return e.ID(), true
return e.id, true
case "event.name":
return e.Name(), true
return e.name, true
case "event.time":
return e.Timestamp(), true
return e.ts, true
case "event.time_unix":
return e.Timestamp().UnixMilli(), true
return e.ts.UnixMilli(), true
case "event.module":
return originModuleID, true
return e.origin.CaddyModule().ID, true
case "event.data":
return e.Data, true
}
@ -266,7 +269,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) c
// invoke handlers bound to the event by name and also all events; this for loop
// iterates twice at most: once for the event name, once for "" (all events)
for {
moduleID := originModuleID
moduleID := e.origin.CaddyModule().ID
// implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "")
for {
@ -289,7 +292,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) c
zap.Any("handler", handler))
if err := handler.Handle(ctx, e); err != nil {
aborted := errors.Is(err, caddy.ErrEventAborted)
aborted := errors.Is(err, ErrAborted)
logger.Error("handler error",
zap.Error(err),
@ -323,9 +326,76 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) c
return e
}
// Event represents something that has happened or is happening.
// An Event value is not synchronized, so it should be copied if
// being used in goroutines.
//
// EXPERIMENTAL: As with the rest of this package, events are
// subject to change.
type Event struct {
// If non-nil, the event has been aborted, meaning
// propagation has stopped to other handlers and
// the code should stop what it was doing. Emitters
// may choose to use this as a signal to adjust their
// code path appropriately.
Aborted error
// The data associated with the event. Usually the
// original emitter will be the only one to set or
// change these values, but the field is exported
// so handlers can have full access if needed.
// However, this map is not synchronized, so
// handlers must not use this map directly in new
// goroutines; instead, copy the map to use it in a
// goroutine.
Data map[string]any
id uuid.UUID
ts time.Time
name string
origin caddy.Module
}
func (e Event) ID() uuid.UUID { return e.id }
func (e Event) Timestamp() time.Time { return e.ts }
func (e Event) Name() string { return e.name }
func (e Event) Origin() caddy.Module { return e.origin }
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
func (e Event) CloudEvent() CloudEvent {
dataJSON, _ := json.Marshal(e.Data)
return CloudEvent{
ID: e.id.String(),
Source: e.origin.CaddyModule().String(),
SpecVersion: "1.0",
Type: e.name,
Time: e.ts,
DataContentType: "application/json",
Data: dataJSON,
}
}
// CloudEvent is a JSON-serializable structure that
// is compatible with the CloudEvents specification.
// See https://cloudevents.io.
type CloudEvent struct {
ID string `json:"id"`
Source string `json:"source"`
SpecVersion string `json:"specversion"`
Type string `json:"type"`
Time time.Time `json:"time"`
DataContentType string `json:"datacontenttype,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// ErrAborted cancels an event.
var ErrAborted = errors.New("event aborted")
// Handler is a type that can handle events.
type Handler interface {
Handle(context.Context, caddy.Event) error
Handle(context.Context, Event) error
}
// Interface guards

View File

@ -69,11 +69,11 @@ func (xs *Filesystems) Provision(ctx caddy.Context) error {
}
// register that module
ctx.Logger().Debug("registering fs", zap.String("fs", f.Key))
ctx.FileSystems().Register(f.Key, f.fileSystem)
ctx.Filesystems().Register(f.Key, f.fileSystem)
// remember to unregister the module when we are done
xs.defers = append(xs.defers, func() {
ctx.Logger().Debug("unregistering fs", zap.String("fs", f.Key))
ctx.FileSystems().Unregister(f.Key)
ctx.Filesystems().Unregister(f.Key)
})
}
return nil

View File

@ -73,7 +73,7 @@ func init() {
// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
// `{http.request.local.port}` | The port part of the local address the connection arrived on
// `{http.request.local}` | The local address the connection arrived on
// `{http.request.remote.host}` | The host (IP) part of the remote client's address, if available (not known with HTTP/3 early data)
// `{http.request.remote.host}` | The host (IP) part of the remote client's address
// `{http.request.remote.port}` | The port part of the remote client's address
// `{http.request.remote}` | The address of the remote client
// `{http.request.scheme}` | The request scheme, typically `http` or `https`
@ -152,7 +152,7 @@ type App struct {
tlsApp *caddytls.TLS
// used temporarily between phases 1 and 2 of auto HTTPS
allCertDomains map[string]struct{}
allCertDomains []string
}
// CaddyModule returns the Caddy module information.
@ -207,7 +207,7 @@ func (app *App) Provision(ctx caddy.Context) error {
if srv.Metrics != nil {
srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead")
app.Metrics = cmp.Or(app.Metrics, &Metrics{
app.Metrics = cmp.Or[*Metrics](app.Metrics, &Metrics{
init: sync.Once{},
httpMetrics: &httpMetrics{},
})
@ -401,9 +401,6 @@ func (app *App) Provision(ctx caddy.Context) error {
if srv.IdleTimeout == 0 {
srv.IdleTimeout = defaultIdleTimeout
}
if srv.ReadHeaderTimeout == 0 {
srv.ReadHeaderTimeout = defaultReadHeaderTimeout // see #6663
}
}
ctx.Context = oldContext
return nil
@ -529,6 +526,21 @@ func (app *App) Start() error {
// enable TLS if there is a policy and if this is not the HTTP port
useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort()
// enable HTTP/3 if configured
if h3ok && useTLS {
app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
return err
}
}
if h3ok && !useTLS {
// Can only serve h3 with TLS enabled
app.logger.Warn("HTTP/3 skipped because it requires TLS",
zap.String("network", listenAddr.Network),
zap.String("addr", hostport))
}
if h1ok || h2ok && useTLS || h2cok {
// create the listener for this socket
lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)})
@ -599,33 +611,6 @@ func (app *App) Start() error {
zap.String("network", listenAddr.Network),
zap.String("addr", hostport))
}
if h3ok {
// Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses
// a different transport mechanism... which is fine, but the OS doesn't
// differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they
// are still one file on the system. So even though "unixpacket" and
// "unixgram" are different network types just as "tcp" and "udp" are,
// the OS will not let us use the same file as both STREAM and DGRAM.
if listenAddr.IsUnixNetwork() {
app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket",
zap.String("file", hostport))
continue
}
if useTLS {
// enable HTTP/3 if configured
app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
return err
}
} else {
// Can only serve h3 with TLS enabled
app.logger.Warn("HTTP/3 skipped because it requires TLS",
zap.String("network", listenAddr.Network),
zap.String("addr", hostport))
}
}
}
}
@ -785,20 +770,11 @@ func (app *App) httpsPort() int {
return app.HTTPSPort
}
const (
// defaultIdleTimeout is the default HTTP server timeout
// for closing idle connections; useful to avoid resource
// exhaustion behind hungry CDNs, for example (we've had
// several complaints without this).
defaultIdleTimeout = caddy.Duration(5 * time.Minute)
// defaultReadHeaderTimeout is the default timeout for
// reading HTTP headers from clients. Headers are generally
// small, often less than 1 KB, so it shouldn't take a
// long time even on legitimately slow connections or
// busy servers to read it.
defaultReadHeaderTimeout = caddy.Duration(time.Minute)
)
// defaultIdleTimeout is the default HTTP server timeout
// for closing idle connections; useful to avoid resource
// exhaustion behind hungry CDNs, for example (we've had
// several complaints without this).
const defaultIdleTimeout = caddy.Duration(5 * time.Minute)
// Interface guards
var (

View File

@ -25,7 +25,6 @@ import (
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/internal"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
@ -66,6 +65,12 @@ type AutoHTTPSConfig struct {
// enabled. To force automated certificate management
// regardless of loaded certificates, set this to true.
IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
// If true, automatic HTTPS will prefer wildcard names
// and ignore non-wildcard names if both are available.
// This allows for writing a config with top-level host
// matchers without having those names produce certificates.
PreferWildcard bool `json:"prefer_wildcard,omitempty"`
}
// automaticHTTPSPhase1 provisions all route matchers, determines
@ -158,13 +163,26 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
}
}
// build the list of domains that could be used with ECH (if enabled)
// so the TLS app can know to publish ECH configs for them
echDomains := make([]string, 0, len(serverDomainSet))
for d := range serverDomainSet {
echDomains = append(echDomains, d)
if srv.AutoHTTPS.PreferWildcard {
wildcards := make(map[string]struct{})
for d := range serverDomainSet {
if strings.HasPrefix(d, "*.") {
wildcards[d[2:]] = struct{}{}
}
}
for d := range serverDomainSet {
if strings.HasPrefix(d, "*.") {
continue
}
base := d
if idx := strings.Index(d, "."); idx != -1 {
base = d[idx+1:]
}
if _, ok := wildcards[base]; ok {
delete(serverDomainSet, d)
}
}
}
app.tlsApp.RegisterServerNames(echDomains)
// nothing more to do here if there are no domains that qualify for
// automatic HTTPS and there are no explicit TLS connection policies:
@ -265,10 +283,19 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
}
}
// we now have a list of all the unique names for which we need certs
// we now have a list of all the unique names for which we need certs;
// turn the set into a slice so that phase 2 can use it
app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
var internal, tailscale []string
uniqueDomainsLoop:
for d := range uniqueDomainsForCerts {
if !isTailscaleDomain(d) {
// whether or not there is already an automation policy for this
// name, we should add it to the list to manage a cert for it,
// unless it's a Tailscale domain, because we don't manage those
app.allCertDomains = append(app.allCertDomains, d)
}
// some names we've found might already have automation policies
// explicitly specified for them; we should exclude those from
// our hidden/implicit policy, since applying a name to more than
@ -307,7 +334,6 @@ uniqueDomainsLoop:
}
if isTailscaleDomain(d) {
tailscale = append(tailscale, d)
delete(uniqueDomainsForCerts, d) // not managed by us; handled separately
} else if shouldUseInternal(d) {
internal = append(internal, d)
}
@ -437,9 +463,6 @@ redirServersLoop:
}
}
// persist the domains/IPs we're managing certs for through provisioning/startup
app.allCertDomains = uniqueDomainsForCerts
logger.Debug("adjusted config",
zap.Reflect("tls", app.tlsApp),
zap.Reflect("http", app))
@ -742,7 +765,7 @@ func (app *App) automaticHTTPSPhase2() error {
return nil
}
app.logger.Info("enabling automatic TLS certificate management",
zap.Strings("domains", internal.MaxSizeSubjectsListForLog(app.allCertDomains, 1000)),
zap.Strings("domains", app.allCertDomains),
)
err := app.tlsApp.Manage(app.allCertDomains)
if err != nil {

View File

@ -37,10 +37,6 @@ func init() {
// `{http.auth.user.*}` placeholders may be set for any authentication
// modules that provide user metadata.
//
// In case of an error, the placeholder `{http.auth.<provider>.error}`
// will be set to the error message returned by the authentication
// provider.
//
// Its API is still experimental and may be subject to change.
type Authentication struct {
// A set of authentication providers. If none are specified,
@ -75,7 +71,6 @@ func (a *Authentication) Provision(ctx caddy.Context) error {
}
func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var user User
var authed bool
var err error
@ -85,9 +80,6 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
if c := a.logger.Check(zapcore.ErrorLevel, "auth provider returned error"); c != nil {
c.Write(zap.String("provider", provName), zap.Error(err))
}
// Set the error from the authentication provider in a placeholder,
// so it can be used in the handle_errors directive.
repl.Set("http.auth."+provName+".error", err.Error())
continue
}
if authed {
@ -98,6 +90,7 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated"))
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
repl.Set("http.auth.user.id", user.ID)
for k, v := range user.Metadata {
repl.Set("http.auth.user."+k, v)

View File

@ -57,7 +57,21 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
prefer := []string{}
remainingArgs := d.RemainingArgs()
for _, arg := range d.RemainingArgs() {
mod, err := caddy.GetModule("http.encoders." + arg)
if err != nil {
return d.Errf("finding encoder module '%s': %v", mod, err)
}
encoding, ok := mod.New().(Encoding)
if !ok {
return d.Errf("module %s is not an HTTP encoding", mod)
}
if enc.EncodingsRaw == nil {
enc.EncodingsRaw = make(caddy.ModuleMap)
}
enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil)
prefer = append(prefer, arg)
}
responseMatchers := make(map[string]caddyhttp.ResponseMatcher)
for d.NextBlock(0) {
@ -97,26 +111,6 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
}
if len(prefer) == 0 && len(remainingArgs) == 0 {
remainingArgs = []string{"zstd", "gzip"}
}
for _, arg := range remainingArgs {
mod, err := caddy.GetModule("http.encoders." + arg)
if err != nil {
return d.Errf("finding encoder module '%s': %v", mod, err)
}
encoding, ok := mod.New().(Encoding)
if !ok {
return d.Errf("module %s is not an HTTP encoding", mod)
}
if enc.EncodingsRaw == nil {
enc.EncodingsRaw = make(caddy.ModuleMap)
}
enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil)
prefer = append(prefer, arg)
}
// use the order in which the encoders were defined.
enc.Prefer = prefer

View File

@ -156,7 +156,7 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
if _, ok := enc.writerPools[encName]; !ok {
continue // encoding not offered
}
w = enc.openResponseWriter(encName, w, r.Method == http.MethodConnect)
w = enc.openResponseWriter(encName, w)
defer w.(*responseWriter).Close()
// to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding
@ -201,14 +201,14 @@ func (enc *Encode) addEncoding(e Encoding) error {
// openResponseWriter creates a new response writer that may (or may not)
// encode the response with encodingName. The returned response writer MUST
// be closed after the handler completes.
func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter, isConnect bool) *responseWriter {
func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter) *responseWriter {
var rw responseWriter
return enc.initResponseWriter(&rw, encodingName, w, isConnect)
return enc.initResponseWriter(&rw, encodingName, w)
}
// initResponseWriter initializes the responseWriter instance
// allocated in openResponseWriter, enabling mid-stack inlining.
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter, isConnect bool) *responseWriter {
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
if rww, ok := wrappedRW.(*caddyhttp.ResponseWriterWrapper); ok {
rw.ResponseWriter = rww
} else {
@ -216,7 +216,6 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
}
rw.encodingName = encodingName
rw.config = enc
rw.isConnect = isConnect
return rw
}
@ -231,7 +230,6 @@ type responseWriter struct {
config *Encode
statusCode int
wroteHeader bool
isConnect bool
}
// WriteHeader stores the status to write when the time comes
@ -247,14 +245,6 @@ func (rw *responseWriter) WriteHeader(status int) {
rw.Header().Add("Vary", "Accept-Encoding")
}
// write status immediately if status is 2xx and the request is CONNECT
// since it means the response is successful.
// see: https://github.com/caddyserver/caddy/issues/6733#issuecomment-2525058845
if rw.isConnect && 200 <= status && status <= 299 {
rw.ResponseWriter.WriteHeader(status)
rw.wroteHeader = true
}
// write status immediately when status code is informational
// see: https://caddy.community/t/disappear-103-early-hints-response-with-encode-enable-caddy-v2-7-6/23081/5
if 100 <= status && status <= 199 {
@ -270,12 +260,6 @@ func (enc *Encode) Match(rw *responseWriter) bool {
// FlushError is an alternative Flush returning an error. It delays the actual Flush of the underlying
// ResponseWriterWrapper until headers were written.
func (rw *responseWriter) FlushError() error {
// WriteHeader wasn't called and is a CONNECT request, treat it as a success.
// otherwise, wait until header is written.
if rw.isConnect && !rw.wroteHeader && rw.statusCode == 0 {
rw.WriteHeader(http.StatusOK)
}
if !rw.wroteHeader {
// flushing the underlying ResponseWriter will write header and status code,
// but we need to delay that until we can determine if we must encode and
@ -304,12 +288,6 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
return 0, nil
}
// WriteHeader wasn't called and is a CONNECT request, treat it as a success.
// otherwise, determine if the response should be compressed.
if rw.isConnect && !rw.wroteHeader && rw.statusCode == 0 {
rw.WriteHeader(http.StatusOK)
}
// sniff content-type and determine content-length
if !rw.wroteHeader && rw.config.MinLength > 0 {
var gtMinLength bool
@ -347,49 +325,6 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
}
}
// used to mask ReadFrom method
type writerOnly struct {
io.Writer
}
// copied from stdlib
const sniffLen = 512
// ReadFrom will try to use sendfile to copy from the reader to the response writer.
// It's only used if the response writer implements io.ReaderFrom and the data can't be compressed.
// It's based on stdlin http1.1 response writer implementation.
// https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/net/http/server.go#L586
func (rw *responseWriter) ReadFrom(r io.Reader) (int64, error) {
rf, ok := rw.ResponseWriter.(io.ReaderFrom)
// sendfile can't be used anyway
if !ok {
// mask ReadFrom to avoid infinite recursion
return io.Copy(writerOnly{rw}, r)
}
var ns int64
// try to sniff the content type and determine if the response should be compressed
if !rw.wroteHeader && rw.config.MinLength > 0 {
var (
err error
buf [sniffLen]byte
)
// mask ReadFrom to let Write determine if the response should be compressed
ns, err = io.CopyBuffer(writerOnly{rw}, io.LimitReader(r, sniffLen), buf[:])
if err != nil || ns < sniffLen {
return ns, err
}
}
// the response will be compressed, no sendfile support
if rw.w != nil {
nr, err := io.Copy(rw.w, r)
return nr + ns, err
}
nr, err := rf.ReadFrom(r)
return nr + ns, err
}
// Close writes any remaining buffered response and
// deallocates any active resources.
func (rw *responseWriter) Close() error {

View File

@ -9,7 +9,7 @@ import (
func BenchmarkOpenResponseWriter(b *testing.B) {
enc := new(Encode)
for n := 0; n < b.N; n++ {
enc.openResponseWriter("test", nil, false)
enc.openResponseWriter("test", nil)
}
}

View File

@ -130,9 +130,9 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
// speed up browser/client experience and caching by supporting If-Modified-Since
if ifModSinceStr := r.Header.Get("If-Modified-Since"); ifModSinceStr != "" {
// basically a copy of stdlib file server's handling of If-Modified-Since
ifModSince, err := http.ParseTime(ifModSinceStr)
if err == nil && listing.lastModified.Truncate(time.Second).Compare(ifModSince) <= 0 {
ifModSince, err := time.ParseInLocation(http.TimeFormat, ifModSinceStr, time.Local)
lastModTrunc := listing.lastModified.Truncate(time.Second)
if err == nil && (lastModTrunc.Equal(ifModSince) || lastModTrunc.Before(ifModSince)) {
w.WriteHeader(http.StatusNotModified)
return nil
}
@ -213,11 +213,6 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
}
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs.FS, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) {
// modTime for the directory itself
stat, err := dir.Stat()
if err != nil {
return nil, err
}
dirLimit := defaultDirEntryLimit
if fsrv.Browse.FileLimit != 0 {
dirLimit = fsrv.Browse.FileLimit
@ -230,7 +225,7 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs
// user can presumably browse "up" to parent folder if path is longer than "/"
canGoUp := len(urlPath) > 1
return fsrv.directoryListing(ctx, fileSystem, stat.ModTime(), files, canGoUp, root, urlPath, repl), nil
return fsrv.directoryListing(ctx, fileSystem, files, canGoUp, root, urlPath, repl), nil
}
// browseApplyQueryParams applies query parameters to the listing.

View File

@ -26,7 +26,7 @@
<path d="M9 7l4 0"/>
<path d="M9 11l4 0"/>
</svg>
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg" ".avif"}}
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}}
{{- if eq .Tpl.Layout "grid"}}
<img loading="lazy" src="{{.Name | pathEscape}}">
{{- else}}
@ -802,7 +802,7 @@ footer {
<b>{{.NumFiles}}</b> file{{if ne 1 .NumFiles}}s{{end}}
</span>
<span class="meta-item">
<b>{{.HumanTotalFileSize}}</b> total
<b>{{.HumanTotalFileSize}}</b> total
</span>
{{- if ne 0 .Limit}}
<span class="meta-item">
@ -868,7 +868,7 @@ footer {
</svg>
</a>
{{- end}}
{{- if and (eq .Sort "name") (ne .Order "desc")}}
<a href="?sort=name&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
Name

View File

@ -35,16 +35,15 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, parentModTime time.Time, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext {
func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext {
filesToHide := fsrv.transformHidePaths(repl)
name, _ := url.PathUnescape(urlPath)
tplCtx := &browseTemplateContext{
Name: path.Base(name),
Path: urlPath,
CanGoUp: canGoUp,
lastModified: parentModTime,
Name: path.Base(name),
Path: urlPath,
CanGoUp: canGoUp,
}
for _, entry := range entries {
@ -132,10 +131,6 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
})
}
// this time is used for the Last-Modified header and comparing If-Modified-Since from client
// both are expected to be in UTC, so we convert to UTC here
// see: https://github.com/caddyserver/caddy/issues/6828
tplCtx.lastModified = tplCtx.lastModified.UTC()
return tplCtx
}

View File

@ -146,13 +146,9 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
case "precompressed":
fsrv.PrecompressedOrder = d.RemainingArgs()
if len(fsrv.PrecompressedOrder) == 0 {
fsrv.PrecompressedOrder = []string{"br", "zstd", "gzip"}
}
for _, format := range fsrv.PrecompressedOrder {
modID := "http.precompressed." + format
var order []string
for d.NextArg() {
modID := "http.precompressed." + d.Val()
mod, err := caddy.GetModule(modID)
if err != nil {
return d.Errf("getting module named '%s': %v", modID, err)
@ -165,8 +161,10 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if fsrv.PrecompressedRaw == nil {
fsrv.PrecompressedRaw = make(caddy.ModuleMap)
}
fsrv.PrecompressedRaw[format] = caddyconfig.JSON(precompress, nil)
fsrv.PrecompressedRaw[d.Val()] = caddyconfig.JSON(precompress, nil)
order = append(order, d.Val())
}
fsrv.PrecompressedOrder = order
case "status":
if !d.NextArg() {
@ -276,7 +274,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
tryPolicy = h.Val()
switch tryPolicy {
case tryPolicyFirstExist, tryPolicyFirstExistFallback, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
case tryPolicyFirstExist, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
default:
return nil, h.Errf("unrecognized try policy: %s", tryPolicy)
}

View File

@ -90,7 +90,6 @@ type MatchFile struct {
// How to choose a file in TryFiles. Can be:
//
// - first_exist
// - first_exist_fallback
// - smallest_size
// - largest_size
// - most_recently_modified
@ -274,7 +273,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
func (m *MatchFile) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger()
m.fsmap = ctx.FileSystems()
m.fsmap = ctx.Filesystems()
if m.Root == "" {
m.Root = "{http.vars.root}"
@ -297,7 +296,6 @@ func (m MatchFile) Validate() error {
switch m.TryPolicy {
case "",
tryPolicyFirstExist,
tryPolicyFirstExistFallback,
tryPolicyLargestSize,
tryPolicySmallestSize,
tryPolicyMostRecentlyMod:
@ -417,13 +415,13 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
}
// setPlaceholders creates the placeholders for the matched file
setPlaceholders := func(candidate matchCandidate, isDir bool) {
setPlaceholders := func(candidate matchCandidate, info fs.FileInfo) {
repl.Set("http.matchers.file.relative", filepath.ToSlash(candidate.relative))
repl.Set("http.matchers.file.absolute", filepath.ToSlash(candidate.fullpath))
repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder))
fileType := "file"
if isDir {
if info.IsDir() {
fileType = "directory"
}
repl.Set("http.matchers.file.type", fileType)
@ -431,13 +429,8 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
// match file according to the configured policy
switch m.TryPolicy {
case "", tryPolicyFirstExist, tryPolicyFirstExistFallback:
maxI := -1
if m.TryPolicy == tryPolicyFirstExistFallback {
maxI = len(m.TryFiles) - 1
}
for i, pattern := range m.TryFiles {
case "", tryPolicyFirstExist:
for _, pattern := range m.TryFiles {
// If the pattern is a status code, emit an error,
// which short-circuits the middleware pipeline and
// writes an HTTP error response.
@ -447,15 +440,8 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
candidates := makeCandidates(pattern)
for _, c := range candidates {
// Skip the IO if using fallback policy and it's the latest item
if i == maxI {
setPlaceholders(c, false)
return true, nil
}
if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists {
setPlaceholders(c, info.IsDir())
setPlaceholders(c, info)
return true, nil
}
}
@ -479,7 +465,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
if largestInfo == nil {
return false, nil
}
setPlaceholders(largest, largestInfo.IsDir())
setPlaceholders(largest, largestInfo)
return true, nil
case tryPolicySmallestSize:
@ -500,7 +486,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
if smallestInfo == nil {
return false, nil
}
setPlaceholders(smallest, smallestInfo.IsDir())
setPlaceholders(smallest, smallestInfo)
return true, nil
case tryPolicyMostRecentlyMod:
@ -520,7 +506,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
if recentInfo == nil {
return false, nil
}
setPlaceholders(recent, recentInfo.IsDir())
setPlaceholders(recent, recentInfo)
return true, nil
}
@ -722,11 +708,10 @@ var globSafeRepl = strings.NewReplacer(
)
const (
tryPolicyFirstExist = "first_exist"
tryPolicyFirstExistFallback = "first_exist_fallback"
tryPolicyLargestSize = "largest_size"
tryPolicySmallestSize = "smallest_size"
tryPolicyMostRecentlyMod = "most_recently_modified"
tryPolicyFirstExist = "first_exist"
tryPolicyLargestSize = "largest_size"
tryPolicySmallestSize = "smallest_size"
tryPolicyMostRecentlyMod = "most_recently_modified"
)
// Interface guards

View File

@ -117,7 +117,7 @@ func TestFileMatcher(t *testing.T) {
},
} {
m := &MatchFile{
fsmap: &filesystems.FileSystemMap{},
fsmap: &filesystems.FilesystemMap{},
Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
}
@ -229,7 +229,7 @@ func TestPHPFileMatcher(t *testing.T) {
},
} {
m := &MatchFile{
fsmap: &filesystems.FileSystemMap{},
fsmap: &filesystems.FilesystemMap{},
Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"},
SplitPath: []string{".php"},
@ -273,7 +273,7 @@ func TestPHPFileMatcher(t *testing.T) {
func TestFirstSplit(t *testing.T) {
m := MatchFile{
SplitPath: []string{".php"},
fsmap: &filesystems.FileSystemMap{},
fsmap: &filesystems.FilesystemMap{},
}
actual, remainder := m.firstSplit("index.PHP/somewhere")
expected := "index.PHP"

View File

@ -186,7 +186,7 @@ func (FileServer) CaddyModule() caddy.ModuleInfo {
func (fsrv *FileServer) Provision(ctx caddy.Context) error {
fsrv.logger = ctx.Logger()
fsrv.fsmap = ctx.FileSystems()
fsrv.fsmap = ctx.Filesystems()
if fsrv.FileSystem == "" {
fsrv.FileSystem = "{http.vars.fs}"
@ -204,7 +204,7 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
// absolute paths before the server starts for very slight performance improvement
for i, h := range fsrv.Hide {
if !strings.Contains(h, "{") && strings.Contains(h, separator) {
if abs, err := caddy.FastAbs(h); err == nil {
if abs, err := filepath.Abs(h); err == nil {
fsrv.Hide[i] = abs
}
}
@ -300,10 +300,8 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
info, err := fs.Stat(fileSystem, filename)
if err != nil {
err = fsrv.mapDirOpenError(fileSystem, err, filename)
if errors.Is(err, fs.ErrNotExist) {
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) {
return fsrv.notFound(w, r, next)
} else if errors.Is(err, fs.ErrInvalid) {
return caddyhttp.Error(http.StatusBadRequest, err)
} else if errors.Is(err, fs.ErrPermission) {
return caddyhttp.Error(http.StatusForbidden, err)
}
@ -613,11 +611,6 @@ func (fsrv *FileServer) mapDirOpenError(fileSystem fs.FS, originalErr error, nam
return originalErr
}
var pathErr *fs.PathError
if errors.As(originalErr, &pathErr) {
return fs.ErrInvalid
}
parts := strings.Split(name, separator)
for i := range parts {
if parts[i] == "" {
@ -643,7 +636,7 @@ func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string {
for i := range fsrv.Hide {
hide[i] = repl.ReplaceAll(fsrv.Hide[i], "")
if strings.Contains(hide[i], separator) {
abs, err := caddy.FastAbs(hide[i])
abs, err := filepath.Abs(hide[i])
if err == nil {
hide[i] = abs
}
@ -662,7 +655,7 @@ func fileHidden(filename string, hide []string) bool {
}
// all path comparisons use the complete absolute path if possible
filenameAbs, err := caddy.FastAbs(filename)
filenameAbs, err := filepath.Abs(filename)
if err == nil {
filename = filenameAbs
}

View File

@ -99,16 +99,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
handler.Response.Deferred = true
continue
}
if field == "match" {
responseMatchers := make(map[string]caddyhttp.ResponseMatcher)
err := caddyhttp.ParseNamedResponseMatcher(h.NewFromNextSegment(), responseMatchers)
if err != nil {
return nil, err
}
matcher := responseMatchers["match"]
handler.Response.Require = &matcher
continue
}
if hasArgs {
return nil, h.Err("cannot specify headers in both arguments and block") // because it would be weird
}

View File

@ -78,7 +78,7 @@ func (h Handler) Validate() error {
return err
}
}
if h.Response != nil && h.Response.HeaderOps != nil {
if h.Response != nil {
err := h.Response.validate()
if err != nil {
return err
@ -133,9 +133,6 @@ type HeaderOps struct {
// Provision sets up the header operations.
func (ops *HeaderOps) Provision(_ caddy.Context) error {
if ops == nil {
return nil // it's possible no ops are configured; fix #6893
}
for fieldName, replacements := range ops.Replace {
for i, r := range replacements {
if r.SearchRegexp == "" {

View File

@ -143,28 +143,6 @@ func TestHandler(t *testing.T) {
"Cache-Control": []string{"no-cache"},
},
},
{ // same as above, but checks that response headers are left alone when "Require" conditions are unmet
handler: Handler{
Response: &RespHeaderOps{
Require: &caddyhttp.ResponseMatcher{
Headers: http.Header{
"Cache-Control": nil,
},
},
HeaderOps: &HeaderOps{
Add: http.Header{
"Cache-Control": []string{"no-cache"},
},
},
},
},
respHeader: http.Header{
"Cache-Control": []string{"something"},
},
expectedRespHeader: http.Header{
"Cache-Control": []string{"something"},
},
},
{
handler: Handler{
Response: &RespHeaderOps{

View File

@ -118,11 +118,6 @@ func (irh interceptedResponseHandler) WriteHeader(statusCode int) {
irh.ResponseRecorder.WriteHeader(statusCode)
}
// EXPERIMENTAL: Subject to change or removal.
func (irh interceptedResponseHandler) Unwrap() http.ResponseWriter {
return irh.ResponseRecorder
}
// EXPERIMENTAL: Subject to change or removal.
func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
buf := bufPool.Get().(*bytes.Buffer)

View File

@ -211,11 +211,6 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi
}
return
}
fields = func() []zapcore.Field {
return []zapcore.Field{
zap.Error(err),
}
}
status = http.StatusInternalServerError
msg = err.Error()
return

View File

@ -51,9 +51,6 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
Header: r.Header,
ShouldLogCredentials: r.ShouldLogCredentials,
})
if r.TransferEncoding != nil {
enc.AddArray("transfer_encoding", LoggableStringArray(r.TransferEncoding))
}
if r.TLS != nil {
enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
}

View File

@ -978,7 +978,7 @@ func (m MatchHeader) Match(r *http.Request) bool {
// MatchWithError returns true if r matches m.
func (m MatchHeader) MatchWithError(r *http.Request) (bool, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
return matchHeaders(r.Header, http.Header(m), r.Host, r.TransferEncoding, repl), nil
return matchHeaders(r.Header, http.Header(m), r.Host, repl), nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@ -1004,26 +1004,22 @@ func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) {
}
// getHeaderFieldVals returns the field values for the given fieldName from input.
// The host parameter should be obtained from the http.Request.Host field, and the
// transferEncoding from http.Request.TransferEncoding, since net/http removes them
// from the header map.
func getHeaderFieldVals(input http.Header, fieldName, host string, transferEncoding []string) []string {
// The host parameter should be obtained from the http.Request.Host field since
// net/http removes it from the header map.
func getHeaderFieldVals(input http.Header, fieldName, host string) []string {
fieldName = textproto.CanonicalMIMEHeaderKey(fieldName)
if fieldName == "Host" && host != "" {
return []string{host}
}
if fieldName == "Transfer-Encoding" && input[fieldName] == nil {
return transferEncoding
}
return input[fieldName]
}
// matchHeaders returns true if input matches the criteria in against without regex.
// The host parameter should be obtained from the http.Request.Host field since
// net/http removes it from the header map.
func matchHeaders(input, against http.Header, host string, transferEncoding []string, repl *caddy.Replacer) bool {
func matchHeaders(input, against http.Header, host string, repl *caddy.Replacer) bool {
for field, allowedFieldVals := range against {
actualFieldVals := getHeaderFieldVals(input, field, host, transferEncoding)
actualFieldVals := getHeaderFieldVals(input, field, host)
if allowedFieldVals != nil && len(allowedFieldVals) == 0 && actualFieldVals != nil {
// a non-nil but empty list of allowed values means
// match if the header field exists at all
@ -1123,7 +1119,7 @@ func (m MatchHeaderRE) Match(r *http.Request) bool {
// MatchWithError returns true if r matches m.
func (m MatchHeaderRE) MatchWithError(r *http.Request) (bool, error) {
for field, rm := range m {
actualFieldVals := getHeaderFieldVals(r.Header, field, r.Host, r.TransferEncoding)
actualFieldVals := getHeaderFieldVals(r.Header, field, r.Host)
match := false
fieldVal:
for _, actualFieldVal := range actualFieldVals {
@ -1342,8 +1338,6 @@ func (m *MatchTLS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
case "early_data":
var false bool
m.HandshakeComplete = &false
default:
return d.Errf("unrecognized option '%s'", d.Val())
}
}
if d.NextArg() {

View File

@ -186,11 +186,6 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
return path.Ext(req.URL.Path), true
case "http.request.uri.query":
return req.URL.RawQuery, true
case "http.request.uri.prefixed_query":
if req.URL.RawQuery == "" {
return "", true
}
return "?" + req.URL.RawQuery, true
case "http.request.duration":
start := GetVar(req.Context(), "start_time").(time.Time)
return time.Since(start), true
@ -244,12 +239,6 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
case "http.request.orig_uri.query":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
return or.URL.RawQuery, true
case "http.request.orig_uri.prefixed_query":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
if or.URL.RawQuery == "" {
return "", true
}
return "?" + or.URL.RawQuery, true
}
// remote IP range/prefix (e.g. keep top 24 bits of 1.2.3.4 => "1.2.3.0/24")

View File

@ -68,12 +68,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
}
rb.WriteTimeout = timeout
case "set":
var setStr string
if !h.AllArgs(&setStr) {
return nil, h.ArgErr()
}
rb.Set = setStr
default:
return nil, h.Errf("unrecognized request_body subdirective '%s'", h.Val())
}

View File

@ -15,10 +15,8 @@
package requestbody
import (
"errors"
"io"
"net/http"
"strings"
"time"
"go.uber.org/zap"
@ -44,10 +42,6 @@ type RequestBody struct {
// EXPERIMENTAL. Subject to change/removal.
WriteTimeout time.Duration `json:"write_timeout,omitempty"`
// This field permit to replace body on the fly
// EXPERIMENTAL. Subject to change/removal.
Set string `json:"set,omitempty"`
logger *zap.Logger
}
@ -65,18 +59,6 @@ func (rb *RequestBody) Provision(ctx caddy.Context) error {
}
func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
if rb.Set != "" {
if r.Body != nil {
err := r.Body.Close()
if err != nil {
return err
}
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
replacedBody := repl.ReplaceAll(rb.Set, "")
r.Body = io.NopCloser(strings.NewReader(replacedBody))
r.ContentLength = int64(len(replacedBody))
}
if r.Body == nil {
return next.ServeHTTP(w, r)
}
@ -112,8 +94,7 @@ type errorWrapper struct {
func (ew errorWrapper) Read(p []byte) (n int, err error) {
n, err = ew.ReadCloser.Read(p)
var mbe *http.MaxBytesError
if errors.As(err, &mbe) {
if err != nil && err.Error() == "http: request body too large" {
err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err)
}
return

View File

@ -41,7 +41,7 @@ func (rm ResponseMatcher) Match(statusCode int, hdr http.Header) bool {
if !rm.matchStatusCode(statusCode) {
return false
}
return matchHeaders(hdr, rm.Headers, "", []string{}, nil)
return matchHeaders(hdr, rm.Headers, "", nil)
}
func (rm ResponseMatcher) matchStatusCode(statusCode int) bool {

View File

@ -154,16 +154,16 @@ func (rr *responseRecorder) WriteHeader(statusCode int) {
// connections by manually setting headers and writing status 101
rr.statusCode = statusCode
// decide whether we should buffer the response
if rr.shouldBuffer == nil {
rr.stream = true
} else {
rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header())
}
// 1xx responses aren't final; just informational
if statusCode < 100 || statusCode > 199 {
rr.wroteHeader = true
// decide whether we should buffer the response
if rr.shouldBuffer == nil {
rr.stream = true
} else {
rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header())
}
}
// if informational or not buffered, immediately write header

View File

@ -1,84 +0,0 @@
package reverseproxy
import (
"io"
"testing"
)
type zeroReader struct{}
func (zeroReader) Read(p []byte) (int, error) {
for i := range p {
p[i] = 0
}
return len(p), nil
}
func TestBuffering(t *testing.T) {
var (
h Handler
zr zeroReader
)
type args struct {
body io.ReadCloser
limit int64
}
tests := []struct {
name string
args args
resultCheck func(io.ReadCloser, int64, args) bool
}{
{
name: "0 limit, body is returned as is",
args: args{
body: io.NopCloser(&zr),
limit: 0,
},
resultCheck: func(res io.ReadCloser, read int64, args args) bool {
return res == args.body && read == args.limit && read == 0
},
},
{
name: "negative limit, body is read completely",
args: args{
body: io.NopCloser(io.LimitReader(&zr, 100)),
limit: -1,
},
resultCheck: func(res io.ReadCloser, read int64, args args) bool {
brc, ok := res.(bodyReadCloser)
return ok && brc.body == nil && brc.buf.Len() == 100 && read == 100
},
},
{
name: "positive limit, body is read partially",
args: args{
body: io.NopCloser(io.LimitReader(&zr, 100)),
limit: 50,
},
resultCheck: func(res io.ReadCloser, read int64, args args) bool {
brc, ok := res.(bodyReadCloser)
return ok && brc.body != nil && brc.buf.Len() == 50 && read == 50
},
},
{
name: "positive limit, body is read completely",
args: args{
body: io.NopCloser(io.LimitReader(&zr, 100)),
limit: 101,
},
resultCheck: func(res io.ReadCloser, read int64, args args) bool {
brc, ok := res.(bodyReadCloser)
return ok && brc.body == nil && brc.buf.Len() == 100 && read == 100
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res, read := h.bufferedBody(tt.args.body, tt.args.limit)
if !tt.resultCheck(res, read, tt.args) {
t.Error("Handler.bufferedBody() test failed")
return
}
})
}
}

View File

@ -33,7 +33,6 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/caddy/v2/modules/internal/network"
)
func init() {
@ -980,9 +979,7 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error
// read_buffer <size>
// write_buffer <size>
// max_response_header <size>
// network_proxy <module> {
// ...
// }
// forward_proxy_url <url>
// dial_timeout <duration>
// dial_fallback_delay <duration>
// response_header_timeout <duration>
@ -993,9 +990,6 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error
// tls_insecure_skip_verify
// tls_timeout <duration>
// tls_trusted_ca_certs <cert_files...>
// tls_trust_pool <module> {
// ...
// }
// tls_server_name <sni>
// tls_renegotiation <level>
// tls_except_ports <ports...>
@ -1074,24 +1068,10 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
case "forward_proxy_url":
caddy.Log().Warn("The 'forward_proxy_url' field is deprecated. Use 'network_proxy <url>' instead.")
if !d.NextArg() {
return d.ArgErr()
}
u := network.ProxyFromURL{URL: d.Val()}
h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil)
case "network_proxy":
if !d.NextArg() {
return d.ArgErr()
}
modStem := d.Val()
modID := "caddy.network_proxy." + modStem
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
h.NetworkProxyRaw = caddyconfig.JSONModuleObject(unm, "from", modStem, nil)
h.ForwardProxyURL = d.Val()
case "dial_timeout":
if !d.NextArg() {

View File

@ -18,7 +18,6 @@ import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@ -131,18 +130,15 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// is equivalent to a route consisting of:
//
// # Add trailing slash for directory requests
// # This redirection is automatically disabled if "{http.request.uri.path}/index.php"
// # doesn't appear in the try_files list
// @canonicalPath {
// file {path}/index.php
// not path */
// }
// redir @canonicalPath {path}/ 308
//
// # If the requested file does not exist, try index files and assume index.php always exists
// # If the requested file does not exist, try index files
// @indexFiles file {
// try_files {path} {path}/index.php index.php
// try_policy first_exist_fallback
// split_path .php
// }
// rewrite @indexFiles {http.matchers.file.relative}
@ -183,7 +179,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
indexFile := "index.php"
// set up for explicitly overriding try_files
var tryFiles []string
tryFiles := []string{}
// if the user specified a matcher token, use that
// matcher in a route that wraps both of our routes;
@ -314,60 +310,37 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// if the index is turned off, we skip the redirect and try_files
if indexFile != "off" {
dirRedir := false
dirIndex := "{http.request.uri.path}/" + indexFile
tryPolicy := "first_exist_fallback"
// route to redirect to canonical path if index PHP file
redirMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}/" + indexFile},
}),
"not": h.JSON(caddyhttp.MatchNot{
MatcherSetsRaw: []caddy.ModuleMap{
{
"path": h.JSON(caddyhttp.MatchPath{"*/"}),
},
},
}),
}
redirHandler := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
}
redirRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
}
// if tryFiles wasn't overridden, use a reasonable default
if len(tryFiles) == 0 {
tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile}
dirRedir = true
} else {
if !strings.HasSuffix(tryFiles[len(tryFiles)-1], ".php") {
// use first_exist strategy if the last file is not a PHP file
tryPolicy = ""
}
for _, tf := range tryFiles {
if tf == dirIndex {
dirRedir = true
break
}
}
}
if dirRedir {
// route to redirect to canonical path if index PHP file
redirMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{dirIndex},
}),
"not": h.JSON(caddyhttp.MatchNot{
MatcherSetsRaw: []caddy.ModuleMap{
{
"path": h.JSON(caddyhttp.MatchPath{"*/"}),
},
},
}),
}
redirHandler := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"}},
}
redirRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
}
routes = append(routes, redirRoute)
tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
}
// route to rewrite to PHP index file
rewriteMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: tryFiles,
TryPolicy: tryPolicy,
SplitPath: extensions,
}),
}
@ -379,7 +352,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
}
routes = append(routes, rewriteRoute)
routes = append(routes, redirRoute, rewriteRoute)
}
// route to actually reverse proxy requests to PHP files;

View File

@ -41,8 +41,6 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
// FCGIListenSockFileno describes listen socket file number.
@ -138,15 +136,6 @@ type client struct {
// Do made the request and returns a io.Reader that translates the data read
// from fcgi responder out of fcgi packet before returning it.
func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
// check for CONTENT_LENGTH, since the lack of it or wrong value will cause the backend to hang
if clStr, ok := p["CONTENT_LENGTH"]; !ok {
return nil, caddyhttp.Error(http.StatusLengthRequired, nil)
} else if _, err := strconv.ParseUint(clStr, 10, 64); err != nil {
// stdlib won't return a negative Content-Length, but we check just in case,
// the most likely cause is from a missing content length, which is -1
return nil, caddyhttp.Error(http.StatusLengthRequired, err)
}
writer := &streamWriter{c: c}
writer.buf = bufPool.Get().(*bytes.Buffer)
writer.buf.Reset()

View File

@ -228,7 +228,7 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
ip = strings.Replace(ip, "]", "", 1)
// make sure file root is absolute
root, err := caddy.FastAbs(repl.ReplaceAll(t.Root, "."))
root, err := filepath.Abs(repl.ReplaceAll(t.Root, "."))
if err != nil {
return nil, err
}

View File

@ -309,9 +309,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
}
}()
repl := caddy.NewReplacer()
networkAddr, err := repl.ReplaceOrErr(upstream.Dial, true, true)
networkAddr, err := caddy.NewReplacer().ReplaceOrErr(upstream.Dial, true, true)
if err != nil {
if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "invalid use of placeholders in dial address for active health checks"); c != nil {
c.Write(
@ -346,24 +344,14 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
return
}
hostAddr := addr.JoinHostPort(0)
dialAddr := hostAddr
if addr.IsUnixNetwork() || addr.IsFdNetwork() {
// this will be used as the Host portion of a http.Request URL, and
// paths to socket files would produce an error when creating URL,
// so use a fake Host value instead; unix sockets are usually local
hostAddr = "localhost"
}
// Fill in the dial info for the upstream
// If the upstream is set, use that instead
dialInfoUpstream := upstream
if h.HealthChecks.Active.Upstream != "" {
dialInfoUpstream = &Upstream{
Dial: h.HealthChecks.Active.Upstream,
}
}
dialInfo, _ := dialInfoUpstream.fillDialInfo(repl)
err = h.doActiveHealthCheck(dialInfo, hostAddr, networkAddr, upstream)
err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, networkAddr, upstream)
if err != nil {
if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health check failed"); c != nil {
c.Write(

View File

@ -17,6 +17,7 @@ package reverseproxy
import (
"context"
"fmt"
"net/http"
"net/netip"
"strconv"
"sync/atomic"
@ -99,7 +100,8 @@ func (u *Upstream) Full() bool {
// fillDialInfo returns a filled DialInfo for upstream u, using the request
// context. Note that the returned value is not a pointer.
func (u *Upstream) fillDialInfo(repl *caddy.Replacer) (DialInfo, error) {
func (u *Upstream) fillDialInfo(r *http.Request) (DialInfo, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var addr caddy.NetworkAddress
// use provided dial address

View File

@ -24,6 +24,7 @@ import (
weakrand "math/rand"
"net"
"net/http"
"net/url"
"os"
"reflect"
"slices"
@ -37,10 +38,8 @@ import (
"golang.org/x/net/http2"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/caddy/v2/modules/internal/network"
)
func init() {
@ -91,7 +90,6 @@ type HTTPTransport struct {
// forward_proxy_url -> upstream
//
// Default: http.ProxyFromEnvironment
// DEPRECATED: Use NetworkProxyRaw|`network_proxy` instead. Subject to removal.
ForwardProxyURL string `json:"forward_proxy_url,omitempty"`
// How long to wait before timing out trying to connect to
@ -143,22 +141,6 @@ type HTTPTransport struct {
// The pre-configured underlying HTTP transport.
Transport *http.Transport `json:"-"`
// The module that provides the network (forward) proxy
// URL that the HTTP transport will use to proxy
// requests to the upstream. See [http.Transport.Proxy](https://pkg.go.dev/net/http#Transport.Proxy)
// for information regarding supported protocols.
//
// Providing a value to this parameter results in requests
// flowing through the reverse_proxy in the following way:
//
// User Agent ->
// reverse_proxy ->
// [proxy provided by the module] -> upstream
//
// If nil, defaults to reading the `HTTP_PROXY`,
// `HTTPS_PROXY`, and `NO_PROXY` environment variables.
NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"`
h2cTransport *http2.Transport
h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024)
}
@ -346,22 +328,16 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
}
// negotiate any HTTP/SOCKS proxy for the HTTP transport
proxy := http.ProxyFromEnvironment
var proxy func(*http.Request) (*url.URL, error)
if h.ForwardProxyURL != "" {
caddyCtx.Logger().Warn("forward_proxy_url is deprecated; use network_proxy instead")
u := network.ProxyFromURL{URL: h.ForwardProxyURL}
h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil)
}
if len(h.NetworkProxyRaw) != 0 {
proxyMod, err := caddyCtx.LoadModule(h, "NetworkProxyRaw")
pUrl, err := url.Parse(h.ForwardProxyURL)
if err != nil {
return nil, fmt.Errorf("failed to load network_proxy module: %v", err)
}
if m, ok := proxyMod.(caddy.ProxyFuncProducer); ok {
proxy = m.ProxyFunc()
} else {
return nil, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``")
return nil, fmt.Errorf("failed to parse transport proxy url: %v", err)
}
caddyCtx.Logger().Info("setting transport proxy url", zap.String("url", h.ForwardProxyURL))
proxy = http.ProxyURL(pUrl)
} else {
proxy = http.ProxyFromEnvironment
}
rt := &http.Transport{
@ -382,36 +358,6 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
if err != nil {
return nil, fmt.Errorf("making TLS client config: %v", err)
}
// servername has a placeholder, so we need to replace it
if strings.Contains(h.TLS.ServerName, "{") {
rt.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
// reuses the dialer from above to establish a plaintext connection
conn, err := dialContext(ctx, network, addr)
if err != nil {
return nil, err
}
// but add our own handshake logic
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
tlsConfig := rt.TLSClientConfig.Clone()
tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
tlsConn := tls.Client(conn, tlsConfig)
// complete the handshake before returning the connection
if rt.TLSHandshakeTimeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, rt.TLSHandshakeTimeout)
defer cancel()
}
err = tlsConn.HandshakeContext(ctx)
if err != nil {
_ = tlsConn.Close()
return nil, err
}
return tlsConn, nil
}
}
}
if h.KeepAlive != nil {
@ -483,9 +429,45 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
return rt, nil
}
// replaceTLSServername checks TLS servername to see if it needs replacing
// if it does need replacing, it creates a new cloned HTTPTransport object to avoid any races
// and does the replacing of the TLS servername on that and returns the new object
// if no replacement is necessary it returns the original
func (h *HTTPTransport) replaceTLSServername(repl *caddy.Replacer) *HTTPTransport {
// check whether we have TLS and need to replace the servername in the TLSClientConfig
if h.TLSEnabled() && strings.Contains(h.TLS.ServerName, "{") {
// make a new h, "copy" the parts we don't need to touch, add a new *tls.Config and replace servername
newtransport := &HTTPTransport{
Resolver: h.Resolver,
TLS: h.TLS,
KeepAlive: h.KeepAlive,
Compression: h.Compression,
MaxConnsPerHost: h.MaxConnsPerHost,
DialTimeout: h.DialTimeout,
FallbackDelay: h.FallbackDelay,
ResponseHeaderTimeout: h.ResponseHeaderTimeout,
ExpectContinueTimeout: h.ExpectContinueTimeout,
MaxResponseHeaderSize: h.MaxResponseHeaderSize,
WriteBufferSize: h.WriteBufferSize,
ReadBufferSize: h.ReadBufferSize,
Versions: h.Versions,
Transport: h.Transport.Clone(),
h2cTransport: h.h2cTransport,
}
newtransport.Transport.TLSClientConfig.ServerName = repl.ReplaceAll(newtransport.Transport.TLSClientConfig.ServerName, "")
return newtransport
}
return h
}
// RoundTrip implements http.RoundTripper.
func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
h.SetScheme(req)
// Try to replace TLS servername if needed
repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
transport := h.replaceTLSServername(repl)
transport.SetScheme(req)
// use HTTP/3 if enabled (TODO: This is EXPERIMENTAL)
if h.h3Transport != nil {
@ -501,7 +483,7 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return h.h2cTransport.RoundTrip(req)
}
return h.Transport.RoundTrip(req)
return transport.Transport.RoundTrip(req)
}
// SetScheme ensures that the outbound request req
@ -646,7 +628,7 @@ func (t *TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error)
return nil, fmt.Errorf("getting tls app: %v", err)
}
tlsApp := tlsAppIface.(*caddytls.TLS)
err = tlsApp.Manage(map[string]struct{}{t.ClientCertificateAutomate: {}})
err = tlsApp.Manage([]string{t.ClientCertificateAutomate})
if err != nil {
return nil, fmt.Errorf("managing client certificate: %v", err)
}

View File

@ -1,47 +1,32 @@
package reverseproxy
import (
"errors"
"runtime/debug"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2"
)
var reverseProxyMetrics = struct {
once sync.Once
init sync.Once
upstreamsHealthy *prometheus.GaugeVec
logger *zap.Logger
}{}
func initReverseProxyMetrics(handler *Handler, registry *prometheus.Registry) {
func initReverseProxyMetrics(handler *Handler) {
const ns, sub = "caddy", "reverse_proxy"
upstreamsLabels := []string{"upstream"}
reverseProxyMetrics.once.Do(func() {
reverseProxyMetrics.upstreamsHealthy = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: ns,
Subsystem: sub,
Name: "upstreams_healthy",
Help: "Health status of reverse proxy upstreams.",
}, upstreamsLabels)
})
// duplicate registration could happen if multiple sites with reverse proxy are configured; so ignore the error because
// there's no good way to capture having multiple sites with reverse proxy. If this happens, the metrics will be
// registered twice, but the second registration will be ignored.
if err := registry.Register(reverseProxyMetrics.upstreamsHealthy); err != nil &&
!errors.Is(err, prometheus.AlreadyRegisteredError{
ExistingCollector: reverseProxyMetrics.upstreamsHealthy,
NewCollector: reverseProxyMetrics.upstreamsHealthy,
}) {
panic(err)
}
reverseProxyMetrics.upstreamsHealthy = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: ns,
Subsystem: sub,
Name: "upstreams_healthy",
Help: "Health status of reverse proxy upstreams.",
}, upstreamsLabels)
reverseProxyMetrics.logger = handler.logger.Named("reverse_proxy.metrics")
}
@ -50,14 +35,17 @@ type metricsUpstreamsHealthyUpdater struct {
handler *Handler
}
func newMetricsUpstreamsHealthyUpdater(handler *Handler, ctx caddy.Context) *metricsUpstreamsHealthyUpdater {
initReverseProxyMetrics(handler, ctx.GetMetricsRegistry())
func newMetricsUpstreamsHealthyUpdater(handler *Handler) *metricsUpstreamsHealthyUpdater {
reverseProxyMetrics.init.Do(func() {
initReverseProxyMetrics(handler)
})
reverseProxyMetrics.upstreamsHealthy.Reset()
return &metricsUpstreamsHealthyUpdater{handler}
}
func (m *metricsUpstreamsHealthyUpdater) init() {
func (m *metricsUpstreamsHealthyUpdater) Init() {
go func() {
defer func() {
if err := recover(); err != nil {

View File

@ -17,8 +17,6 @@ package reverseproxy
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@ -110,6 +108,11 @@ type Handler struct {
// response is recognized as a streaming response, or if its
// content length is -1; for such responses, writes are flushed
// to the client immediately.
//
// Normally, a request will be canceled if the client disconnects
// before the response is received from the backend. If explicitly
// set to -1, client disconnection will be ignored and the request
// will be completed to help facilitate low-latency streaming.
FlushInterval caddy.Duration `json:"flush_interval,omitempty"`
// A list of IP ranges (supports CIDR notation) from which
@ -243,19 +246,6 @@ func (h *Handler) Provision(ctx caddy.Context) error {
return fmt.Errorf("loading transport: %v", err)
}
h.Transport = mod.(http.RoundTripper)
// enable request buffering for fastcgi if not configured
// This is because most fastcgi servers are php-fpm that require the content length to be set to read the body, golang
// std has fastcgi implementation that doesn't need this value to process the body, but we can safely assume that's
// not used.
// http3 requests have a negative content length for GET and HEAD requests, if that header is not sent.
// see: https://github.com/caddyserver/caddy/issues/6678#issuecomment-2472224182
// Though it appears even if CONTENT_LENGTH is invalid, php-fpm can handle just fine if the body is empty (no Stdin records sent).
// php-fpm will hang if there is any data in the body though, https://github.com/caddyserver/caddy/issues/5420#issuecomment-2415943516
// TODO: better default buffering for fastcgi requests without content length, in theory a value of 1 should be enough, make it bigger anyway
if module, ok := h.Transport.(caddy.Module); ok && module.CaddyModule().ID.Name() == "fastcgi" && h.RequestBuffers == 0 {
h.RequestBuffers = 4096
}
}
if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil {
mod, err := ctx.LoadModule(h.LoadBalancing, "SelectionPolicyRaw")
@ -382,8 +372,8 @@ func (h *Handler) Provision(ctx caddy.Context) error {
}
}
upstreamHealthyUpdater := newMetricsUpstreamsHealthyUpdater(h, ctx)
upstreamHealthyUpdater.init()
upstreamHealthyUpdater := newMetricsUpstreamsHealthyUpdater(h)
upstreamHealthyUpdater.Init()
return nil
}
@ -409,23 +399,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
return caddyhttp.Error(http.StatusInternalServerError,
fmt.Errorf("preparing request for upstream round-trip: %v", err))
}
// websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
// TODO: once we can reliably detect backend support this, it can be removed for those backends
if r.ProtoMajor == 2 && r.Method == http.MethodConnect && r.Header.Get(":protocol") == "websocket" {
clonedReq.Header.Del(":protocol")
// keep the body for later use. http1.1 upgrade uses http.NoBody
caddyhttp.SetVar(clonedReq.Context(), "h2_websocket_body", clonedReq.Body)
clonedReq.Body = http.NoBody
clonedReq.Method = http.MethodGet
clonedReq.Header.Set("Upgrade", "websocket")
clonedReq.Header.Set("Connection", "Upgrade")
key := make([]byte, 16)
_, randErr := rand.Read(key)
if randErr != nil {
return randErr
}
clonedReq.Header["Sec-WebSocket-Key"] = []string{base64.StdEncoding.EncodeToString(key)}
}
// we will need the original headers and Host value if
// header operations are configured; this is so that each
@ -532,7 +505,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
// the dial address may vary per-request if placeholders are
// used, so perform those replacements here; the resulting
// DialInfo struct should have valid network address syntax
dialInfo, err := upstream.fillDialInfo(repl)
dialInfo, err := upstream.fillDialInfo(r)
if err != nil {
return true, fmt.Errorf("making dial info: %v", err)
}
@ -581,7 +554,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
// ding the health status of the upstream (an error can still
// occur after the roundtrip if, for example, a response handler
// after the roundtrip returns an error)
if succ, ok := proxyErr.(roundtripSucceededError); ok {
if succ, ok := proxyErr.(roundtripSucceeded); ok {
return true, succ.error
}
@ -652,8 +625,7 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
if h.RequestBuffers != 0 && req.Body != nil {
var readBytes int64
req.Body, readBytes = h.bufferedBody(req.Body, h.RequestBuffers)
// set Content-Length when body is fully buffered
if b, ok := req.Body.(bodyReadCloser); ok && b.body == nil {
if h.RequestBuffers == -1 {
req.ContentLength = readBytes
req.Header.Set("Content-Length", strconv.FormatInt(req.ContentLength, 10))
}
@ -683,7 +655,7 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
req.Header.Set("Early-Data", "1")
}
reqUpgradeType := upgradeType(req.Header)
reqUpType := upgradeType(req.Header)
removeConnectionHeaders(req.Header)
// Remove hop-by-hop headers to the backend. Especially
@ -704,9 +676,9 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
// After stripping all the hop-by-hop connection headers above, add back any
// necessary for protocol upgrades, such as for websockets.
if reqUpgradeType != "" {
if reqUpType != "" {
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", reqUpgradeType)
req.Header.Set("Upgrade", reqUpType)
normalizeWebsocketHeaders(req.Header)
}
@ -732,9 +704,6 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
return nil, err
}
// Via header(s)
req.Header.Add("Via", fmt.Sprintf("%d.%d Caddy", req.ProtoMajor, req.ProtoMinor))
return req, nil
}
@ -864,6 +833,15 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
// if FlushInterval is explicitly configured to -1 (i.e. flush continuously to achieve
// low-latency streaming), don't let the transport cancel the request if the client
// disconnects: user probably wants us to finish sending the data to the upstream
// regardless, and we should expect client disconnection in low-latency streaming
// scenarios (see issue #4922)
if h.FlushInterval == -1 {
req = req.WithContext(context.WithoutCancel(req.Context()))
}
// do the round-trip
start := time.Now()
res, err := h.Transport.RoundTrip(req)
@ -885,15 +863,13 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
}),
)
const logMessage = "upstream roundtrip"
if err != nil {
if c := logger.Check(zapcore.DebugLevel, logMessage); c != nil {
if c := logger.Check(zapcore.DebugLevel, "upstream roundtrip"); c != nil {
c.Write(zap.Error(err))
}
return err
}
if c := logger.Check(zapcore.DebugLevel, logMessage); c != nil {
if c := logger.Check(zapcore.DebugLevel, "upstream roundtrip"); c != nil {
c.Write(
zap.Object("headers", caddyhttp.LoggableHTTPHeader{
Header: res.Header,
@ -991,10 +967,10 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
res.Body.Close()
}
// wrap any route error in roundtripSucceededError so caller knows that
// wrap any route error in roundtripSucceeded so caller knows that
// the roundtrip was successful and to not retry
if routeErr != nil {
return roundtripSucceededError{routeErr}
return roundtripSucceeded{routeErr}
}
// we're done handling the response, and we don't want to
@ -1029,14 +1005,6 @@ func (h *Handler) finalizeResponse(
res.Header.Del(h)
}
// delete our Server header and use Via instead (see #6275)
rw.Header().Del("Server")
var protoPrefix string
if !strings.HasPrefix(strings.ToUpper(res.Proto), "HTTP/") {
protoPrefix = res.Proto[:strings.Index(res.Proto, "/")+1]
}
rw.Header().Add("Via", fmt.Sprintf("%s%d.%d Caddy", protoPrefix, res.ProtoMajor, res.ProtoMinor))
// apply any response header operations
if h.Headers != nil && h.Headers.Response != nil {
if h.Headers.Response.Require == nil ||
@ -1234,10 +1202,6 @@ func (h Handler) provisionUpstream(upstream *Upstream) {
// then returns a reader for the buffer along with how many bytes were buffered. Always close
// the return value when done with it, just like if it was the original body! If limit is 0
// (which it shouldn't be), this function returns its input; i.e. is a no-op, for safety.
// Otherwise, it returns bodyReadCloser, the original body will be closed and body will be nil
// if it's explicitly configured to buffer all or EOF is reached when reading.
// TODO: the error during reading is discarded if the limit is negative, should the error be propagated
// to upstream/downstream?
func (h Handler) bufferedBody(originalBody io.ReadCloser, limit int64) (io.ReadCloser, int64) {
if limit == 0 {
return originalBody, 0
@ -1246,14 +1210,13 @@ func (h Handler) bufferedBody(originalBody io.ReadCloser, limit int64) (io.ReadC
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
if limit > 0 {
var err error
written, err = io.CopyN(buf, originalBody, limit)
if (err != nil && err != io.EOF) || written == limit {
n, err := io.CopyN(buf, originalBody, limit)
if (err != nil && err != io.EOF) || n == limit {
return bodyReadCloser{
Reader: io.MultiReader(buf, originalBody),
buf: buf,
body: originalBody,
}, written
}, n
}
} else {
written, _ = io.Copy(buf, originalBody)
@ -1484,9 +1447,9 @@ type TLSTransport interface {
EnableTLS(base *TLSConfig) error
}
// roundtripSucceededError is an error type that is returned if the
// roundtripSucceeded is an error type that is returned if the
// roundtrip succeeded, but an error occurred after-the-fact.
type roundtripSucceededError struct{ error }
type roundtripSucceeded struct{ error }
// bodyReadCloser is a reader that, upon closing, will return
// its buffer to the pool and close the underlying body reader.

Some files were not shown because too many files have changed in this diff Show More