mirror of
https://github.com/caddyserver/caddy.git
synced 2025-11-11 00:56:56 -05:00
Merge branch 'master' into add-tests
This commit is contained in:
commit
a0f2922157
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -65,7 +65,7 @@ jobs:
|
||||
actions: write # to allow uploading artifacts and cache
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -73,7 +73,7 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
@ -162,7 +162,7 @@ jobs:
|
||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
allowed-endpoints: ci-s390x.caddyserver.com:22
|
||||
@ -221,7 +221,7 @@ jobs:
|
||||
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -233,7 +233,7 @@ jobs:
|
||||
version: latest
|
||||
args: check
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: "~1.25"
|
||||
check-latest: true
|
||||
|
||||
4
.github/workflows/cross-build.yml
vendored
4
.github/workflows/cross-build.yml
vendored
@ -51,7 +51,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -59,7 +59,7 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
|
||||
10
.github/workflows/lint.yml
vendored
10
.github/workflows/lint.yml
vendored
@ -45,12 +45,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '~1.25'
|
||||
check-latest: true
|
||||
@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -90,14 +90,14 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3
|
||||
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
||||
with:
|
||||
comment-summary-in-pr: on-failure
|
||||
# https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -49,7 +49,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
@ -109,11 +109,11 @@ jobs:
|
||||
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # main
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main
|
||||
- name: Cosign version
|
||||
run: cosign version
|
||||
- name: Install Syft
|
||||
uses: anchore/sbom-action/download-syft@da167eac915b4e86f08b264dbdbc867b61be6f0c # main
|
||||
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main
|
||||
- name: Syft version
|
||||
run: syft version
|
||||
- name: Install xcaddy
|
||||
|
||||
6
.github/workflows/release_published.yml
vendored
6
.github/workflows/release_published.yml
vendored
@ -24,12 +24,12 @@ jobs:
|
||||
|
||||
# See https://github.com/peter-evans/repository-dispatch
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Trigger event on caddyserver/dist
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: caddyserver/dist
|
||||
@ -37,7 +37,7 @@ jobs:
|
||||
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
||||
|
||||
- name: Trigger event on caddyserver/caddy-docker
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: caddyserver/caddy-docker
|
||||
|
||||
6
.github/workflows/scorecard.yml
vendored
6
.github/workflows/scorecard.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -47,7 +47,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@ -81,6 +81,6 @@ jobs:
|
||||
# Upload the results to GitHub's code scanning dashboard (optional).
|
||||
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
7
admin.go
7
admin.go
@ -1029,6 +1029,13 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this request changed the config, clear the last
|
||||
// config info we have stored, if it is different from
|
||||
// the original source.
|
||||
ClearLastConfigIfDifferent(
|
||||
r.Header.Get("Caddy-Config-Source-File"),
|
||||
r.Header.Get("Caddy-Config-Source-Adapter"))
|
||||
|
||||
default:
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusMethodNotAllowed,
|
||||
|
||||
91
caddy.go
91
caddy.go
@ -975,11 +975,11 @@ func Version() (simple, full string) {
|
||||
if CustomVersion != "" {
|
||||
full = CustomVersion
|
||||
simple = CustomVersion
|
||||
return
|
||||
return simple, full
|
||||
}
|
||||
full = "unknown"
|
||||
simple = "unknown"
|
||||
return
|
||||
return simple, full
|
||||
}
|
||||
// find the Caddy module in the dependency list
|
||||
for _, dep := range bi.Deps {
|
||||
@ -1059,7 +1059,7 @@ func Version() (simple, full string) {
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return simple, full
|
||||
}
|
||||
|
||||
// Event represents something that has happened or is happening.
|
||||
@ -1197,6 +1197,91 @@ var (
|
||||
rawCfgMu sync.RWMutex
|
||||
)
|
||||
|
||||
// lastConfigFile and lastConfigAdapter remember the source config
|
||||
// file and adapter used when Caddy was started via the CLI "run" command.
|
||||
// These are consulted by the SIGUSR1 handler to attempt reloading from
|
||||
// the same source. They are intentionally not set for other entrypoints
|
||||
// such as "caddy start" or subcommands like file-server.
|
||||
var (
|
||||
lastConfigMu sync.RWMutex
|
||||
lastConfigFile string
|
||||
lastConfigAdapter string
|
||||
)
|
||||
|
||||
// reloadFromSourceFunc is the type of stored callback
|
||||
// which is called when we receive a SIGUSR1 signal.
|
||||
type reloadFromSourceFunc func(file, adapter string) error
|
||||
|
||||
// reloadFromSourceCallback is the stored callback
|
||||
// which is called when we receive a SIGUSR1 signal.
|
||||
var reloadFromSourceCallback reloadFromSourceFunc
|
||||
|
||||
// errReloadFromSourceUnavailable is returned when no reload-from-source callback is set.
|
||||
var errReloadFromSourceUnavailable = errors.New("reload from source unavailable in this process") //nolint:unused
|
||||
|
||||
// SetLastConfig records the given source file and adapter as the
|
||||
// last-known external configuration source. Intended to be called
|
||||
// only when starting via "caddy run --config <file> --adapter <adapter>".
|
||||
func SetLastConfig(file, adapter string, fn reloadFromSourceFunc) {
|
||||
lastConfigMu.Lock()
|
||||
lastConfigFile = file
|
||||
lastConfigAdapter = adapter
|
||||
reloadFromSourceCallback = fn
|
||||
lastConfigMu.Unlock()
|
||||
}
|
||||
|
||||
// ClearLastConfigIfDifferent clears the recorded last-config if the provided
|
||||
// source file/adapter do not match the recorded last-config. If both srcFile
|
||||
// and srcAdapter are empty, the last-config is cleared.
|
||||
func ClearLastConfigIfDifferent(srcFile, srcAdapter string) {
|
||||
if (srcFile != "" || srcAdapter != "") && lastConfigMatches(srcFile, srcAdapter) {
|
||||
return
|
||||
}
|
||||
SetLastConfig("", "", nil)
|
||||
}
|
||||
|
||||
// getLastConfig returns the last-known config file and adapter.
|
||||
func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) {
|
||||
lastConfigMu.RLock()
|
||||
f, a, cb := lastConfigFile, lastConfigAdapter, reloadFromSourceCallback
|
||||
lastConfigMu.RUnlock()
|
||||
return f, a, cb
|
||||
}
|
||||
|
||||
// lastConfigMatches returns true if the provided source file and/or adapter
|
||||
// matches the recorded last-config. Matching rules (in priority order):
|
||||
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
|
||||
// 2. If srcFile exactly equals the recorded file, match.
|
||||
// 3. If both sides can be made absolute and equal, match.
|
||||
// 4. If basenames are equal, match.
|
||||
func lastConfigMatches(srcFile, srcAdapter string) bool {
|
||||
lf, la, _ := getLastConfig()
|
||||
|
||||
// If adapter is provided, it must match.
|
||||
if srcAdapter != "" && srcAdapter != la {
|
||||
return false
|
||||
}
|
||||
|
||||
// Quick equality check.
|
||||
if srcFile == lf {
|
||||
return true
|
||||
}
|
||||
|
||||
// Try absolute path comparison.
|
||||
sAbs, sErr := filepath.Abs(srcFile)
|
||||
lAbs, lErr := filepath.Abs(lf)
|
||||
if sErr == nil && lErr == nil && sAbs == lAbs {
|
||||
return true
|
||||
}
|
||||
|
||||
// Final fallback: basename equality.
|
||||
if filepath.Base(srcFile) == filepath.Base(lf) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// errSameConfig is returned if the new config is the same
|
||||
// as the old one. This isn't usually an actual, actionable
|
||||
// error; it's mostly a sentinel value.
|
||||
|
||||
@ -91,7 +91,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
|
||||
// curves <curves...>
|
||||
// client_auth {
|
||||
// mode [request|require|verify_if_given|require_and_verify]
|
||||
// trust_pool <module_name> [...]
|
||||
// trust_pool <module_name> [...]
|
||||
// trusted_leaf_cert <base64_der>
|
||||
// trusted_leaf_cert_file <filename>
|
||||
// }
|
||||
@ -481,7 +481,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
// Validate DNS challenge config: any DNS challenge option except "dns" requires a DNS provider
|
||||
if acmeIssuer != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
|
||||
dnsCfg := acmeIssuer.Challenges.DNS
|
||||
providerSet := dnsCfg.ProviderRaw != nil || h.Option("dns") != nil
|
||||
providerSet := dnsCfg.ProviderRaw != nil || h.Option("dns") != nil || h.Option("acme_dns") != nil
|
||||
if len(dnsOptionsSet) > 0 && !providerSet {
|
||||
return nil, h.Errf(
|
||||
"setting DNS challenge options [%s] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option)",
|
||||
|
||||
@ -18,6 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
@ -42,12 +43,15 @@ type serverOptions struct {
|
||||
WriteTimeout caddy.Duration
|
||||
IdleTimeout caddy.Duration
|
||||
KeepAliveInterval caddy.Duration
|
||||
KeepAliveIdle caddy.Duration
|
||||
KeepAliveCount int
|
||||
MaxHeaderBytes int
|
||||
EnableFullDuplex bool
|
||||
Protocols []string
|
||||
StrictSNIHost *bool
|
||||
TrustedProxiesRaw json.RawMessage
|
||||
TrustedProxiesStrict int
|
||||
TrustedProxiesUnix bool
|
||||
ClientIPHeaders []string
|
||||
ShouldLogCredentials bool
|
||||
Metrics *caddyhttp.Metrics
|
||||
@ -142,6 +146,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
case "keepalive_interval":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
@ -152,6 +157,26 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
}
|
||||
serverOpts.KeepAliveInterval = caddy.Duration(dur)
|
||||
|
||||
case "keepalive_idle":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
dur, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return nil, d.Errf("parsing keepalive idle duration: %v", err)
|
||||
}
|
||||
serverOpts.KeepAliveIdle = caddy.Duration(dur)
|
||||
|
||||
case "keepalive_count":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
cnt, err := strconv.ParseInt(d.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return nil, d.Errf("parsing keepalive count int: %v", err)
|
||||
}
|
||||
serverOpts.KeepAliveCount = int(cnt)
|
||||
|
||||
case "max_header_size":
|
||||
var sizeStr string
|
||||
if !d.AllArgs(&sizeStr) {
|
||||
@ -227,6 +252,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
}
|
||||
serverOpts.TrustedProxiesStrict = 1
|
||||
|
||||
case "trusted_proxies_unix":
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
serverOpts.TrustedProxiesUnix = true
|
||||
|
||||
case "client_ip_headers":
|
||||
headers := d.RemainingArgs()
|
||||
for _, header := range headers {
|
||||
@ -309,6 +340,8 @@ func applyServerOptions(
|
||||
server.WriteTimeout = opts.WriteTimeout
|
||||
server.IdleTimeout = opts.IdleTimeout
|
||||
server.KeepAliveInterval = opts.KeepAliveInterval
|
||||
server.KeepAliveIdle = opts.KeepAliveIdle
|
||||
server.KeepAliveCount = opts.KeepAliveCount
|
||||
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||
server.EnableFullDuplex = opts.EnableFullDuplex
|
||||
server.Protocols = opts.Protocols
|
||||
@ -316,6 +349,7 @@ func applyServerOptions(
|
||||
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
||||
server.ClientIPHeaders = opts.ClientIPHeaders
|
||||
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
|
||||
server.TrustedProxiesUnix = opts.TrustedProxiesUnix
|
||||
server.Metrics = opts.Metrics
|
||||
if opts.ShouldLogCredentials {
|
||||
if server.Logs == nil {
|
||||
|
||||
@ -554,6 +554,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
||||
globalPreferredChains := options["preferred_chains"]
|
||||
globalCertLifetime := options["cert_lifetime"]
|
||||
globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
|
||||
globalDefaultBind := options["default_bind"]
|
||||
|
||||
if globalEmail != nil && acmeIssuer.Email == "" {
|
||||
acmeIssuer.Email = globalEmail.(string)
|
||||
@ -564,23 +565,22 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
||||
if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
|
||||
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
|
||||
}
|
||||
if globalACMEDNSok {
|
||||
if globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil || acmeIssuer.Challenges.DNS.ProviderRaw == nil) {
|
||||
globalDNS := options["dns"]
|
||||
if globalDNS != nil {
|
||||
// If global `dns` is set, do NOT set provider in issuer, just set empty dns config
|
||||
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
|
||||
DNS: &caddytls.DNSChallengeConfig{},
|
||||
}
|
||||
} else if globalACMEDNS != nil {
|
||||
// Set a global DNS provider if `acme_dns` is set and `dns` is NOT set
|
||||
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
|
||||
DNS: &caddytls.DNSChallengeConfig{
|
||||
ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
if globalDNS == nil && globalACMEDNS == nil {
|
||||
return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option")
|
||||
}
|
||||
if acmeIssuer.Challenges == nil {
|
||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||
}
|
||||
if acmeIssuer.Challenges.DNS == nil {
|
||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||
}
|
||||
// If global `dns` is set, do NOT set provider in issuer, just set empty dns config
|
||||
if globalDNS == nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil {
|
||||
// Set a global DNS provider if `acme_dns` is set and `dns` is NOT set
|
||||
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
|
||||
}
|
||||
}
|
||||
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
|
||||
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
|
||||
@ -607,6 +607,20 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
||||
}
|
||||
acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int)
|
||||
}
|
||||
// If BindHost is still unset, fall back to the first default_bind address if set
|
||||
// This avoids binding the automation policy to the wildcard socket, which is unexpected behavior when a more selective socket is specified via default_bind
|
||||
// In BSD it is valid to bind to the wildcard socket even though a more selective socket is already open (still unexpected behavior by the caller though)
|
||||
// In Linux the same call will error with EADDRINUSE whenever the listener for the automation policy is opened
|
||||
if acmeIssuer.Challenges == nil || (acmeIssuer.Challenges.DNS == nil && acmeIssuer.Challenges.BindHost == "") {
|
||||
if defBinds, ok := globalDefaultBind.([]ConfigValue); ok && len(defBinds) > 0 {
|
||||
if abp, ok := defBinds[0].Value.(addressesWithProtocols); ok && len(abp.addresses) > 0 {
|
||||
if acmeIssuer.Challenges == nil {
|
||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||
}
|
||||
acmeIssuer.Challenges.BindHost = abp.addresses[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
|
||||
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
|
||||
}
|
||||
|
||||
@ -121,6 +121,13 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
}
|
||||
|
||||
// If this request changed the config, clear the last
|
||||
// config info we have stored, if it is different from
|
||||
// the original source.
|
||||
caddy.ClearLastConfigIfDifferent(
|
||||
r.Header.Get("Caddy-Config-Source-File"),
|
||||
r.Header.Get("Caddy-Config-Source-Adapter"))
|
||||
|
||||
caddy.Log().Named("admin.api").Info("load complete")
|
||||
|
||||
return nil
|
||||
|
||||
@ -53,6 +53,7 @@ example.com {
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "foo",
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,9 @@
|
||||
trusted_proxies static private_ranges
|
||||
client_ip_headers Custom-Real-Client-IP X-Forwarded-For
|
||||
client_ip_headers A-Third-One
|
||||
keepalive_interval 20s
|
||||
keepalive_idle 20s
|
||||
keepalive_count 10
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +48,9 @@ foo.com {
|
||||
"read_header_timeout": 30000000000,
|
||||
"write_timeout": 30000000000,
|
||||
"idle_timeout": 30000000000,
|
||||
"keepalive_interval": 20000000000,
|
||||
"keepalive_idle": 20000000000,
|
||||
"keepalive_count": 10,
|
||||
"max_header_bytes": 100000000,
|
||||
"enable_full_duplex": true,
|
||||
"routes": [
|
||||
@ -89,4 +95,4 @@ foo.com {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
:80
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format filter {
|
||||
wrap console
|
||||
|
||||
# Multiple regexp filters for the same field - this should work now!
|
||||
request>headers>Authorization regexp "Bearer\s+([A-Za-z0-9_-]+)" "Bearer [REDACTED]"
|
||||
request>headers>Authorization regexp "Basic\s+([A-Za-z0-9+/=]+)" "Basic [REDACTED]"
|
||||
request>headers>Authorization regexp "token=([^&\s]+)" "token=[REDACTED]"
|
||||
|
||||
# Single regexp filter - this should continue to work as before
|
||||
request>headers>Cookie regexp "sessionid=[^;]+" "sessionid=[REDACTED]"
|
||||
|
||||
# Mixed filters (non-regexp) - these should work normally
|
||||
request>headers>Server delete
|
||||
request>remote_ip ip_mask {
|
||||
ipv4 24
|
||||
ipv6 32
|
||||
}
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"logging": {
|
||||
"logs": {
|
||||
"default": {
|
||||
"exclude": [
|
||||
"http.log.access.log0"
|
||||
]
|
||||
},
|
||||
"log0": {
|
||||
"writer": {
|
||||
"output": "stdout"
|
||||
},
|
||||
"encoder": {
|
||||
"fields": {
|
||||
"request\u003eheaders\u003eAuthorization": {
|
||||
"filter": "multi_regexp",
|
||||
"operations": [
|
||||
{
|
||||
"regexp": "Bearer\\s+([A-Za-z0-9_-]+)",
|
||||
"value": "Bearer [REDACTED]"
|
||||
},
|
||||
{
|
||||
"regexp": "Basic\\s+([A-Za-z0-9+/=]+)",
|
||||
"value": "Basic [REDACTED]"
|
||||
},
|
||||
{
|
||||
"regexp": "token=([^\u0026\\s]+)",
|
||||
"value": "token=[REDACTED]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request\u003eheaders\u003eCookie": {
|
||||
"filter": "regexp",
|
||||
"regexp": "sessionid=[^;]+",
|
||||
"value": "sessionid=[REDACTED]"
|
||||
},
|
||||
"request\u003eheaders\u003eServer": {
|
||||
"filter": "delete"
|
||||
},
|
||||
"request\u003eremote_ip": {
|
||||
"filter": "ip_mask",
|
||||
"ipv4_cidr": 24,
|
||||
"ipv6_cidr": 32
|
||||
}
|
||||
},
|
||||
"format": "filter",
|
||||
"wrap": {
|
||||
"format": "console"
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"http.log.access.log0"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"logs": {
|
||||
"default_logger_name": "log0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
{
|
||||
servers {
|
||||
trusted_proxies_unix
|
||||
}
|
||||
}
|
||||
|
||||
example.com {
|
||||
reverse_proxy https://local:8080
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"transport": {
|
||||
"protocol": "http",
|
||||
"tls": {}
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "local:8080"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
],
|
||||
"trusted_proxies_unix": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
{
|
||||
acme_dns mock foo
|
||||
}
|
||||
|
||||
localhost {
|
||||
tls {
|
||||
dns mock bar
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "bar",
|
||||
"name": "mock"
|
||||
},
|
||||
"resolvers": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "foo",
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
{
|
||||
dns mock foo
|
||||
}
|
||||
|
||||
localhost {
|
||||
tls {
|
||||
dns mock bar
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "bar",
|
||||
"name": "mock"
|
||||
},
|
||||
"resolvers": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"dns": {
|
||||
"argument": "foo",
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
{
|
||||
acme_dns mock
|
||||
}
|
||||
|
||||
localhost {
|
||||
tls {
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"name": "mock"
|
||||
},
|
||||
"resolvers": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
caddytest/integration/h2listener_test.go
Normal file
129
caddytest/integration/h2listener_test.go
Normal file
@ -0,0 +1,129 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester {
|
||||
const baseConfig = `
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
servers :9443 {
|
||||
protocols %s
|
||||
}
|
||||
}
|
||||
localhost {
|
||||
respond "{http.request.tls.proto} {http.request.proto}"
|
||||
}
|
||||
`
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile")
|
||||
|
||||
tr := tester.Client.Transport.(*http.Transport)
|
||||
tr.TLSClientConfig.NextProtos = clientVersions
|
||||
tr.Protocols = new(http.Protocols)
|
||||
if slices.Contains(clientVersions, "h2") {
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.Protocols.SetHTTP2(true)
|
||||
}
|
||||
if !slices.Contains(clientVersions, "http/1.1") {
|
||||
tr.Protocols.SetHTTP1(false)
|
||||
}
|
||||
|
||||
return tester
|
||||
}
|
||||
|
||||
func TestH2ListenerWithTLS(t *testing.T) {
|
||||
tests := []struct {
|
||||
serverVersions []string
|
||||
clientVersions []string
|
||||
expectedBody string
|
||||
failed bool
|
||||
}{
|
||||
{[]string{"h2"}, []string{"h2"}, "h2 HTTP/2.0", false},
|
||||
{[]string{"h2"}, []string{"http/1.1"}, "", true},
|
||||
{[]string{"h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false},
|
||||
{[]string{"h1"}, []string{"h2"}, "", true},
|
||||
{[]string{"h2", "h1"}, []string{"h2"}, "h2 HTTP/2.0", false},
|
||||
{[]string{"h2", "h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tester := newH2ListenerWithVersionsWithTLSTester(t, tc.serverVersions, tc.clientVersions)
|
||||
t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions)
|
||||
if tc.failed {
|
||||
resp, err := tester.Client.Get("https://localhost:9443")
|
||||
if err == nil {
|
||||
t.Errorf("unexpected response: %d", resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
tester.AssertGetResponse("https://localhost:9443", 200, tc.expectedBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester {
|
||||
const baseConfig = `
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
servers :9080 {
|
||||
protocols %s
|
||||
}
|
||||
}
|
||||
http://localhost {
|
||||
respond "{http.request.proto}"
|
||||
}
|
||||
`
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile")
|
||||
|
||||
tr := tester.Client.Transport.(*http.Transport)
|
||||
tr.Protocols = new(http.Protocols)
|
||||
if slices.Contains(clientVersions, "h2c") {
|
||||
tr.Protocols.SetHTTP1(false)
|
||||
tr.Protocols.SetUnencryptedHTTP2(true)
|
||||
} else if slices.Contains(clientVersions, "http/1.1") {
|
||||
tr.Protocols.SetHTTP1(true)
|
||||
tr.Protocols.SetUnencryptedHTTP2(false)
|
||||
}
|
||||
|
||||
return tester
|
||||
}
|
||||
|
||||
func TestH2ListenerWithoutTLS(t *testing.T) {
|
||||
tests := []struct {
|
||||
serverVersions []string
|
||||
clientVersions []string
|
||||
expectedBody string
|
||||
failed bool
|
||||
}{
|
||||
{[]string{"h2c"}, []string{"h2c"}, "HTTP/2.0", false},
|
||||
{[]string{"h2c"}, []string{"http/1.1"}, "", true},
|
||||
{[]string{"h1"}, []string{"http/1.1"}, "HTTP/1.1", false},
|
||||
{[]string{"h1"}, []string{"h2c"}, "", true},
|
||||
{[]string{"h2c", "h1"}, []string{"h2c"}, "HTTP/2.0", false},
|
||||
{[]string{"h2c", "h1"}, []string{"http/1.1"}, "HTTP/1.1", false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tester := newH2ListenerWithVersionsWithoutTLSTester(t, tc.serverVersions, tc.clientVersions)
|
||||
t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions)
|
||||
if tc.failed {
|
||||
resp, err := tester.Client.Get("http://localhost:9080")
|
||||
if err == nil {
|
||||
t.Errorf("unexpected response: %d", resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
tester.AssertGetResponse("http://localhost:9080", 200, tc.expectedBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,9 @@ func init() {
|
||||
}
|
||||
|
||||
// MockDNSProvider is a mock DNS provider, for testing config with DNS modules.
|
||||
type MockDNSProvider struct{}
|
||||
type MockDNSProvider struct {
|
||||
Argument string `json:"argument,omitempty"` // optional argument useful for testing
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (MockDNSProvider) CaddyModule() caddy.ModuleInfo {
|
||||
@ -31,7 +33,15 @@ func (MockDNSProvider) Provision(ctx caddy.Context) error {
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
|
||||
func (MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
func (p *MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
d.Next() // consume directive name
|
||||
|
||||
if d.NextArg() {
|
||||
p.Argument = d.Val()
|
||||
}
|
||||
if d.NextArg() {
|
||||
return d.Errf("unexpected argument '%s'", d.Val())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -231,8 +231,9 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
|
||||
var configFile string
|
||||
var adapterUsed string
|
||||
if !resumeFlag {
|
||||
config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
|
||||
config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
logBuffer.FlushTo(defaultLogger)
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
@ -249,6 +250,19 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a source config file (we're running via 'caddy run --config ...'),
|
||||
// record it so SIGUSR1 can reload from the same file. Also provide a callback
|
||||
// that knows how to load/adapt that source when requested by the main process.
|
||||
if configFile != "" {
|
||||
caddy.SetLastConfig(configFile, adapterUsed, func(file, adapter string) error {
|
||||
cfg, _, _, err := LoadConfig(file, adapter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return caddy.Load(cfg, true)
|
||||
})
|
||||
}
|
||||
|
||||
// run the initial config
|
||||
err = caddy.Load(config, true)
|
||||
if err != nil {
|
||||
@ -295,7 +309,7 @@ func cmdRun(fl Flags) (int, error) {
|
||||
// if enabled, reload config file automatically on changes
|
||||
// (this better only be used in dev!)
|
||||
if watchFlag {
|
||||
go watchConfigFile(configFile, configAdapterFlag)
|
||||
go watchConfigFile(configFile, adapterUsed)
|
||||
}
|
||||
|
||||
// warn if the environment does not provide enough information about the disk
|
||||
@ -350,7 +364,7 @@ func cmdReload(fl Flags) (int, error) {
|
||||
forceFlag := fl.Bool("force")
|
||||
|
||||
// get the config in caddy's native format
|
||||
config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
|
||||
config, configFile, adapterUsed, err := LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@ -368,6 +382,10 @@ func cmdReload(fl Flags) (int, error) {
|
||||
if forceFlag {
|
||||
headers.Set("Cache-Control", "must-revalidate")
|
||||
}
|
||||
// Provide the source file/adapter to the running process so it can
|
||||
// preserve its last-config knowledge if this reload came from the same source.
|
||||
headers.Set("Caddy-Config-Source-File", configFile)
|
||||
headers.Set("Caddy-Config-Source-Adapter", adapterUsed)
|
||||
|
||||
resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
|
||||
if err != nil {
|
||||
@ -582,7 +600,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
|
||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
||||
}
|
||||
|
||||
input, _, err := LoadConfig(configFlag, adapterFlag)
|
||||
input, _, _, err := LoadConfig(configFlag, adapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@ -797,7 +815,7 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
|
||||
loadedConfig := config
|
||||
if len(loadedConfig) == 0 {
|
||||
// get the config in caddy's native format
|
||||
loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter)
|
||||
loadedConfig, loadedConfigFile, _, err = LoadConfig(configFile, configAdapter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
31
cmd/main.go
31
cmd/main.go
@ -100,7 +100,12 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
|
||||
// there is no config available. It prints any warnings to stderr,
|
||||
// and returns the resulting JSON config bytes along with
|
||||
// the name of the loaded config file (if any).
|
||||
func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
// The return values are:
|
||||
// - config bytes (nil if no config)
|
||||
// - config file used ("" if none)
|
||||
// - adapter used ("" if none)
|
||||
// - error, if any
|
||||
func LoadConfig(configFile, adapterName string) ([]byte, string, string, error) {
|
||||
return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
|
||||
}
|
||||
|
||||
@ -138,7 +143,7 @@ func isCaddyfile(configFile, adapterName string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
|
||||
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, string, error) {
|
||||
// if no logger is provided, use a nop logger
|
||||
// just so we don't have to check for nil
|
||||
if logger == nil {
|
||||
@ -147,7 +152,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
|
||||
// specifying an adapter without a config file is ambiguous
|
||||
if adapterName != "" && configFile == "" {
|
||||
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||
return nil, "", "", fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||
}
|
||||
|
||||
// load initial config and adapter
|
||||
@ -158,13 +163,13 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
if configFile == "-" {
|
||||
config, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("reading config from stdin: %v", err)
|
||||
return nil, "", "", fmt.Errorf("reading config from stdin: %v", err)
|
||||
}
|
||||
logger.Info("using config from stdin")
|
||||
} else {
|
||||
config, err = os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("reading config from file: %v", err)
|
||||
return nil, "", "", fmt.Errorf("reading config from file: %v", err)
|
||||
}
|
||||
logger.Info("using config from file", zap.String("file", configFile))
|
||||
}
|
||||
@ -179,7 +184,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
cfgAdapter = nil
|
||||
} else if err != nil {
|
||||
// default Caddyfile exists, but error reading it
|
||||
return nil, "", fmt.Errorf("reading default Caddyfile: %v", err)
|
||||
return nil, "", "", fmt.Errorf("reading default Caddyfile: %v", err)
|
||||
} else {
|
||||
// success reading default Caddyfile
|
||||
configFile = "Caddyfile"
|
||||
@ -191,14 +196,14 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
if yes, err := isCaddyfile(configFile, adapterName); yes {
|
||||
adapterName = "caddyfile"
|
||||
} else if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
// load config adapter
|
||||
if adapterName != "" {
|
||||
cfgAdapter = caddyconfig.GetAdapter(adapterName)
|
||||
if cfgAdapter == nil {
|
||||
return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
|
||||
return nil, "", "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,7 +213,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
"filename": configFile,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
|
||||
return nil, "", "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
|
||||
}
|
||||
logger.Info("adapted config to JSON", zap.String("adapter", adapterName))
|
||||
for _, warn := range warnings {
|
||||
@ -226,11 +231,11 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
// validate that the config is at least valid JSON
|
||||
err = json.Unmarshal(config, new(any))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
|
||||
return nil, "", "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
|
||||
}
|
||||
}
|
||||
|
||||
return config, configFile, nil
|
||||
return config, configFile, adapterName, nil
|
||||
}
|
||||
|
||||
// watchConfigFile watches the config file at filename for changes
|
||||
@ -256,7 +261,7 @@ func watchConfigFile(filename, adapterName string) {
|
||||
}
|
||||
|
||||
// get current config
|
||||
lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
lastCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
return
|
||||
@ -268,7 +273,7 @@ func watchConfigFile(filename, adapterName string) {
|
||||
//nolint:staticcheck
|
||||
for range time.Tick(1 * time.Second) {
|
||||
// get current config
|
||||
newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
newCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
return
|
||||
|
||||
@ -62,7 +62,7 @@ func splitModule(arg string) (module, version string, err error) {
|
||||
err = fmt.Errorf("module name is required")
|
||||
}
|
||||
|
||||
return
|
||||
return module, version, err
|
||||
}
|
||||
|
||||
func cmdAddPackage(fl Flags) (int, error) {
|
||||
@ -217,7 +217,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
err = fmt.Errorf("no build info")
|
||||
return
|
||||
return standard, nonstandard, unknown, err
|
||||
}
|
||||
|
||||
for _, modID := range caddy.Modules() {
|
||||
@ -260,7 +260,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
|
||||
nonstandard = append(nonstandard, caddyModGoMod)
|
||||
}
|
||||
}
|
||||
return
|
||||
return standard, nonstandard, unknown, err
|
||||
}
|
||||
|
||||
func listModules(path string) error {
|
||||
|
||||
@ -36,7 +36,7 @@ type storVal struct {
|
||||
// determineStorage returns the top-level storage module from the given config.
|
||||
// It may return nil even if no error.
|
||||
func determineStorage(configFile string, configAdapter string) (*storVal, error) {
|
||||
cfg, _, err := LoadConfig(configFile, configAdapter)
|
||||
cfg, _, _, err := LoadConfig(configFile, configAdapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
44
go.mod
44
go.mod
@ -4,6 +4,7 @@ go 1.25
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/DeRuina/timberjack v1.3.8
|
||||
github.com/KimMachineGun/automemlimit v0.7.4
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/alecthomas/chroma/v2 v2.20.0
|
||||
@ -17,9 +18,9 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/klauspost/cpuid/v2 v2.3.0
|
||||
github.com/mholt/acmez/v3 v3.1.3
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/quic-go/quic-go v0.54.0
|
||||
github.com/mholt/acmez/v3 v3.1.4
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/quic-go/quic-go v0.55.0
|
||||
github.com/smallstep/certificates v0.28.4
|
||||
github.com/smallstep/nosql v0.7.0
|
||||
github.com/smallstep/truststore v0.13.0
|
||||
@ -37,13 +38,12 @@ 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.41.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/term v0.34.0
|
||||
golang.org/x/time v0.12.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/term v0.36.0
|
||||
golang.org/x/time v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@ -89,12 +89,12 @@ require (
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
google.golang.org/api v0.247.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
)
|
||||
|
||||
@ -135,8 +135,8 @@ require (
|
||||
github.com/pires/go-proxyproto v0.8.1
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/common v0.67.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
@ -153,11 +153,11 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.step.sm/crypto v0.70.0
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
google.golang.org/grpc v1.75.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sys v0.37.0
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
|
||||
88
go.sum
88
go.sum
@ -32,6 +32,8 @@ github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOv
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/DeRuina/timberjack v1.3.8 h1:lLxmRExvZygKSbb27Vp9hS0Tv8mL0WmFbwfRF29nY0Q=
|
||||
github.com/DeRuina/timberjack v1.3.8/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||
github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk=
|
||||
github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
@ -149,6 +151,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@ -274,8 +278,8 @@ 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.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc=
|
||||
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
|
||||
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
@ -309,21 +313,21 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
|
||||
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
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.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
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=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
@ -490,6 +494,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@ -502,13 +508,13 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 h1:CH0o4/bZX6KIUCjjgjmtNtfM/kXSkTYlzTOB9vZF45g=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@ -517,8 +523,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
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=
|
||||
@ -535,14 +541,14 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
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=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -556,8 +562,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -582,8 +588,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@ -594,8 +600,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@ -607,12 +613,12 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
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.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/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=
|
||||
@ -623,8 +629,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
@ -646,25 +652,23 @@ google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuO
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@ -261,14 +261,14 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
|
||||
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
||||
return
|
||||
return n, addr, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
return n, addr, err
|
||||
}
|
||||
|
||||
return
|
||||
return n, addr, err
|
||||
}
|
||||
|
||||
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
||||
|
||||
@ -382,7 +382,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) {
|
||||
a = afterSlash
|
||||
if IsUnixNetwork(network) || IsFdNetwork(network) {
|
||||
host = a
|
||||
return
|
||||
return network, host, port, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,7 +402,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) {
|
||||
err = errors.Join(firstErr, err)
|
||||
}
|
||||
|
||||
return
|
||||
return network, host, port, err
|
||||
}
|
||||
|
||||
// JoinNetworkAddress combines network, host, and port into a single
|
||||
|
||||
@ -345,9 +345,11 @@ func StrictUnmarshalJSON(data []byte, v any) error {
|
||||
return dec.Decode(v)
|
||||
}
|
||||
|
||||
var JSONRawMessageType = reflect.TypeFor[json.RawMessage]()
|
||||
|
||||
// isJSONRawMessage returns true if the type is encoding/json.RawMessage.
|
||||
func isJSONRawMessage(typ reflect.Type) bool {
|
||||
return typ.PkgPath() == "encoding/json" && typ.Name() == "RawMessage"
|
||||
return typ == JSONRawMessageType
|
||||
}
|
||||
|
||||
// isModuleMapType returns true if the type is map[string]json.RawMessage.
|
||||
|
||||
@ -466,7 +466,14 @@ func (app *App) Start() error {
|
||||
ErrorLog: serverLogger,
|
||||
Protocols: new(http.Protocols),
|
||||
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
|
||||
return context.WithValue(ctx, ConnCtxKey, c)
|
||||
if nc, ok := c.(interface{ tlsNetConn() net.Conn }); ok {
|
||||
getTlsConStateFunc := sync.OnceValue(func() *tls.ConnectionState {
|
||||
tlsConnState := nc.tlsNetConn().(connectionStater).ConnectionState()
|
||||
return &tlsConnState
|
||||
})
|
||||
ctx = context.WithValue(ctx, tlsConnectionStateFuncCtxKey, getTlsConStateFunc)
|
||||
}
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
|
||||
@ -538,6 +545,8 @@ func (app *App) Start() error {
|
||||
KeepAliveConfig: net.KeepAliveConfig{
|
||||
Enable: srv.KeepAliveInterval >= 0,
|
||||
Interval: time.Duration(srv.KeepAliveInterval),
|
||||
Idle: time.Duration(srv.KeepAliveIdle),
|
||||
Count: srv.KeepAliveCount,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
188
modules/caddyhttp/caddyauth/argon2id.go
Normal file
188
modules/caddyhttp/caddyauth/argon2id.go
Normal file
@ -0,0 +1,188 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyauth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(Argon2idHash{})
|
||||
}
|
||||
|
||||
const (
|
||||
argon2idName = "argon2id"
|
||||
defaultArgon2idTime = 1
|
||||
defaultArgon2idMemory = 46 * 1024
|
||||
defaultArgon2idThreads = 1
|
||||
defaultArgon2idKeylen = 32
|
||||
defaultSaltLength = 16
|
||||
)
|
||||
|
||||
// Argon2idHash implements the Argon2id password hashing.
|
||||
type Argon2idHash struct {
|
||||
salt []byte
|
||||
time uint32
|
||||
memory uint32
|
||||
threads uint8
|
||||
keyLen uint32
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (Argon2idHash) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.authentication.hashes.argon2id",
|
||||
New: func() caddy.Module { return new(Argon2idHash) },
|
||||
}
|
||||
}
|
||||
|
||||
// Compare checks if the plaintext password matches the given Argon2id hash.
|
||||
func (Argon2idHash) Compare(hashed, plaintext []byte) (bool, error) {
|
||||
argHash, storedKey, err := DecodeHash(hashed)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
computedKey := argon2.IDKey(
|
||||
plaintext,
|
||||
argHash.salt,
|
||||
argHash.time,
|
||||
argHash.memory,
|
||||
argHash.threads,
|
||||
argHash.keyLen,
|
||||
)
|
||||
|
||||
return subtle.ConstantTimeCompare(storedKey, computedKey) == 1, nil
|
||||
}
|
||||
|
||||
// Hash generates an Argon2id hash of the given plaintext using the configured parameters and salt.
|
||||
func (b Argon2idHash) Hash(plaintext []byte) ([]byte, error) {
|
||||
if b.salt == nil {
|
||||
s, err := generateSalt(defaultSaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.salt = s
|
||||
}
|
||||
|
||||
key := argon2.IDKey(
|
||||
plaintext,
|
||||
b.salt,
|
||||
b.time,
|
||||
b.memory,
|
||||
b.threads,
|
||||
b.keyLen,
|
||||
)
|
||||
|
||||
hash := fmt.Sprintf(
|
||||
"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
argon2.Version,
|
||||
b.memory,
|
||||
b.time,
|
||||
b.threads,
|
||||
base64.RawStdEncoding.EncodeToString(b.salt),
|
||||
base64.RawStdEncoding.EncodeToString(key),
|
||||
)
|
||||
|
||||
return []byte(hash), nil
|
||||
}
|
||||
|
||||
// DecodeHash parses an Argon2id PHC string into an Argon2idHash struct and returns the struct along with the derived key.
|
||||
func DecodeHash(hash []byte) (*Argon2idHash, []byte, error) {
|
||||
parts := strings.Split(string(hash), "$")
|
||||
if len(parts) != 6 {
|
||||
return nil, nil, fmt.Errorf("invalid hash format")
|
||||
}
|
||||
|
||||
if parts[1] != argon2idName {
|
||||
return nil, nil, fmt.Errorf("unsupported variant: %s", parts[1])
|
||||
}
|
||||
|
||||
version, err := strconv.Atoi(strings.TrimPrefix(parts[2], "v="))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid version: %w", err)
|
||||
}
|
||||
if version != argon2.Version {
|
||||
return nil, nil, fmt.Errorf("incompatible version: %d", version)
|
||||
}
|
||||
|
||||
params := strings.Split(parts[3], ",")
|
||||
if len(params) != 3 {
|
||||
return nil, nil, fmt.Errorf("invalid parameters")
|
||||
}
|
||||
|
||||
mem, err := strconv.ParseUint(strings.TrimPrefix(params[0], "m="), 10, 32)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid memory parameter: %w", err)
|
||||
}
|
||||
|
||||
iter, err := strconv.ParseUint(strings.TrimPrefix(params[1], "t="), 10, 32)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid iterations parameter: %w", err)
|
||||
}
|
||||
|
||||
threads, err := strconv.ParseUint(strings.TrimPrefix(params[2], "p="), 10, 8)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid parallelism parameter: %w", err)
|
||||
}
|
||||
|
||||
salt, err := base64.RawStdEncoding.Strict().DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("decode salt: %w", err)
|
||||
}
|
||||
|
||||
key, err := base64.RawStdEncoding.Strict().DecodeString(parts[5])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("decode key: %w", err)
|
||||
}
|
||||
|
||||
return &Argon2idHash{
|
||||
salt: salt,
|
||||
time: uint32(iter),
|
||||
memory: uint32(mem),
|
||||
threads: uint8(threads),
|
||||
keyLen: uint32(len(key)),
|
||||
}, key, nil
|
||||
}
|
||||
|
||||
// FakeHash returns a constant fake hash for timing attacks mitigation.
|
||||
func (Argon2idHash) FakeHash() []byte {
|
||||
// hashed with the following command:
|
||||
// caddy hash-password --plaintext "antitiming" --algorithm "argon2id"
|
||||
return []byte("$argon2id$v=19$m=47104,t=1,p=1$P2nzckEdTZ3bxCiBCkRTyA$xQL3Z32eo5jKl7u5tcIsnEKObYiyNZQQf5/4sAau6Pg")
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ Comparer = (*Argon2idHash)(nil)
|
||||
_ Hasher = (*Argon2idHash)(nil)
|
||||
)
|
||||
|
||||
func generateSalt(length int) ([]byte, error) {
|
||||
salt := make([]byte, length)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate salt: %w", err)
|
||||
}
|
||||
return salt, nil
|
||||
}
|
||||
@ -27,7 +27,10 @@ func init() {
|
||||
}
|
||||
|
||||
// defaultBcryptCost cost 14 strikes a solid balance between security, usability, and hardware performance
|
||||
const defaultBcryptCost = 14
|
||||
const (
|
||||
bcryptName = "bcrypt"
|
||||
defaultBcryptCost = 14
|
||||
)
|
||||
|
||||
// BcryptHash implements the bcrypt hash.
|
||||
type BcryptHash struct {
|
||||
@ -51,7 +51,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
var hashName string
|
||||
switch len(args) {
|
||||
case 0:
|
||||
hashName = "bcrypt"
|
||||
hashName = bcryptName
|
||||
case 1:
|
||||
hashName = args[0]
|
||||
case 2:
|
||||
@ -62,8 +62,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
}
|
||||
|
||||
switch hashName {
|
||||
case "bcrypt":
|
||||
case bcryptName:
|
||||
cmp = BcryptHash{}
|
||||
case argon2idName:
|
||||
cmp = Argon2idHash{}
|
||||
default:
|
||||
return nil, h.Errf("unrecognized hash algorithm: %s", hashName)
|
||||
}
|
||||
|
||||
@ -32,28 +32,55 @@ import (
|
||||
func init() {
|
||||
caddycmd.RegisterCommand(caddycmd.Command{
|
||||
Name: "hash-password",
|
||||
Usage: "[--plaintext <password>] [--algorithm <name>] [--bcrypt-cost <difficulty>]",
|
||||
Usage: "[--plaintext <password>] [--algorithm <argon2id|bcrypt>] [--bcrypt-cost <difficulty>] [--argon2id-time <iterations>] [--argon2id-memory <KiB>] [--argon2id-threads <n>] [--argon2id-keylen <bytes>]",
|
||||
Short: "Hashes a password and writes base64",
|
||||
Long: `
|
||||
Convenient way to hash a plaintext password. The resulting
|
||||
hash is written to stdout as a base64 string.
|
||||
|
||||
--plaintext, when omitted, will be read from stdin. If
|
||||
Caddy is attached to a controlling tty, the plaintext will
|
||||
not be echoed.
|
||||
--plaintext
|
||||
The password to hash. If omitted, it will be read from stdin.
|
||||
If Caddy is attached to a controlling TTY, the input will not be echoed.
|
||||
|
||||
--algorithm currently only supports 'bcrypt', and is the default.
|
||||
--algorithm
|
||||
Selects the hashing algorithm. Valid options are:
|
||||
* 'argon2id' (recommended for modern security)
|
||||
* 'bcrypt' (legacy, slower, configurable cost)
|
||||
|
||||
--bcrypt-cost sets the bcrypt hashing difficulty.
|
||||
Higher values increase security by making the hash computation slower and more CPU-intensive.
|
||||
If the provided cost is not within the valid range [bcrypt.MinCost, bcrypt.MaxCost],
|
||||
the default value (defaultBcryptCost) will be used instead.
|
||||
Note: Higher cost values can significantly degrade performance on slower systems.
|
||||
bcrypt-specific parameters:
|
||||
|
||||
--bcrypt-cost
|
||||
Sets the bcrypt hashing difficulty. Higher values increase security by
|
||||
making the hash computation slower and more CPU-intensive.
|
||||
Must be within the valid range [bcrypt.MinCost, bcrypt.MaxCost].
|
||||
If omitted or invalid, the default cost is used.
|
||||
|
||||
Argon2id-specific parameters:
|
||||
|
||||
--argon2id-time
|
||||
Number of iterations to perform. Increasing this makes
|
||||
hashing slower and more resistant to brute-force attacks.
|
||||
|
||||
--argon2id-memory
|
||||
Amount of memory to use during hashing.
|
||||
Larger values increase resistance to GPU/ASIC attacks.
|
||||
|
||||
--argon2id-threads
|
||||
Number of CPU threads to use. Increase for faster hashing
|
||||
on multi-core systems.
|
||||
|
||||
--argon2id-keylen
|
||||
Length of the resulting hash in bytes. Longer keys increase
|
||||
security but slightly increase storage size.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("plaintext", "p", "", "The plaintext password")
|
||||
cmd.Flags().StringP("algorithm", "a", "bcrypt", "Name of the hash algorithm")
|
||||
cmd.Flags().StringP("algorithm", "a", bcryptName, "Name of the hash algorithm")
|
||||
cmd.Flags().Int("bcrypt-cost", defaultBcryptCost, "Bcrypt hashing cost (only used with 'bcrypt' algorithm)")
|
||||
cmd.Flags().Uint32("argon2id-time", defaultArgon2idTime, "Number of iterations for Argon2id hashing. Increasing this makes the hash slower and more resistant to brute-force attacks.")
|
||||
cmd.Flags().Uint32("argon2id-memory", defaultArgon2idMemory, "Memory to use in KiB for Argon2id hashing. Larger values increase resistance to GPU/ASIC attacks.")
|
||||
cmd.Flags().Uint8("argon2id-threads", defaultArgon2idThreads, "Number of CPU threads to use for Argon2id hashing. Increase for faster hashing on multi-core systems.")
|
||||
cmd.Flags().Uint32("argon2id-keylen", defaultArgon2idKeylen, "Length of the resulting Argon2id hash in bytes. Longer hashes increase security but slightly increase storage size.")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword)
|
||||
},
|
||||
})
|
||||
@ -115,8 +142,34 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
|
||||
var hash []byte
|
||||
var hashString string
|
||||
switch algorithm {
|
||||
case "bcrypt":
|
||||
case bcryptName:
|
||||
hash, err = BcryptHash{cost: bcryptCost}.Hash(plaintext)
|
||||
hashString = string(hash)
|
||||
case argon2idName:
|
||||
time, err := fs.GetUint32("argon2id-time")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id time parameter: %w", err)
|
||||
}
|
||||
memory, err := fs.GetUint32("argon2id-memory")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id memory parameter: %w", err)
|
||||
}
|
||||
threads, err := fs.GetUint8("argon2id-threads")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id threads parameter: %w", err)
|
||||
}
|
||||
keyLen, err := fs.GetUint32("argon2id-keylen")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id keylen parameter: %w", err)
|
||||
}
|
||||
|
||||
hash, _ = Argon2idHash{
|
||||
time: time,
|
||||
memory: memory,
|
||||
threads: threads,
|
||||
keyLen: keyLen,
|
||||
}.Hash(plaintext)
|
||||
|
||||
hashString = string(hash)
|
||||
default:
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)
|
||||
|
||||
@ -50,7 +50,7 @@ type Encode struct {
|
||||
// Only encode responses that are at least this many bytes long.
|
||||
MinLength int `json:"minimum_length,omitempty"`
|
||||
|
||||
// Only encode responses that match against this ResponseMmatcher.
|
||||
// Only encode responses that match against this ResponseMatcher.
|
||||
// The default is a collection of text-based Content-Type headers.
|
||||
Matcher *caddyhttp.ResponseMatcher `json:"match,omitempty"`
|
||||
|
||||
|
||||
@ -36,6 +36,12 @@ func (h *http2Listener) Accept() (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// *tls.Conn doesn't need to be wrapped because we already removed unwanted alpns
|
||||
// and handshake won't succeed without mutually supported alpns
|
||||
if tlsConn, ok := conn.(*tls.Conn); ok {
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
_, isConnectionStater := conn.(connectionStater)
|
||||
// emit a warning
|
||||
if h.useTLS && !isConnectionStater {
|
||||
@ -46,6 +52,9 @@ func (h *http2Listener) Accept() (net.Conn, error) {
|
||||
|
||||
// if both h1 and h2 are enabled, we don't need to check the preface
|
||||
if h.useH1 && h.useH2 {
|
||||
if isConnectionStater {
|
||||
return tlsStateConn{conn}, nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
@ -53,14 +62,26 @@ func (h *http2Listener) Accept() (net.Conn, error) {
|
||||
// or else the listener wouldn't be created
|
||||
h2Conn := &http2Conn{
|
||||
h2Expected: h.useH2,
|
||||
logger: h.logger,
|
||||
Conn: conn,
|
||||
}
|
||||
if isConnectionStater {
|
||||
return http2StateConn{h2Conn}, nil
|
||||
return tlsStateConn{http2StateConn{h2Conn}}, nil
|
||||
}
|
||||
return h2Conn, nil
|
||||
}
|
||||
|
||||
// tlsStateConn wraps a net.Conn that implements connectionStater to hide that method
|
||||
// we can call netConn to get the original net.Conn and get the tls connection state
|
||||
// golang 1.25 will call that method, and it breaks h2 with connections other than *tls.Conn
|
||||
type tlsStateConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (conn tlsStateConn) tlsNetConn() net.Conn {
|
||||
return conn.Conn
|
||||
}
|
||||
|
||||
type http2StateConn struct {
|
||||
*http2Conn
|
||||
}
|
||||
|
||||
@ -209,7 +209,7 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi
|
||||
zap.String("err_trace", handlerErr.Trace),
|
||||
}
|
||||
}
|
||||
return
|
||||
return status, msg, fields
|
||||
}
|
||||
fields = func() []zapcore.Field {
|
||||
return []zapcore.Field{
|
||||
@ -218,7 +218,7 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi
|
||||
}
|
||||
status = http.StatusInternalServerError
|
||||
msg = err.Error()
|
||||
return
|
||||
return status, msg, fields
|
||||
}
|
||||
|
||||
// ExtraLogFields is a list of extra fields to log with every request.
|
||||
|
||||
@ -992,7 +992,6 @@ func TestVarREMatcher(t *testing.T) {
|
||||
expect: true,
|
||||
},
|
||||
} {
|
||||
tc := tc // capture range value
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// compile the regexp and validate its name
|
||||
|
||||
@ -116,7 +116,7 @@ func (ew errorWrapper) Read(p []byte) (n int, err error) {
|
||||
if errors.As(err, &mbe) {
|
||||
err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err)
|
||||
}
|
||||
return
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
|
||||
@ -75,8 +75,8 @@ For proxying:
|
||||
cmd.Flags().BoolP("insecure", "", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING TLS CERTIFICATES!)")
|
||||
cmd.Flags().BoolP("disable-redirects", "r", false, "Disable HTTP->HTTPS redirects")
|
||||
cmd.Flags().BoolP("internal-certs", "i", false, "Use internal CA for issuing certs")
|
||||
cmd.Flags().StringSliceP("header-up", "H", []string{}, "Set a request header to send to the upstream (format: \"Field: value\")")
|
||||
cmd.Flags().StringSliceP("header-down", "d", []string{}, "Set a response header to send back to the client (format: \"Field: value\")")
|
||||
cmd.Flags().StringArrayP("header-up", "H", []string{}, "Set a request header to send to the upstream (format: \"Field: value\")")
|
||||
cmd.Flags().StringArrayP("header-down", "d", []string{}, "Set a response header to send back to the client (format: \"Field: value\")")
|
||||
cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
|
||||
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdReverseProxy)
|
||||
@ -182,7 +182,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
|
||||
// set up header_up
|
||||
headerUp, err := fs.GetStringSlice("header-up")
|
||||
headerUp, err := fs.GetStringArray("header-up")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
|
||||
}
|
||||
@ -204,7 +204,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
|
||||
// set up header_down
|
||||
headerDown, err := fs.GetStringSlice("header-down")
|
||||
headerDown, err := fs.GetStringArray("header-down")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
|
||||
}
|
||||
|
||||
@ -154,13 +154,13 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error)
|
||||
|
||||
err = writer.writeBeginRequest(uint16(Responder), 0)
|
||||
if err != nil {
|
||||
return
|
||||
return r, err
|
||||
}
|
||||
|
||||
writer.recType = Params
|
||||
err = writer.writePairs(p)
|
||||
if err != nil {
|
||||
return
|
||||
return r, err
|
||||
}
|
||||
|
||||
writer.recType = Stdin
|
||||
@ -176,7 +176,7 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error)
|
||||
}
|
||||
|
||||
r = &streamReader{c: c}
|
||||
return
|
||||
return r, err
|
||||
}
|
||||
|
||||
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
|
||||
@ -213,7 +213,7 @@ func (f clientCloser) Close() error {
|
||||
func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
|
||||
r, err := c.Do(p, req)
|
||||
if err != nil {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
|
||||
rb := bufio.NewReader(r)
|
||||
@ -223,7 +223,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
|
||||
// Parse the response headers.
|
||||
mimeHeader, err := tp.ReadMIMEHeader()
|
||||
if err != nil && err != io.EOF {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
resp.Header = http.Header(mimeHeader)
|
||||
|
||||
@ -231,7 +231,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
|
||||
statusNumber, statusInfo, statusIsCut := strings.Cut(resp.Header.Get("Status"), " ")
|
||||
resp.StatusCode, err = strconv.Atoi(statusNumber)
|
||||
if err != nil {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
if statusIsCut {
|
||||
resp.Status = statusInfo
|
||||
@ -260,7 +260,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
|
||||
}
|
||||
resp.Body = closer
|
||||
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Get issues a GET request to the fcgi responder.
|
||||
@ -329,7 +329,7 @@ func (c *client) PostFile(p map[string]string, data url.Values, file map[string]
|
||||
for _, v0 := range val {
|
||||
err = writer.WriteField(key, v0)
|
||||
if err != nil {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -347,13 +347,13 @@ func (c *client) PostFile(p map[string]string, data url.Values, file map[string]
|
||||
}
|
||||
_, err = io.Copy(part, fd)
|
||||
if err != nil {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return c.Post(p, "POST", bodyType, buf, int64(buf.Len()))
|
||||
|
||||
@ -120,7 +120,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
conn, err := net.Dial("tcp", ipPort)
|
||||
if err != nil {
|
||||
log.Println("err:", err)
|
||||
return
|
||||
return content
|
||||
}
|
||||
|
||||
fcgi := client{rwc: conn, reqID: 1}
|
||||
@ -162,7 +162,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
|
||||
if err != nil {
|
||||
log.Println("err:", err)
|
||||
return
|
||||
return content
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
@ -176,7 +176,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
globalt.Error("Server return failed message")
|
||||
}
|
||||
|
||||
return
|
||||
return content
|
||||
}
|
||||
|
||||
func generateRandFile(size int) (p string, m string) {
|
||||
@ -206,7 +206,7 @@ func generateRandFile(size int) (p string, m string) {
|
||||
}
|
||||
}
|
||||
m = fmt.Sprintf("%x", h.Sum(nil))
|
||||
return
|
||||
return p, m
|
||||
}
|
||||
|
||||
func DisabledTest(t *testing.T) {
|
||||
|
||||
@ -30,23 +30,23 @@ func (rec *record) fill(r io.Reader) (err error) {
|
||||
rec.lr.N = rec.padding
|
||||
rec.lr.R = r
|
||||
if _, err = io.Copy(io.Discard, rec); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
if rec.h.Version != 1 {
|
||||
err = errors.New("fcgi: invalid header version")
|
||||
return
|
||||
return err
|
||||
}
|
||||
if rec.h.Type == EndRequest {
|
||||
err = io.EOF
|
||||
return
|
||||
return err
|
||||
}
|
||||
rec.lr.N = int64(rec.h.ContentLength)
|
||||
rec.padding = int64(rec.h.PaddingLength)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
func (rec *record) Read(p []byte) (n int, err error) {
|
||||
|
||||
@ -84,7 +84,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
|
||||
// create the reverse proxy handler
|
||||
rpHandler := &reverseproxy.Handler{
|
||||
// set up defaults for header_up; reverse_proxy already deals with
|
||||
// adding the other three X-Forwarded-* headers, but for this flow,
|
||||
// adding the other three X-Forwarded-* headers, but for this flow,
|
||||
// we want to also send along the incoming method and URI since this
|
||||
// request will have a rewritten URI and method.
|
||||
Headers: &headers.Handler{
|
||||
|
||||
@ -281,3 +281,7 @@ const proxyProtocolInfoVarKey = "reverse_proxy.proxy_protocol_info"
|
||||
type ProxyProtocolInfo struct {
|
||||
AddrPort netip.AddrPort
|
||||
}
|
||||
|
||||
// tlsH1OnlyVarKey is the key used that indicates the connection will use h1 only for TLS.
|
||||
// https://github.com/caddyserver/caddy/issues/7292
|
||||
const tlsH1OnlyVarKey = "reverse_proxy.tls_h1_only"
|
||||
|
||||
@ -409,6 +409,14 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
|
||||
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
tlsConfig := rt.TLSClientConfig.Clone()
|
||||
tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
|
||||
|
||||
// h1 only
|
||||
if caddyhttp.GetVar(ctx, tlsH1OnlyVarKey) == true {
|
||||
// stdlib does this
|
||||
// https://github.com/golang/go/blob/4837fbe4145cd47b43eed66fee9eed9c2b988316/src/net/http/transport.go#L1701
|
||||
tlsConfig.NextProtos = nil
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(conn, tlsConfig)
|
||||
|
||||
// complete the handshake before returning the connection
|
||||
|
||||
@ -409,12 +409,16 @@ 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
|
||||
|
||||
// websocket over http2 or http3 if extended connect is enabled, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
|
||||
// Both use the same upgrade mechanism: server advertizes extended connect support, and client sends the pseudo header :protocol in a CONNECT request
|
||||
// The quic-go http3 implementation also puts :protocol in r.Proto for CONNECT requests (quic-go/http3/headers.go@70-72,185,203)
|
||||
// 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" {
|
||||
if (r.ProtoMajor == 2 && r.Method == http.MethodConnect && r.Header.Get(":protocol") == "websocket") ||
|
||||
(r.ProtoMajor == 3 && r.Method == http.MethodConnect && r.Proto == "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)
|
||||
caddyhttp.SetVar(clonedReq.Context(), "extended_connect_websocket_body", clonedReq.Body)
|
||||
clonedReq.Body = http.NoBody
|
||||
clonedReq.Method = http.MethodGet
|
||||
clonedReq.Header.Set("Upgrade", "websocket")
|
||||
@ -726,6 +730,12 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
|
||||
proxyProtocolInfo := ProxyProtocolInfo{AddrPort: addrPort}
|
||||
caddyhttp.SetVar(req.Context(), proxyProtocolInfoVarKey, proxyProtocolInfo)
|
||||
|
||||
// some of the outbound requests require h1 (e.g. websocket)
|
||||
// https://github.com/golang/go/blob/4837fbe4145cd47b43eed66fee9eed9c2b988316/src/net/http/request.go#L1579
|
||||
if isWebsocket(req) {
|
||||
caddyhttp.SetVar(req.Context(), tlsH1OnlyVarKey, true)
|
||||
}
|
||||
|
||||
// Add the supported X-Forwarded-* headers
|
||||
err = h.addForwardedHeaders(req)
|
||||
if err != nil {
|
||||
|
||||
@ -94,9 +94,9 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
|
||||
conn io.ReadWriteCloser
|
||||
brw *bufio.ReadWriter
|
||||
)
|
||||
// websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
|
||||
// websocket over http2 or http3 if extended connect is enabled, 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 body, ok := caddyhttp.GetVar(req.Context(), "h2_websocket_body").(io.ReadCloser); ok {
|
||||
if body, ok := caddyhttp.GetVar(req.Context(), "extended_connect_websocket_body").(io.ReadCloser); ok {
|
||||
req.Body = body
|
||||
rw.Header().Del("Upgrade")
|
||||
rw.Header().Del("Connection")
|
||||
@ -588,11 +588,11 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
|
||||
m.logger.Debug("flushing immediately")
|
||||
//nolint:errcheck
|
||||
m.flush()
|
||||
return
|
||||
return n, err
|
||||
}
|
||||
if m.flushPending {
|
||||
m.logger.Debug("delayed flush already pending")
|
||||
return
|
||||
return n, err
|
||||
}
|
||||
if m.t == nil {
|
||||
m.t = time.AfterFunc(m.latency, m.delayedFlush)
|
||||
@ -603,7 +603,7 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
|
||||
c.Write(zap.Duration("duration", m.latency))
|
||||
}
|
||||
m.flushPending = true
|
||||
return
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (m *maxLatencyWriter) delayedFlush() {
|
||||
|
||||
@ -213,12 +213,12 @@ func (su SRVUpstreams) expandedAddr(r *http.Request) (addr, service, proto, name
|
||||
name = repl.ReplaceAll(su.Name, "")
|
||||
if su.Service == "" && su.Proto == "" {
|
||||
addr = name
|
||||
return
|
||||
return addr, service, proto, name
|
||||
}
|
||||
service = repl.ReplaceAll(su.Service, "")
|
||||
proto = repl.ReplaceAll(su.Proto, "")
|
||||
addr = su.formattedAddr(service, proto, name)
|
||||
return
|
||||
return addr, service, proto, name
|
||||
}
|
||||
|
||||
// formattedAddr the RFC 2782 representation of the SRV domain, in
|
||||
|
||||
@ -76,9 +76,25 @@ type Server struct {
|
||||
|
||||
// KeepAliveInterval is the interval at which TCP keepalive packets
|
||||
// are sent to keep the connection alive at the TCP layer when no other
|
||||
// data is being transmitted. The default is 15s.
|
||||
// data is being transmitted.
|
||||
// If zero, the default is 15s.
|
||||
// If negative, keepalive packets are not sent and other keepalive parameters
|
||||
// are ignored.
|
||||
KeepAliveInterval caddy.Duration `json:"keepalive_interval,omitempty"`
|
||||
|
||||
// KeepAliveIdle is the time that the connection must be idle before
|
||||
// the first TCP keep-alive probe is sent when no other data is being
|
||||
// transmitted.
|
||||
// If zero, the default is 15s.
|
||||
// If negative, underlying socket value is unchanged.
|
||||
KeepAliveIdle caddy.Duration `json:"keepalive_idle,omitempty"`
|
||||
|
||||
// KeepAliveCount is the maximum number of TCP keep-alive probes that
|
||||
// should be sent before dropping a connection.
|
||||
// If zero, the default is 9.
|
||||
// If negative, underlying socket value is unchanged.
|
||||
KeepAliveCount int `json:"keepalive_count,omitempty"`
|
||||
|
||||
// MaxHeaderBytes is the maximum size to parse from a client's
|
||||
// HTTP request headers.
|
||||
MaxHeaderBytes int `json:"max_header_bytes,omitempty"`
|
||||
@ -186,6 +202,13 @@ type Server struct {
|
||||
// This option is disabled by default.
|
||||
TrustedProxiesStrict int `json:"trusted_proxies_strict,omitempty"`
|
||||
|
||||
// If greater than zero, enables trusting socket connections
|
||||
// (e.g. Unix domain sockets) as coming from a trusted
|
||||
// proxy.
|
||||
//
|
||||
// This option is disabled by default.
|
||||
TrustedProxiesUnix bool `json:"trusted_proxies_unix,omitempty"`
|
||||
|
||||
// Enables access logging and configures how access logs are handled
|
||||
// in this server. To minimally enable access logs, simply set this
|
||||
// to a non-null, empty struct.
|
||||
@ -265,14 +288,9 @@ type Server struct {
|
||||
// ServeHTTP is the entry point for all HTTP requests.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// If there are listener wrappers that process tls connections but don't return a *tls.Conn, this field will be nil.
|
||||
// TODO: Scheduled to be removed later because https://github.com/golang/go/pull/56110 has been merged.
|
||||
if r.TLS == nil {
|
||||
// not all requests have a conn (like virtual requests) - see #5698
|
||||
if conn, ok := r.Context().Value(ConnCtxKey).(net.Conn); ok {
|
||||
if csc, ok := conn.(connectionStater); ok {
|
||||
r.TLS = new(tls.ConnectionState)
|
||||
*r.TLS = csc.ConnectionState()
|
||||
}
|
||||
if tlsConnStateFunc, ok := r.Context().Value(tlsConnectionStateFuncCtxKey).(func() *tls.ConnectionState); ok {
|
||||
r.TLS = tlsConnStateFunc()
|
||||
}
|
||||
}
|
||||
|
||||
@ -925,6 +943,17 @@ func determineTrustedProxy(r *http.Request, s *Server) (bool, string) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if s.TrustedProxiesUnix && r.RemoteAddr == "@" {
|
||||
if s.TrustedProxiesStrict > 0 {
|
||||
ipRanges := []netip.Prefix{}
|
||||
if s.trustedProxies != nil {
|
||||
ipRanges = s.trustedProxies.GetIPRanges(r)
|
||||
}
|
||||
return true, strictUntrustedClientIp(r, s.ClientIPHeaders, ipRanges, "@")
|
||||
} else {
|
||||
return true, trustedRealClientIP(r, s.ClientIPHeaders, "@")
|
||||
}
|
||||
}
|
||||
// Parse the remote IP, ignore the error as non-fatal,
|
||||
// but the remote IP is required to continue, so we
|
||||
// just return early. This should probably never happen
|
||||
@ -1081,11 +1110,14 @@ const (
|
||||
// originally came into the server's entry handler
|
||||
OriginalRequestCtxKey caddy.CtxKey = "original_request"
|
||||
|
||||
// For referencing underlying net.Conn
|
||||
// This will eventually be deprecated and not used. To refer to the underlying connection, implement a middleware plugin
|
||||
// DEPRECATED: not used anymore.
|
||||
// To refer to the underlying connection, implement a middleware plugin
|
||||
// that RegisterConnContext during provisioning.
|
||||
ConnCtxKey caddy.CtxKey = "conn"
|
||||
|
||||
// used to get the tls connection state in the context, if available
|
||||
tlsConnectionStateFuncCtxKey caddy.CtxKey = "tls_connection_state_func"
|
||||
|
||||
// For tracking whether the client is a trusted proxy
|
||||
TrustedProxyVarKey string = "trusted_proxy"
|
||||
|
||||
|
||||
@ -297,6 +297,39 @@ func TestServer_DetermineTrustedProxy_TrustedLoopback(t *testing.T) {
|
||||
assert.Equal(t, clientIP, "31.40.0.10")
|
||||
}
|
||||
|
||||
func TestServer_DetermineTrustedProxy_UnixSocket(t *testing.T) {
|
||||
server := &Server{
|
||||
ClientIPHeaders: []string{"X-Forwarded-For"},
|
||||
TrustedProxiesUnix: true,
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "@"
|
||||
req.Header.Set("X-Forwarded-For", "2.2.2.2, 3.3.3.3")
|
||||
|
||||
trusted, clientIP := determineTrustedProxy(req, server)
|
||||
|
||||
assert.True(t, trusted)
|
||||
assert.Equal(t, "2.2.2.2", clientIP)
|
||||
}
|
||||
|
||||
func TestServer_DetermineTrustedProxy_UnixSocketStrict(t *testing.T) {
|
||||
server := &Server{
|
||||
ClientIPHeaders: []string{"X-Forwarded-For"},
|
||||
TrustedProxiesUnix: true,
|
||||
TrustedProxiesStrict: 1,
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "@"
|
||||
req.Header.Set("X-Forwarded-For", "2.2.2.2, 3.3.3.3")
|
||||
|
||||
trusted, clientIP := determineTrustedProxy(req, server)
|
||||
|
||||
assert.True(t, trusted)
|
||||
assert.Equal(t, "3.3.3.3", clientIP)
|
||||
}
|
||||
|
||||
func TestServer_DetermineTrustedProxy_UntrustedPrefix(t *testing.T) {
|
||||
loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8")
|
||||
|
||||
|
||||
@ -79,7 +79,7 @@ Response headers may be added using the --header flag for each header field.
|
||||
cmd.Flags().StringP("body", "b", "", "The body of the HTTP response")
|
||||
cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
|
||||
cmd.Flags().BoolP("debug", "v", false, "Enable more verbose debug-level logging")
|
||||
cmd.Flags().StringSliceP("header", "H", []string{}, "Set a header on the response (format: \"Field: value\")")
|
||||
cmd.Flags().StringArrayP("header", "H", []string{}, "Set a header on the response (format: \"Field: value\")")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdRespond)
|
||||
},
|
||||
})
|
||||
@ -359,7 +359,7 @@ func cmdRespond(fl caddycmd.Flags) (int, error) {
|
||||
}
|
||||
|
||||
// build headers map
|
||||
headers, err := fl.GetStringSlice("header")
|
||||
headers, err := fl.GetStringArray("header")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
|
||||
}
|
||||
|
||||
@ -220,13 +220,13 @@ func (a *adminAPI) getCAFromAPIRequestPath(r *http.Request) (*CA, error) {
|
||||
func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) {
|
||||
root, err = pemEncodeCert(ca.RootCertificate().Raw)
|
||||
if err != nil {
|
||||
return
|
||||
return root, inter, err
|
||||
}
|
||||
inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw)
|
||||
if err != nil {
|
||||
return
|
||||
return root, inter, err
|
||||
}
|
||||
return
|
||||
return root, inter, err
|
||||
}
|
||||
|
||||
// caInfo is the response structure for the CA info API endpoint.
|
||||
|
||||
@ -124,8 +124,6 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error {
|
||||
}
|
||||
if ca.IntermediateLifetime == 0 {
|
||||
ca.IntermediateLifetime = caddy.Duration(defaultIntermediateLifetime)
|
||||
} else if time.Duration(ca.IntermediateLifetime) >= defaultRootLifetime {
|
||||
return fmt.Errorf("intermediate certificate lifetime must be less than root certificate lifetime (%s)", defaultRootLifetime)
|
||||
}
|
||||
|
||||
// load the certs and key that will be used for signing
|
||||
@ -144,6 +142,10 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actualRootLifetime := time.Until(rootCert.NotAfter)
|
||||
if time.Duration(ca.IntermediateLifetime) >= actualRootLifetime {
|
||||
return fmt.Errorf("intermediate certificate lifetime must be less than actual root certificate lifetime (%s)", actualRootLifetime)
|
||||
}
|
||||
if ca.Intermediate != nil {
|
||||
interCert, interKey, err = ca.Intermediate.Load()
|
||||
} else {
|
||||
|
||||
@ -22,9 +22,11 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DeRuina/timberjack"
|
||||
"github.com/dustin/go-humanize"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
@ -96,6 +98,21 @@ type FileWriter struct {
|
||||
// it will be rotated.
|
||||
RollSizeMB int `json:"roll_size_mb,omitempty"`
|
||||
|
||||
// Roll log file after some time
|
||||
RollInterval time.Duration `json:"roll_interval,omitempty"`
|
||||
|
||||
// Roll log file at fix minutes
|
||||
// For example []int{0, 30} will roll file at xx:00 and xx:30 each hour
|
||||
// Invalid value are ignored with a warning on stderr
|
||||
// See https://github.com/DeRuina/timberjack#%EF%B8%8F-rotation-notes--warnings for caveats
|
||||
RollAtMinutes []int `json:"roll_minutes,omitempty"`
|
||||
|
||||
// Roll log file at fix time
|
||||
// For example []string{"00:00", "12:00"} will roll file at 00:00 and 12:00 each day
|
||||
// Invalid value are ignored with a warning on stderr
|
||||
// See https://github.com/DeRuina/timberjack#%EF%B8%8F-rotation-notes--warnings for caveats
|
||||
RollAt []string `json:"roll_at,omitempty"`
|
||||
|
||||
// Whether to compress rolled files. Default: true
|
||||
RollCompress *bool `json:"roll_gzip,omitempty"`
|
||||
|
||||
@ -109,6 +126,11 @@ type FileWriter struct {
|
||||
|
||||
// How many days to keep rolled log files. Default: 90
|
||||
RollKeepDays int `json:"roll_keep_days,omitempty"`
|
||||
|
||||
// Rotated file will have format <logfilename>-<format>-<criterion>.log
|
||||
// Optional. If unset or invalid, defaults to 2006-01-02T15-04-05.000 (with fallback warning)
|
||||
// <format> must be a Go time compatible format, see https://pkg.go.dev/time#pkg-constants
|
||||
BackupTimeFormat string `json:"backup_time_format,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
@ -156,7 +178,7 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
|
||||
roll := fw.Roll == nil || *fw.Roll
|
||||
|
||||
// create the file if it does not exist; create with the configured mode, or default
|
||||
// to restrictive if not set. (lumberjack will reuse the file mode across log rotation)
|
||||
// to restrictive if not set. (timberjack will reuse the file mode across log rotation)
|
||||
if err := os.MkdirAll(filepath.Dir(fw.Filename), 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -166,7 +188,7 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if roll {
|
||||
file.Close() // lumberjack will reopen it on its own
|
||||
file.Close() // timberjack will reopen it on its own
|
||||
}
|
||||
|
||||
// Ensure already existing files have the right mode, since OpenFile will not set the mode in such case.
|
||||
@ -201,13 +223,17 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
|
||||
if fw.RollKeepDays == 0 {
|
||||
fw.RollKeepDays = 90
|
||||
}
|
||||
return &lumberjack.Logger{
|
||||
Filename: fw.Filename,
|
||||
MaxSize: fw.RollSizeMB,
|
||||
MaxAge: fw.RollKeepDays,
|
||||
MaxBackups: fw.RollKeep,
|
||||
LocalTime: fw.RollLocalTime,
|
||||
Compress: *fw.RollCompress,
|
||||
return &timberjack.Logger{
|
||||
Filename: fw.Filename,
|
||||
MaxSize: fw.RollSizeMB,
|
||||
MaxAge: fw.RollKeepDays,
|
||||
MaxBackups: fw.RollKeep,
|
||||
LocalTime: fw.RollLocalTime,
|
||||
Compress: *fw.RollCompress,
|
||||
RotationInterval: fw.RollInterval,
|
||||
RotateAtMinutes: fw.RollAtMinutes,
|
||||
RotateAt: fw.RollAt,
|
||||
BackupTimeFormat: fw.BackupTimeFormat,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -314,6 +340,53 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
}
|
||||
fw.RollKeepDays = int(math.Ceil(keepFor.Hours() / 24))
|
||||
|
||||
case "roll_interval":
|
||||
var durationStr string
|
||||
if !d.AllArgs(&durationStr) {
|
||||
return d.ArgErr()
|
||||
}
|
||||
duration, err := time.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
return d.Errf("parsing roll_interval duration: %v", err)
|
||||
}
|
||||
fw.RollInterval = duration
|
||||
|
||||
case "roll_minutes":
|
||||
var minutesArrayStr string
|
||||
if !d.AllArgs(&minutesArrayStr) {
|
||||
return d.ArgErr()
|
||||
}
|
||||
minutesStr := strings.Split(minutesArrayStr, ",")
|
||||
minutes := make([]int, len(minutesStr))
|
||||
for i := range minutesStr {
|
||||
ms := strings.Trim(minutesStr[i], " ")
|
||||
m, err := strconv.Atoi(ms)
|
||||
if err != nil {
|
||||
return d.Errf("parsing roll_minutes number: %v", err)
|
||||
}
|
||||
minutes[i] = m
|
||||
}
|
||||
fw.RollAtMinutes = minutes
|
||||
|
||||
case "roll_at":
|
||||
var timeArrayStr string
|
||||
if !d.AllArgs(&timeArrayStr) {
|
||||
return d.ArgErr()
|
||||
}
|
||||
timeStr := strings.Split(timeArrayStr, ",")
|
||||
times := make([]string, len(timeStr))
|
||||
for i := range timeStr {
|
||||
times[i] = strings.Trim(timeStr[i], " ")
|
||||
}
|
||||
fw.RollAt = times
|
||||
|
||||
case "backup_time_format":
|
||||
var format string
|
||||
if !d.AllArgs(&format) {
|
||||
return d.ArgErr()
|
||||
}
|
||||
fw.BackupTimeFormat = format
|
||||
|
||||
default:
|
||||
return d.Errf("unrecognized subdirective '%s'", d.Val())
|
||||
}
|
||||
|
||||
@ -152,6 +152,9 @@ func (fe *FilterEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error {
|
||||
func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
d.Next() // consume encoder name
|
||||
|
||||
// Track regexp filters for automatic merging
|
||||
regexpFilters := make(map[string][]*RegexpFilter)
|
||||
|
||||
// parse a field
|
||||
parseField := func() error {
|
||||
if fe.FieldsRaw == nil {
|
||||
@ -171,6 +174,23 @@ func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
if !ok {
|
||||
return d.Errf("module %s (%T) is not a logging.LogFieldFilter", moduleID, unm)
|
||||
}
|
||||
|
||||
// Special handling for regexp filters to support multiple instances
|
||||
if regexpFilter, isRegexp := filter.(*RegexpFilter); isRegexp {
|
||||
regexpFilters[field] = append(regexpFilters[field], regexpFilter)
|
||||
return nil // Don't set FieldsRaw yet, we'll merge them later
|
||||
}
|
||||
|
||||
// Check if we're trying to add a non-regexp filter to a field that already has regexp filters
|
||||
if _, hasRegexpFilters := regexpFilters[field]; hasRegexpFilters {
|
||||
return d.Errf("cannot mix regexp filters with other filter types for field %s", field)
|
||||
}
|
||||
|
||||
// Check if field already has a filter and it's not regexp-related
|
||||
if _, exists := fe.FieldsRaw[field]; exists {
|
||||
return d.Errf("field %s already has a filter; multiple non-regexp filters per field are not supported", field)
|
||||
}
|
||||
|
||||
fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filter, "filter", filterName, nil)
|
||||
return nil
|
||||
}
|
||||
@ -210,6 +230,25 @@ func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After parsing all fields, merge multiple regexp filters into MultiRegexpFilter
|
||||
for field, filters := range regexpFilters {
|
||||
if len(filters) == 1 {
|
||||
// Single regexp filter, use the original RegexpFilter
|
||||
fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filters[0], "filter", "regexp", nil)
|
||||
} else {
|
||||
// Multiple regexp filters, merge into MultiRegexpFilter
|
||||
multiFilter := &MultiRegexpFilter{}
|
||||
for _, regexpFilter := range filters {
|
||||
err := multiFilter.AddOperation(regexpFilter.RawRegexp, regexpFilter.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding regexp operation for field %s: %v", field, err)
|
||||
}
|
||||
}
|
||||
fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(multiFilter, "filter", "multi_regexp", nil)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -41,6 +41,7 @@ func init() {
|
||||
caddy.RegisterModule(CookieFilter{})
|
||||
caddy.RegisterModule(RegexpFilter{})
|
||||
caddy.RegisterModule(RenameFilter{})
|
||||
caddy.RegisterModule(MultiRegexpFilter{})
|
||||
}
|
||||
|
||||
// LogFieldFilter can filter (or manipulate)
|
||||
@ -625,6 +626,222 @@ func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||
return in
|
||||
}
|
||||
|
||||
// regexpFilterOperation represents a single regexp operation
|
||||
// within a MultiRegexpFilter.
|
||||
type regexpFilterOperation struct {
|
||||
// The regular expression pattern defining what to replace.
|
||||
RawRegexp string `json:"regexp,omitempty"`
|
||||
|
||||
// The value to use as replacement
|
||||
Value string `json:"value,omitempty"`
|
||||
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// MultiRegexpFilter is a Caddy log field filter that
|
||||
// can apply multiple regular expression replacements to
|
||||
// the same field. This filter processes operations in the
|
||||
// order they are defined, applying each regexp replacement
|
||||
// sequentially to the result of the previous operation.
|
||||
//
|
||||
// This allows users to define multiple regexp filters for
|
||||
// the same field without them overwriting each other.
|
||||
//
|
||||
// Security considerations:
|
||||
// - Uses Go's regexp package (RE2 engine) which is safe from ReDoS attacks
|
||||
// - Validates all patterns during provisioning
|
||||
// - Limits the maximum number of operations to prevent resource exhaustion
|
||||
// - Sanitizes input to prevent injection attacks
|
||||
type MultiRegexpFilter struct {
|
||||
// A list of regexp operations to apply in sequence.
|
||||
// Maximum of 50 operations allowed for security and performance.
|
||||
Operations []regexpFilterOperation `json:"operations"`
|
||||
}
|
||||
|
||||
// Security constants
|
||||
const (
|
||||
maxRegexpOperations = 50 // Maximum operations to prevent resource exhaustion
|
||||
maxPatternLength = 1000 // Maximum pattern length to prevent abuse
|
||||
)
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (MultiRegexpFilter) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "caddy.logging.encoders.filter.multi_regexp",
|
||||
New: func() caddy.Module { return new(MultiRegexpFilter) },
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
|
||||
// Syntax:
|
||||
//
|
||||
// multi_regexp {
|
||||
// regexp <pattern> <replacement>
|
||||
// regexp <pattern> <replacement>
|
||||
// ...
|
||||
// }
|
||||
func (f *MultiRegexpFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
d.Next() // consume filter name
|
||||
for d.NextBlock(0) {
|
||||
switch d.Val() {
|
||||
case "regexp":
|
||||
// Security check: limit number of operations
|
||||
if len(f.Operations) >= maxRegexpOperations {
|
||||
return d.Errf("too many regexp operations (maximum %d allowed)", maxRegexpOperations)
|
||||
}
|
||||
|
||||
op := regexpFilterOperation{}
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
op.RawRegexp = d.Val()
|
||||
|
||||
// Security validation: check pattern length
|
||||
if len(op.RawRegexp) > maxPatternLength {
|
||||
return d.Errf("regexp pattern too long (maximum %d characters)", maxPatternLength)
|
||||
}
|
||||
|
||||
// Security validation: basic pattern validation
|
||||
if op.RawRegexp == "" {
|
||||
return d.Errf("regexp pattern cannot be empty")
|
||||
}
|
||||
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
op.Value = d.Val()
|
||||
f.Operations = append(f.Operations, op)
|
||||
default:
|
||||
return d.Errf("unrecognized subdirective %s", d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
// Security check: ensure at least one operation is defined
|
||||
if len(f.Operations) == 0 {
|
||||
return d.Err("multi_regexp filter requires at least one regexp operation")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Provision compiles all regexp patterns with security validation.
|
||||
func (f *MultiRegexpFilter) Provision(ctx caddy.Context) error {
|
||||
// Security check: validate operation count
|
||||
if len(f.Operations) > maxRegexpOperations {
|
||||
return fmt.Errorf("too many regexp operations: %d (maximum %d allowed)", len(f.Operations), maxRegexpOperations)
|
||||
}
|
||||
|
||||
if len(f.Operations) == 0 {
|
||||
return fmt.Errorf("multi_regexp filter requires at least one operation")
|
||||
}
|
||||
|
||||
for i := range f.Operations {
|
||||
// Security validation: pattern length check
|
||||
if len(f.Operations[i].RawRegexp) > maxPatternLength {
|
||||
return fmt.Errorf("regexp pattern %d too long: %d characters (maximum %d)", i, len(f.Operations[i].RawRegexp), maxPatternLength)
|
||||
}
|
||||
|
||||
// Security validation: empty pattern check
|
||||
if f.Operations[i].RawRegexp == "" {
|
||||
return fmt.Errorf("regexp pattern %d cannot be empty", i)
|
||||
}
|
||||
|
||||
// Compile and validate the pattern (uses RE2 engine - safe from ReDoS)
|
||||
r, err := regexp.Compile(f.Operations[i].RawRegexp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling regexp pattern %d (%s): %v", i, f.Operations[i].RawRegexp, err)
|
||||
}
|
||||
f.Operations[i].regexp = r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate ensures the filter is properly configured with security checks.
|
||||
func (f *MultiRegexpFilter) Validate() error {
|
||||
if len(f.Operations) == 0 {
|
||||
return fmt.Errorf("multi_regexp filter requires at least one operation")
|
||||
}
|
||||
|
||||
if len(f.Operations) > maxRegexpOperations {
|
||||
return fmt.Errorf("too many regexp operations: %d (maximum %d allowed)", len(f.Operations), maxRegexpOperations)
|
||||
}
|
||||
|
||||
for i, op := range f.Operations {
|
||||
if op.RawRegexp == "" {
|
||||
return fmt.Errorf("regexp pattern %d cannot be empty", i)
|
||||
}
|
||||
if len(op.RawRegexp) > maxPatternLength {
|
||||
return fmt.Errorf("regexp pattern %d too long: %d characters (maximum %d)", i, len(op.RawRegexp), maxPatternLength)
|
||||
}
|
||||
if op.regexp == nil {
|
||||
return fmt.Errorf("regexp pattern %d not compiled (call Provision first)", i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter applies all regexp operations sequentially to the input field.
|
||||
// Input is sanitized and validated for security.
|
||||
func (f *MultiRegexpFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
|
||||
newArray := make(caddyhttp.LoggableStringArray, len(array))
|
||||
for i, s := range array {
|
||||
newArray[i] = f.processString(s)
|
||||
}
|
||||
in.Interface = newArray
|
||||
} else {
|
||||
in.String = f.processString(in.String)
|
||||
}
|
||||
|
||||
return in
|
||||
}
|
||||
|
||||
// processString applies all regexp operations to a single string with input validation.
|
||||
func (f *MultiRegexpFilter) processString(s string) string {
|
||||
// Security: validate input string length to prevent resource exhaustion
|
||||
const maxInputLength = 1000000 // 1MB max input size
|
||||
if len(s) > maxInputLength {
|
||||
// Log warning but continue processing (truncated)
|
||||
s = s[:maxInputLength]
|
||||
}
|
||||
|
||||
result := s
|
||||
for _, op := range f.Operations {
|
||||
// Each regexp operation is applied sequentially
|
||||
// Using RE2 engine which is safe from ReDoS attacks
|
||||
result = op.regexp.ReplaceAllString(result, op.Value)
|
||||
|
||||
// Ensure result doesn't exceed max length after each operation
|
||||
if len(result) > maxInputLength {
|
||||
result = result[:maxInputLength]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// AddOperation adds a single regexp operation to the filter with validation.
|
||||
// This is used when merging multiple RegexpFilter instances.
|
||||
func (f *MultiRegexpFilter) AddOperation(rawRegexp, value string) error {
|
||||
// Security checks
|
||||
if len(f.Operations) >= maxRegexpOperations {
|
||||
return fmt.Errorf("cannot add operation: maximum %d operations allowed", maxRegexpOperations)
|
||||
}
|
||||
|
||||
if rawRegexp == "" {
|
||||
return fmt.Errorf("regexp pattern cannot be empty")
|
||||
}
|
||||
|
||||
if len(rawRegexp) > maxPatternLength {
|
||||
return fmt.Errorf("regexp pattern too long: %d characters (maximum %d)", len(rawRegexp), maxPatternLength)
|
||||
}
|
||||
|
||||
f.Operations = append(f.Operations, regexpFilterOperation{
|
||||
RawRegexp: rawRegexp,
|
||||
Value: value,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenameFilter is a Caddy log field filter that
|
||||
// renames the field's key with the indicated name.
|
||||
type RenameFilter struct {
|
||||
@ -664,6 +881,7 @@ var (
|
||||
_ LogFieldFilter = (*CookieFilter)(nil)
|
||||
_ LogFieldFilter = (*RegexpFilter)(nil)
|
||||
_ LogFieldFilter = (*RenameFilter)(nil)
|
||||
_ LogFieldFilter = (*MultiRegexpFilter)(nil)
|
||||
|
||||
_ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
|
||||
_ caddyfile.Unmarshaler = (*HashFilter)(nil)
|
||||
@ -673,9 +891,12 @@ var (
|
||||
_ caddyfile.Unmarshaler = (*CookieFilter)(nil)
|
||||
_ caddyfile.Unmarshaler = (*RegexpFilter)(nil)
|
||||
_ caddyfile.Unmarshaler = (*RenameFilter)(nil)
|
||||
_ caddyfile.Unmarshaler = (*MultiRegexpFilter)(nil)
|
||||
|
||||
_ caddy.Provisioner = (*IPMaskFilter)(nil)
|
||||
_ caddy.Provisioner = (*RegexpFilter)(nil)
|
||||
_ caddy.Provisioner = (*MultiRegexpFilter)(nil)
|
||||
|
||||
_ caddy.Validator = (*QueryFilter)(nil)
|
||||
_ caddy.Validator = (*MultiRegexpFilter)(nil)
|
||||
)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
@ -239,3 +241,198 @@ func TestHashFilterMultiValue(t *testing.T) {
|
||||
t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiRegexpFilterSingleOperation(t *testing.T) {
|
||||
f := MultiRegexpFilter{
|
||||
Operations: []regexpFilterOperation{
|
||||
{RawRegexp: `secret`, Value: "REDACTED"},
|
||||
},
|
||||
}
|
||||
err := f.Provision(caddy.Context{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error provisioning: %v", err)
|
||||
}
|
||||
|
||||
out := f.Filter(zapcore.Field{String: "foo-secret-bar"})
|
||||
if out.String != "foo-REDACTED-bar" {
|
||||
t.Fatalf("field has not been filtered: %s", out.String)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiRegexpFilterMultipleOperations(t *testing.T) {
|
||||
f := MultiRegexpFilter{
|
||||
Operations: []regexpFilterOperation{
|
||||
{RawRegexp: `secret`, Value: "REDACTED"},
|
||||
{RawRegexp: `password`, Value: "HIDDEN"},
|
||||
{RawRegexp: `token`, Value: "XXX"},
|
||||
},
|
||||
}
|
||||
err := f.Provision(caddy.Context{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error provisioning: %v", err)
|
||||
}
|
||||
|
||||
// Test sequential application
|
||||
out := f.Filter(zapcore.Field{String: "my-secret-password-token-data"})
|
||||
expected := "my-REDACTED-HIDDEN-XXX-data"
|
||||
if out.String != expected {
|
||||
t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiRegexpFilterMultiValue(t *testing.T) {
|
||||
f := MultiRegexpFilter{
|
||||
Operations: []regexpFilterOperation{
|
||||
{RawRegexp: `secret`, Value: "REDACTED"},
|
||||
{RawRegexp: `\d+`, Value: "NUM"},
|
||||
},
|
||||
}
|
||||
err := f.Provision(caddy.Context{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error provisioning: %v", err)
|
||||
}
|
||||
|
||||
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
|
||||
"foo-secret-123",
|
||||
"bar-secret-456",
|
||||
}})
|
||||
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
|
||||
if !ok {
|
||||
t.Fatalf("field is wrong type: %T", out.Interface)
|
||||
}
|
||||
if arr[0] != "foo-REDACTED-NUM" {
|
||||
t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
|
||||
}
|
||||
if arr[1] != "bar-REDACTED-NUM" {
|
||||
t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiRegexpFilterAddOperation(t *testing.T) {
|
||||
f := MultiRegexpFilter{}
|
||||
err := f.AddOperation("secret", "REDACTED")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error adding operation: %v", err)
|
||||
}
|
||||
err = f.AddOperation("password", "HIDDEN")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error adding operation: %v", err)
|
||||
}
|
||||
err = f.Provision(caddy.Context{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error provisioning: %v", err)
|
||||
}
|
||||
|
||||
if len(f.Operations) != 2 {
|
||||
t.Fatalf("expected 2 operations, got %d", len(f.Operations))
|
||||
}
|
||||
|
||||
out := f.Filter(zapcore.Field{String: "my-secret-password"})
|
||||
expected := "my-REDACTED-HIDDEN"
|
||||
if out.String != expected {
|
||||
t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiRegexpFilterSecurityLimits(t *testing.T) {
|
||||
f := MultiRegexpFilter{}
|
||||
|
||||
// Test maximum operations limit
|
||||
for i := 0; i < 51; i++ {
|
||||
err := f.AddOperation(fmt.Sprintf("pattern%d", i), "replacement")
|
||||
if i < 50 {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error adding operation %d: %v", i, err)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error when adding operation %d (exceeds limit)", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test empty pattern validation
|
||||
f2 := MultiRegexpFilter{}
|
||||
err := f2.AddOperation("", "replacement")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for empty pattern")
|
||||
}
|
||||
|
||||
// Test pattern length limit
|
||||
f3 := MultiRegexpFilter{}
|
||||
longPattern := strings.Repeat("a", 1001)
|
||||
err = f3.AddOperation(longPattern, "replacement")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for pattern exceeding length limit")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiRegexpFilterValidation(t *testing.T) {
|
||||
// Test validation with empty operations
|
||||
f := MultiRegexpFilter{}
|
||||
err := f.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error for empty operations")
|
||||
}
|
||||
|
||||
// Test validation with valid operations
|
||||
err = f.AddOperation("valid", "replacement")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error adding operation: %v", err)
|
||||
}
|
||||
err = f.Provision(caddy.Context{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error provisioning: %v", err)
|
||||
}
|
||||
err = f.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected validation error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiRegexpFilterInputSizeLimit(t *testing.T) {
|
||||
f := MultiRegexpFilter{
|
||||
Operations: []regexpFilterOperation{
|
||||
{RawRegexp: `test`, Value: "REPLACED"},
|
||||
},
|
||||
}
|
||||
err := f.Provision(caddy.Context{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error provisioning: %v", err)
|
||||
}
|
||||
|
||||
// Test with very large input (should be truncated)
|
||||
largeInput := strings.Repeat("test", 300000) // Creates ~1.2MB string
|
||||
out := f.Filter(zapcore.Field{String: largeInput})
|
||||
|
||||
// The input should be truncated to 1MB and still processed
|
||||
if len(out.String) > 1000000 {
|
||||
t.Fatalf("output string not truncated: length %d", len(out.String))
|
||||
}
|
||||
|
||||
// Should still contain replacements within the truncated portion
|
||||
if !strings.Contains(out.String, "REPLACED") {
|
||||
t.Fatalf("replacements not applied to truncated input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiRegexpFilterOverlappingPatterns(t *testing.T) {
|
||||
f := MultiRegexpFilter{
|
||||
Operations: []regexpFilterOperation{
|
||||
{RawRegexp: `secret.*password`, Value: "SENSITIVE"},
|
||||
{RawRegexp: `password`, Value: "HIDDEN"},
|
||||
},
|
||||
}
|
||||
err := f.Provision(caddy.Context{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error provisioning: %v", err)
|
||||
}
|
||||
|
||||
// The first pattern should match and replace the entire "secret...password" portion
|
||||
// Then the second pattern should not find "password" anymore since it was already replaced
|
||||
out := f.Filter(zapcore.Field{String: "my-secret-data-password-end"})
|
||||
expected := "my-SENSITIVE-end"
|
||||
if out.String != expected {
|
||||
t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) {
|
||||
reconn.connMu.RUnlock()
|
||||
if conn != nil {
|
||||
if n, err = conn.Write(b); err == nil {
|
||||
return
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,7 +184,7 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) {
|
||||
// one of them might have already re-dialed by now; try writing again
|
||||
if reconn.Conn != nil {
|
||||
if n, err = reconn.Conn.Write(b); err == nil {
|
||||
return
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,7 +198,7 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) {
|
||||
if err2 != nil {
|
||||
// logger socket still offline; instead of discarding the log, dump it to stderr
|
||||
os.Stderr.Write(b)
|
||||
return
|
||||
return n, err
|
||||
}
|
||||
if n, err = conn2.Write(b); err == nil {
|
||||
if reconn.Conn != nil {
|
||||
@ -211,7 +211,7 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) {
|
||||
os.Stderr.Write(b)
|
||||
}
|
||||
|
||||
return
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (reconn *redialerConn) dial() (net.Conn, error) {
|
||||
|
||||
@ -18,6 +18,7 @@ package caddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@ -48,7 +49,31 @@ func trapSignalsPosix() {
|
||||
exitProcessFromSignal("SIGTERM")
|
||||
|
||||
case syscall.SIGUSR1:
|
||||
Log().Info("not implemented", zap.String("signal", "SIGUSR1"))
|
||||
logger := Log().With(zap.String("signal", "SIGUSR1"))
|
||||
// If we know the last source config file/adapter (set when starting
|
||||
// via `caddy run --config <file> --adapter <adapter>`), attempt
|
||||
// to reload from that source. Otherwise, ignore the signal.
|
||||
file, adapter, reloadCallback := getLastConfig()
|
||||
if file == "" {
|
||||
logger.Info("last config unknown, ignored SIGUSR1")
|
||||
break
|
||||
}
|
||||
logger = logger.With(
|
||||
zap.String("file", file),
|
||||
zap.String("adapter", adapter))
|
||||
if reloadCallback == nil {
|
||||
logger.Warn("no reload helper available, ignored SIGUSR1")
|
||||
break
|
||||
}
|
||||
logger.Info("reloading config from last-known source")
|
||||
if err := reloadCallback(file, adapter); errors.Is(err, errReloadFromSourceUnavailable) {
|
||||
// No reload helper available (likely not started via caddy run).
|
||||
logger.Warn("reload from source unavailable in this process; ignored SIGUSR1")
|
||||
} else if err != nil {
|
||||
logger.Error("failed to reload config from file", zap.Error(err))
|
||||
} else {
|
||||
logger.Info("successfully reloaded config from file")
|
||||
}
|
||||
|
||||
case syscall.SIGUSR2:
|
||||
Log().Info("not implemented", zap.String("signal", "SIGUSR2"))
|
||||
|
||||
@ -106,7 +106,7 @@ func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loade
|
||||
}
|
||||
upv.Unlock()
|
||||
}
|
||||
return
|
||||
return value, loaded, err
|
||||
}
|
||||
|
||||
// LoadOrStore loads the value associated with key from the pool if it
|
||||
@ -134,7 +134,7 @@ func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) {
|
||||
up.Unlock()
|
||||
value = val
|
||||
}
|
||||
return
|
||||
return value, loaded
|
||||
}
|
||||
|
||||
// Range iterates the pool similarly to how sync.Map.Range() does:
|
||||
@ -191,7 +191,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) {
|
||||
upv.value, upv.refs))
|
||||
}
|
||||
}
|
||||
return
|
||||
return deleted, err
|
||||
}
|
||||
|
||||
// References returns the number of references (count of usages) to a
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user