From b3f2db233b5a71df6680b43d025bdb62ee2a999c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 12 Nov 2025 21:29:47 +0100 Subject: [PATCH 001/206] core: custom slog handlers for modules (log contextual data) (#7346) --- context.go | 49 ++++++++++++++++++---- modules/caddyhttp/logging.go | 79 +++++++++++++++++++++++++++++++++++- modules/caddyhttp/server.go | 8 ++-- 3 files changed, 125 insertions(+), 11 deletions(-) diff --git a/context.go b/context.go index 4c1139936..095598682 100644 --- a/context.go +++ b/context.go @@ -21,12 +21,14 @@ import ( "log" "log/slog" "reflect" + "sync" "github.com/caddyserver/certmagic" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "go.uber.org/zap" "go.uber.org/zap/exp/zapslog" + "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2/internal/filesystems" ) @@ -583,24 +585,57 @@ func (ctx Context) Logger(module ...Module) *zap.Logger { return ctx.cfg.Logging.Logger(mod) } +type slogHandlerFactory func(handler slog.Handler, core zapcore.Core, moduleID string) slog.Handler + +var ( + slogHandlerFactories []slogHandlerFactory + slogHandlerFactoriesMu sync.RWMutex +) + +// RegisterSlogHandlerFactory allows modules to register custom log/slog.Handler, +// for instance, to add contextual data to the logs. +func RegisterSlogHandlerFactory(factory slogHandlerFactory) { + slogHandlerFactoriesMu.Lock() + slogHandlerFactories = append(slogHandlerFactories, factory) + slogHandlerFactoriesMu.Unlock() +} + // Slogger returns a slog logger that is intended for use by // the most recent module associated with the context. func (ctx Context) Slogger() *slog.Logger { + var ( + handler slog.Handler + core zapcore.Core + moduleID string + ) if ctx.cfg == nil { // often the case in tests; just use a dev logger l, err := zap.NewDevelopment() if err != nil { panic("config missing, unable to create dev logger: " + err.Error()) } - return slog.New(zapslog.NewHandler(l.Core())) + + core = l.Core() + handler = zapslog.NewHandler(core) + } else { + mod := ctx.Module() + if mod == nil { + core = Log().Core() + handler = zapslog.NewHandler(core) + } else { + moduleID = string(mod.CaddyModule().ID) + core = ctx.cfg.Logging.Logger(mod).Core() + handler = zapslog.NewHandler(core, zapslog.WithName(moduleID)) + } } - mod := ctx.Module() - if mod == nil { - return slog.New(zapslog.NewHandler(Log().Core())) + + slogHandlerFactoriesMu.RLock() + for _, f := range slogHandlerFactories { + handler = f(handler, core, moduleID) } - return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(), - zapslog.WithName(string(mod.CaddyModule().ID)), - )) + slogHandlerFactoriesMu.RUnlock() + + return slog.New(handler) } // Modules returns the lineage of modules that this context provisioned, diff --git a/modules/caddyhttp/logging.go b/modules/caddyhttp/logging.go index e8a1316bd..b937a6f1e 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -15,18 +15,28 @@ package caddyhttp import ( + "context" "encoding/json" "errors" + "log/slog" "net" "net/http" "strings" + "sync" "go.uber.org/zap" + "go.uber.org/zap/exp/zapslog" "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2" ) +func init() { + caddy.RegisterSlogHandlerFactory(func(handler slog.Handler, core zapcore.Core, moduleID string) slog.Handler { + return &extraFieldsSlogHandler{defaultHandler: handler, core: core, moduleID: moduleID} + }) +} + // ServerLogConfig describes a server's logging configuration. If // enabled without customization, all requests to this server are // logged to the default logger; logger destinations may be @@ -223,17 +233,21 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi // ExtraLogFields is a list of extra fields to log with every request. type ExtraLogFields struct { - fields []zapcore.Field + fields []zapcore.Field + handlers sync.Map } // Add adds a field to the list of extra fields to log. func (e *ExtraLogFields) Add(field zap.Field) { + e.handlers.Clear() e.fields = append(e.fields, field) } // Set sets a field in the list of extra fields to log. // If the field already exists, it is replaced. func (e *ExtraLogFields) Set(field zap.Field) { + e.handlers.Clear() + for i := range e.fields { if e.fields[i].Key == field.Key { e.fields[i] = field @@ -243,6 +257,29 @@ func (e *ExtraLogFields) Set(field zap.Field) { e.fields = append(e.fields, field) } +func (e *ExtraLogFields) getSloggerHandler(handler *extraFieldsSlogHandler) (h slog.Handler) { + if existing, ok := e.handlers.Load(handler); ok { + return existing.(slog.Handler) + } + + if handler.moduleID == "" { + h = zapslog.NewHandler(handler.core.With(e.fields)) + } else { + h = zapslog.NewHandler(handler.core.With(e.fields), zapslog.WithName(handler.moduleID)) + } + + if handler.group != "" { + h = h.WithGroup(handler.group) + } + if handler.attrs != nil { + h = h.WithAttrs(handler.attrs) + } + + e.handlers.Store(handler, h) + + return h +} + const ( // Variable name used to indicate that this request // should be omitted from the access logs @@ -254,3 +291,43 @@ const ( // Variable name used to indicate the logger to be used AccessLoggerNameVarKey string = "access_logger_names" ) + +type extraFieldsSlogHandler struct { + defaultHandler slog.Handler + core zapcore.Core + moduleID string + group string + attrs []slog.Attr +} + +func (e *extraFieldsSlogHandler) Enabled(ctx context.Context, level slog.Level) bool { + return e.defaultHandler.Enabled(ctx, level) +} + +func (e *extraFieldsSlogHandler) Handle(ctx context.Context, record slog.Record) error { + if elf, ok := ctx.Value(ExtraLogFieldsCtxKey).(*ExtraLogFields); ok { + return elf.getSloggerHandler(e).Handle(ctx, record) + } + + return e.defaultHandler.Handle(ctx, record) +} + +func (e *extraFieldsSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &extraFieldsSlogHandler{ + e.defaultHandler.WithAttrs(attrs), + e.core, + e.moduleID, + e.group, + append(e.attrs, attrs...), + } +} + +func (e *extraFieldsSlogHandler) WithGroup(name string) slog.Handler { + return &extraFieldsSlogHandler{ + e.defaultHandler.WithGroup(name), + e.core, + e.moduleID, + name, + e.attrs, + } +} diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index a5f740170..94b8febfa 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -793,8 +793,10 @@ func (s *Server) logRequest( accLog *zap.Logger, r *http.Request, wrec ResponseRecorder, duration *time.Duration, repl *caddy.Replacer, bodyReader *lengthReader, shouldLogCredentials bool, ) { + ctx := r.Context() + // this request may be flagged as omitted from the logs - if skip, ok := GetVar(r.Context(), LogSkipVar).(bool); ok && skip { + if skip, ok := GetVar(ctx, LogSkipVar).(bool); ok && skip { return } @@ -812,7 +814,7 @@ func (s *Server) logRequest( } message := "handled request" - if nop, ok := GetVar(r.Context(), "unhandled").(bool); ok && nop { + if nop, ok := GetVar(ctx, "unhandled").(bool); ok && nop { message = "NOP" } @@ -836,7 +838,7 @@ func (s *Server) logRequest( reqBodyLength = bodyReader.Length } - extra := r.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields) + extra := ctx.Value(ExtraLogFieldsCtxKey).(*ExtraLogFields) fieldCount := 6 fields = make([]zapcore.Field, 0, fieldCount+len(extra.fields)) From 56282c5737ce3e96267d00ce6832bf3ee51525d6 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sat, 15 Nov 2025 00:55:30 +0300 Subject: [PATCH 002/206] ci: implement new release flow (#7341) * ci: implement new release flow Signed-off-by: Mohammed Al Sahaf * remove redundant validation Signed-off-by: Mohammed Al Sahaf * extract key sha Signed-off-by: Mohammed Al Sahaf * pin github-scripts Signed-off-by: Mohammed Al Sahaf * switch to PR-based flow Signed-off-by: Mohammed Al Sahaf * don't use top-level permissions Signed-off-by: Mohammed Al Sahaf * restricted global perms + specific local perms Signed-off-by: Mohammed Al Sahaf * make PR draft Signed-off-by: Mohammed Al Sahaf --------- Signed-off-by: Mohammed Al Sahaf --- .github/workflows/auto-release-pr.yml | 221 ++++++++++++++ .github/workflows/release-proposal.yml | 248 ++++++++++++++++ .github/workflows/release.yml | 395 ++++++++++++++++++++++++- 3 files changed, 854 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/auto-release-pr.yml create mode 100644 .github/workflows/release-proposal.yml diff --git a/.github/workflows/auto-release-pr.yml b/.github/workflows/auto-release-pr.yml new file mode 100644 index 000000000..c8440d32c --- /dev/null +++ b/.github/workflows/auto-release-pr.yml @@ -0,0 +1,221 @@ +name: Release Proposal Approval Tracker + +on: + pull_request_review: + types: [submitted, dismissed] + pull_request: + types: [labeled, unlabeled, synchronize, closed] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + check-approvals: + name: Track Maintainer Approvals + runs-on: ubuntu-latest + # Only run on PRs with release-proposal label + if: contains(github.event.pull_request.labels.*.name, 'release-proposal') && github.event.pull_request.state == 'open' + + steps: + - name: Check approvals and update PR + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + MAINTAINER_LOGINS: ${{ secrets.MAINTAINER_LOGINS }} + with: + script: | + const pr = context.payload.pull_request; + + // Extract version from PR title (e.g., "Release Proposal: v1.2.3") + const versionMatch = pr.title.match(/Release Proposal:\s*(v[\d.]+(?:-[\w.]+)?)/); + const commitMatch = pr.body.match(/\*\*Target Commit:\*\*\s*`([a-f0-9]+)`/); + + if (!versionMatch || !commitMatch) { + console.log('Could not extract version from title or commit from body'); + return; + } + + const version = versionMatch[1]; + const targetCommit = commitMatch[1]; + + console.log(`Version: ${version}, Target Commit: ${targetCommit}`); + + // Get all reviews + const reviews = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number + }); + + // Get list of maintainers + const maintainerLoginsRaw = process.env.MAINTAINER_LOGINS || ''; + const maintainerLogins = maintainerLoginsRaw + .split(/[,;]/) + .map(login => login.trim()) + .filter(login => login.length > 0); + + console.log(`Maintainer logins: ${maintainerLogins.join(', ')}`); + + // Get the latest review from each user + const latestReviewsByUser = {}; + reviews.data.forEach(review => { + const username = review.user.login; + if (!latestReviewsByUser[username] || new Date(review.submitted_at) > new Date(latestReviewsByUser[username].submitted_at)) { + latestReviewsByUser[username] = review; + } + }); + + // Count approvals from maintainers + const maintainerApprovals = Object.entries(latestReviewsByUser) + .filter(([username, review]) => + maintainerLogins.includes(username) && + review.state === 'APPROVED' + ) + .map(([username, review]) => username); + + const approvalCount = maintainerApprovals.length; + console.log(`Found ${approvalCount} maintainer approvals from: ${maintainerApprovals.join(', ')}`); + + // Get current labels + const currentLabels = pr.labels.map(label => label.name); + const hasApprovedLabel = currentLabels.includes('approved'); + const hasAwaitingApprovalLabel = currentLabels.includes('awaiting-approval'); + + if (approvalCount >= 2 && !hasApprovedLabel) { + console.log('✅ Quorum reached! Updating PR...'); + + // Remove awaiting-approval label if present + if (hasAwaitingApprovalLabel) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name: 'awaiting-approval' + }).catch(e => console.log('Label not found:', e.message)); + } + + // Add approved label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: ['approved'] + }); + + // Add comment with tagging instructions + const approversList = maintainerApprovals.map(u => `@${u}`).join(', '); + const commentBody = [ + '## ✅ Approval Quorum Reached', + '', + `This release proposal has been approved by ${approvalCount} maintainers: ${approversList}`, + '', + '### Tagging Instructions', + '', + 'A maintainer should now create and push the signed tag:', + '', + '```bash', + `git checkout ${targetCommit}`, + `git tag -s ${version} -m "Release ${version}"`, + `git push origin ${version}`, + `git checkout -`, + '```', + '', + 'The release workflow will automatically start when the tag is pushed.' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: commentBody + }); + + console.log('Posted tagging instructions'); + } else if (approvalCount < 2 && hasApprovedLabel) { + console.log('⚠️ Approval count dropped below quorum, removing approved label'); + + // Remove approved label + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name: 'approved' + }).catch(e => console.log('Label not found:', e.message)); + + // Add awaiting-approval label + if (!hasAwaitingApprovalLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: ['awaiting-approval'] + }); + } + } else { + console.log(`⏳ Waiting for more approvals (${approvalCount}/2 required)`); + } + + handle-pr-closed: + name: Handle PR Closed Without Tag + runs-on: ubuntu-latest + if: | + contains(github.event.pull_request.labels.*.name, 'release-proposal') && + github.event.action == 'closed' && !contains(github.event.pull_request.labels.*.name, 'released') + + steps: + - name: Add cancelled label and comment + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const pr = context.payload.pull_request; + + // Check if the release-in-progress label is present + const hasReleaseInProgress = pr.labels.some(label => label.name === 'release-in-progress'); + + if (hasReleaseInProgress) { + // PR was closed while release was in progress - this is unusual + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: '⚠️ **Warning:** This PR was closed while a release was in progress. This may indicate an error. Please verify the release status.' + }); + } else { + // PR was closed before tag was created - this is normal cancellation + const versionMatch = pr.title.match(/Release Proposal:\s*(v[\d.]+(?:-[\w.]+)?)/); + const version = versionMatch ? versionMatch[1] : 'unknown'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: `## 🚫 Release Proposal Cancelled\n\nThis release proposal for ${version} was closed without creating the tag.\n\nIf you want to proceed with this release later, you can create a new release proposal.` + }); + } + + // Add cancelled label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: ['cancelled'] + }); + + // Remove other workflow labels if present + const labelsToRemove = ['awaiting-approval', 'approved', 'release-in-progress']; + for (const label of labelsToRemove) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name: label + }); + } catch (e) { + console.log(`Label ${label} not found or already removed`); + } + } + + console.log('Added cancelled label and cleaned up workflow labels'); + diff --git a/.github/workflows/release-proposal.yml b/.github/workflows/release-proposal.yml new file mode 100644 index 000000000..afde7965a --- /dev/null +++ b/.github/workflows/release-proposal.yml @@ -0,0 +1,248 @@ +name: Release Proposal + +# This workflow creates a release proposal as a PR that requires approval from maintainers +# Triggered manually by maintainers when ready to prepare a release +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., v2.8.0)' + required: true + type: string + commit_hash: + description: 'Commit hash to release from' + required: true + type: string + +permissions: + contents: read + +jobs: + create-proposal: + name: Create Release Proposal + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + + - name: Trim and validate inputs + id: inputs + run: | + # Trim whitespace from inputs + VERSION=$(echo "${{ inputs.version }}" | xargs) + COMMIT_HASH=$(echo "${{ inputs.commit_hash }}" | xargs) + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT + + # Validate version format + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "Error: Version must follow semver format (e.g., v2.8.0 or v2.8.0-beta.1)" + exit 1 + fi + + # Validate commit hash format + if [[ ! "$COMMIT_HASH" =~ ^[a-f0-9]{7,40}$ ]]; then + echo "Error: Commit hash must be a valid SHA (7-40 characters)" + exit 1 + fi + + # Check if commit exists + if ! git cat-file -e "$COMMIT_HASH"; then + echo "Error: Commit $COMMIT_HASH does not exist" + exit 1 + fi + + - name: Check if tag already exists + run: | + if git rev-parse "${{ steps.inputs.outputs.version }}" >/dev/null 2>&1; then + echo "Error: Tag ${{ steps.inputs.outputs.version }} already exists" + exit 1 + fi + + - name: Check for existing proposal PR + id: check_existing + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const version = '${{ steps.inputs.outputs.version }}'; + + // Search for existing open PRs with release-proposal label that match this version + const openPRs = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + sort: 'updated', + direction: 'desc' + }); + + const existingOpenPR = openPRs.data.find(pr => + pr.title.includes(version) && + pr.labels.some(label => label.name === 'release-proposal') + ); + + if (existingOpenPR) { + const hasReleased = existingOpenPR.labels.some(label => label.name === 'released'); + const hasReleaseInProgress = existingOpenPR.labels.some(label => label.name === 'release-in-progress'); + + if (hasReleased || hasReleaseInProgress) { + core.setFailed(`A release for ${version} is already in progress or completed: ${existingOpenPR.html_url}`); + } else { + core.setFailed(`An open release proposal already exists for ${version}: ${existingOpenPR.html_url}\n\nPlease use the existing PR or close it first.`); + } + return; + } + + // Check for closed PRs with this version that were cancelled + const closedPRs = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'closed', + sort: 'updated', + direction: 'desc' + }); + + const cancelledPR = closedPRs.data.find(pr => + pr.title.includes(version) && + pr.labels.some(label => label.name === 'release-proposal') && + pr.labels.some(label => label.name === 'cancelled') + ); + + if (cancelledPR) { + console.log(`Found previously cancelled proposal for ${version}: ${cancelledPR.html_url}`); + console.log('Creating new proposal to replace cancelled one...'); + } else { + console.log(`No existing proposal found for ${version}, proceeding...`); + } + + - name: Generate changelog and create branch + id: setup + run: | + VERSION="${{ steps.inputs.outputs.version }}" + COMMIT_HASH="${{ steps.inputs.outputs.commit_hash }}" + + # Create a new branch for the release proposal + BRANCH_NAME="release_proposal-$VERSION" + git checkout -b "$BRANCH_NAME" + + # Calculate how many commits behind HEAD + COMMITS_BEHIND=$(git rev-list --count ${COMMIT_HASH}..HEAD) + + if [ "$COMMITS_BEHIND" -eq 0 ]; then + BEHIND_INFO="This is the latest commit (HEAD)" + else + BEHIND_INFO="This commit is **${COMMITS_BEHIND} commits behind HEAD**" + fi + + echo "commits_behind=$COMMITS_BEHIND" >> $GITHUB_OUTPUT + echo "behind_info=$BEHIND_INFO" >> $GITHUB_OUTPUT + + # Get the last tag + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [ -z "$LAST_TAG" ]; then + echo "No previous tag found, generating full changelog" + COMMITS=$(git log --pretty=format:"- %s (%h)" --reverse "$COMMIT_HASH") + else + echo "Generating changelog since $LAST_TAG" + COMMITS=$(git log --pretty=format:"- %s (%h)" --reverse "${LAST_TAG}..$COMMIT_HASH") + fi + + # Store changelog for PR body + echo "changelog<> $GITHUB_OUTPUT + echo "$COMMITS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Create empty commit for the PR + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit --allow-empty -m "Release proposal for $VERSION" + + # Push the branch + git push origin "$BRANCH_NAME" + + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + + - name: Create release proposal PR + id: create_pr + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const changelog = `${{ steps.setup.outputs.changelog }}`; + + const pr = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Release Proposal: ${{ steps.inputs.outputs.version }}`, + head: '${{ steps.setup.outputs.branch_name }}', + base: 'master', + body: `## Release Proposal: ${{ steps.inputs.outputs.version }} + + **Target Commit:** \`${{ steps.inputs.outputs.commit_hash }}\` + **Requested by:** @${{ github.actor }} + **Commit Status:** ${{ steps.setup.outputs.behind_info }} + + This PR proposes creating release tag \`${{ steps.inputs.outputs.version }}\` at commit \`${{ steps.inputs.outputs.commit_hash }}\`. + + ### Approval Process + + This PR requires **approval from 2+ maintainers** before the tag can be created. + + ### What happens next? + + 1. Maintainers review this proposal + 2. When 2+ maintainer approvals are received, an automated workflow will post tagging instructions + 3. A maintainer manually creates and pushes the signed tag + 4. The release workflow is triggered automatically by the tag push + 5. Upon release completion, this PR is closed and the branch is deleted + + ### Changes Since Last Release + + ${changelog} + + ### Release Checklist + + - [ ] All tests pass + - [ ] Security review completed + - [ ] Documentation updated + - [ ] Breaking changes documented + + --- + + **Note:** Tag creation is manual and requires a signed tag from a maintainer.`, + draft: true + }); + + // Add labels + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.data.number, + labels: ['release-proposal', 'awaiting-approval'] + }); + + console.log(`Created PR: ${pr.data.html_url}`); + + return { number: pr.data.number, url: pr.data.html_url }; + result-encoding: json + + - name: Post summary + run: | + echo "## Release Proposal PR Created! 🚀" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Version: **${{ steps.inputs.outputs.version }}**" >> $GITHUB_STEP_SUMMARY + echo "Commit: **${{ steps.inputs.outputs.commit_hash }}**" >> $GITHUB_STEP_SUMMARY + echo "Status: ${{ steps.setup.outputs.behind_info }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "PR: ${{ fromJson(steps.create_pr.outputs.result).url }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 397df5ea2..e4880a64c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,322 @@ permissions: contents: read jobs: + verify-tag: + name: Verify Tag Signature and Approvals + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + + outputs: + verification_passed: ${{ steps.verify.outputs.passed }} + tag_version: ${{ steps.info.outputs.version }} + proposal_issue_number: ${{ steps.find_proposal.outputs.result && fromJson(steps.find_proposal.outputs.result).number || '' }} + + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + # Force fetch upstream tags -- because 65 minutes + # tl;dr: actions/checkout@v3 runs this line: + # git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/ + # which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran: + # git fetch --prune --unshallow + # which doesn't overwrite that tag because that would be destructive. + # Credit to @francislavoie for the investigation. + # https://github.com/actions/checkout/issues/290#issuecomment-680260080 + - name: Force fetch upstream tags + run: git fetch --tags --force + + - name: Get tag info + id: info + run: | + echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + + # https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 + - name: Print Go version and environment + id: vars + run: | + printf "Using go at: $(which go)\n" + printf "Go version: $(go version)\n" + printf "\n\nGo environment:\n\n" + go env + printf "\n\nSystem environment:\n\n" + env + echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + # Add "pip install" CLI tools to PATH + echo ~/.local/bin >> $GITHUB_PATH + + # Parse semver + TAG=${GITHUB_REF/refs\/tags\//} + SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)' + TAG_MAJOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\1#"` + TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"` + TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"` + TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"` + echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT + echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT + echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT + echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT + + - name: Validate commits and tag signatures + id: verify + env: + signing_keys: ${{ secrets.SIGNING_KEYS }} + run: | + # Read the string into an array, splitting by IFS + IFS=";" read -ra keys_collection <<< "$signing_keys" + + # ref: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#example-usage-of-the-runner-context + touch "${{ runner.temp }}/allowed_signers" + + # Iterate and print the split elements + for item in "${keys_collection[@]}"; do + + # trim leading whitespaces + item="${item##*( )}" + + # trim trailing whitespaces + item="${item%%*( )}" + + IFS=" " read -ra key_components <<< "$item" + # git wants it in format: email address, type, public key + # ssh has it in format: type, public key, email address + echo "${key_components[2]} namespaces=\"git\" ${key_components[0]} ${key_components[1]}" >> "${{ runner.temp }}/allowed_signers" + done + + git config set --global gpg.ssh.allowedSignersFile "${{ runner.temp }}/allowed_signers" + + echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}" + + # Verify the tag is signed + if ! git verify-tag -v "${{ steps.vars.outputs.version_tag }}" 2>&1; then + echo "❌ Tag verification failed!" + echo "passed=false" >> $GITHUB_OUTPUT + git push --delete origin "${{ steps.vars.outputs.version_tag }}" + exit 1 + fi + # Run it again to capture the output + git verify-tag -v "${{ steps.vars.outputs.version_tag }}" 2>&1 | tee /tmp/verify-output.txt; + + # SSH verification output typically includes the key fingerprint + # Use GNU grep with Perl regex for cleaner extraction (Linux environment) + KEY_SHA256=$(grep -oP "SHA256:[\"']?\K[A-Za-z0-9+/=]+(?=[\"']?)" /tmp/verify-output.txt | head -1 || echo "") + + if [ -z "$KEY_SHA256" ]; then + # Try alternative pattern with "key" prefix + KEY_SHA256=$(grep -oP "key SHA256:[\"']?\K[A-Za-z0-9+/=]+(?=[\"']?)" /tmp/verify-output.txt | head -1 || echo "") + fi + + if [ -z "$KEY_SHA256" ]; then + # Fallback: extract any base64-like string (40+ chars) + KEY_SHA256=$(grep -oP '[A-Za-z0-9+/]{40,}=?' /tmp/verify-output.txt | head -1 || echo "") + fi + + if [ -z "$KEY_SHA256" ]; then + echo "Somehow could not extract SSH key fingerprint from git verify-tag output" + echo "Cancelling flow and deleting tag" + echo "passed=false" >> $GITHUB_OUTPUT + git push --delete origin "${{ steps.vars.outputs.version_tag }}" + exit 1 + fi + + echo "✅ Tag verification succeeded!" + echo "passed=true" >> $GITHUB_OUTPUT + echo "key_id=$KEY_SHA256" >> $GITHUB_OUTPUT + + - name: Find related release proposal + id: find_proposal + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const version = '${{ steps.vars.outputs.version_tag }}'; + + // Search for PRs with release-proposal label that match this version + const prs = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', // Changed to 'all' to find both open and closed PRs + sort: 'updated', + direction: 'desc' + }); + + // Find the most recent PR for this version + const proposal = prs.data.find(pr => + pr.title.includes(version) && + pr.labels.some(label => label.name === 'release-proposal') + ); + + if (!proposal) { + console.log(`⚠️ No release proposal PR found for ${version}`); + console.log('This might be a hotfix or emergency release'); + return { number: null, approved: true, approvals: 0, proposedCommit: null }; + } + + console.log(`Found proposal PR #${proposal.number} for version ${version}`); + + // Extract commit hash from PR body + const commitMatch = proposal.body.match(/\*\*Target Commit:\*\*\s*`([a-f0-9]+)`/); + const proposedCommit = commitMatch ? commitMatch[1] : null; + + if (proposedCommit) { + console.log(`Proposal was for commit: ${proposedCommit}`); + } else { + console.log('⚠️ No target commit hash found in PR body'); + } + + // Get PR reviews to extract approvers + let approvers = 'Validated by automation'; + let approvalCount = 2; // Minimum required + + try { + const reviews = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: proposal.number + }); + + // Get latest review per user and filter for approvals + const latestReviewsByUser = {}; + reviews.data.forEach(review => { + const username = review.user.login; + if (!latestReviewsByUser[username] || new Date(review.submitted_at) > new Date(latestReviewsByUser[username].submitted_at)) { + latestReviewsByUser[username] = review; + } + }); + + const approvalReviews = Object.values(latestReviewsByUser).filter(review => + review.state === 'APPROVED' + ); + + if (approvalReviews.length > 0) { + approvers = approvalReviews.map(r => '@' + r.user.login).join(', '); + approvalCount = approvalReviews.length; + console.log(`Found ${approvalCount} approvals from: ${approvers}`); + } + } catch (error) { + console.log(`Could not fetch reviews: ${error.message}`); + } + + return { + number: proposal.number, + approved: true, + approvals: approvalCount, + approvers: approvers, + proposedCommit: proposedCommit + }; + result-encoding: json + + - name: Verify proposal commit + run: | + APPROVALS='${{ steps.find_proposal.outputs.result }}' + + # Parse JSON + PROPOSED_COMMIT=$(echo "$APPROVALS" | jq -r '.proposedCommit') + CURRENT_COMMIT="${{ steps.info.outputs.sha }}" + + echo "Proposed commit: $PROPOSED_COMMIT" + echo "Current commit: $CURRENT_COMMIT" + + # Check if commits match (if proposal had a target commit) + if [ "$PROPOSED_COMMIT" != "null" ] && [ -n "$PROPOSED_COMMIT" ]; then + # Normalize both commits to full SHA for comparison + PROPOSED_FULL=$(git rev-parse "$PROPOSED_COMMIT" 2>/dev/null || echo "") + CURRENT_FULL=$(git rev-parse "$CURRENT_COMMIT" 2>/dev/null || echo "") + + if [ -z "$PROPOSED_FULL" ]; then + echo "⚠️ Could not resolve proposed commit: $PROPOSED_COMMIT" + elif [ "$PROPOSED_FULL" != "$CURRENT_FULL" ]; then + echo "❌ Commit mismatch!" + echo "The tag points to commit $CURRENT_FULL but the proposal was for $PROPOSED_FULL" + echo "This indicates an error in tag creation." + # Delete the tag remotely + git push --delete origin "${{ steps.vars.outputs.version_tag }}" + echo "Tag ${{steps.vars.outputs.version_tag}} has been deleted" + exit 1 + else + echo "✅ Commit hash matches proposal" + fi + else + echo "⚠️ No target commit found in proposal (might be legacy release)" + fi + + echo "✅ Tag verification completed" + + - name: Update release proposal PR + if: fromJson(steps.find_proposal.outputs.result).number != null + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const result = ${{ steps.find_proposal.outputs.result }}; + + if (result.number) { + // Add in-progress label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: result.number, + labels: ['release-in-progress'] + }); + + // Remove approved label if present + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: result.number, + name: 'approved' + }); + } catch (e) { + console.log('Approved label not found:', e.message); + } + + const commentBody = [ + '## 🚀 Release Workflow Started', + '', + '- **Tag:** ${{ steps.info.outputs.version }}', + '- **Signed by key:** ${{ steps.verify.outputs.key_id }}', + '- **Commit:** ${{ steps.info.outputs.sha }}', + '- **Approved by:** ' + result.approvers, + '', + 'Release workflow is now running. This PR will be updated when the release is published.' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: result.number, + body: commentBody + }); + } + + - name: Summary + run: | + APPROVALS='${{ steps.find_proposal.outputs.result }}' + PROPOSED_COMMIT=$(echo "$APPROVALS" | jq -r '.proposedCommit // "N/A"') + APPROVERS=$(echo "$APPROVALS" | jq -r '.approvers // "N/A"') + + echo "## Tag Verification Summary 🔐" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Tag:** ${{ steps.info.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Commit:** ${{ steps.info.outputs.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **Proposed Commit:** $PROPOSED_COMMIT" >> $GITHUB_STEP_SUMMARY + echo "- **Signature:** ✅ Verified" >> $GITHUB_STEP_SUMMARY + echo "- **Signed by:** ${{ steps.verify.outputs.key_id }}" >> $GITHUB_STEP_SUMMARY + echo "- **Approvals:** ✅ Sufficient" >> $GITHUB_STEP_SUMMARY + echo "- **Approved by:** $APPROVERS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Proceeding with release build..." >> $GITHUB_STEP_SUMMARY + release: name: Release + needs: verify-tag + if: ${{ needs.verify-tag.outputs.verification_passed == 'true' }} strategy: matrix: os: @@ -36,6 +350,8 @@ jobs: # https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents # "Releases" is part of `contents`, so it needs the `write` contents: write + issues: write + pull-requests: write steps: - name: Harden the runner (Audit all outbound calls) @@ -98,16 +414,6 @@ jobs: - name: Install Cloudsmith CLI run: pip install --upgrade cloudsmith-cli - - name: Validate commits and tag signatures - run: | - - # Import Matt Holt's key - curl 'https://github.com/mholt.gpg' | gpg --import - - echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}" - # tags are only accepted if signed by Matt's key - git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1 - - name: Install Cosign uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main - name: Cosign version @@ -188,3 +494,72 @@ jobs: echo "Pushing $filename to 'testing'" cloudsmith push deb caddy/testing/any-distro/any-version $filename done + + - name: Update release proposal PR + if: needs.verify-tag.outputs.proposal_issue_number != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const prNumber = parseInt('${{ needs.verify-tag.outputs.proposal_issue_number }}'); + + if (prNumber) { + // Get PR details to find the branch + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + const branchName = pr.data.head.ref; + + // Remove in-progress label + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: 'release-in-progress' + }); + } catch (e) { + console.log('Label not found:', e.message); + } + + // Add released label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: ['released'] + }); + + // Add final comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: '## ✅ Release Published\n\nThe release has been successfully published and is now available.' + }); + + // Close the PR if it's still open + if (pr.data.state === 'open') { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + state: 'closed' + }); + console.log(`Closed PR #${prNumber}`); + } + + // Delete the branch + try { + await github.rest.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `heads/${branchName}` + }); + console.log(`Deleted branch: ${branchName}`); + } catch (e) { + console.log(`Could not delete branch ${branchName}: ${e.message}`); + } + } From a6da1acdc86199a7f99fa2347d5f91cd59ff90d8 Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Tue, 18 Nov 2025 00:51:37 +0800 Subject: [PATCH 003/206] reverse_proxy: use interfaces to modify the behaviors of the transports (#7353) --- modules/caddyhttp/reverseproxy/caddyfile.go | 7 ++- .../caddyhttp/reverseproxy/fastcgi/fastcgi.go | 19 +++++++- .../caddyhttp/reverseproxy/healthchecks.go | 12 ++--- .../caddyhttp/reverseproxy/httptransport.go | 31 ++++++++++-- .../caddyhttp/reverseproxy/reverseproxy.go | 48 ++++++++++++++----- 5 files changed, 88 insertions(+), 29 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 8439d1d51..12d610800 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -888,8 +888,11 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if commonScheme == "http" && te.TLSEnabled() { return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)") } - if te, ok := transport.(*HTTPTransport); ok && commonScheme == "h2c" { - te.Versions = []string{"h2c", "2"} + if h2ct, ok := transport.(H2CTransport); ok && commonScheme == "h2c" { + err := h2ct.EnableH2C() + if err != nil { + return err + } } } else if commonScheme == "https" { return d.Errf("upstreams are configured for HTTPS but transport module does not support TLS: %T", transport) diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index d451dd380..5c68c3ad5 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -112,6 +112,20 @@ func (t *Transport) Provision(ctx caddy.Context) error { return nil } +// DefaultBufferSizes enables request buffering for fastcgi if not configured. +// This is because most fastcgi servers are php-fpm that require the content length to be set to read the body, golang +// std has fastcgi implementation that doesn't need this value to process the body, but we can safely assume that's +// not used. +// http3 requests have a negative content length for GET and HEAD requests, if that header is not sent. +// see: https://github.com/caddyserver/caddy/issues/6678#issuecomment-2472224182 +// Though it appears even if CONTENT_LENGTH is invalid, php-fpm can handle just fine if the body is empty (no Stdin records sent). +// php-fpm will hang if there is any data in the body though, https://github.com/caddyserver/caddy/issues/5420#issuecomment-2415943516 + +// TODO: better default buffering for fastcgi requests without content length, in theory a value of 1 should be enough, make it bigger anyway +func (t Transport) DefaultBufferSizes() (int64, int64) { + return 4096, 0 +} + // RoundTrip implements http.RoundTripper. func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server) @@ -427,6 +441,7 @@ var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") var ( _ zapcore.ObjectMarshaler = (*loggableEnv)(nil) - _ caddy.Provisioner = (*Transport)(nil) - _ http.RoundTripper = (*Transport)(nil) + _ caddy.Provisioner = (*Transport)(nil) + _ http.RoundTripper = (*Transport)(nil) + _ reverseproxy.BufferedTransport = (*Transport)(nil) ) diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index ac42570b2..b72e723e0 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -23,7 +23,6 @@ import ( "net/url" "regexp" "runtime/debug" - "slices" "strconv" "strings" "time" @@ -405,14 +404,9 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ u.Host = net.JoinHostPort(host, port) } - // this is kind of a hacky way to know if we should use HTTPS, but whatever - if tt, ok := h.Transport.(TLSTransport); ok && tt.TLSEnabled() { - u.Scheme = "https" - - // if the port is in the except list, flip back to HTTP - if ht, ok := h.Transport.(*HTTPTransport); ok && slices.Contains(ht.TLS.ExceptPorts, port) { - u.Scheme = "http" - } + // override health check schemes if applicable + if hcsot, ok := h.Transport.(HealthCheckSchemeOverriderTransport); ok { + hcsot.OverrideHealthCheckScheme(u, port) } // if we have a provisioned uri, use that, otherwise use diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 1e4cfa743..8edc585e7 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -564,6 +564,26 @@ func (h *HTTPTransport) EnableTLS(base *TLSConfig) error { return nil } +// EnableH2C enables H2C (HTTP/2 over Cleartext) on the transport. +func (h *HTTPTransport) EnableH2C() error { + h.Versions = []string{"h2c", "2"} + return nil +} + +// OverrideHealthCheckScheme overrides the scheme of the given URL +// used for health checks. +func (h HTTPTransport) OverrideHealthCheckScheme(base *url.URL, port string) { + // if tls is enabled and the port isn't in the except list, use HTTPs + if h.TLSEnabled() && !slices.Contains(h.TLS.ExceptPorts, port) { + base.Scheme = "https" + } +} + +// ProxyProtocolEnabled returns true if proxy protocol is enabled. +func (h HTTPTransport) ProxyProtocolEnabled() bool { + return h.ProxyProtocol != "" +} + // Cleanup implements caddy.CleanerUpper and closes any idle connections. func (h HTTPTransport) Cleanup() error { if h.Transport == nil { @@ -820,8 +840,11 @@ func decodeBase64DERCert(certStr string) (*x509.Certificate, error) { // Interface guards var ( - _ caddy.Provisioner = (*HTTPTransport)(nil) - _ http.RoundTripper = (*HTTPTransport)(nil) - _ caddy.CleanerUpper = (*HTTPTransport)(nil) - _ TLSTransport = (*HTTPTransport)(nil) + _ caddy.Provisioner = (*HTTPTransport)(nil) + _ http.RoundTripper = (*HTTPTransport)(nil) + _ caddy.CleanerUpper = (*HTTPTransport)(nil) + _ TLSTransport = (*HTTPTransport)(nil) + _ H2CTransport = (*HTTPTransport)(nil) + _ HealthCheckSchemeOverriderTransport = (*HTTPTransport)(nil) + _ ProxyProtocolTransport = (*HTTPTransport)(nil) ) diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 794860d8e..d207e240e 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -243,18 +243,16 @@ func (h *Handler) Provision(ctx caddy.Context) error { return fmt.Errorf("loading transport: %v", err) } h.Transport = mod.(http.RoundTripper) - // enable request buffering for fastcgi if not configured - // This is because most fastcgi servers are php-fpm that require the content length to be set to read the body, golang - // std has fastcgi implementation that doesn't need this value to process the body, but we can safely assume that's - // not used. - // http3 requests have a negative content length for GET and HEAD requests, if that header is not sent. - // see: https://github.com/caddyserver/caddy/issues/6678#issuecomment-2472224182 - // Though it appears even if CONTENT_LENGTH is invalid, php-fpm can handle just fine if the body is empty (no Stdin records sent). - // php-fpm will hang if there is any data in the body though, https://github.com/caddyserver/caddy/issues/5420#issuecomment-2415943516 - // TODO: better default buffering for fastcgi requests without content length, in theory a value of 1 should be enough, make it bigger anyway - if module, ok := h.Transport.(caddy.Module); ok && module.CaddyModule().ID.Name() == "fastcgi" && h.RequestBuffers == 0 { - h.RequestBuffers = 4096 + // set default buffer sizes if applicable + if bt, ok := h.Transport.(BufferedTransport); ok { + reqBuffers, respBuffers := bt.DefaultBufferSizes() + if h.RequestBuffers == 0 { + h.RequestBuffers = reqBuffers + } + if h.ResponseBuffers == 0 { + h.ResponseBuffers = respBuffers + } } } if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil { @@ -1210,7 +1208,7 @@ func (h *Handler) directRequest(req *http.Request, di DialInfo) { } // add client address to the host to let transport differentiate requests from different clients - if ht, ok := h.Transport.(*HTTPTransport); ok && ht.ProxyProtocol != "" { + if ppt, ok := h.Transport.(ProxyProtocolTransport); ok && ppt.ProxyProtocolEnabled() { if proxyProtocolInfo, ok := caddyhttp.GetVar(req.Context(), proxyProtocolInfoVarKey).(ProxyProtocolInfo); ok { reqHost = proxyProtocolInfo.AddrPort.String() + "->" + reqHost } @@ -1501,6 +1499,32 @@ type TLSTransport interface { EnableTLS(base *TLSConfig) error } +// H2CTransport is implemented by transports +// that are capable of using h2c. +type H2CTransport interface { + EnableH2C() error +} + +// ProxyProtocolTransport is implemented by transports +// that are capable of using proxy protocol. +type ProxyProtocolTransport interface { + ProxyProtocolEnabled() bool +} + +// HealthCheckSchemeOverriderTransport is implemented by transports +// that can override the scheme used for health checks. +type HealthCheckSchemeOverriderTransport interface { + OverrideHealthCheckScheme(base *url.URL, port string) +} + +// BufferedTransport is implemented by transports +// that needs to buffer requests and/or responses. +type BufferedTransport interface { + // DefaultBufferSizes returns the default buffer sizes + // for requests and responses, respectively if buffering isn't enabled. + DefaultBufferSizes() (int64, int64) +} + // roundtripSucceededError is an error type that is returned if the // roundtrip succeeded, but an error occurred after-the-fact. type roundtripSucceededError struct{ error } From eead249382ac14c29d82ec75f30df7638e879567 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:55:13 -0700 Subject: [PATCH 004/206] build(deps): bump golang.org/x/crypto from 0.43.0 to 0.45.0 (#7355) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.43.0 to 0.45.0. - [Commits](https://github.com/golang/crypto/compare/v0.43.0...v0.45.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.45.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 799a8450e..e2224a433 100644 --- a/go.mod +++ b/go.mod @@ -38,11 +38,11 @@ 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.43.0 + golang.org/x/crypto v0.45.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/net v0.47.0 + golang.org/x/sync v0.18.0 + golang.org/x/term v0.37.0 golang.org/x/time v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -169,8 +169,8 @@ require ( go.step.sm/crypto v0.74.0 go.uber.org/multierr v1.11.0 // 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/sys v0.38.0 + golang.org/x/text v0.31.0 // indirect golang.org/x/tools v0.38.0 // indirect google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect diff --git a/go.sum b/go.sum index 623476ade..6854d9474 100644 --- a/go.sum +++ b/go.sum @@ -452,8 +452,8 @@ 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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 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-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= @@ -473,8 +473,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -485,8 +485,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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -507,8 +507,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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.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= @@ -519,8 +519,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.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -531,8 +531,8 @@ 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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From b9e6f3b2278f61f24435eab9212bb6ad59f3ee4f Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Fri, 21 Nov 2025 19:46:47 +0800 Subject: [PATCH 005/206] update quic-go to v0.57.0 (#7359) --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e2224a433..d4e660b2a 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 github.com/mholt/acmez/v3 v3.1.4 github.com/prometheus/client_golang v1.23.2 - github.com/quic-go/quic-go v0.56.0 + github.com/quic-go/quic-go v0.57.0 github.com/smallstep/certificates v0.28.4 github.com/smallstep/nosql v0.7.0 github.com/smallstep/truststore v0.13.0 @@ -77,7 +77,7 @@ require ( github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/otlptranslator v0.0.2 // indirect - github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect github.com/smallstep/cli-utils v0.12.1 // indirect github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect github.com/smallstep/linkedca v0.23.0 // indirect diff --git a/go.sum b/go.sum index 6854d9474..55245e72c 100644 --- a/go.sum +++ b/go.sum @@ -272,10 +272,10 @@ github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DR github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= 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.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= -github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= +github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 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= From 2cb426776c091febccc6b9a6669f1c5a745648a4 Mon Sep 17 00:00:00 2001 From: ledigang Date: Sat, 22 Nov 2025 06:30:26 +0800 Subject: [PATCH 006/206] encode: modernize, replace HasSuffix+TrimSuffix with CutSuffix (#7357) Signed-off-by: ledigang --- modules/caddyhttp/encode/encode.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index e23d9109c..ac995c37b 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -168,8 +168,8 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh // caches without knowing about our changes... if etag := r.Header.Get("If-None-Match"); etag != "" && !strings.HasPrefix(etag, "W/") { ourSuffix := "-" + encName + `"` - if strings.HasSuffix(etag, ourSuffix) { - etag = strings.TrimSuffix(etag, ourSuffix) + `"` + if before, ok := strings.CutSuffix(etag, ourSuffix); ok { + etag = before + `"` r.Header.Set("If-None-Match", etag) } } From 67a9e0657e60df8c78510065e8977d86ee17d01c Mon Sep 17 00:00:00 2001 From: Petr <26906365+chebyrash@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:03:18 +0400 Subject: [PATCH 007/206] reverseproxy: Fix retries for requests with bodies (#7360) * capture the buffered body once, then reset clonedReq.Body before each retry * no copy * keep receiver name * set the buf to nil after extraction and only return it to pool if not nil --------- Co-authored-by: WeidiDeng --- .../caddyhttp/reverseproxy/reverseproxy.go | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index d207e240e..13bbee422 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -437,6 +437,20 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht reqHost := clonedReq.Host reqHeader := clonedReq.Header + // If the cloned request body was fully buffered, keep a reference to its + // buffer so we can reuse it across retries and return it to the pool + // once we’re done. + var bufferedReqBody *bytes.Buffer + if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil { + bufferedReqBody = reqBodyBuf.buf + reqBodyBuf.buf = nil + + defer func() { + bufferedReqBody.Reset() + bufPool.Put(bufferedReqBody) + }() + } + start := time.Now() defer func() { // total proxying duration, including time spent on LB and retries @@ -455,8 +469,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht // and reusable, so if a backend partially or fully reads the body but then // produces an error, the request can be repeated to the next backend with // the full body (retries should only happen for idempotent requests) (see #6259) - if reqBodyBuf, ok := r.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil { - r.Body = io.NopCloser(bytes.NewReader(reqBodyBuf.buf.Bytes())) + if bufferedReqBody != nil { + clonedReq.Body = io.NopCloser(bytes.NewReader(bufferedReqBody.Bytes())) } var done bool @@ -1538,7 +1552,12 @@ type bodyReadCloser struct { } func (brc bodyReadCloser) Close() error { - bufPool.Put(brc.buf) + // Inside this package this will be set to nil for fully-buffered + // requests due to the possibility of retrial. + if brc.buf != nil { + bufPool.Put(brc.buf) + } + // For fully-buffered bodies, body is nil, so Close is a no-op. if brc.body != nil { return brc.body.Close() } From 786d5378771d068440b7ddd8c310851bb7b9afe0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:32:58 +0300 Subject: [PATCH 008/206] build(deps): bump the all-updates group with 3 updates (#7376) Bumps the all-updates group with 3 updates: [github.com/klauspost/compress](https://github.com/klauspost/compress), [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) and [go.uber.org/zap](https://github.com/uber-go/zap). Updates `github.com/klauspost/compress` from 1.18.1 to 1.18.2 - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.1...v1.18.2) Updates `github.com/quic-go/quic-go` from 0.57.0 to 0.57.1 - [Release notes](https://github.com/quic-go/quic-go/releases) - [Commits](https://github.com/quic-go/quic-go/compare/v0.57.0...v0.57.1) Updates `go.uber.org/zap` from 1.27.0 to 1.27.1 - [Release notes](https://github.com/uber-go/zap/releases) - [Changelog](https://github.com/uber-go/zap/blob/master/CHANGELOG.md) - [Commits](https://github.com/uber-go/zap/compare/v1.27.0...v1.27.1) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.18.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: github.com/quic-go/quic-go dependency-version: 0.57.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: go.uber.org/zap dependency-version: 1.27.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index d4e660b2a..5e4fffe2f 100644 --- a/go.mod +++ b/go.mod @@ -16,11 +16,11 @@ require ( github.com/go-chi/chi/v5 v5.2.3 github.com/google/cel-go v0.26.1 github.com/google/uuid v1.6.0 - github.com/klauspost/compress v1.18.1 + github.com/klauspost/compress v1.18.2 github.com/klauspost/cpuid/v2 v2.3.0 github.com/mholt/acmez/v3 v3.1.4 github.com/prometheus/client_golang v1.23.2 - github.com/quic-go/quic-go v0.57.0 + github.com/quic-go/quic-go v0.57.1 github.com/smallstep/certificates v0.28.4 github.com/smallstep/nosql v0.7.0 github.com/smallstep/truststore v0.13.0 @@ -36,7 +36,7 @@ require ( go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/sdk v1.38.0 go.uber.org/automaxprocs v1.6.0 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 golang.org/x/crypto v0.45.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 diff --git a/go.sum b/go.sum index 55245e72c..7e9f7a66b 100644 --- a/go.sum +++ b/go.sum @@ -208,8 +208,8 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -274,8 +274,8 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= -github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= +github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 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= @@ -438,8 +438,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -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 v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= From df9386fa128ff8746d24afeda1d0f37085a58f8d Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Wed, 3 Dec 2025 19:40:31 +0300 Subject: [PATCH 009/206] ci: escape backticks in changelogs embedded in JS (#7382) Signed-off-by: Mohammed Al Sahaf --- .github/workflows/release-proposal.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-proposal.yml b/.github/workflows/release-proposal.yml index afde7965a..8cfb63cb6 100644 --- a/.github/workflows/release-proposal.yml +++ b/.github/workflows/release-proposal.yml @@ -160,8 +160,9 @@ jobs: fi # Store changelog for PR body + CLEANSED_COMMITS=$(echo "$COMMITS" | sed 's/`/\\`/g') echo "changelog<> $GITHUB_OUTPUT - echo "$COMMITS" >> $GITHUB_OUTPUT + echo "$CLEANSED_COMMITS" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT # Create empty commit for the PR From 8a87bb3ffb8706bbfcaada1d52008a8eb07ad790 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:45:18 -0700 Subject: [PATCH 010/206] build(deps): bump github.com/smallstep/certificates (#7381) Bumps [github.com/smallstep/certificates](https://github.com/smallstep/certificates) from 0.28.4 to 0.29.0. - [Release notes](https://github.com/smallstep/certificates/releases) - [Changelog](https://github.com/smallstep/certificates/blob/master/CHANGELOG.md) - [Commits](https://github.com/smallstep/certificates/compare/v0.28.4...v0.29.0) --- updated-dependencies: - dependency-name: github.com/smallstep/certificates dependency-version: 0.29.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 36 +++++++++++------------ go.sum | 92 +++++++++++++++++++++++++++------------------------------- 2 files changed, 60 insertions(+), 68 deletions(-) diff --git a/go.mod b/go.mod index 5e4fffe2f..29198a510 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/mholt/acmez/v3 v3.1.4 github.com/prometheus/client_golang v1.23.2 github.com/quic-go/quic-go v0.57.1 - github.com/smallstep/certificates v0.28.4 + github.com/smallstep/certificates v0.29.0 github.com/smallstep/nosql v0.7.0 github.com/smallstep/truststore v0.13.0 github.com/spf13/cobra v1.10.1 @@ -55,18 +55,18 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/ccoveille/go-safecast v1.6.1 // indirect + github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/coreos/go-oidc/v3 v3.14.1 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect - github.com/google/go-tpm v0.9.6 // indirect + github.com/google/go-tpm v0.9.7 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect @@ -78,14 +78,14 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/otlptranslator v0.0.2 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/smallstep/cli-utils v0.12.1 // indirect + github.com/smallstep/cli-utils v0.12.2 // indirect github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect - github.com/smallstep/linkedca v0.23.0 // indirect + github.com/smallstep/linkedca v0.25.0 // indirect github.com/smallstep/pkcs7 v0.2.1 // indirect - github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 // indirect + github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect go.opentelemetry.io/contrib/propagators/aws v1.38.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.38.0 // indirect @@ -106,10 +106,10 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.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.32.0 // indirect - google.golang.org/api v0.254.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + google.golang.org/api v0.256.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -117,7 +117,7 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 @@ -140,7 +140,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/libdns/libdns v1.1.1 github.com/manifoldco/promptui v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/dns v1.1.68 // indirect @@ -172,7 +172,7 @@ require ( golang.org/x/sys v0.38.0 golang.org/x/text v0.31.0 // indirect golang.org/x/tools v0.38.0 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 7e9f7a66b..a9910e7bb 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= -cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -12,8 +12,8 @@ cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k= cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= -cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= -cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -29,8 +29,8 @@ github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43Dw github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= @@ -84,8 +84,8 @@ github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx6 github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= -github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q= -github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8= +github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0= +github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -105,8 +105,8 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= -github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -140,14 +140,14 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= -github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -172,8 +172,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA= -github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA= +github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.6 h1:hwIwPG7w4z5eQEBq11gYw8YYr9xXLfBQ/0JsKyq5AJM= github.com/google/go-tpm-tools v0.4.6/go.mod h1:MsVQbJnRhKDfWwf5zgr3cDGpj13P1uLAFF0wMEP/n5w= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= @@ -182,8 +182,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= @@ -227,9 +227,8 @@ github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfs github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= @@ -276,8 +275,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= -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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -296,21 +295,20 @@ github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE= github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw= -github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA= -github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE= -github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20= +github.com/smallstep/certificates v0.29.0 h1:f90szTKYTW62bmCc+qE5doGqIGPVxTQb8Ba37e/K8Zs= +github.com/smallstep/certificates v0.29.0/go.mod h1:27WI0od6gu84mvE4mYQ/QZGyYwHXvhsiSRNC+y3t+mo= +github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k= +github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y= github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= -github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU= -github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8= +github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo= +github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc= github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE= github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU= -github.com/smallstep/pkcs7 v0.0.0-20240911091500-b1cae6277023/go.mod h1:CM5KrX7rxWgwDdMj9yef/pJB2OPgy/56z4IEx2UIbpc= github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= -github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 h1:LyZqn24/ZiVg8v9Hq07K6mx6RqPtpDeK+De5vf4QEY4= -github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101/go.mod h1:EuKQjYGQwhUa1mgD21zxIgOgUYLsqikJmvxNscxpS/Y= +github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA= +github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ= github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4= github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -368,8 +366,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4= go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E= go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY= @@ -450,7 +448,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= @@ -475,15 +472,14 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= @@ -498,14 +494,12 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= @@ -517,7 +511,6 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= @@ -529,7 +522,6 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= @@ -546,16 +538,16 @@ 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= -google.golang.org/api v0.254.0 h1:jl3XrGj7lRjnlUvZAbAdhINTLbsg5dbjmR90+pTQvt4= -google.golang.org/api v0.254.0/go.mod h1:5BkSURm3D9kAqjGvBNgf0EcbX6Rnrf6UArKkwBzAyqQ= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= 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-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= 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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= From 7ebe72bbfe342688a325325ea79e46970d017eb7 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 3 Dec 2025 19:30:00 +0100 Subject: [PATCH 011/206] caddypki: Add support for multiple intermediates in signing chain (#7057) * caddypki: Add support for multiple intermediates in signing chain * Move intermediate lifetime configuration check In #7272 a check was changed to ensure that generated intermediate certificates would always use a lifetime that falls within the lifetime of the root. However, when a root and intermediate(s) are supplied, the configuration value was being used instead of the actual lifetimes of the certificates. The check was moved to only be performed when an intermediate is generated; not when loaded from disk. * Add tests for `pemDecodeCertificateChain` and `pemDecodeCertificate` * Use `crypto.Signer` instead of `any` in appropriate places * Use latest Smallstep packages --------- Co-authored-by: Matt Holt --- go.mod | 2 +- modules/caddypki/adminapi.go | 13 +- modules/caddypki/ca.go | 73 +++--- modules/caddypki/crypto.go | 65 ++++- modules/caddypki/crypto_test.go | 314 ++++++++++++++++++++++++ modules/caddypki/maintain.go | 10 +- modules/caddytls/capools.go | 6 +- modules/caddytls/internalissuer.go | 3 +- modules/caddytls/internalissuer_test.go | 262 ++++++++++++++++++++ 9 files changed, 700 insertions(+), 48 deletions(-) create mode 100644 modules/caddypki/crypto_test.go create mode 100644 modules/caddytls/internalissuer_test.go diff --git a/go.mod b/go.mod index 29198a510..5269a0841 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/sdk v1.38.0 + go.step.sm/crypto v0.74.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 @@ -166,7 +167,6 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 go.opentelemetry.io/proto/otlp v1.7.1 // indirect - go.step.sm/crypto v0.74.0 go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/sys v0.38.0 diff --git a/modules/caddypki/adminapi.go b/modules/caddypki/adminapi.go index 463e31f35..c37b8d7b6 100644 --- a/modules/caddypki/adminapi.go +++ b/modules/caddypki/adminapi.go @@ -222,11 +222,16 @@ func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) { if err != nil { return root, inter, err } - inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw) - if err != nil { - return root, inter, err + + for _, interCert := range ca.IntermediateCertificateChain() { + pemBytes, err := pemEncodeCert(interCert.Raw) + if err != nil { + return nil, nil, err + } + inter = append(inter, pemBytes...) } - return root, inter, err + + return } // caInfo is the response structure for the CA info API endpoint. diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go index 5b17518ca..8f6fd3afe 100644 --- a/modules/caddypki/ca.go +++ b/modules/caddypki/ca.go @@ -75,10 +75,11 @@ type CA struct { // and module provisioning. ID string `json:"-"` - storage certmagic.Storage - root, inter *x509.Certificate - interKey any // TODO: should we just store these as crypto.Signer? - mu *sync.RWMutex + storage certmagic.Storage + root *x509.Certificate + interChain []*x509.Certificate + interKey crypto.Signer + mu *sync.RWMutex rootCertPath string // mainly used for logging purposes if trusting log *zap.Logger @@ -127,14 +128,16 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { } // load the certs and key that will be used for signing - var rootCert, interCert *x509.Certificate + var rootCert *x509.Certificate + var rootCertChain, interCertChain []*x509.Certificate var rootKey, interKey crypto.Signer var err error if ca.Root != nil { if ca.Root.Format == "" || ca.Root.Format == "pem_file" { ca.rootCertPath = ca.Root.Certificate } - rootCert, rootKey, err = ca.Root.Load() + rootCertChain, rootKey, err = ca.Root.Load() + rootCert = rootCertChain[0] } else { ca.rootCertPath = "storage:" + ca.storageKeyRootCert() rootCert, rootKey, err = ca.loadOrGenRoot() @@ -142,21 +145,23 @@ 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() + interCertChain, interKey, err = ca.Intermediate.Load() } else { - interCert, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey) + 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) + } + + interCertChain, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey) } if err != nil { return err } ca.mu.Lock() - ca.root, ca.inter, ca.interKey = rootCert, interCert, interKey + ca.root, ca.interChain, ca.interKey = rootCert, interCertChain, interKey ca.mu.Unlock() return nil @@ -172,21 +177,21 @@ func (ca CA) RootCertificate() *x509.Certificate { // RootKey returns the CA's root private key. Since the root key is // not cached in memory long-term, it needs to be loaded from storage, // which could yield an error. -func (ca CA) RootKey() (any, error) { +func (ca CA) RootKey() (crypto.Signer, error) { _, rootKey, err := ca.loadOrGenRoot() return rootKey, err } -// IntermediateCertificate returns the CA's intermediate -// certificate (public key). -func (ca CA) IntermediateCertificate() *x509.Certificate { +// IntermediateCertificateChain returns the CA's intermediate +// certificate chain. +func (ca CA) IntermediateCertificateChain() []*x509.Certificate { ca.mu.RLock() defer ca.mu.RUnlock() - return ca.inter + return ca.interChain } // IntermediateKey returns the CA's intermediate private key. -func (ca CA) IntermediateKey() any { +func (ca CA) IntermediateKey() crypto.Signer { ca.mu.RLock() defer ca.mu.RUnlock() return ca.interKey @@ -207,26 +212,27 @@ func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authorit // cert/key directly, since it's unlikely to expire // while Caddy is running (long lifetime) var issuerCert *x509.Certificate - var issuerKey any + var issuerKey crypto.Signer issuerCert = rootCert var err error issuerKey, err = ca.RootKey() if err != nil { return nil, fmt.Errorf("loading signing key: %v", err) } - signerOption = authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)) + signerOption = authority.WithX509Signer(issuerCert, issuerKey) } else { // if we're signing with intermediate, we need to make // sure it's always fresh, because the intermediate may // renew while Caddy is running (medium lifetime) signerOption = authority.WithX509SignerFunc(func() ([]*x509.Certificate, crypto.Signer, error) { - issuerCert := ca.IntermediateCertificate() - issuerKey := ca.IntermediateKey().(crypto.Signer) + issuerChain := ca.IntermediateCertificateChain() + issuerCert := issuerChain[0] + issuerKey := ca.IntermediateKey() ca.log.Debug("using intermediate signer", zap.String("serial", issuerCert.SerialNumber.String()), zap.String("not_before", issuerCert.NotBefore.String()), zap.String("not_after", issuerCert.NotAfter.String())) - return []*x509.Certificate{issuerCert}, issuerKey, nil + return issuerChain, issuerKey, nil }) } @@ -252,7 +258,11 @@ func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authorit func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, err error) { if ca.Root != nil { - return ca.Root.Load() + rootChain, rootSigner, err := ca.Root.Load() + if err != nil { + return nil, nil, err + } + return rootChain[0], rootSigner, nil } rootCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyRootCert()) if err != nil { @@ -268,7 +278,7 @@ func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, } if rootCert == nil { - rootCert, err = pemDecodeSingleCert(rootCertPEM) + rootCert, err = pemDecodeCertificate(rootCertPEM) if err != nil { return nil, nil, fmt.Errorf("parsing root certificate PEM: %v", err) } @@ -314,7 +324,8 @@ func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, err e return rootCert, rootKey, nil } -func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) { +func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCertChain []*x509.Certificate, interKey crypto.Signer, err error) { + var interCert *x509.Certificate interCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyIntermediateCert()) if err != nil { if !errors.Is(err, fs.ErrNotExist) { @@ -326,10 +337,12 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Si if err != nil { return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err) } + + interCertChain = append(interCertChain, interCert) } - if interCert == nil { - interCert, err = pemDecodeSingleCert(interCertPEM) + if len(interCertChain) == 0 { + interCertChain, err = pemDecodeCertificateChain(interCertPEM) if err != nil { return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err) } @@ -346,7 +359,7 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Si } } - return interCert, interKey, nil + return interCertChain, interKey, nil } func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) { diff --git a/modules/caddypki/crypto.go b/modules/caddypki/crypto.go index 324a4fcfa..715155eb2 100644 --- a/modules/caddypki/crypto.go +++ b/modules/caddypki/crypto.go @@ -17,15 +17,20 @@ package caddypki import ( "bytes" "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" "crypto/x509" "encoding/pem" + "errors" "fmt" "os" "github.com/caddyserver/certmagic" + "go.step.sm/crypto/pemutil" ) -func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) { +func pemDecodeCertificate(pemDER []byte) (*x509.Certificate, error) { pemBlock, remaining := pem.Decode(pemDER) if pemBlock == nil { return nil, fmt.Errorf("no PEM block found") @@ -39,6 +44,15 @@ func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) { return x509.ParseCertificate(pemBlock.Bytes) } +func pemDecodeCertificateChain(pemDER []byte) ([]*x509.Certificate, error) { + chain, err := pemutil.ParseCertificateBundle(pemDER) + if err != nil { + return nil, fmt.Errorf("failed parsing certificate chain: %w", err) + } + + return chain, nil +} + func pemEncodeCert(der []byte) ([]byte, error) { return pemEncode("CERTIFICATE", der) } @@ -70,15 +84,18 @@ type KeyPair struct { Format string `json:"format,omitempty"` } -// Load loads the certificate and key. -func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) { +// Load loads the certificate chain and (optional) private key from +// the corresponding files, using the configured format. If a +// private key is read, it will be verified to belong to the first +// certificate in the chain. +func (kp KeyPair) Load() ([]*x509.Certificate, crypto.Signer, error) { switch kp.Format { case "", "pem_file": certData, err := os.ReadFile(kp.Certificate) if err != nil { return nil, nil, err } - cert, err := pemDecodeSingleCert(certData) + chain, err := pemDecodeCertificateChain(certData) if err != nil { return nil, nil, err } @@ -93,11 +110,49 @@ func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) { if err != nil { return nil, nil, err } + if err := verifyKeysMatch(chain[0], key); err != nil { + return nil, nil, err + } } - return cert, key, nil + return chain, key, nil default: return nil, nil, fmt.Errorf("unsupported format: %s", kp.Format) } } + +// verifyKeysMatch verifies that the public key in the [x509.Certificate] matches +// the public key of the [crypto.Signer]. +func verifyKeysMatch(crt *x509.Certificate, signer crypto.Signer) error { + switch pub := crt.PublicKey.(type) { + case *rsa.PublicKey: + pk, ok := signer.Public().(*rsa.PublicKey) + if !ok { + return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub) + } + if !pub.Equal(pk) { + return errors.New("private key does not match issuer public key") + } + case *ecdsa.PublicKey: + pk, ok := signer.Public().(*ecdsa.PublicKey) + if !ok { + return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub) + } + if !pub.Equal(pk) { + return errors.New("private key does not match issuer public key") + } + case ed25519.PublicKey: + pk, ok := signer.Public().(ed25519.PublicKey) + if !ok { + return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub) + } + if !pub.Equal(pk) { + return errors.New("private key does not match issuer public key") + } + default: + return fmt.Errorf("unsupported key type: %T", pub) + } + + return nil +} diff --git a/modules/caddypki/crypto_test.go b/modules/caddypki/crypto_test.go new file mode 100644 index 000000000..a07763d14 --- /dev/null +++ b/modules/caddypki/crypto_test.go @@ -0,0 +1,314 @@ +// 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 caddypki + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "os" + "path/filepath" + "testing" + "time" + + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/pemutil" +) + +func TestKeyPair_Load(t *testing.T) { + rootSigner, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatalf("Failed creating signer: %v", err) + } + + tmpl := &x509.Certificate{ + Subject: pkix.Name{CommonName: "test-root"}, + IsCA: true, + MaxPathLen: 3, + } + rootBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootSigner.Public(), rootSigner) + if err != nil { + t.Fatalf("Creating root certificate failed: %v", err) + } + + root, err := x509.ParseCertificate(rootBytes) + if err != nil { + t.Fatalf("Parsing root certificate failed: %v", err) + } + + intermediateSigner, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatalf("Creating intermedaite signer failed: %v", err) + } + + intermediateBytes, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ + Subject: pkix.Name{CommonName: "test-first-intermediate"}, + IsCA: true, + MaxPathLen: 2, + NotAfter: time.Now().Add(time.Hour), + }, root, intermediateSigner.Public(), rootSigner) + if err != nil { + t.Fatalf("Creating intermediate certificate failed: %v", err) + } + + intermediate, err := x509.ParseCertificate(intermediateBytes) + if err != nil { + t.Fatalf("Parsing intermediate certificate failed: %v", err) + } + + var chainContents []byte + chain := []*x509.Certificate{intermediate, root} + for _, cert := range chain { + b, err := pemutil.Serialize(cert) + if err != nil { + t.Fatalf("Failed serializing intermediate certificate: %v", err) + } + chainContents = append(chainContents, pem.EncodeToMemory(b)...) + } + + dir := t.TempDir() + rootCertFile := filepath.Join(dir, "root.pem") + if _, err = pemutil.Serialize(root, pemutil.WithFilename(rootCertFile)); err != nil { + t.Fatalf("Failed serializing root certificate: %v", err) + } + rootKeyFile := filepath.Join(dir, "root.key") + if _, err = pemutil.Serialize(rootSigner, pemutil.WithFilename(rootKeyFile)); err != nil { + t.Fatalf("Failed serializing root key: %v", err) + } + intermediateCertFile := filepath.Join(dir, "intermediate.pem") + if _, err = pemutil.Serialize(intermediate, pemutil.WithFilename(intermediateCertFile)); err != nil { + t.Fatalf("Failed serializing intermediate certificate: %v", err) + } + intermediateKeyFile := filepath.Join(dir, "intermediate.key") + if _, err = pemutil.Serialize(intermediateSigner, pemutil.WithFilename(intermediateKeyFile)); err != nil { + t.Fatalf("Failed serializing intermediate key: %v", err) + } + chainFile := filepath.Join(dir, "chain.pem") + if err := os.WriteFile(chainFile, chainContents, 0644); err != nil { + t.Fatalf("Failed writing intermediate chain: %v", err) + } + + t.Run("ok/single-certificate-without-signer", func(t *testing.T) { + kp := KeyPair{ + Certificate: rootCertFile, + } + chain, signer, err := kp.Load() + if err != nil { + t.Fatalf("Failed loading KeyPair: %v", err) + } + if len(chain) != 1 { + t.Errorf("Expected 1 certificate in chain; got %d", len(chain)) + } + if signer != nil { + t.Error("Expected no signer to be returned") + } + }) + + t.Run("ok/single-certificate-with-signer", func(t *testing.T) { + kp := KeyPair{ + Certificate: rootCertFile, + PrivateKey: rootKeyFile, + } + chain, signer, err := kp.Load() + if err != nil { + t.Fatalf("Failed loading KeyPair: %v", err) + } + if len(chain) != 1 { + t.Errorf("Expected 1 certificate in chain; got %d", len(chain)) + } + if signer == nil { + t.Error("Expected signer to be returned") + } + }) + + t.Run("ok/multiple-certificates-with-signer", func(t *testing.T) { + kp := KeyPair{ + Certificate: chainFile, + PrivateKey: intermediateKeyFile, + } + chain, signer, err := kp.Load() + if err != nil { + t.Fatalf("Failed loading KeyPair: %v", err) + } + if len(chain) != 2 { + t.Errorf("Expected 2 certificates in chain; got %d", len(chain)) + } + if signer == nil { + t.Error("Expected signer to be returned") + } + }) + + t.Run("fail/non-matching-public-key", func(t *testing.T) { + kp := KeyPair{ + Certificate: intermediateCertFile, + PrivateKey: rootKeyFile, + } + chain, signer, err := kp.Load() + if err == nil { + t.Error("Expected loading KeyPair to return an error") + } + if chain != nil { + t.Error("Expected no chain to be returned") + } + if signer != nil { + t.Error("Expected no signer to be returned") + } + }) +} + +func Test_pemDecodeCertificate(t *testing.T) { + signer, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatalf("Failed creating signer: %v", err) + } + + tmpl := &x509.Certificate{ + Subject: pkix.Name{CommonName: "test-cert"}, + IsCA: true, + MaxPathLen: 3, + } + derBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, signer.Public(), signer) + if err != nil { + t.Fatalf("Creating root certificate failed: %v", err) + } + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + t.Fatalf("Parsing root certificate failed: %v", err) + } + + pemBlock, err := pemutil.Serialize(cert) + if err != nil { + t.Fatalf("Failed serializing certificate: %v", err) + } + pemData := pem.EncodeToMemory(pemBlock) + + t.Run("ok", func(t *testing.T) { + cert, err := pemDecodeCertificate(pemData) + if err != nil { + t.Fatalf("Failed decoding PEM data: %v", err) + } + if cert == nil { + t.Errorf("Expected a certificate in PEM data") + } + }) + + t.Run("fail/no-pem-data", func(t *testing.T) { + cert, err := pemDecodeCertificate(nil) + if err == nil { + t.Fatalf("Expected pemDecodeCertificate to return an error") + } + if cert != nil { + t.Errorf("Expected pemDecodeCertificate to return nil") + } + }) + + t.Run("fail/multiple", func(t *testing.T) { + multiplePEMData := append(pemData, pemData...) + cert, err := pemDecodeCertificate(multiplePEMData) + if err == nil { + t.Fatalf("Expected pemDecodeCertificate to return an error") + } + if cert != nil { + t.Errorf("Expected pemDecodeCertificate to return nil") + } + }) + + t.Run("fail/no-pem-certificate", func(t *testing.T) { + pkData := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: []byte("some-bogus-private-key"), + }) + cert, err := pemDecodeCertificate(pkData) + if err == nil { + t.Fatalf("Expected pemDecodeCertificate to return an error") + } + if cert != nil { + t.Errorf("Expected pemDecodeCertificate to return nil") + } + }) +} + +func Test_pemDecodeCertificateChain(t *testing.T) { + signer, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatalf("Failed creating signer: %v", err) + } + + tmpl := &x509.Certificate{ + Subject: pkix.Name{CommonName: "test-cert"}, + IsCA: true, + MaxPathLen: 3, + } + derBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, signer.Public(), signer) + if err != nil { + t.Fatalf("Creating root certificate failed: %v", err) + } + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + t.Fatalf("Parsing root certificate failed: %v", err) + } + + pemBlock, err := pemutil.Serialize(cert) + if err != nil { + t.Fatalf("Failed serializing certificate: %v", err) + } + pemData := pem.EncodeToMemory(pemBlock) + + t.Run("ok/single", func(t *testing.T) { + certs, err := pemDecodeCertificateChain(pemData) + if err != nil { + t.Fatalf("Failed decoding PEM data: %v", err) + } + if len(certs) != 1 { + t.Errorf("Expected 1 certificate in PEM data; got %d", len(certs)) + } + }) + + t.Run("ok/multiple", func(t *testing.T) { + multiplePEMData := append(pemData, pemData...) + certs, err := pemDecodeCertificateChain(multiplePEMData) + if err != nil { + t.Fatalf("Failed decoding PEM data: %v", err) + } + if len(certs) != 2 { + t.Errorf("Expected 2 certificates in PEM data; got %d", len(certs)) + } + }) + + t.Run("fail/no-pem-certificate", func(t *testing.T) { + pkData := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: []byte("some-bogus-private-key"), + }) + certs, err := pemDecodeCertificateChain(pkData) + if err == nil { + t.Fatalf("Expected pemDecodeCertificateChain to return an error") + } + if len(certs) != 0 { + t.Errorf("Expected 0 certificates in PEM data; got %d", len(certs)) + } + }) + + t.Run("fail/no-der-certificate", func(t *testing.T) { + certs, err := pemDecodeCertificateChain([]byte("invalid-der-data")) + if err == nil { + t.Fatalf("Expected pemDecodeCertificateChain to return an error") + } + if len(certs) != 0 { + t.Errorf("Expected 0 certificates in PEM data; got %d", len(certs)) + } + }) +} diff --git a/modules/caddypki/maintain.go b/modules/caddypki/maintain.go index 31e453ff9..091e71243 100644 --- a/modules/caddypki/maintain.go +++ b/modules/caddypki/maintain.go @@ -66,16 +66,16 @@ func (p *PKI) renewCertsForCA(ca *CA) error { if needsRenewal(ca.root) { // TODO: implement root renewal (use same key) log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)", - zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)), + zap.Duration("time_remaining", time.Until(ca.interChain[0].NotAfter)), ) } } // only maintain the intermediate if it's not manually provided in the config if ca.Intermediate == nil { - if needsRenewal(ca.inter) { + if needsRenewal(ca.interChain[0]) { log.Info("intermediate expires soon; renewing", - zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)), + zap.Duration("time_remaining", time.Until(ca.interChain[0].NotAfter)), ) rootCert, rootKey, err := ca.loadOrGenRoot() @@ -86,10 +86,10 @@ func (p *PKI) renewCertsForCA(ca *CA) error { if err != nil { return fmt.Errorf("generating new certificate: %v", err) } - ca.inter, ca.interKey = interCert, interKey + ca.interChain, ca.interKey = []*x509.Certificate{interCert}, interKey log.Info("renewed intermediate", - zap.Time("new_expiration", ca.inter.NotAfter), + zap.Time("new_expiration", ca.interChain[0].NotAfter), ) } } diff --git a/modules/caddytls/capools.go b/modules/caddytls/capools.go index c73bc4832..5ed6a82a9 100644 --- a/modules/caddytls/capools.go +++ b/modules/caddytls/capools.go @@ -257,7 +257,7 @@ func (PKIIntermediateCAPool) CaddyModule() caddy.ModuleInfo { } } -// Loads the PKI app and load the intermediate certificates into the certificate pool +// Loads the PKI app and loads the intermediate certificates into the certificate pool func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error { pkiApp, err := ctx.AppIfConfigured("pki") if err != nil { @@ -274,7 +274,9 @@ func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error { caPool := x509.NewCertPool() for _, ca := range p.ca { - caPool.AddCert(ca.IntermediateCertificate()) + for _, c := range ca.IntermediateCertificateChain() { + caPool.AddCert(c) + } } p.pool = caPool return nil diff --git a/modules/caddytls/internalissuer.go b/modules/caddytls/internalissuer.go index be779757a..cad19f063 100644 --- a/modules/caddytls/internalissuer.go +++ b/modules/caddytls/internalissuer.go @@ -115,7 +115,8 @@ func (iss InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateReques if iss.SignWithRoot { issuerCert = iss.ca.RootCertificate() } else { - issuerCert = iss.ca.IntermediateCertificate() + chain := iss.ca.IntermediateCertificateChain() + issuerCert = chain[0] } // ensure issued certificate does not expire later than its issuer diff --git a/modules/caddytls/internalissuer_test.go b/modules/caddytls/internalissuer_test.go new file mode 100644 index 000000000..d39d8373b --- /dev/null +++ b/modules/caddytls/internalissuer_test.go @@ -0,0 +1,262 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddytls + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddypki" + "go.uber.org/zap" + + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/pemutil" +) + +func TestInternalIssuer_Issue(t *testing.T) { + rootSigner, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatalf("Creating root signer failed: %v", err) + } + + tmpl := &x509.Certificate{ + Subject: pkix.Name{CommonName: "test-root"}, + IsCA: true, + MaxPathLen: 3, + NotAfter: time.Now().Add(7 * 24 * time.Hour), + NotBefore: time.Now().Add(-7 * 24 * time.Hour), + } + rootBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootSigner.Public(), rootSigner) + if err != nil { + t.Fatalf("Creating root certificate failed: %v", err) + } + + root, err := x509.ParseCertificate(rootBytes) + if err != nil { + t.Fatalf("Parsing root certificate failed: %v", err) + } + + firstIntermediateSigner, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatalf("Creating intermedaite signer failed: %v", err) + } + + firstIntermediateBytes, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ + Subject: pkix.Name{CommonName: "test-first-intermediate"}, + IsCA: true, + MaxPathLen: 2, + NotAfter: time.Now().Add(24 * time.Hour), + NotBefore: time.Now().Add(-24 * time.Hour), + }, root, firstIntermediateSigner.Public(), rootSigner) + if err != nil { + t.Fatalf("Creating intermediate certificate failed: %v", err) + } + + firstIntermediate, err := x509.ParseCertificate(firstIntermediateBytes) + if err != nil { + t.Fatalf("Parsing intermediate certificate failed: %v", err) + } + + secondIntermediateSigner, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatalf("Creating second intermedaite signer failed: %v", err) + } + + secondIntermediateBytes, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ + Subject: pkix.Name{CommonName: "test-second-intermediate"}, + IsCA: true, + MaxPathLen: 2, + NotAfter: time.Now().Add(24 * time.Hour), + NotBefore: time.Now().Add(-24 * time.Hour), + }, firstIntermediate, secondIntermediateSigner.Public(), firstIntermediateSigner) + if err != nil { + t.Fatalf("Creating second intermediate certificate failed: %v", err) + } + + secondIntermediate, err := x509.ParseCertificate(secondIntermediateBytes) + if err != nil { + t.Fatalf("Parsing second intermediate certificate failed: %v", err) + } + + dir := t.TempDir() + storageDir := filepath.Join(dir, "certmagic") + rootCertFile := filepath.Join(dir, "root.pem") + if _, err = pemutil.Serialize(root, pemutil.WithFilename(rootCertFile)); err != nil { + t.Fatalf("Failed serializing root certificate: %v", err) + } + intermediateCertFile := filepath.Join(dir, "intermediate.pem") + if _, err = pemutil.Serialize(firstIntermediate, pemutil.WithFilename(intermediateCertFile)); err != nil { + t.Fatalf("Failed serializing intermediate certificate: %v", err) + } + intermediateKeyFile := filepath.Join(dir, "intermediate.key") + if _, err = pemutil.Serialize(firstIntermediateSigner, pemutil.WithFilename(intermediateKeyFile)); err != nil { + t.Fatalf("Failed serializing intermediate key: %v", err) + } + + var intermediateChainContents []byte + intermediateChain := []*x509.Certificate{secondIntermediate, firstIntermediate} + for _, cert := range intermediateChain { + b, err := pemutil.Serialize(cert) + if err != nil { + t.Fatalf("Failed serializing intermediate certificate: %v", err) + } + intermediateChainContents = append(intermediateChainContents, pem.EncodeToMemory(b)...) + } + intermediateChainFile := filepath.Join(dir, "intermediates.pem") + if err := os.WriteFile(intermediateChainFile, intermediateChainContents, 0644); err != nil { + t.Fatalf("Failed writing intermediate chain: %v", err) + } + intermediateChainKeyFile := filepath.Join(dir, "intermediates.key") + if _, err = pemutil.Serialize(secondIntermediateSigner, pemutil.WithFilename(intermediateChainKeyFile)); err != nil { + t.Fatalf("Failed serializing intermediate key: %v", err) + } + + signer, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatalf("Failed creating signer: %v", err) + } + + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "test"}, + }, signer) + if err != nil { + t.Fatalf("Failed creating CSR: %v", err) + } + + csr, err := x509.ParseCertificateRequest(csrBytes) + if err != nil { + t.Fatalf("Failed parsing CSR: %v", err) + } + + t.Run("generated-with-defaults", func(t *testing.T) { + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()}) + t.Cleanup(cancel) + logger := zap.NewNop() + + ca := &caddypki.CA{ + StorageRaw: []byte(fmt.Sprintf(`{"module": "file_system", "root": %q}`, storageDir)), + } + if err := ca.Provision(caddyCtx, "local-test-generated", logger); err != nil { + t.Fatalf("Failed provisioning CA: %v", err) + } + + iss := InternalIssuer{ + SignWithRoot: false, + ca: ca, + logger: logger, + } + + c, err := iss.Issue(t.Context(), csr) + if err != nil { + t.Fatalf("Failed issuing certificate: %v", err) + } + + chain, err := pemutil.ParseCertificateBundle(c.Certificate) + if err != nil { + t.Errorf("Failed issuing certificate: %v", err) + } + if len(chain) != 2 { + t.Errorf("Expected 2 certificates in chain; got %d", len(chain)) + } + }) + + t.Run("single-intermediate-from-disk", func(t *testing.T) { + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()}) + t.Cleanup(cancel) + logger := zap.NewNop() + + ca := &caddypki.CA{ + Root: &caddypki.KeyPair{ + Certificate: rootCertFile, + }, + Intermediate: &caddypki.KeyPair{ + Certificate: intermediateCertFile, + PrivateKey: intermediateKeyFile, + }, + StorageRaw: []byte(fmt.Sprintf(`{"module": "file_system", "root": %q}`, storageDir)), + } + + if err := ca.Provision(caddyCtx, "local-test-single-intermediate", logger); err != nil { + t.Fatalf("Failed provisioning CA: %v", err) + } + + iss := InternalIssuer{ + ca: ca, + SignWithRoot: false, + logger: logger, + } + + c, err := iss.Issue(t.Context(), csr) + if err != nil { + t.Fatalf("Failed issuing certificate: %v", err) + } + + chain, err := pemutil.ParseCertificateBundle(c.Certificate) + if err != nil { + t.Errorf("Failed issuing certificate: %v", err) + } + if len(chain) != 2 { + t.Errorf("Expected 2 certificates in chain; got %d", len(chain)) + } + }) + + t.Run("multiple-intermediates-from-disk", func(t *testing.T) { + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()}) + t.Cleanup(cancel) + logger := zap.NewNop() + + ca := &caddypki.CA{ + Root: &caddypki.KeyPair{ + Certificate: rootCertFile, + }, + Intermediate: &caddypki.KeyPair{ + Certificate: intermediateChainFile, + PrivateKey: intermediateChainKeyFile, + }, + StorageRaw: []byte(fmt.Sprintf(`{"module": "file_system", "root": %q}`, storageDir)), + } + + if err := ca.Provision(caddyCtx, "local-test", zap.NewNop()); err != nil { + t.Fatalf("Failed provisioning CA: %v", err) + } + + iss := InternalIssuer{ + ca: ca, + SignWithRoot: false, + logger: logger, + } + + c, err := iss.Issue(t.Context(), csr) + if err != nil { + t.Fatalf("Failed issuing certificate: %v", err) + } + + chain, err := pemutil.ParseCertificateBundle(c.Certificate) + if err != nil { + t.Errorf("Failed issuing certificate: %v", err) + } + if len(chain) != 3 { + t.Errorf("Expected 3 certificates in chain; got %d", len(chain)) + } + }) +} From be5f49fbeb046fb123583e5887ddf576a312a8e4 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Wed, 3 Dec 2025 13:46:11 -0500 Subject: [PATCH 012/206] caddyhttp: Fix logging on wildcard sites when SkipUnmappedHosts is true (#7372) --- modules/caddyhttp/server.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 94b8febfa..49aa3a730 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -763,9 +763,11 @@ func (s *Server) shouldLogRequest(r *http.Request) bool { hostWithoutPort = r.Host } - if _, ok := s.Logs.LoggerNames[hostWithoutPort]; ok { - // this host is mapped to a particular logger name - return true + for loggerName := range s.Logs.LoggerNames { + if certmagic.MatchWildcard(hostWithoutPort, loggerName) { + // this host is mapped to a particular logger name + return true + } } for _, dh := range s.Logs.SkipHosts { // logging for this particular host is disabled From 31960dc998b1f88e912e950f0c00815ba032aeef Mon Sep 17 00:00:00 2001 From: vnxme <46669194+vnxme@users.noreply.github.com> Date: Fri, 5 Dec 2025 00:15:56 +0300 Subject: [PATCH 013/206] Introduce packet conn wrappers (#7180) * packet_conn_wrappers: Initial changes * packet_conn_wrappers: Unwrap a packet conn only if there are no wrappers --------- Co-authored-by: Matt Holt --- caddyconfig/httpcaddyfile/httptype.go | 14 +++++ caddyconfig/httpcaddyfile/serveroptions.go | 62 +++++++++++++++------- listeners.go | 34 +++++++++--- modules/caddyhttp/app.go | 14 +++++ modules/caddyhttp/server.go | 9 +++- 5 files changed, 104 insertions(+), 29 deletions(-) diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 3dcd3ea5b..49cf40497 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -851,6 +851,20 @@ func (st *ServerType) serversFromPairings( srv.ListenerWrappersRaw = append(srv.ListenerWrappersRaw, jsonListenerWrapper) } + // Look for any config values that provide packet conn wrappers on the server block + for _, listenerConfig := range sblock.pile["packet_conn_wrapper"] { + packetConnWrapper, ok := listenerConfig.Value.(caddy.PacketConnWrapper) + if !ok { + return nil, fmt.Errorf("config for a packet conn wrapper did not provide a value that implements caddy.PacketConnWrapper") + } + jsonPacketConnWrapper := caddyconfig.JSONModuleObject( + packetConnWrapper, + "wrapper", + packetConnWrapper.(caddy.Module).CaddyModule().ID.Name(), + warnings) + srv.PacketConnWrappersRaw = append(srv.PacketConnWrappersRaw, jsonPacketConnWrapper) + } + // set up each handler directive, making sure to honor directive order dirRoutes := sblock.pile["route"] siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true) diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go index 9431f1aed..06ceea3c3 100644 --- a/caddyconfig/httpcaddyfile/serveroptions.go +++ b/caddyconfig/httpcaddyfile/serveroptions.go @@ -36,26 +36,27 @@ type serverOptions struct { ListenerAddress string // These will all map 1:1 to the caddyhttp.Server struct - Name string - ListenerWrappersRaw []json.RawMessage - ReadTimeout caddy.Duration - ReadHeaderTimeout caddy.Duration - 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 - Trace bool // TODO: EXPERIMENTAL + Name string + ListenerWrappersRaw []json.RawMessage + PacketConnWrappersRaw []json.RawMessage + ReadTimeout caddy.Duration + ReadHeaderTimeout caddy.Duration + 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 + Trace bool // TODO: EXPERIMENTAL } func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { @@ -99,6 +100,26 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper) } + case "packet_conn_wrappers": + for nesting := d.Nesting(); d.NextBlock(nesting); { + modID := "caddy.packetconns." + d.Val() + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return nil, err + } + packetConnWrapper, ok := unm.(caddy.PacketConnWrapper) + if !ok { + return nil, fmt.Errorf("module %s (%T) is not a packet conn wrapper", modID, unm) + } + jsonPacketConnWrapper := caddyconfig.JSONModuleObject( + packetConnWrapper, + "wrapper", + packetConnWrapper.(caddy.Module).CaddyModule().ID.Name(), + nil, + ) + serverOpts.PacketConnWrappersRaw = append(serverOpts.PacketConnWrappersRaw, jsonPacketConnWrapper) + } + case "timeouts": for nesting := d.Nesting(); d.NextBlock(nesting); { switch d.Val() { @@ -335,6 +356,7 @@ func applyServerOptions( // set all the options server.ListenerWrappersRaw = opts.ListenerWrappersRaw + server.PacketConnWrappersRaw = opts.PacketConnWrappersRaw server.ReadTimeout = opts.ReadTimeout server.ReadHeaderTimeout = opts.ReadHeaderTimeout server.WriteTimeout = opts.WriteTimeout diff --git a/listeners.go b/listeners.go index a1540521d..b64011939 100644 --- a/listeners.go +++ b/listeners.go @@ -511,7 +511,7 @@ func JoinNetworkAddress(network, host, port string) string { // // NOTE: This API is EXPERIMENTAL and may be changed or removed. // NOTE: user should close the returned listener twice, once to stop accepting new connections, the second time to free up the packet conn. -func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config) (http3.QUICListener, error) { +func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config, pcWrappers []PacketConnWrapper) (http3.QUICListener, error) { lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset)) sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { @@ -523,12 +523,19 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config ln := lnAny.(net.PacketConn) h3ln := ln - for { - // retrieve the underlying socket, so quic-go can optimize. - if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok { - h3ln = unwrapper.Unwrap() - } else { - break + if len(pcWrappers) == 0 { + for { + // retrieve the underlying socket, so quic-go can optimize. + if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok { + h3ln = unwrapper.Unwrap() + } else { + break + } + } + } else { + // wrap packet conn before QUIC + for _, pcWrapper := range pcWrappers { + h3ln = pcWrapper.WrapPacketConn(h3ln) } } @@ -775,6 +782,19 @@ type ListenerWrapper interface { WrapListener(net.Listener) net.Listener } +// PacketConnWrapper is a type that wraps a packet conn +// so it can modify the input packet conn methods. +// Modules that implement this interface are found +// in the caddy.packetconns namespace. Usually, to +// wrap a packet conn, you will define your own struct +// type that embeds the input packet conn, then +// implement your own methods that you want to wrap, +// calling the underlying packet conn methods where +// appropriate. +type PacketConnWrapper interface { + WrapPacketConn(net.PacketConn) net.PacketConn +} + // listenerPool stores and allows reuse of active listeners. var listenerPool = NewUsagePool() diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 6ad18d051..3c81d1d60 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -346,6 +346,20 @@ func (app *App) Provision(ctx caddy.Context) error { srv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...) } } + + // set up each packet conn modifier + if srv.PacketConnWrappersRaw != nil { + vals, err := ctx.LoadModule(srv, "PacketConnWrappersRaw") + if err != nil { + return fmt.Errorf("loading packet conn wrapper modules: %v", err) + } + // if any wrappers were configured, they come before the QUIC handshake; + // unlike TLS above, there is no QUIC placeholder + for _, val := range vals.([]any) { + srv.packetConnWrappers = append(srv.packetConnWrappers, val.(caddy.PacketConnWrapper)) + } + } + // pre-compile the primary handler chain, and be sure to wrap it in our // route handler so that important security checks are done, etc. primaryRoute := emptyHandler diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 49aa3a730..dd47ec8a3 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -55,6 +55,10 @@ type Server struct { // of the base listener. They are applied in the given order. ListenerWrappersRaw []json.RawMessage `json:"listener_wrappers,omitempty" caddy:"namespace=caddy.listeners inline_key=wrapper"` + // A list of packet conn wrapper modules, which can modify the behavior + // of the base packet conn. They are applied in the given order. + PacketConnWrappersRaw []json.RawMessage `json:"packet_conn_wrappers,omitempty" caddy:"namespace=caddy.packetconns inline_key=wrapper"` + // How long to allow a read from a client's upload. Setting this // to a short, non-zero value can mitigate slowloris attacks, but // may also affect legitimately slow clients. @@ -258,7 +262,8 @@ type Server struct { primaryHandlerChain Handler errorHandlerChain Handler listenerWrappers []caddy.ListenerWrapper - listeners []net.Listener // stdlib http.Server will close these + packetConnWrappers []caddy.PacketConnWrapper + listeners []net.Listener quicListeners []http3.QUICListener // http3 now leave the quic.Listener management to us tlsApp *caddytls.TLS @@ -625,7 +630,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err) } addr.Network = h3net - h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg) + h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg, s.packetConnWrappers) if err != nil { return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err) } From bfdb04912d26e367986984ec435a7879a817614f Mon Sep 17 00:00:00 2001 From: Steffen Busch <37350514+steffenbusch@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:51:28 +0100 Subject: [PATCH 014/206] docs: add maybe template function documentation (#7388) --- modules/caddyhttp/templates/templates.go | 26 ++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index eb6488659..994beefab 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -306,6 +306,13 @@ func init() { // find the documentation on time layouts [in Go's docs](https://pkg.go.dev/time#pkg-constants). // The default time layout is `RFC1123Z`, i.e. `Mon, 02 Jan 2006 15:04:05 -0700`. // +// ``` +// {{humanize "size" "2048000"}} +// {{placeholder "http.response.header.Content-Length" | humanize "size"}} +// {{humanize "time" "Fri, 05 May 2022 15:04:05 +0200"}} +// {{humanize "time:2006-Jan-02" "2022-May-05"}} +// ``` +// // ##### `pathEscape` // // Passes a string through `url.PathEscape`, replacing characters that have @@ -318,11 +325,22 @@ func init() { // {{pathEscape "50%_valid_filename?.jpg"}} // ``` // +// ##### `maybe` +// +// Invokes a custom template function only if it is registered (plugged-in) +// in the `http.handlers.templates.functions.*` namespace. +// +// The first argument is the function name, and any subsequent arguments +// are forwarded to that function. If the named function is not available, +// the invocation is ignored and a log message is emitted. +// +// This is useful for templates that optionally use components which may +// not be present in every build or environment. +// +// NOTE: This function is EXPERIMENTAL and subject to change or removal. +// // ``` -// {{humanize "size" "2048000"}} -// {{placeholder "http.response.header.Content-Length" | humanize "size"}} -// {{humanize "time" "Fri, 05 May 2022 15:04:05 +0200"}} -// {{humanize "time:2006-Jan-02" "2022-May-05"}} +// {{ maybe "myOptionalFunc" "arg1" 2 }} // ``` type Templates struct { // The root path from which to load files. Required if template functions From 6e0cbd0fa0d3022b41fc2c17b80f15402e685643 Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Mon, 8 Dec 2025 00:01:58 +0800 Subject: [PATCH 015/206] caddyhttp: create a placeholder for and log ech status (#7328) Co-authored-by: Francis Lavoie --- modules/caddyhttp/app.go | 1 + modules/caddyhttp/marshalers.go | 1 + modules/caddyhttp/replacer.go | 2 ++ 3 files changed, 4 insertions(+) diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 3c81d1d60..ac2a29c19 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -82,6 +82,7 @@ func init() { // `{http.request.tls.proto}` | The negotiated next protocol // `{http.request.tls.proto_mutual}` | The negotiated next protocol was advertised by the server // `{http.request.tls.server_name}` | The server name requested by the client, if any +// `{http.request.tls.ech}` | Whether ECH was offered by the client and accepted by the server // `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate // `{http.request.tls.client.public_key}` | The public key of the client certificate. // `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key. diff --git a/modules/caddyhttp/marshalers.go b/modules/caddyhttp/marshalers.go index 9bce377f4..2a40b6cd7 100644 --- a/modules/caddyhttp/marshalers.go +++ b/modules/caddyhttp/marshalers.go @@ -110,6 +110,7 @@ func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error enc.AddUint16("cipher_suite", t.CipherSuite) enc.AddString("proto", t.NegotiatedProtocol) enc.AddString("server_name", t.ServerName) + enc.AddBool("ech", t.ECHAccepted) if len(t.PeerCertificates) > 0 { enc.AddString("client_common_name", t.PeerCertificates[0].Subject.CommonName) enc.AddString("client_serial", t.PeerCertificates[0].SerialNumber.String()) diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 9c3ab85f2..2c372a9e8 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -511,6 +511,8 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) { return true, true case "server_name": return req.TLS.ServerName, true + case "ech": + return req.TLS.ECHAccepted, true } return nil, false } From 374b7a637f2b6f8d0f2723ad2e7908a41023e8d5 Mon Sep 17 00:00:00 2001 From: okrc Date: Mon, 8 Dec 2025 00:19:01 +0800 Subject: [PATCH 016/206] caddytls: fix preferred chains options by appending values instead of replacing (#7387) --- modules/caddytls/acmeissuer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 7f13fd71f..34bcfc0dc 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -671,7 +671,7 @@ func ParseCaddyfilePreferredChainsOptions(d *caddyfile.Dispenser) (*ChainPrefere switch d.Val() { case "root_common_name": rootCommonNameOpt := d.RemainingArgs() - chainPref.RootCommonName = rootCommonNameOpt + chainPref.RootCommonName = append(chainPref.RootCommonName, rootCommonNameOpt...) if rootCommonNameOpt == nil { return nil, d.ArgErr() } @@ -681,7 +681,7 @@ func ParseCaddyfilePreferredChainsOptions(d *caddyfile.Dispenser) (*ChainPrefere case "any_common_name": anyCommonNameOpt := d.RemainingArgs() - chainPref.AnyCommonName = anyCommonNameOpt + chainPref.AnyCommonName = append(chainPref.AnyCommonName, anyCommonNameOpt...) if anyCommonNameOpt == nil { return nil, d.ArgErr() } From 598b08f9ae6c6ab9d19142bc3992d07fa3155628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 8 Dec 2025 23:32:00 +0100 Subject: [PATCH 017/206] test: mark `Assert*` functions as test helpers (#7380) --- caddytest/caddytest.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 7b56bb281..dfced29cf 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -362,6 +362,8 @@ func CreateTestingTransport() *http.Transport { // AssertLoadError will load a config and expect an error func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { + t.Helper() + tc := NewTester(t) err := tc.initServer(rawConfig, configType) @@ -372,6 +374,8 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected // AssertRedirect makes a request and asserts the redirection happens func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response { + tc.t.Helper() + redirectPolicyFunc := func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } @@ -409,6 +413,8 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e // CompareAdapt adapts a config and then compares it against an expected result func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool { + t.Helper() + cfgAdapter := caddyconfig.GetAdapter(adapterName) if cfgAdapter == nil { t.Logf("unrecognized config adapter '%s'", adapterName) @@ -468,6 +474,8 @@ func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, // AssertAdapt adapts a config and then tests it against an expected result func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) { + t.Helper() + ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse) if !ok { t.Fail() @@ -496,6 +504,8 @@ func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) { // AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response { + tc.t.Helper() + resp, err := tc.Client.Do(req) if err != nil { tc.t.Fatalf("failed to call server %s", err) @@ -510,6 +520,8 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) // AssertResponse request a URI and assert the status code and the body contains a string func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) { + tc.t.Helper() + resp := tc.AssertResponseCode(req, expectedStatusCode) defer resp.Body.Close() @@ -531,6 +543,8 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe // AssertGetResponse GET a URI and expect a statusCode and body text func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { + tc.t.Helper() + req, err := http.NewRequest("GET", requestURI, nil) if err != nil { tc.t.Fatalf("unable to create request %s", err) @@ -541,6 +555,8 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e // AssertDeleteResponse request a URI and expect a statusCode and body text func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { + tc.t.Helper() + req, err := http.NewRequest("DELETE", requestURI, nil) if err != nil { tc.t.Fatalf("unable to create request %s", err) @@ -551,6 +567,8 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int // AssertPostResponseBody POST to a URI and assert the response code and body func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { + tc.t.Helper() + req, err := http.NewRequest("POST", requestURI, requestBody) if err != nil { tc.t.Errorf("failed to create request %s", err) @@ -564,6 +582,8 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str // AssertPutResponseBody PUT to a URI and assert the response code and body func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { + tc.t.Helper() + req, err := http.NewRequest("PUT", requestURI, requestBody) if err != nil { tc.t.Errorf("failed to create request %s", err) @@ -577,6 +597,8 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri // AssertPatchResponseBody PATCH to a URI and assert the response code and body func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { + tc.t.Helper() + req, err := http.NewRequest("PATCH", requestURI, requestBody) if err != nil { tc.t.Errorf("failed to create request %s", err) From 3c9c67e804eb3db9a3ac6532ddd2434246058cd5 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Wed, 10 Dec 2025 11:50:35 -0700 Subject: [PATCH 018/206] caddytls: ECH key rotation (#7356) * caddytls: ECH key rotation * Stop rotation goroutine on config unload * Publish ECH keys after rotating --- modules/caddytls/connpolicy.go | 18 +-- modules/caddytls/ech.go | 263 +++++++++++++++++++++++++-------- modules/caddytls/tls.go | 34 ++++- 3 files changed, 238 insertions(+), 77 deletions(-) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 724271a8e..036c5fb92 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -168,21 +168,11 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { tlsApp.RegisterServerNames(echNames) } - // TODO: Ideally, ECH keys should be rotated. However, as of Go 1.24, the std lib implementation - // does not support safely modifying the tls.Config's EncryptedClientHelloKeys field. - // So, we implement static ECH keys temporarily. See https://github.com/golang/go/issues/71920. - // Revisit this after Go 1.25 is released and implement key rotation. - var stdECHKeys []tls.EncryptedClientHelloKey - for _, echConfigs := range tlsApp.EncryptedClientHello.configs { - for _, c := range echConfigs { - stdECHKeys = append(stdECHKeys, tls.EncryptedClientHelloKey{ - Config: c.configBin, - PrivateKey: c.privKeyBin, - SendAsRetry: c.sendAsRetry, - }) - } + tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { + tlsApp.EncryptedClientHello.configsMu.RLock() + defer tlsApp.EncryptedClientHello.configsMu.RUnlock() + return tlsApp.EncryptedClientHello.stdlibReady, nil } - tlsCfg.EncryptedClientHelloKeys = stdECHKeys } } diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index 1b3bacbd2..a5b70d17d 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -2,6 +2,7 @@ package caddytls import ( "context" + "crypto/tls" "encoding/base64" "encoding/json" "errors" @@ -11,6 +12,7 @@ import ( "path" "strconv" "strings" + "sync" "time" "github.com/caddyserver/certmagic" @@ -73,14 +75,17 @@ type ECH struct { // DNS RRs. (This also typically requires that they use DoH or DoT.) Publication []*ECHPublication `json:"publication,omitempty"` - // map of public_name to list of configs - configs map[string][]echConfig + configsMu *sync.RWMutex // protects both configs and the list of configs/keys the standard library uses + configs map[string][]echConfig // map of public_name to list of configs + stdlibReady []tls.EncryptedClientHelloKey // ECH configs+keys in a format the standard library can use } // Provision loads or creates ECH configs and returns outer names (for certificate // management), but does not publish any ECH configs. The DNS module is used as // a default for later publishing if needed. func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { + ech.configsMu = new(sync.RWMutex) + logger := ctx.Logger().Named("ech") // set up publication modules before we need to obtain a lock in storage, @@ -98,17 +103,57 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { // the rest of provisioning needs an exclusive lock so that instances aren't // stepping on each other when setting up ECH configs storage := ctx.Storage() - const echLockName = "ech_provision" - if err := storage.Lock(ctx, echLockName); err != nil { + if err := storage.Lock(ctx, echStorageLockName); err != nil { return nil, err } defer func() { - if err := storage.Unlock(ctx, echLockName); err != nil { + if err := storage.Unlock(ctx, echStorageLockName); err != nil { logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err)) } }() - var outerNames []string //nolint:prealloc // (FALSE POSITIVE - see https://github.com/alexkohler/prealloc/issues/30) + ech.configsMu.Lock() + defer ech.configsMu.Unlock() + + outerNames, err := ech.setConfigsFromStorage(ctx, logger) + if err != nil { + return nil, fmt.Errorf("loading configs from storage: %w", err) + } + + // see if we need to make any new ones based on the input configuration + for _, cfg := range ech.Configs { + publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName)) + + if list, ok := ech.configs[publicName]; !ok || len(list) == 0 { + // no config with this public name was loaded, so create one + echCfg, err := generateAndStoreECHConfig(ctx, publicName) + if err != nil { + return nil, err + } + logger.Debug("generated new ECH config", + zap.String("public_name", echCfg.RawPublicName), + zap.Uint8("id", echCfg.ConfigID)) + ech.configs[publicName] = append(ech.configs[publicName], echCfg) + outerNames = append(outerNames, publicName) + } + } + + // ensure old keys are rotated out + if err = ech.rotateECHKeys(ctx, logger, true); err != nil { + return nil, fmt.Errorf("rotating ECH configs: %w", err) + } + + return outerNames, nil +} + +// setConfigsFromStorage sets the ECH configs in memory to those in storage. +// It must be called in a write lock on ech.configsMu. +func (ech *ECH) setConfigsFromStorage(ctx caddy.Context, logger *zap.Logger) ([]string, error) { + storage := ctx.Storage() + + ech.configs = make(map[string][]echConfig) + + var outerNames []string // start by loading all the existing configs (even the older ones on the way out, // since some clients may still be using them if they haven't yet picked up on the @@ -131,48 +176,143 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { logger.Debug("loaded ECH config", zap.String("public_name", cfg.RawPublicName), zap.Uint8("id", cfg.ConfigID)) - ech.configs[cfg.RawPublicName] = append(ech.configs[cfg.RawPublicName], cfg) - outerNames = append(outerNames, cfg.RawPublicName) - } - - // all existing configs are now loaded; see if we need to make any new ones - // based on the input configuration, and also mark the most recent one(s) as - // current/active, so they can be used for ECH retries - for _, cfg := range ech.Configs { - publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName)) - - if list, ok := ech.configs[publicName]; ok && len(list) > 0 { - // at least one config with this public name was loaded, so find the - // most recent one and mark it as active to be used with retries - var mostRecentDate time.Time - var mostRecentIdx int - for i, c := range list { - if mostRecentDate.IsZero() || c.meta.Created.After(mostRecentDate) { - mostRecentDate = c.meta.Created - mostRecentIdx = i - } - } - list[mostRecentIdx].sendAsRetry = true - } else { - // no config with this public name was loaded, so create one - echCfg, err := generateAndStoreECHConfig(ctx, publicName) - if err != nil { - return nil, err - } - logger.Debug("generated new ECH config", - zap.String("public_name", echCfg.RawPublicName), - zap.Uint8("id", echCfg.ConfigID)) - ech.configs[publicName] = append(ech.configs[publicName], echCfg) - outerNames = append(outerNames, publicName) + if _, seen := ech.configs[cfg.RawPublicName]; !seen { + outerNames = append(outerNames, cfg.RawPublicName) } + ech.configs[cfg.RawPublicName] = append(ech.configs[cfg.RawPublicName], cfg) } return outerNames, nil } -func (t *TLS) publishECHConfigs() error { - logger := t.logger.Named("ech") +// rotateECHKeys updates the ECH keys/configs that are outdated. It should be called +// in a write lock on ech.configsMu. If a lock is already obtained in storage, then +// pass true for storageSynced. +func (ech *ECH) rotateECHKeys(ctx caddy.Context, logger *zap.Logger, storageSynced bool) error { + storage := ctx.Storage() + // all existing configs are now loaded; rotate keys "regularly" as recommended by the spec + // (also: "Rotating too frequently limits the client anonymity set." - but the more server + // names, the more frequently rotation can be done safely) + const ( + rotationInterval = 24 * time.Hour * 30 + deleteAfter = 24 * time.Hour * 90 + ) + + if !ech.rotationNeeded(rotationInterval, deleteAfter) { + return nil + } + + // sync this operation across cluster if not already + if !storageSynced { + if err := storage.Lock(ctx, echStorageLockName); err != nil { + return err + } + defer func() { + if err := storage.Unlock(ctx, echStorageLockName); err != nil { + logger.Error("unable to unlock ECH rotation in storage", zap.Error(err)) + } + }() + } + + // update what storage has, in case another instance already updated things + if _, err := ech.setConfigsFromStorage(ctx, logger); err != nil { + return fmt.Errorf("updating ECH keys from storage: %v", err) + } + + // iterate the updated list and do any updates as needed + for publicName := range ech.configs { + for i := 0; i < len(ech.configs[publicName]); i++ { + cfg := ech.configs[publicName][i] + if time.Since(cfg.meta.Created) >= rotationInterval && cfg.meta.Replaced.IsZero() { + // key is due for rotation and it hasn't been replaced yet; do that now + logger.Debug("ECH config is due for rotation", + zap.String("public_name", cfg.RawPublicName), + zap.Uint8("id", cfg.ConfigID), + zap.Time("created", cfg.meta.Created), + zap.Duration("age", time.Since(cfg.meta.Created)), + zap.Duration("rotation_interval", rotationInterval)) + + // start by generating and storing the replacement ECH config + newCfg, err := generateAndStoreECHConfig(ctx, publicName) + if err != nil { + return fmt.Errorf("generating and storing new replacement ECH config: %w", err) + } + + // mark the key as replaced so we don't rotate it again, and instead delete it later + ech.configs[publicName][i].meta.Replaced = time.Now() + + // persist the updated metadata + metaBytes, err := json.Marshal(ech.configs[publicName][i].meta) + if err != nil { + return fmt.Errorf("marshaling updated ECH config metadata: %v", err) + } + if err := storage.Store(ctx, echMetaKey(cfg.ConfigID), metaBytes); err != nil { + return fmt.Errorf("storing updated ECH config metadata: %v", err) + } + + ech.configs[publicName] = append(ech.configs[publicName], newCfg) + + logger.Debug("rotated ECH key", + zap.String("public_name", cfg.RawPublicName), + zap.Uint8("old_id", cfg.ConfigID), + zap.Uint8("new_id", newCfg.ConfigID)) + } else if time.Since(cfg.meta.Created) >= deleteAfter && !cfg.meta.Replaced.IsZero() { + // key has expired and is no longer supported; delete it from storage and memory + cfgIDKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID))) + if err := storage.Delete(ctx, cfgIDKey); err != nil { + return fmt.Errorf("deleting expired ECH config: %v", err) + } + + ech.configs[publicName] = append(ech.configs[publicName][:i], ech.configs[publicName][i+1:]...) + i-- + + logger.Debug("deleted expired ECH key", + zap.String("public_name", cfg.RawPublicName), + zap.Uint8("id", cfg.ConfigID), + zap.Duration("age", time.Since(cfg.meta.Created))) + } + } + } + + ech.updateKeyList() + + return nil +} + +// rotationNeeded returns true if any ECH key needs to be replaced, or deleted. +// It must be called inside a read or write lock of ech.configsMu (probably a +// write lock, so that the rotation can occur correctly in the same lock).) +func (ech *ECH) rotationNeeded(rotationInterval, deleteAfter time.Duration) bool { + for publicName := range ech.configs { + for i := 0; i < len(ech.configs[publicName]); i++ { + cfg := ech.configs[publicName][i] + if (time.Since(cfg.meta.Created) >= rotationInterval && cfg.meta.Replaced.IsZero()) || + (time.Since(cfg.meta.Created) >= deleteAfter && !cfg.meta.Replaced.IsZero()) { + return true + } + } + } + return false +} + +// updateKeyList updates the list of ECH keys the std lib uses to serve ECH. +// It must be called inside a write lock on ech.configsMu. +func (ech *ECH) updateKeyList() { + ech.stdlibReady = []tls.EncryptedClientHelloKey{} + for _, cfgs := range ech.configs { + for _, cfg := range cfgs { + ech.stdlibReady = append(ech.stdlibReady, tls.EncryptedClientHelloKey{ + Config: cfg.configBin, + PrivateKey: cfg.privKeyBin, + SendAsRetry: cfg.meta.Replaced.IsZero(), // only send during retries if key has not been rotated out + }) + } + } +} + +// publishECHConfigs publishes any configs that are configured for publication and which haven't been published already. +func (t *TLS) publishECHConfigs(logger *zap.Logger) error { // make publication exclusive, since we don't need to repeat this unnecessarily storage := t.ctx.Storage() const echLockName = "ech_publish" @@ -197,7 +337,7 @@ func (t *TLS) publishECHConfigs() error { publishers: []ECHPublisher{ &ECHDNSPublisher{ provider: dnsProv, - logger: t.logger, + logger: logger, }, }, }, @@ -209,6 +349,7 @@ func (t *TLS) publishECHConfigs() error { // publish with it, and figure out which inner names to publish // to/for, then publish for _, publication := range publicationList { + t.EncryptedClientHello.configsMu.RLock() // this publication is either configured for specific ECH configs, // or we just use an implied default of all ECH configs var echCfgList echConfigList @@ -231,6 +372,7 @@ func (t *TLS) publishECHConfigs() error { } } } + t.EncryptedClientHello.configsMu.RUnlock() // marshal the ECH config list as binary for publication echCfgListBin, err := echCfgList.MarshalBinary() @@ -304,7 +446,7 @@ func (t *TLS) publishECHConfigs() error { // at least a partial failure, maybe a complete failure, but we can // log each error by domain for innerName, domainErr := range publishErrs { - t.logger.Error("failed to publish ECH configuration list", + logger.Error("failed to publish ECH configuration list", zap.String("publisher", publisherKey), zap.String("domain", innerName), zap.Uint8s("config_ids", configIDs), @@ -312,7 +454,7 @@ func (t *TLS) publishECHConfigs() error { } } else if err != nil { // generic error; assume the entire thing failed, I guess - t.logger.Error("failed publishing ECH configuration list", + logger.Error("failed publishing ECH configuration list", zap.String("publisher", publisherKey), zap.Strings("domains", dnsNamesToPublish), zap.Uint8s("config_ids", configIDs), @@ -334,7 +476,7 @@ func (t *TLS) publishECHConfigs() error { successNames = append(successNames, name) } } - t.logger.Info("successfully published ECH configuration list for "+someAll+" domains", + logger.Info("successfully published ECH configuration list for "+someAll+" domains", zap.String("publisher", publisherKey), zap.Strings("domains", successNames), zap.Uint8s("config_ids", configIDs)) @@ -353,13 +495,12 @@ func (t *TLS) publishECHConfigs() error { if err != nil { return fmt.Errorf("marshaling ECH config metadata: %v", err) } - metaKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID)), "meta.json") - if err := t.ctx.Storage().Store(t.ctx, metaKey, metaBytes); err != nil { + if err := t.ctx.Storage().Store(t.ctx, echMetaKey(cfg.ConfigID), metaBytes); err != nil { return fmt.Errorf("storing updated ECH config metadata: %v", err) } } } else { - t.logger.Error("all domains failed to publish ECH configuration list (see earlier errors)", + logger.Error("all domains failed to publish ECH configuration list (see earlier errors)", zap.String("publisher", publisherKey), zap.Strings("domains", dnsNamesToPublish), zap.Uint8s("config_ids", configIDs)) @@ -489,7 +630,7 @@ func generateAndStoreECHConfig(ctx caddy.Context, publicName string) (echConfig, echCfg := echConfig{ PublicKey: publicKey, - Version: draftTLSESNI22, + Version: draftTLSESNI25, ConfigID: configID, RawPublicName: publicName, KEMID: kemChoice, @@ -507,7 +648,6 @@ func generateAndStoreECHConfig(ctx caddy.Context, publicName string) (echConfig, AEADID: hpke.AEAD_ChaCha20Poly1305, }, }, - sendAsRetry: true, } meta := echConfigMeta{ Created: time.Now(), @@ -786,10 +926,9 @@ type echConfig struct { // these fields are not part of the spec, but are here for // our use when setting up TLS servers or maintenance - configBin []byte - privKeyBin []byte - meta echConfigMeta - sendAsRetry bool + configBin []byte + privKeyBin []byte + meta echConfigMeta } func (echCfg echConfig) MarshalBinary() ([]byte, error) { @@ -811,8 +950,8 @@ func (echCfg *echConfig) UnmarshalBinary(data []byte) error { if !b.ReadUint16(&echCfg.Version) { return errInvalidLen } - if echCfg.Version != draftTLSESNI22 { - return fmt.Errorf("supported version must be %d: got %d", draftTLSESNI22, echCfg.Version) + if echCfg.Version != draftTLSESNI25 { + return fmt.Errorf("supported version must be %d: got %d", draftTLSESNI25, echCfg.Version) } if !b.ReadUint16LengthPrefixed(&content) || !b.Empty() { @@ -1022,19 +1161,27 @@ func (p PublishECHConfigListErrors) Error() string { type echConfigMeta struct { Created time.Time `json:"created"` + Replaced time.Time `json:"replaced,omitzero"` Publications publicationHistory `json:"publications"` } +func echMetaKey(configID uint8) string { + return path.Join(echConfigsKey, strconv.Itoa(int(configID)), "meta.json") +} + // publicationHistory is a map of publisher key to // map of inner name to timestamp type publicationHistory map[string]map[string]time.Time +// echStorageLockName is the name of the storage lock to sync ECH updates. +const echStorageLockName = "ech_rotation" + // The key prefix when putting ECH configs in storage. After this // comes the config ID. const echConfigsKey = "ech/configs" -// https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html -const draftTLSESNI22 = 0xfe0d +// https://www.ietf.org/archive/id/draft-ietf-tls-esni-25.html +const draftTLSESNI25 = 0xfe0d // Interface guard var _ ECHPublisher = (*ECHDNSPublisher)(nil) diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 7b49c0208..0d2dfcb6c 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -335,7 +335,6 @@ func (t *TLS) Provision(ctx caddy.Context) error { // ECH (Encrypted ClientHello) initialization if t.EncryptedClientHello != nil { - t.EncryptedClientHello.configs = make(map[string][]echConfig) outerNames, err := t.EncryptedClientHello.Provision(ctx) if err != nil { return fmt.Errorf("provisioning Encrypted ClientHello components: %v", err) @@ -411,12 +410,37 @@ func (t *TLS) Start() error { return fmt.Errorf("automate: managing %v: %v", t.automateNames, err) } - // publish ECH configs in the background; does not need to block - // server startup, as it could take a while if t.EncryptedClientHello != nil { + echLogger := t.logger.Named("ech") + + // publish ECH configs in the background; does not need to block + // server startup, as it could take a while; then keep keys rotated go func() { - if err := t.publishECHConfigs(); err != nil { - t.logger.Named("ech").Error("publication(s) failed", zap.Error(err)) + // publish immediately first + if err := t.publishECHConfigs(echLogger); err != nil { + echLogger.Error("publication(s) failed", zap.Error(err)) + } + + // then every so often, rotate and publish if needed + // (both of these functions only do something if needed) + for { + select { + case <-time.After(1 * time.Hour): + // ensure old keys are rotated out + t.EncryptedClientHello.configsMu.Lock() + err = t.EncryptedClientHello.rotateECHKeys(t.ctx, echLogger, false) + t.EncryptedClientHello.configsMu.Unlock() + if err != nil { + echLogger.Error("rotating ECH configs failed", zap.Error(err)) + return + } + err := t.publishECHConfigs(echLogger) + if err != nil { + echLogger.Error("publication(s) failed", zap.Error(err)) + } + case <-t.ctx.Done(): + return + } } }() } From 6a4296b1a45d81f52bf8ab01d0bcaa4423fee3ff Mon Sep 17 00:00:00 2001 From: Paul B Date: Thu, 11 Dec 2025 14:27:15 -0500 Subject: [PATCH 019/206] caddytls: panic when using tls.ca_pool.source.http -> tls.ca (#7393) --- modules/caddytls/capools.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/caddytls/capools.go b/modules/caddytls/capools.go index 5ed6a82a9..bcc9ec6e8 100644 --- a/modules/caddytls/capools.go +++ b/modules/caddytls/capools.go @@ -502,7 +502,7 @@ func (t *TLSConfig) unmarshalCaddyfile(d *caddyfile.Dispenser) error { // If there is no custom TLS configuration, a nil config may be returned. // copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go func (t *TLSConfig) makeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) { - repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + repl, _ := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) if repl == nil { repl = caddy.NewReplacer() } From 409a0721354354a2d13e5c110be0f7f03363652b Mon Sep 17 00:00:00 2001 From: EINIER FREYRE CORONA Date: Fri, 12 Dec 2025 12:56:30 +0000 Subject: [PATCH 020/206] notify: implement windows service status and error notifications (#7389) * implement service status and error notifications * adjust return of Error function * configure accepts on status * align windows with linux semantics --- notify/notify_windows.go | 69 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/notify/notify_windows.go b/notify/notify_windows.go index 5666a4c22..33f947565 100644 --- a/notify/notify_windows.go +++ b/notify/notify_windows.go @@ -14,16 +14,26 @@ package notify -import "golang.org/x/sys/windows/svc" +import ( + "log" + "strings" + + "golang.org/x/sys/windows/svc" +) // globalStatus store windows service status, it can be // use to notify caddy status. var globalStatus chan<- svc.Status +// SetGlobalStatus assigns the channel through which status updates +// will be sent to the SCM. This is typically provided by the service +// handler when the service starts. func SetGlobalStatus(status chan<- svc.Status) { globalStatus = status } +// Ready notifies the SCM that the service is fully running and ready +// to accept stop or shutdown control requests. func Ready() error { if globalStatus != nil { globalStatus <- svc.Status{ @@ -34,6 +44,8 @@ func Ready() error { return nil } +// Reloading notifies the SCM that the service is entering a transitional +// state. func Reloading() error { if globalStatus != nil { globalStatus <- svc.Status{State: svc.StartPending} @@ -41,6 +53,8 @@ func Reloading() error { return nil } +// Stopping notifies the SCM that the service is in the process of stopping. +// This allows Windows to track the shutdown transition properly. func Stopping() error { if globalStatus != nil { globalStatus <- svc.Status{State: svc.StopPending} @@ -48,8 +62,53 @@ func Stopping() error { return nil } -// TODO: not implemented -func Status(_ string) error { return nil } +// Status sends an arbitrary service state to the SCM based on a string +// identifier of [svc.State]. +// The unknown states will be logged. +func Status(name string) error { + if globalStatus == nil { + return nil + } -// TODO: not implemented -func Error(_ error, _ int) error { return nil } + var state svc.State + var accepts svc.Accepted + accepts = 0 + + switch strings.ToLower(name) { + case "stopped": + state = svc.Stopped + case "start_pending": + state = svc.StartPending + case "stop_pending": + state = svc.StopPending + case "running": + state = svc.Running + accepts = svc.AcceptStop | svc.AcceptShutdown + case "continue_pending": + state = svc.ContinuePending + case "pause_pending": + state = svc.PausePending + case "paused": + state = svc.Paused + accepts = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue + default: + log.Printf("unknown state: %s", name) + return nil + } + + globalStatus <- svc.Status{State: state, Accepts: accepts} + return nil +} + +// Error notifies the SCM that the service is stopping due to a failure, +// including a service-specific exit code. +func Error(err error, code int) error { + if globalStatus != nil { + globalStatus <- svc.Status{ + State: svc.StopPending, + ServiceSpecificExitCode: uint32(code), + } + } + + return nil +} From 4037d0576094c5f9f570825601615b62cb8d85b1 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Sat, 13 Dec 2025 23:01:12 -0500 Subject: [PATCH 021/206] caddyhttp: {http.request.body_base64} placeholder (#7367) --- modules/caddyhttp/app.go | 1 + modules/caddyhttp/replacer.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index ac2a29c19..8058dbf33 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -51,6 +51,7 @@ func init() { // Placeholder | Description // ------------|--------------- // `{http.request.body}` | The request body (⚠️ inefficient; use only for debugging) +// `{http.request.body_base64}` | The request body, base64-encoded (⚠️ for debugging) // `{http.request.cookie.*}` | HTTP request cookie // `{http.request.duration}` | Time up to now spent handling the request (after decoding headers from client) // `{http.request.duration_ms}` | Same as 'duration', but in milliseconds. diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 2c372a9e8..5d600c334 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -229,6 +229,21 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo req.Body = io.NopCloser(buf) // replace real body with buffered data return buf.String(), true + case "http.request.body_base64": + if req.Body == nil { + return "", true + } + // normally net/http will close the body for us, but since we + // are replacing it with a fake one, we have to ensure we close + // the real body ourselves when we're done + defer req.Body.Close() + // read the request body into a buffer (can't pool because we + // don't know its lifetime and would have to make a copy anyway) + buf := new(bytes.Buffer) + _, _ = io.Copy(buf, req.Body) // can't handle error, so just ignore it + req.Body = io.NopCloser(buf) // replace real body with buffered data + return base64.StdEncoding.EncodeToString(buf.Bytes()), true + // original request, before any internal changes case "http.request.orig_method": or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request) From 34fd2dfcff4a9533bd3b63da56edb782effe561d Mon Sep 17 00:00:00 2001 From: Will Norris Date: Tue, 16 Dec 2025 07:38:32 -0800 Subject: [PATCH 022/206] go.mod: update tscert package to latest (aea342f6) (#7397) --- go.mod | 4 ++-- go.sum | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 5269a0841..43aa01d4e 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 + github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 github.com/yuin/goldmark v1.7.13 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 @@ -54,7 +54,6 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.1 // indirect - github.com/Microsoft/go-winio v0.6.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -84,6 +83,7 @@ require ( github.com/smallstep/linkedca v0.25.0 // indirect github.com/smallstep/pkcs7 v0.2.1 // indirect github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect diff --git a/go.sum b/go.sum index a9910e7bb..72a6b989d 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,6 @@ github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7r github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= -github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= @@ -288,7 +286,6 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE= @@ -344,8 +341,10 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ= -github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 h1:RnBbFMmodYzhC6adOjTbtUQXyzV8dcvKYbolzs6Qch0= +github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747/go.mod h1:ejPAJui3kVK4u5TgMtqtXlWf5HnKh9fLy5kvpaeuas0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= @@ -487,7 +486,6 @@ golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From decc8a4d6fc476b21165400ddc3c02d94ee9f47f Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Tue, 16 Dec 2025 23:42:42 -0500 Subject: [PATCH 023/206] logging: `log_append` Early option, Supports `{http.response.body}` (#7368) * logging: `log_append` early option * logging: `log_append` supports `{http.response.body}` * Convenience auto-early for request body --- modules/caddyhttp/logging/caddyfile.go | 8 +- .../logging/{logadd.go => logappend.go} | 89 +++++++++++++++++-- 2 files changed, 91 insertions(+), 6 deletions(-) rename modules/caddyhttp/logging/{logadd.go => logappend.go} (51%) diff --git a/modules/caddyhttp/logging/caddyfile.go b/modules/caddyhttp/logging/caddyfile.go index 010b48919..38d79014b 100644 --- a/modules/caddyhttp/logging/caddyfile.go +++ b/modules/caddyhttp/logging/caddyfile.go @@ -15,6 +15,8 @@ package logging import ( + "strings" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" @@ -26,7 +28,7 @@ func init() { // parseCaddyfile sets up the log_append handler from Caddyfile tokens. Syntax: // -// log_append [] +// log_append [] [<] func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { handler := new(LogAppend) err := handler.UnmarshalCaddyfile(h.Dispenser) @@ -43,6 +45,10 @@ func (h *LogAppend) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !d.NextArg() { return d.ArgErr() } + if strings.HasPrefix(h.Key, "<") && len(h.Key) > 1 { + h.Early = true + h.Key = h.Key[1:] + } h.Value = d.Val() return nil } diff --git a/modules/caddyhttp/logging/logadd.go b/modules/caddyhttp/logging/logappend.go similarity index 51% rename from modules/caddyhttp/logging/logadd.go rename to modules/caddyhttp/logging/logappend.go index 3b554367f..56758d68c 100644 --- a/modules/caddyhttp/logging/logadd.go +++ b/modules/caddyhttp/logging/logappend.go @@ -15,6 +15,8 @@ package logging import ( + "bytes" + "encoding/base64" "net/http" "strings" @@ -42,6 +44,12 @@ type LogAppend struct { // map, the value of that key will be used. Otherwise // the value will be used as-is as a constant string. Value string `json:"value,omitempty"` + + // Early, if true, adds the log field before calling + // the next handler in the chain. By default, the log + // field is added on the way back up the middleware chain, + // after all subsequent handlers have completed. + Early bool `json:"early,omitempty"` } // CaddyModule returns the Caddy module information. @@ -53,13 +61,63 @@ func (LogAppend) CaddyModule() caddy.ModuleInfo { } func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { - // Run the next handler in the chain first. + // Determine if we need to add the log field early. + // We do if the Early flag is set, or for convenience, + // if the value is a special placeholder for the request body. + needsEarly := h.Early || h.Value == placeholderRequestBody || h.Value == placeholderRequestBodyBase64 + + // Check if we need to buffer the response for special placeholders + needsResponseBody := h.Value == placeholderResponseBody || h.Value == placeholderResponseBodyBase64 + + if needsEarly && !needsResponseBody { + // Add the log field before calling the next handler + // (but not if we need the response body, which isn't available yet) + h.addLogField(r, nil) + } + + var rec caddyhttp.ResponseRecorder + var buf *bytes.Buffer + + if needsResponseBody { + // Wrap the response writer with a recorder to capture the response body + buf = new(bytes.Buffer) + rec = caddyhttp.NewResponseRecorder(w, buf, func(status int, header http.Header) bool { + // Always buffer the response when we need to log the body + return true + }) + w = rec + } + + // Run the next handler in the chain. // If an error occurs, we still want to add // any extra log fields that we can, so we // hold onto the error and return it later. handlerErr := next.ServeHTTP(w, r) - // On the way back up the chain, add the extra log field + if needsResponseBody { + // Write the buffered response to the client + if rec.Buffered() { + h.addLogField(r, buf) + err := rec.WriteResponse() + if err != nil { + return err + } + } + return handlerErr + } + + if !h.Early { + // Add the log field after the handler completes + h.addLogField(r, buf) + } + + return handlerErr +} + +// addLogField adds the log field to the request's extra log fields. +// If buf is not nil, it contains the buffered response body for special +// response body placeholders. +func (h LogAppend) addLogField(r *http.Request, buf *bytes.Buffer) { ctx := r.Context() vars := ctx.Value(caddyhttp.VarsCtxKey).(map[string]any) @@ -67,7 +125,21 @@ func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh extra := ctx.Value(caddyhttp.ExtraLogFieldsCtxKey).(*caddyhttp.ExtraLogFields) var varValue any - if strings.HasPrefix(h.Value, "{") && + + // Handle special case placeholders for response body + if h.Value == placeholderResponseBody { + if buf != nil { + varValue = buf.String() + } else { + varValue = "" + } + } else if h.Value == placeholderResponseBodyBase64 { + if buf != nil { + varValue = base64.StdEncoding.EncodeToString(buf.Bytes()) + } else { + varValue = "" + } + } else if strings.HasPrefix(h.Value, "{") && strings.HasSuffix(h.Value, "}") && strings.Count(h.Value, "{") == 1 { // the value looks like a placeholder, so get its value @@ -84,10 +156,17 @@ func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh // We use zap.Any because it will reflect // to the correct type for us. extra.Add(zap.Any(h.Key, varValue)) - - return handlerErr } +const ( + // Special placeholder values that are handled by log_append + // rather than by the replacer. + placeholderRequestBody = "{http.request.body}" + placeholderRequestBodyBase64 = "{http.request.body_base64}" + placeholderResponseBody = "{http.response.body}" + placeholderResponseBodyBase64 = "{http.response.body_base64}" +) + // Interface guards var ( _ caddyhttp.MiddlewareHandler = (*LogAppend)(nil) From 5640611dfc60a2c4862025b1836dc91a8b5bc41a Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Sun, 21 Dec 2025 10:09:55 +0100 Subject: [PATCH 024/206] chore: update quic-go to v0.58.0 (#7404) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 43aa01d4e..19d93023d 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 github.com/mholt/acmez/v3 v3.1.4 github.com/prometheus/client_golang v1.23.2 - github.com/quic-go/quic-go v0.57.1 + github.com/quic-go/quic-go v0.58.0 github.com/smallstep/certificates v0.29.0 github.com/smallstep/nosql v0.7.0 github.com/smallstep/truststore v0.13.0 diff --git a/go.sum b/go.sum index 72a6b989d..5eec4cd35 100644 --- a/go.sum +++ b/go.sum @@ -271,8 +271,8 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= -github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= +github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= From 9eabd443cb170ed846a70a84acf87002683ad29b Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 26 Dec 2025 14:32:03 -0300 Subject: [PATCH 025/206] cmd: Add --json flag to list-modules command (#7409) --- cmd/commandfuncs.go | 68 +++++++++++++++++++++++++++++++++++++-------- cmd/commands.go | 3 +- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 75d114992..8e46ba63b 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -411,11 +411,65 @@ func cmdBuildInfo(_ Flags) (int, error) { return caddy.ExitCodeSuccess, nil } +// jsonModuleInfo holds metadata about a Caddy module for JSON output. +type jsonModuleInfo struct { + ModuleName string `json:"module_name"` + ModuleType string `json:"module_type"` + Version string `json:"version,omitempty"` + PackageURL string `json:"package_url,omitempty"` +} + func cmdListModules(fl Flags) (int, error) { packages := fl.Bool("packages") versions := fl.Bool("versions") skipStandard := fl.Bool("skip-standard") + jsonOutput := fl.Bool("json") + // Organize modules by whether they come with the standard distribution + standard, nonstandard, unknown, err := getModules() + if err != nil { + // If module info can't be fetched, just print the IDs and exit + for _, m := range caddy.Modules() { + fmt.Println(m) + } + return caddy.ExitCodeSuccess, nil + } + + // Logic for JSON output + if jsonOutput { + output := []jsonModuleInfo{} + + // addToOutput is a helper to convert internal module info to the JSON-serializable struct + addToOutput := func(list []moduleInfo, moduleType string) { + for _, mi := range list { + item := jsonModuleInfo{ + ModuleName: mi.caddyModuleID, + ModuleType: moduleType, // Mapping the type here + } + if mi.goModule != nil { + item.Version = mi.goModule.Version + item.PackageURL = mi.goModule.Path + } + output = append(output, item) + } + } + + // Pass the respective type for each category + if !skipStandard { + addToOutput(standard, "standard") + } + addToOutput(nonstandard, "non-standard") + addToOutput(unknown, "unknown") + + jsonBytes, err := json.MarshalIndent(output, "", " ") + if err != nil { + return caddy.ExitCodeFailedQuit, err + } + fmt.Println(string(jsonBytes)) + return caddy.ExitCodeSuccess, nil + } + + // Logic for Text output (Fallback) printModuleInfo := func(mi moduleInfo) { fmt.Print(mi.caddyModuleID) if versions && mi.goModule != nil { @@ -433,16 +487,6 @@ func cmdListModules(fl Flags) (int, error) { fmt.Println() } - // organize modules by whether they come with the standard distribution - standard, nonstandard, unknown, err := getModules() - if err != nil { - // oh well, just print the module IDs and exit - for _, m := range caddy.Modules() { - fmt.Println(m) - } - return caddy.ExitCodeSuccess, nil - } - // Standard modules (always shipped with Caddy) if !skipStandard { if len(standard) > 0 { @@ -461,8 +505,8 @@ func cmdListModules(fl Flags) (int, error) { for _, mod := range nonstandard { printModuleInfo(mod) } + fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard)) } - fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard)) // Unknown modules (couldn't get Caddy module info) if len(unknown) > 0 { @@ -472,8 +516,8 @@ func cmdListModules(fl Flags) (int, error) { for _, mod := range unknown { printModuleInfo(mod) } + fmt.Printf("\n Unknown modules: %d\n", len(unknown)) } - fmt.Printf("\n Unknown modules: %d\n", len(unknown)) return caddy.ExitCodeSuccess, nil } diff --git a/cmd/commands.go b/cmd/commands.go index c9ea636b9..417720f06 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -229,12 +229,13 @@ documentation: https://go.dev/doc/modules/version-numbers RegisterCommand(Command{ Name: "list-modules", - Usage: "[--packages] [--versions] [--skip-standard]", + Usage: "[--packages] [--versions] [--skip-standard] [--json]", Short: "Lists the installed Caddy modules", CobraFunc: func(cmd *cobra.Command) { cmd.Flags().BoolP("packages", "", false, "Print package paths") cmd.Flags().BoolP("versions", "", false, "Print version information") cmd.Flags().BoolP("skip-standard", "s", false, "Skip printing standard modules") + cmd.Flags().BoolP("json", "", false, "Print modules in JSON format") cmd.RunE = WrapCommandFuncForCobra(cmdListModules) }, }) From 1f1be3f4fe281643fd4ac8c1217d5ade7f82f6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Wed, 31 Dec 2025 20:33:18 +0200 Subject: [PATCH 026/206] tracing: Add span attributes to tracing module (#7269) * WIP tracing span attributes * better test * only write attributes after other middleware (and request) * Fix test to use header response placeholders --- README.md | 9 +- modules/caddyhttp/tracing/module.go | 37 +++- modules/caddyhttp/tracing/module_test.go | 224 ++++++++++++++++++++++- modules/caddyhttp/tracing/tracer.go | 22 ++- modules/caddyhttp/tracing/tracer_test.go | 1 + 5 files changed, 280 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4c091f714..a54dd438d 100644 --- a/README.md +++ b/README.md @@ -117,11 +117,18 @@ username ALL=(ALL:ALL) NOPASSWD: /usr/sbin/setcap replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments. +Then you can run the tests in all modules or a specific one: + +````bash +$ go test ./... +$ go test ./modules/caddyhttp/tracing/ +``` + ### With version information and/or plugins Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)... -``` +```bash $ xcaddy build ``` diff --git a/modules/caddyhttp/tracing/module.go b/modules/caddyhttp/tracing/module.go index 85fd63002..c312bfd8a 100644 --- a/modules/caddyhttp/tracing/module.go +++ b/modules/caddyhttp/tracing/module.go @@ -27,6 +27,9 @@ type Tracing struct { // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#span SpanName string `json:"span"` + // SpanAttributes are custom key-value pairs to be added to spans + SpanAttributes map[string]string `json:"span_attributes,omitempty"` + // otel implements opentelemetry related logic. otel openTelemetryWrapper @@ -46,7 +49,7 @@ func (ot *Tracing) Provision(ctx caddy.Context) error { ot.logger = ctx.Logger() var err error - ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName) + ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName, ot.SpanAttributes) return err } @@ -69,6 +72,10 @@ func (ot *Tracing) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh // // tracing { // [span ] +// [span_attributes { +// attr1 value1 +// attr2 value2 +// }] // } func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { setParameter := func(d *caddyfile.Dispenser, val *string) error { @@ -94,12 +101,30 @@ func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } for d.NextBlock(0) { - if dst, ok := paramsMap[d.Val()]; ok { - if err := setParameter(d, dst); err != nil { - return err + switch d.Val() { + case "span_attributes": + if ot.SpanAttributes == nil { + ot.SpanAttributes = make(map[string]string) + } + for d.NextBlock(1) { + key := d.Val() + if !d.NextArg() { + return d.ArgErr() + } + value := d.Val() + if d.NextArg() { + return d.ArgErr() + } + ot.SpanAttributes[key] = value + } + default: + if dst, ok := paramsMap[d.Val()]; ok { + if err := setParameter(d, dst); err != nil { + return err + } + } else { + return d.ArgErr() } - } else { - return d.ArgErr() } } return nil diff --git a/modules/caddyhttp/tracing/module_test.go b/modules/caddyhttp/tracing/module_test.go index 2a775fc18..a35ea3b35 100644 --- a/modules/caddyhttp/tracing/module_test.go +++ b/modules/caddyhttp/tracing/module_test.go @@ -2,12 +2,16 @@ package tracing import ( "context" + "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" + "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" @@ -15,17 +19,26 @@ import ( func TestTracing_UnmarshalCaddyfile(t *testing.T) { tests := []struct { - name string - spanName string - d *caddyfile.Dispenser - wantErr bool + name string + spanName string + spanAttributes map[string]string + d *caddyfile.Dispenser + wantErr bool }{ { name: "Full config", spanName: "my-span", + spanAttributes: map[string]string{ + "attr1": "value1", + "attr2": "value2", + }, d: caddyfile.NewTestDispenser(` tracing { span my-span + span_attributes { + attr1 value1 + attr2 value2 + } }`), wantErr: false, }, @@ -42,6 +55,21 @@ tracing { name: "Empty config", d: caddyfile.NewTestDispenser(` tracing { +}`), + wantErr: false, + }, + { + name: "Only span attributes", + spanAttributes: map[string]string{ + "service.name": "my-service", + "service.version": "1.0.0", + }, + d: caddyfile.NewTestDispenser(` +tracing { + span_attributes { + service.name my-service + service.version 1.0.0 + } }`), wantErr: false, }, @@ -56,6 +84,20 @@ tracing { if ot.SpanName != tt.spanName { t.Errorf("UnmarshalCaddyfile() SpanName = %v, want SpanName %v", ot.SpanName, tt.spanName) } + + if len(tt.spanAttributes) > 0 { + if ot.SpanAttributes == nil { + t.Errorf("UnmarshalCaddyfile() SpanAttributes is nil, expected %v", tt.spanAttributes) + } else { + for key, expectedValue := range tt.spanAttributes { + if actualValue, exists := ot.SpanAttributes[key]; !exists { + t.Errorf("UnmarshalCaddyfile() SpanAttributes missing key %v", key) + } else if actualValue != expectedValue { + t.Errorf("UnmarshalCaddyfile() SpanAttributes[%v] = %v, want %v", key, actualValue, expectedValue) + } + } + } + } }) } } @@ -79,6 +121,26 @@ func TestTracing_UnmarshalCaddyfile_Error(t *testing.T) { d: caddyfile.NewTestDispenser(` tracing { span +}`), + wantErr: true, + }, + { + name: "Span attributes missing value", + d: caddyfile.NewTestDispenser(` +tracing { + span_attributes { + key + } +}`), + wantErr: true, + }, + { + name: "Span attributes too many arguments", + d: caddyfile.NewTestDispenser(` +tracing { + span_attributes { + key value extra + } }`), wantErr: true, }, @@ -181,6 +243,160 @@ func TestTracing_ServeHTTP_Next_Error(t *testing.T) { } } +func TestTracing_JSON_Configuration(t *testing.T) { + // Test that our struct correctly marshals to and from JSON + original := &Tracing{ + SpanName: "test-span", + SpanAttributes: map[string]string{ + "service.name": "test-service", + "service.version": "1.0.0", + "env": "test", + }, + } + + jsonData, err := json.Marshal(original) + if err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + var unmarshaled Tracing + if err := json.Unmarshal(jsonData, &unmarshaled); err != nil { + t.Fatalf("Failed to unmarshal from JSON: %v", err) + } + + if unmarshaled.SpanName != original.SpanName { + t.Errorf("Expected SpanName %s, got %s", original.SpanName, unmarshaled.SpanName) + } + + if len(unmarshaled.SpanAttributes) != len(original.SpanAttributes) { + t.Errorf("Expected %d span attributes, got %d", len(original.SpanAttributes), len(unmarshaled.SpanAttributes)) + } + + for key, expectedValue := range original.SpanAttributes { + if actualValue, exists := unmarshaled.SpanAttributes[key]; !exists { + t.Errorf("Expected span attribute %s to exist", key) + } else if actualValue != expectedValue { + t.Errorf("Expected span attribute %s = %s, got %s", key, expectedValue, actualValue) + } + } + + t.Logf("JSON representation: %s", string(jsonData)) +} + +func TestTracing_OpenTelemetry_Span_Attributes(t *testing.T) { + // Create an in-memory span recorder to capture actual span data + spanRecorder := tracetest.NewSpanRecorder() + provider := trace.NewTracerProvider( + trace.WithSpanProcessor(spanRecorder), + ) + + // Create our tracing module with span attributes that include placeholders + ot := &Tracing{ + SpanName: "test-span", + SpanAttributes: map[string]string{ + "static": "test-service", + "request-placeholder": "{http.request.method}", + "response-placeholder": "{http.response.header.X-Some-Header}", + "mixed": "prefix-{http.request.method}-{http.response.header.X-Some-Header}", + }, + } + + // Create a specific request to test against + req, _ := http.NewRequest("POST", "https://api.example.com/v1/users?id=123", nil) + req.Host = "api.example.com" + + w := httptest.NewRecorder() + + // Set up the replacer + repl := caddy.NewReplacer() + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, make(map[string]any)) + req = req.WithContext(ctx) + + // Set up request placeholders + repl.Set("http.request.method", req.Method) + repl.Set("http.request.uri", req.URL.RequestURI()) + + // Handler to generate the response + var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error { + writer.Header().Set("X-Some-Header", "some-value") + writer.WriteHeader(200) + + // Make response headers available to replacer + repl.Set("http.response.header.X-Some-Header", writer.Header().Get("X-Some-Header")) + + return nil + } + + // Set up Caddy context + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + // Override the global tracer provider with our test provider + // This is a bit hacky but necessary to capture the actual spans + originalProvider := globalTracerProvider + globalTracerProvider = &tracerProvider{ + tracerProvider: provider, + tracerProvidersCounter: 1, // Simulate one user + } + defer func() { + globalTracerProvider = originalProvider + }() + + // Provision the tracing module + if err := ot.Provision(caddyCtx); err != nil { + t.Errorf("Provision error: %v", err) + t.FailNow() + } + + // Execute the request + if err := ot.ServeHTTP(w, req, handler); err != nil { + t.Errorf("ServeHTTP error: %v", err) + } + + // Get the recorded spans + spans := spanRecorder.Ended() + if len(spans) == 0 { + t.Fatal("Expected at least one span to be recorded") + } + + // Find our span (should be the one with our test span name) + var testSpan trace.ReadOnlySpan + for _, span := range spans { + if span.Name() == "test-span" { + testSpan = span + break + } + } + + if testSpan == nil { + t.Fatal("Could not find test span in recorded spans") + } + + // Verify that the span attributes were set correctly with placeholder replacement + expectedAttributes := map[string]string{ + "static": "test-service", + "request-placeholder": "POST", + "response-placeholder": "some-value", + "mixed": "prefix-POST-some-value", + } + + actualAttributes := make(map[string]string) + for _, attr := range testSpan.Attributes() { + actualAttributes[string(attr.Key)] = attr.Value.AsString() + } + + for key, expectedValue := range expectedAttributes { + if actualValue, exists := actualAttributes[key]; !exists { + t.Errorf("Expected span attribute %s to be set", key) + } else if actualValue != expectedValue { + t.Errorf("Expected span attribute %s = %s, got %s", key, expectedValue, actualValue) + } + } + + t.Logf("Recorded span attributes: %+v", actualAttributes) +} + func createRequestWithContext(method string, url string) *http.Request { r, _ := http.NewRequest(method, url, nil) repl := caddy.NewReplacer() diff --git a/modules/caddyhttp/tracing/tracer.go b/modules/caddyhttp/tracing/tracer.go index ab2ddf8a2..bb0f81fc3 100644 --- a/modules/caddyhttp/tracing/tracer.go +++ b/modules/caddyhttp/tracing/tracer.go @@ -8,6 +8,7 @@ import ( "go.opentelemetry.io/contrib/exporters/autoexport" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/contrib/propagators/autoprop" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -37,20 +38,23 @@ type openTelemetryWrapper struct { handler http.Handler - spanName string + spanName string + spanAttributes map[string]string } // newOpenTelemetryWrapper is responsible for the openTelemetryWrapper initialization using provided configuration. func newOpenTelemetryWrapper( ctx context.Context, spanName string, + spanAttributes map[string]string, ) (openTelemetryWrapper, error) { if spanName == "" { spanName = defaultSpanName } ot := openTelemetryWrapper{ - spanName: spanName, + spanName: spanName, + spanAttributes: spanAttributes, } version, _ := caddy.Version() @@ -99,8 +103,22 @@ func (ot *openTelemetryWrapper) serveHTTP(w http.ResponseWriter, r *http.Request extra.Add(zap.String("spanID", spanID)) } } + next := ctx.Value(nextCallCtxKey).(*nextCall) next.err = next.next.ServeHTTP(w, r) + + // Add custom span attributes to the current span + span := trace.SpanFromContext(ctx) + if span.IsRecording() && len(ot.spanAttributes) > 0 { + replacer := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + attributes := make([]attribute.KeyValue, 0, len(ot.spanAttributes)) + for key, value := range ot.spanAttributes { + // Allow placeholder replacement in attribute values + replacedValue := replacer.ReplaceAll(value, "") + attributes = append(attributes, attribute.String(key, replacedValue)) + } + span.SetAttributes(attributes...) + } } // ServeHTTP propagates call to the by wrapped by `otelhttp` next handler. diff --git a/modules/caddyhttp/tracing/tracer_test.go b/modules/caddyhttp/tracing/tracer_test.go index 36a32ff46..5ca423aa9 100644 --- a/modules/caddyhttp/tracing/tracer_test.go +++ b/modules/caddyhttp/tracing/tracer_test.go @@ -16,6 +16,7 @@ func TestOpenTelemetryWrapper_newOpenTelemetryWrapper(t *testing.T) { if otw, err = newOpenTelemetryWrapper(ctx, "", + nil, ); err != nil { t.Errorf("newOpenTelemetryWrapper() error = %v", err) t.FailNow() From 99d84be6dda21e9d9cdeb5a7d719e0fc3a4509f8 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Fri, 2 Jan 2026 18:51:36 +0300 Subject: [PATCH 027/206] readme: fix fence (#7416) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a54dd438d..bd048fa2f 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ replacing `username` with your actual username. Please be careful and only do th Then you can run the tests in all modules or a specific one: -````bash +```bash $ go test ./... $ go test ./modules/caddyhttp/tracing/ ``` From b2d21f650a7ecd61d0a7ee85f3d423b89cddc371 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 5 Jan 2026 12:28:52 -0700 Subject: [PATCH 028/206] go.mod: Upgrade CertMagic and ZeroSSL deps --- go.mod | 22 +++++++++++----------- go.sum | 44 ++++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index 19d93023d..ea9cddf96 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.20.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.25.0 - github.com/caddyserver/zerossl v0.1.3 + github.com/caddyserver/certmagic v0.25.1 + github.com/caddyserver/zerossl v0.1.4 github.com/cloudflare/circl v1.6.1 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.2.3 @@ -39,11 +39,11 @@ require ( go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.45.0 + golang.org/x/crypto v0.46.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 - golang.org/x/net v0.47.0 - golang.org/x/sync v0.18.0 - golang.org/x/term v0.37.0 + golang.org/x/net v0.48.0 + golang.org/x/sync v0.19.0 + golang.org/x/term v0.38.0 golang.org/x/time v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -144,7 +144,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/miekg/dns v1.1.68 // indirect + github.com/miekg/dns v1.1.69 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -168,10 +168,10 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/sys v0.38.0 - golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/sys v0.39.0 + golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect howett.net/plist v1.0.0 // indirect diff --git a/go.sum b/go.sum index 5eec4cd35..8072f1dd7 100644 --- a/go.sum +++ b/go.sum @@ -78,10 +78,10 @@ github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic= -github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA= -github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= -github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/caddyserver/certmagic v0.25.1 h1:4sIKKbOt5pg6+sL7tEwymE1x2bj6CHr80da1CRRIPbY= +github.com/caddyserver/certmagic v0.25.1/go.mod h1:VhyvndxtVton/Fo/wKhRoC46Rbw1fmjvQ3GjHYSQTEY= +github.com/caddyserver/zerossl v0.1.4 h1:CVJOE3MZeFisCERZjkxIcsqIH4fnFdlYWnPYeFtBHRw= +github.com/caddyserver/zerossl v0.1.4/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0= github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= @@ -233,8 +233,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 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/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= -github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= +github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -448,8 +448,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= @@ -459,8 +459,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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -469,8 +469,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -480,8 +480,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -499,8 +499,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.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= @@ -510,8 +510,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -521,8 +521,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -531,8 +531,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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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= From 7b031e1eb5bc0bae9a8394d93f20399932ebd4d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:50:46 +0300 Subject: [PATCH 029/206] build(deps): bump the all-updates group across 1 directory with 12 updates (#7421) Bumps the all-updates group with 9 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) | `1.5.0` | `1.6.0` | | [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.20.0` | `2.21.1` | | [github.com/cloudflare/circl](https://github.com/cloudflare/circl) | `1.6.1` | `1.6.2` | | [github.com/spf13/cobra](https://github.com/spf13/cobra) | `1.10.1` | `1.10.2` | | [github.com/yuin/goldmark](https://github.com/yuin/goldmark) | `1.7.13` | `1.7.15` | | [go.opentelemetry.io/contrib/exporters/autoexport](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.63.0` | `0.64.0` | | [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.63.0` | `0.64.0` | | [go.opentelemetry.io/contrib/propagators/autoprop](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.63.0` | `0.64.0` | | [go.step.sm/crypto](https://github.com/smallstep/crypto) | `0.74.0` | `0.75.0` | Updates `github.com/BurntSushi/toml` from 1.5.0 to 1.6.0 - [Release notes](https://github.com/BurntSushi/toml/releases) - [Commits](https://github.com/BurntSushi/toml/compare/v1.5.0...v1.6.0) Updates `github.com/alecthomas/chroma/v2` from 2.20.0 to 2.21.1 - [Release notes](https://github.com/alecthomas/chroma/releases) - [Commits](https://github.com/alecthomas/chroma/compare/v2.20.0...v2.21.1) Updates `github.com/cloudflare/circl` from 1.6.1 to 1.6.2 - [Release notes](https://github.com/cloudflare/circl/releases) - [Commits](https://github.com/cloudflare/circl/compare/v1.6.1...v1.6.2) Updates `github.com/spf13/cobra` from 1.10.1 to 1.10.2 - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2) Updates `github.com/yuin/goldmark` from 1.7.13 to 1.7.15 - [Release notes](https://github.com/yuin/goldmark/releases) - [Commits](https://github.com/yuin/goldmark/compare/v1.7.13...v1.7.15) Updates `go.opentelemetry.io/contrib/exporters/autoexport` from 0.63.0 to 0.64.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.63.0...zpages/v0.64.0) Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.63.0 to 0.64.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.63.0...zpages/v0.64.0) Updates `go.opentelemetry.io/contrib/propagators/autoprop` from 0.63.0 to 0.64.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.63.0...zpages/v0.64.0) Updates `go.opentelemetry.io/otel` from 1.38.0 to 1.39.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.39.0) Updates `go.opentelemetry.io/otel/sdk` from 1.38.0 to 1.39.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.39.0) Updates `go.step.sm/crypto` from 0.74.0 to 0.75.0 - [Release notes](https://github.com/smallstep/crypto/releases) - [Commits](https://github.com/smallstep/crypto/compare/v0.74.0...v0.75.0) Updates `go.opentelemetry.io/otel/trace` from 1.38.0 to 1.39.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.39.0) --- updated-dependencies: - dependency-name: github.com/BurntSushi/toml dependency-version: 1.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: github.com/alecthomas/chroma/v2 dependency-version: 2.21.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: github.com/cloudflare/circl dependency-version: 1.6.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: github.com/spf13/cobra dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: github.com/yuin/goldmark dependency-version: 1.7.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/exporters/autoexport dependency-version: 0.64.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp dependency-version: 0.64.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/propagators/autoprop dependency-version: 0.64.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/otel dependency-version: 1.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/otel/sdk dependency-version: 1.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.step.sm/crypto dependency-version: 0.75.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/otel/trace dependency-version: 1.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 80 ++++++++++----------- go.sum | 221 +++++++++++++++++++++++++++++---------------------------- 2 files changed, 152 insertions(+), 149 deletions(-) diff --git a/go.mod b/go.mod index ea9cddf96..1b12f8661 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,15 @@ module github.com/caddyserver/caddy/v2 go 1.25 require ( - github.com/BurntSushi/toml v1.5.0 + github.com/BurntSushi/toml v1.6.0 github.com/DeRuina/timberjack v1.3.9 github.com/KimMachineGun/automemlimit v0.7.5 github.com/Masterminds/sprig/v3 v3.3.0 - github.com/alecthomas/chroma/v2 v2.20.0 + github.com/alecthomas/chroma/v2 v2.21.1 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/caddyserver/certmagic v0.25.1 github.com/caddyserver/zerossl v0.1.4 - github.com/cloudflare/circl v1.6.1 + github.com/cloudflare/circl v1.6.2 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.2.3 github.com/google/cel-go v0.26.1 @@ -24,18 +24,18 @@ require ( github.com/smallstep/certificates v0.29.0 github.com/smallstep/nosql v0.7.0 github.com/smallstep/truststore v0.13.0 - github.com/spf13/cobra v1.10.1 + github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 - github.com/yuin/goldmark v1.7.13 + github.com/yuin/goldmark v1.7.15 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/sdk v1.38.0 - go.step.sm/crypto v0.74.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.64.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/sdk v1.39.0 + go.step.sm/crypto v0.75.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 @@ -68,15 +68,14 @@ require ( github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect - github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/smallstep/cli-utils v0.12.2 // indirect github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect @@ -87,30 +86,31 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect - go.opentelemetry.io/contrib/propagators/aws v1.38.0 // indirect - 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.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect - go.opentelemetry.io/otel/log v0.14.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.64.0 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.39.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.61.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect + go.opentelemetry.io/otel/log v0.15.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.15.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect golang.org/x/oauth2 v0.33.0 // indirect google.golang.org/api v0.256.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -151,8 +151,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.67.1 // indirect - github.com/prometheus/procfs v0.17.0 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // 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 @@ -163,10 +163,10 @@ require ( github.com/stoewer/go-strcase v1.2.0 // indirect github.com/urfave/cli v1.22.17 // indirect go.etcd.io/bbolt v1.3.10 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 - go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/sys v0.39.0 diff --git a/go.sum b/go.sum index 8072f1dd7..46ca64c7d 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,9 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 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/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo= github.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk= @@ -38,44 +39,46 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= -github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= +github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA= +github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= -github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= -github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-sdk-go-v2 v1.39.5 h1:e/SXuia3rkFtapghJROrydtQpfQaaUgd1cUvyO1mp2w= -github.com/aws/aws-sdk-go-v2 v1.39.5/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM= -github.com/aws/aws-sdk-go-v2/config v1.31.16 h1:E4Tz+tJiPc7kGnXwIfCyUj6xHJNpENlY11oKpRTgsjc= -github.com/aws/aws-sdk-go-v2/config v1.31.16/go.mod h1:2S9hBElpCyGMifv14WxQ7EfPumgoeCPZUpuPX8VtW34= -github.com/aws/aws-sdk-go-v2/credentials v1.18.20 h1:KFndAnHd9NUuzikHjQ8D5CfFVO+bgELkmcGY8yAw98Q= -github.com/aws/aws-sdk-go-v2/credentials v1.18.20/go.mod h1:9mCi28a+fmBHSQ0UM79omkz6JtN+PEsvLrnG36uoUv0= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 h1:VO3FIM2TDbm0kqp6sFNR0PbioXJb/HzCDW6NtIZpIWE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12/go.mod h1:6C39gB8kg82tx3r72muZSrNhHia9rjGkX7ORaS2GKNE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12 h1:p/9flfXdoAnwJnuW9xHEAFY22R3A6skYkW19JFF9F+8= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12/go.mod h1:ZTLHakoVCTtW8AaLGSwJ3LXqHD9uQKnOcv1TrpO6u2k= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12 h1:2lTWFvRcnWFFLzHWmtddu5MTchc5Oj2OOey++99tPZ0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12/go.mod h1:hI92pK+ho8HVcWMHKHrK3Uml4pfG7wvL86FzO0LVtQQ= +github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= +github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2/config v1.32.1 h1:iODUDLgk3q8/flEC7ymhmxjfoAnBDwEEYEVyKZ9mzjU= +github.com/aws/aws-sdk-go-v2/config v1.32.1/go.mod h1:xoAgo17AGrPpJBSLg81W+ikM0cpOZG8ad04T2r+d5P0= +github.com/aws/aws-sdk-go-v2/credentials v1.19.1 h1:JeW+EwmtTE0yXFK8SmklrFh/cGTTXsQJumgMZNlbxfM= +github.com/aws/aws-sdk-go-v2/credentials v1.19.1/go.mod h1:BOoXiStwTF+fT2XufhO0Efssbi1CNIO/ZXpZu87N0pw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12 h1:MM8imH7NZ0ovIVX7D2RxfMDv7Jt9OiUXkcQ+GqywA7M= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12/go.mod h1:gf4OGwdNkbEsb7elw2Sy76odfhwNktWII3WgvQgQQ6w= -github.com/aws/aws-sdk-go-v2/service/kms v1.47.0 h1:A97YCVyGz19rRs3+dWf3GpMPflCswgETA9r6/Q0JNSY= -github.com/aws/aws-sdk-go-v2/service/kms v1.47.0/go.mod h1:ZJ1ghBt9gQM8JoNscUua1siIgao8w74o3kvdWUU6N/Q= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 h1:xHXvxst78wBpJFgDW07xllOx0IAzbryrSdM4nMVQ4Dw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.0/go.mod h1:/e8m+AO6HNPPqMyfKRtzZ9+mBF5/x1Wk8QiDva4m07I= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 h1:tBw2Qhf0kj4ZwtsVpDiVRU3zKLvjvjgIjHMKirxXg8M= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4/go.mod h1:Deq4B7sRM6Awq/xyOBlxBdgW8/Z926KYNNaGMW2lrkA= -github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 h1:C+BRMnasSYFcgDw8o9H5hzehKzXyAb9GY5v/8bP9DUY= -github.com/aws/aws-sdk-go-v2/service/sts v1.39.0/go.mod h1:4EjU+4mIx6+JqKQkruye+CaigV7alL3thVPfDd9VlMs= -github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= -github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E= +github.com/aws/aws-sdk-go-v2/service/kms v1.48.0 h1:pQgVxqqNOacqb19+xaoih/wNLil4d8tgi+FxtBi/qQY= +github.com/aws/aws-sdk-go-v2/service/kms v1.48.0/go.mod h1:VJcNH6BLr+3VJwinRKdotLOMglHO8mIKlD3ea5c7hbw= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9 h1:LU8S9W/mPDAU9q0FjCLi0TrCheLMGwzbRpvUMwYspcA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caddyserver/certmagic v0.25.1 h1:4sIKKbOt5pg6+sL7tEwymE1x2bj6CHr80da1CRRIPbY= @@ -99,8 +102,8 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= +github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= @@ -172,8 +175,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA= github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/go-tpm-tools v0.4.6 h1:hwIwPG7w4z5eQEBq11gYw8YYr9xXLfBQ/0JsKyq5AJM= -github.com/google/go-tpm-tools v0.4.6/go.mod h1:MsVQbJnRhKDfWwf5zgr3cDGpj13P1uLAFF0wMEP/n5w= +github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws= +github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -184,10 +187,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAV github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= -github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= -github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= @@ -263,12 +264,12 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 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.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= -github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= -github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= -github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= -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/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= @@ -316,8 +317,8 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -353,8 +354,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= -github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.7.15 h1:xYJWgq3Qd8qsaZpj5pHKoEI4mosqVZi/qRpq/MdKyyk= +github.com/yuin/goldmark v1.7.15/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= @@ -367,66 +368,66 @@ go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4= -go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E= -go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY= -go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g= +go.opentelemetry.io/contrib/bridges/prometheus v0.64.0 h1:7TYhBCu6Xz6vDJGNtEslWZLuuX2IJ/aH50hBY4MVeUg= +go.opentelemetry.io/contrib/bridges/prometheus v0.64.0/go.mod h1:tHQctZfAe7e4PBPGyt3kae6mQFXNpj+iiDJa3ithM50= +go.opentelemetry.io/contrib/exporters/autoexport v0.64.0 h1:9pzPj3RFyKOxBAMkM2w84LpT+rdHam1XoFA+QhARiRw= +go.opentelemetry.io/contrib/exporters/autoexport v0.64.0/go.mod h1:hlVZx1btWH0XTfXpuGX9dsquB50s+tc3fYFOO5elo2M= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 h1:S3+4UwR3Y1tUKklruMwOacAFInNvtuOexz4ZTmJNAyw= -go.opentelemetry.io/contrib/propagators/autoprop v0.63.0/go.mod h1:qpIuOggbbw2T9nKRaO1je/oTRKd4zslAcJonN8LYbTg= -go.opentelemetry.io/contrib/propagators/aws v1.38.0 h1:eRZ7asSbLc5dH7+TBzL6hFKb1dabz0IV51uUUwYRZts= -go.opentelemetry.io/contrib/propagators/aws v1.38.0/go.mod h1:wXqc9NTGcXapBExHBDVLEZlByu6quiQL8w7Tjgv8TCg= -go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo= -go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU= -go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc= -go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo= -go.opentelemetry.io/contrib/propagators/ot v1.38.0 h1:k4gSyyohaDXI8F9BDXYC3uO2vr5sRNeQFMsN9Zn0EoI= -go.opentelemetry.io/contrib/propagators/ot v1.38.0/go.mod h1:2hDsuiHRO39SRUMhYGqmj64z/IuMRoxE4bBSFR82Lo8= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= -go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= -go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= -go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= -go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= -go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 h1:VVrb1ErDD0Tlh/0K0rUqjky1e8AekjspTFN9sU2ekaA= +go.opentelemetry.io/contrib/propagators/autoprop v0.64.0/go.mod h1:QCsOQk+9Ep8Mkp4/aPtSzUT0dc8SaPYzBAE6o1jYuSE= +go.opentelemetry.io/contrib/propagators/aws v1.39.0 h1:IvNR8pAVGpkK1CHMjU/YE6B6TlnAPGFvogkMWRWU6wo= +go.opentelemetry.io/contrib/propagators/aws v1.39.0/go.mod h1:TUsFCERuGM4IGhJG9w+9l0nzmHUKHuaDYYNF6mtNgjY= +go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk= +go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY= +go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 h1:Gz3yKzfMSEFzF0Vy5eIpu9ndpo4DhXMCxsLMF0OOApo= +go.opentelemetry.io/contrib/propagators/jaeger v1.39.0/go.mod h1:2D/cxxCqTlrday0rZrPujjg5aoAdqk1NaNyoXn8FJn8= +go.opentelemetry.io/contrib/propagators/ot v1.39.0 h1:vKTve1W/WKPVp1fzJamhCDDECt+5upJJ65bPyWoddGg= +go.opentelemetry.io/contrib/propagators/ot v1.39.0/go.mod h1:FH5VB2N19duNzh1Q8ks6CsZFyu3LFhNLiA9lPxyEkvU= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/exporters/prometheus v0.61.0 h1:cCyZS4dr67d30uDyh8etKM2QyDsQ4zC9ds3bdbrVoD0= +go.opentelemetry.io/otel/exporters/prometheus v0.61.0/go.mod h1:iivMuj3xpR2DkUrUya3TPS/Z9h3dz7h01GxU+fQBRNg= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 h1:0BSddrtQqLEylcErkeFrJBmwFzcqfQq9+/uxfTZq+HE= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0/go.mod h1:87sjYuAPzaRCtdd09GU5gM1U9wQLrrcYrm77mh5EBoc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g= +go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= +go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= +go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= -go.step.sm/crypto v0.74.0 h1:/APBEv45yYR4qQFg47HA8w1nesIGcxh44pGyQNw6JRA= -go.step.sm/crypto v0.74.0/go.mod h1:UoXqCAJjjRgzPte0Llaqen7O9P7XjPmgjgTHQGkKCDk= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.step.sm/crypto v0.75.0 h1:UAHYD6q6ggYyzLlIKHv1MCUVjZIesXRZpGTlRC/HSHw= +go.step.sm/crypto v0.75.0/go.mod h1:wwQ57+ajmDype9mrI/2hRyrvJd7yja5xVgWYqpUN3PE= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -441,6 +442,8 @@ go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -540,10 +543,10 @@ google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= From 80f2ae92cdb504164bac648501e07e0e7c84770e Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Tue, 6 Jan 2026 17:55:47 +0800 Subject: [PATCH 030/206] reverseproxy: make error chan bigger when reverse proxying websocket (#7419) --- modules/caddyhttp/reverseproxy/streaming.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index 66dd106d5..99e3cd009 100644 --- a/modules/caddyhttp/reverseproxy/streaming.go +++ b/modules/caddyhttp/reverseproxy/streaming.go @@ -214,7 +214,10 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, timeoutc = timer.C } - errc := make(chan error, 1) + // when a stream timeout is encountered, no error will be read from errc + // a buffer size of 2 will allow both the read and write goroutines to send the error and exit + // see: https://github.com/caddyserver/caddy/issues/7418 + errc := make(chan error, 2) wg.Add(2) go spc.copyToBackend(errc) go spc.copyFromBackend(errc) From 6a571428964aead1470d42eab105cfd57bc68783 Mon Sep 17 00:00:00 2001 From: Tom Paulus Date: Tue, 6 Jan 2026 14:39:58 -0800 Subject: [PATCH 031/206] headers: Make ApplyTo nil-safe (#7426) --- modules/caddyhttp/headers/headers.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index 33d9e39ee..b8226ceec 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -217,7 +217,10 @@ type RespHeaderOps struct { } // ApplyTo applies ops to hdr using repl. -func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { +func (ops *HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { + if ops == nil { + return + } // before manipulating headers in other ways, check if there // is configuration to delete all headers, and do that first // because if a header is to be added, we don't want to delete From 28103aafba3490eedb626823f9641b766c7c99fd Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 6 Jan 2026 16:44:11 -0700 Subject: [PATCH 032/206] Revise top of readme to include Warp sponsorship section --- README.md | 62 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index bd048fa2f..7bcbc9397 100644 --- a/README.md +++ b/README.md @@ -12,24 +12,52 @@

Every site on HTTPS

Caddy is an extensible server platform that uses TLS by default.

-

- - - -
- @caddyserver on Twitter - Caddy Forum -
- Caddy on Sourcegraph - Cloudsmith -

Releases · Documentation · Get Help

+

+ +   + +   + +   + @caddyserver on Twitter +   + Caddy Forum +
+ Caddy on Sourcegraph +   + Cloudsmith +

+

+ Powered by +
+ + + + + CertMagic + + +

+ +
+
+ Special thanks to: +
+ + Warp sponsorship + +### [Warp, built for coding with multiple AI agents](https://go.warp.dev/caddy) +[Available for MacOS, Linux, & Windows](https://go.warp.dev/caddy)
+
+ +
### Menu @@ -44,18 +72,6 @@ - [Getting help](#getting-help) - [About](#about) -

- Powered by -
- - - - - CertMagic - - -

- ## [Features](https://caddyserver.com/features) From 90972fbebc74cd62ff50c0e0f6a1c99deca098ac Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Tue, 13 Jan 2026 14:13:43 -0500 Subject: [PATCH 033/206] chore: Dumb `prealloc` lint fix (#7430) --- caddyconfig/caddyfile/parse.go | 2 +- caddyconfig/httpcaddyfile/builtins.go | 1 + modules/caddyhttp/fileserver/matcher.go | 2 +- modules/caddyhttp/headers/caddyfile.go | 4 +--- modules/caddyhttp/push/caddyfile.go | 1 + modules/caddyhttp/rewrite/caddyfile.go | 1 + modules/caddyhttp/server.go | 1 + modules/caddyhttp/staticresp.go | 11 ++++++++++- 8 files changed, 17 insertions(+), 6 deletions(-) diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index 8439f3731..5e6a21e2d 100644 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -761,7 +761,7 @@ type ServerBlock struct { } func (sb ServerBlock) GetKeysText() []string { - res := []string{} + res := make([]string, 0, len(sb.Keys)) for _, k := range sb.Keys { res = append(res, k.Text) } diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 061aaa48b..cf8ad044f 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -930,6 +930,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue // modifications to the parsing behavior. parseAsGlobalOption := globalLogNames != nil + // nolint:prealloc var configValues []ConfigValue // Logic below expects that a name is always present when a diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index fbcd36e0a..152f31430 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -404,7 +404,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) { } // for each glob result, combine all the forms of the path - var candidates []matchCandidate + candidates := make([]matchCandidate, 0, len(globResults)) for _, result := range globResults { candidates = append(candidates, matchCandidate{ fullpath: result, diff --git a/modules/caddyhttp/headers/caddyfile.go b/modules/caddyhttp/headers/caddyfile.go index f060471b1..2bf7dd4bf 100644 --- a/modules/caddyhttp/headers/caddyfile.go +++ b/modules/caddyhttp/headers/caddyfile.go @@ -168,8 +168,6 @@ func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, } h.Next() // consume the directive name again (matcher parsing resets) - configValues := []httpcaddyfile.ConfigValue{} - if !h.NextArg() { return nil, h.ArgErr() } @@ -204,7 +202,7 @@ func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, return nil, h.Err(err.Error()) } - configValues = append(configValues, h.NewRoute(matcherSet, hdr)...) + configValues := h.NewRoute(matcherSet, hdr) if h.NextArg() { return nil, h.ArgErr() diff --git a/modules/caddyhttp/push/caddyfile.go b/modules/caddyhttp/push/caddyfile.go index f56db81f9..e931849a4 100644 --- a/modules/caddyhttp/push/caddyfile.go +++ b/modules/caddyhttp/push/caddyfile.go @@ -64,6 +64,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) var err error // include current token, which we treat as an argument here + // nolint:prealloc args := []string{h.Val()} args = append(args, h.RemainingArgs()...) diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go index 5f9b97adf..0f406161a 100644 --- a/modules/caddyhttp/rewrite/caddyfile.go +++ b/modules/caddyhttp/rewrite/caddyfile.go @@ -173,6 +173,7 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err if hasArgs { return nil, h.Err("Cannot specify uri query rewrites in both argument and block") } + // nolint:prealloc queryArgs := []string{h.Val()} queryArgs = append(queryArgs, h.RemainingArgs()...) err := applyQueryOps(h, rewr.Query, queryArgs) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index dd47ec8a3..1f1a14b2d 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -1010,6 +1010,7 @@ func isTrustedClientIP(ipAddr netip.Addr, trusted []netip.Prefix) bool { // then the first value from those headers is used. func trustedRealClientIP(r *http.Request, headers []string, clientIP string) string { // Read all the values of the configured client IP headers, in order + // nolint:prealloc var values []string for _, field := range headers { values = append(values, r.Header.Values(field)...) diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index d783d1b04..1a5bbb9e1 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -257,7 +257,16 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, next H return nil } -func buildHTTPServer(i int, port uint, addr string, statusCode int, hdr http.Header, body string, accessLog bool) (*Server, error) { +func buildHTTPServer( + i int, + port uint, + addr string, + statusCode int, + hdr http.Header, + body string, + accessLog bool, +) (*Server, error) { + // nolint:prealloc var handlers []json.RawMessage // response body supports a basic template; evaluate it From 5168acfb9c83d3b43d031c5ff5c05df04576b48c Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Wed, 14 Jan 2026 03:47:36 +0800 Subject: [PATCH 034/206] update quic-go to v0.59.0 (#7431) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1b12f8661..8fc21ed50 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 github.com/mholt/acmez/v3 v3.1.4 github.com/prometheus/client_golang v1.23.2 - github.com/quic-go/quic-go v0.58.0 + github.com/quic-go/quic-go v0.59.0 github.com/smallstep/certificates v0.29.0 github.com/smallstep/nosql v0.7.0 github.com/smallstep/truststore v0.13.0 diff --git a/go.sum b/go.sum index 46ca64c7d..de5352c9c 100644 --- a/go.sum +++ b/go.sum @@ -272,8 +272,8 @@ github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4 github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= -github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= From 62134d65af6847782a527612e71d8c1977119365 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 13 Jan 2026 16:52:56 -0300 Subject: [PATCH 035/206] reverseproxy: fix error when remote address is not an IP (#7429) --- .../caddyhttp/reverseproxy/headers_test.go | 34 +++++++++++++++++++ .../caddyhttp/reverseproxy/reverseproxy.go | 17 ++++++---- 2 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 modules/caddyhttp/reverseproxy/headers_test.go diff --git a/modules/caddyhttp/reverseproxy/headers_test.go b/modules/caddyhttp/reverseproxy/headers_test.go new file mode 100644 index 000000000..22f589141 --- /dev/null +++ b/modules/caddyhttp/reverseproxy/headers_test.go @@ -0,0 +1,34 @@ +package reverseproxy + +import ( + "context" + "net/http/httptest" + "testing" + + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func TestAddForwardedHeadersNonIP(t *testing.T) { + h := Handler{} + + // Simulate a request with a non-IP remote address (e.g. SCION, abstract socket, or hostname) + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "my-weird-network:12345" + + // Mock the context variables required by Caddy. + // We need to inject the variable map manually since we aren't running the full server. + vars := map[string]interface{}{ + caddyhttp.TrustedProxyVarKey: false, + } + ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars) + req = req.WithContext(ctx) + + // Execute the unexported function + err := h.addForwardedHeaders(req) + + // Expectation: No error should be returned for non-IP addresses. + // The function should simply skip the trusted proxy check. + if err != nil { + t.Errorf("expected no error for non-IP address, got: %v", err) + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 13bbee422..2ea17046a 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -789,16 +789,19 @@ func (h Handler) addForwardedHeaders(req *http.Request) error { // to pull that out before parsing the IP clientIP, _, _ = strings.Cut(clientIP, "%") ipAddr, err := netip.ParseAddr(clientIP) - if err != nil { - return fmt.Errorf("invalid IP address: '%s': %v", clientIP, err) - } // Check if the client is a trusted proxy trusted := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool) - for _, ipRange := range h.trustedProxies { - if ipRange.Contains(ipAddr) { - trusted = true - break + + // If ParseAddr fails (e.g. non-IP network like SCION), we cannot check + // if it is a trusted proxy by IP range. In this case, we ignore the + // error and treat the connection as untrusted (or retain existing status). + if err == nil { + for _, ipRange := range h.trustedProxies { + if ipRange.Contains(ipAddr) { + trusted = true + break + } } } From e9d290de2f349d774127b46670c086588bcaf5b0 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 13 Jan 2026 17:22:23 -0300 Subject: [PATCH 036/206] caddyconfig: Fix indentation of multiline strings in fmt (#7425) (#7433) --- caddyconfig/caddyfile/formatter.go | 10 ++++++++++ caddyconfig/caddyfile/formatter_test.go | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/caddyconfig/caddyfile/formatter.go b/caddyconfig/caddyfile/formatter.go index 833aff353..dfd316b16 100644 --- a/caddyconfig/caddyfile/formatter.go +++ b/caddyconfig/caddyfile/formatter.go @@ -18,6 +18,7 @@ import ( "bytes" "io" "slices" + "strings" "unicode" ) @@ -209,6 +210,15 @@ func Format(input []byte) []byte { } } + if strings.Contains(quotes, "`") { + if ch == '`' && space && !beginningOfLine { + write(' ') + } + write(ch) + space = false + continue + } + if unicode.IsSpace(ch) { space = true heredocEscaped = false diff --git a/caddyconfig/caddyfile/formatter_test.go b/caddyconfig/caddyfile/formatter_test.go index 0092d1311..6ab293615 100644 --- a/caddyconfig/caddyfile/formatter_test.go +++ b/caddyconfig/caddyfile/formatter_test.go @@ -464,6 +464,17 @@ block2 { } `, }, + { + description: "issue #7425: multiline backticked string indentation", + input: `https://localhost:8953 { + respond ` + "`" + `Here are some random numbers: + +{{randNumeric 16}} + +Hope this helps.` + "`" + ` +}`, + expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}", + }, } { // the formatter should output a trailing newline, // even if the tests aren't written to expect that From cbebc1292b83ace15798b94e2d3fed50646eb94c Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Tue, 13 Jan 2026 17:11:35 -0500 Subject: [PATCH 037/206] core: Embed `time/tzdata` (#7432) --- cmd/caddy/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/caddy/main.go b/cmd/caddy/main.go index 48fa149aa..5002605b6 100644 --- a/cmd/caddy/main.go +++ b/cmd/caddy/main.go @@ -29,6 +29,8 @@ package main import ( + _ "time/tzdata" + caddycmd "github.com/caddyserver/caddy/v2/cmd" // plug in Caddy modules here From e40bd019ff4599b34769d249ee37934217ed9fa4 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Wed, 14 Jan 2026 03:06:16 +0300 Subject: [PATCH 038/206] caddyfile: add `observe_catchall_hosts` option (#7434) * caddyfile: add `observe_catchall_hosts` option Signed-off-by: Mohammed Al Sahaf * correct JSON field name and doc comment Signed-off-by: Mohammed Al Sahaf --------- Signed-off-by: Mohammed Al Sahaf --- caddyconfig/httpcaddyfile/options.go | 2 ++ modules/caddyhttp/metrics.go | 6 +++--- modules/caddyhttp/metrics_test.go | 32 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index 336c6999f..58a75e9a2 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -472,6 +472,8 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) { switch d.Val() { case "per_host": metrics.PerHost = true + case "observe_catchall_hosts": + metrics.ObserveCatchallHosts = true default: return nil, d.Errf("unrecognized servers option '%s'", d.Val()) } diff --git a/modules/caddyhttp/metrics.go b/modules/caddyhttp/metrics.go index 424170732..8b4d380f0 100644 --- a/modules/caddyhttp/metrics.go +++ b/modules/caddyhttp/metrics.go @@ -25,7 +25,7 @@ import ( // "http": { // "metrics": { // "per_host": true, -// "allow_catch_all_hosts": false +// "observe_catchall_hosts": false // }, // "servers": { // "srv0": { @@ -65,7 +65,7 @@ type Metrics struct { // // Set to true to allow all hosts to get individual metrics (NOT RECOMMENDED // for production environments exposed to the internet). - AllowCatchAllHosts bool `json:"allow_catch_all_hosts,omitempty"` + ObserveCatchallHosts bool `json:"observe_catchall_hosts,omitempty"` init sync.Once httpMetrics *httpMetrics @@ -200,7 +200,7 @@ func (m *Metrics) shouldAllowHostMetrics(host string, isHTTPS bool) bool { } // For catch-all requests (not in allowed hosts) - allowCatchAll := m.AllowCatchAllHosts || (isHTTPS && m.hasHTTPSServer) + allowCatchAll := m.ObserveCatchallHosts || (isHTTPS && m.hasHTTPSServer) return allowCatchAll } diff --git a/modules/caddyhttp/metrics_test.go b/modules/caddyhttp/metrics_test.go index 9f6f59858..58b6a09ad 100644 --- a/modules/caddyhttp/metrics_test.go +++ b/modules/caddyhttp/metrics_test.go @@ -207,11 +207,11 @@ func TestMetricsInstrumentedHandler(t *testing.T) { func TestMetricsInstrumentedHandlerPerHost(t *testing.T) { ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) metrics := &Metrics{ - PerHost: true, - AllowCatchAllHosts: true, // Allow all hosts for testing - init: sync.Once{}, - httpMetrics: &httpMetrics{}, - allowedHosts: make(map[string]struct{}), + PerHost: true, + ObserveCatchallHosts: true, // Allow all hosts for testing + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + allowedHosts: make(map[string]struct{}), } handlerErr := errors.New("oh noes") response := []byte("hello world!") @@ -387,11 +387,11 @@ func TestMetricsCardinalityProtection(t *testing.T) { // Test 1: Without AllowCatchAllHosts, arbitrary hosts should be mapped to "_other" metrics := &Metrics{ - PerHost: true, - AllowCatchAllHosts: false, // Default - should map unknown hosts to "_other" - init: sync.Once{}, - httpMetrics: &httpMetrics{}, - allowedHosts: make(map[string]struct{}), + PerHost: true, + ObserveCatchallHosts: false, // Default - should map unknown hosts to "_other" + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + allowedHosts: make(map[string]struct{}), } // Add one allowed host @@ -444,12 +444,12 @@ func TestMetricsHTTPSCatchAll(t *testing.T) { // Test that HTTPS requests allow catch-all even when AllowCatchAllHosts is false metrics := &Metrics{ - PerHost: true, - AllowCatchAllHosts: false, - hasHTTPSServer: true, // Simulate having HTTPS servers - init: sync.Once{}, - httpMetrics: &httpMetrics{}, - allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts + PerHost: true, + ObserveCatchallHosts: false, + hasHTTPSServer: true, // Simulate having HTTPS servers + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts } mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { From d269405eabf30250283c271c4ff6095a2c5ded2c Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Wed, 14 Jan 2026 22:54:19 -0500 Subject: [PATCH 039/206] core: Show JSON error offsets where possible (#7437) --- admin.go | 5 ++++- caddyconfig/configadapters.go | 6 +++++- cmd/main.go | 5 ++++- modules.go | 6 +++++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/admin.go b/admin.go index 6ccec41e7..0b8aedf9f 100644 --- a/admin.go +++ b/admin.go @@ -1110,7 +1110,10 @@ func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error if len(body) > 0 { err = json.Unmarshal(body, &val) if err != nil { - return fmt.Errorf("decoding request body: %v", err) + if jsonErr, ok := err.(*json.SyntaxError); ok { + return fmt.Errorf("decoding request body: %w, at offset %d", jsonErr, jsonErr.Offset) + } + return fmt.Errorf("decoding request body: %w", err) } } diff --git a/caddyconfig/configadapters.go b/caddyconfig/configadapters.go index 0ca3c3af1..8a5a37f08 100644 --- a/caddyconfig/configadapters.go +++ b/caddyconfig/configadapters.go @@ -81,7 +81,11 @@ func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning) err = json.Unmarshal(enc, &tmp) if err != nil { if warnings != nil { - *warnings = append(*warnings, Warning{Message: err.Error()}) + message := err.Error() + if jsonErr, ok := err.(*json.SyntaxError); ok { + message = fmt.Sprintf("%v, at offset %d", jsonErr.Error(), jsonErr.Offset) + } + *warnings = append(*warnings, Warning{Message: message}) } return nil } diff --git a/cmd/main.go b/cmd/main.go index 411f4545d..4a969573e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -231,7 +231,10 @@ 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) + if jsonErr, ok := err.(*json.SyntaxError); ok { + return nil, "", "", fmt.Errorf("config is not valid JSON: %w, at offset %d; did you mean to use a config adapter (the --adapter flag)?", err, jsonErr.Offset) + } + return nil, "", "", fmt.Errorf("config is not valid JSON: %w; did you mean to use a config adapter (the --adapter flag)?", err) } } diff --git a/modules.go b/modules.go index 24c452589..93a9343f6 100644 --- a/modules.go +++ b/modules.go @@ -342,7 +342,11 @@ func ParseStructTag(tag string) (map[string]string, error) { func StrictUnmarshalJSON(data []byte, v any) error { dec := json.NewDecoder(bytes.NewReader(data)) dec.DisallowUnknownFields() - return dec.Decode(v) + err := dec.Decode(v) + if jsonErr, ok := err.(*json.SyntaxError); ok { + return fmt.Errorf("%w, at offset %d", jsonErr, jsonErr.Offset) + } + return err } var JSONRawMessageType = reflect.TypeFor[json.RawMessage]() From 565c1c3054e1cd60b64275217b8e8c4da3b18856 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 16 Jan 2026 12:51:23 -0300 Subject: [PATCH 040/206] autohttps: deterministic logic and strict bind checking on Linux (#7435) * http: fix non-deterministic auto-https and improve Linux bind matching * docs: restore historical context about Linux bind behavior --- modules/caddyhttp/autohttps.go | 26 +++++++++++++++++++++++--- modules/caddyhttp/server.go | 24 +++++++++++++++--------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 05f8a7517..8bcaebe69 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -90,7 +90,16 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // the log configuration for an HTTPS enabled server var logCfg *ServerLogConfig - for srvName, srv := range app.Servers { + // Sort server names to ensure deterministic iteration. + // This prevents race conditions where the order of server processing + // could affect which server gets assigned the HTTP->HTTPS redirect listener. + srvNames := make([]string, 0, len(app.Servers)) + for name := range app.Servers { + srvNames = append(srvNames, name) + } + slices.Sort(srvNames) + for _, srvName := range srvNames { + srv := app.Servers[srvName] // as a prerequisite, provision route matchers; this is // required for all routes on all servers, and must be // done before we attempt to do phase 1 of auto HTTPS, @@ -398,15 +407,26 @@ uniqueDomainsLoop: return append(routes, app.makeRedirRoute(uint(app.httpsPort()), MatcherSet{MatchProtocol("http")})) } + // Sort redirect addresses to ensure deterministic process + redirServerAddrsSorted := make([]string, 0, len(redirServers)) + for addr := range redirServers { + redirServerAddrsSorted = append(redirServerAddrsSorted, addr) + } + slices.Sort(redirServerAddrsSorted) + redirServersLoop: - for redirServerAddr, routes := range redirServers { + for _, redirServerAddr := range redirServerAddrsSorted { + routes := redirServers[redirServerAddr] // for each redirect listener, see if there's already a // server configured to listen on that exact address; if so, // insert the redirect route to the end of its route list // after any other routes with host matchers; otherwise, // we'll create a new server for all the listener addresses // that are unused and serve the remaining redirects from it - for _, srv := range app.Servers { + + // Use the sorted srvNames to consistently find the target server + for _, srvName := range srvNames { + srv := app.Servers[srvName] // only look at servers which listen on an address which // we want to add redirects to if !srv.hasListenerAddress(redirServerAddr) { diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 1f1a14b2d..7d2f41a12 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -556,15 +556,21 @@ func (s *Server) hasListenerAddress(fullAddr string) bool { // The second issue seems very similar to a discussion here: // https://github.com/nodejs/node/issues/9390 // - // This is very easy to reproduce by creating an HTTP server - // that listens to both addresses or just one with a host - // interface; or for a more confusing reproduction, try - // listening on "127.0.0.1:80" and ":443" and you'll see - // the error, if you take away the GOOS condition below. - // - // So, an address is equivalent if the port is in the port - // range, and if not on Linux, the host is the same... sigh. - if (runtime.GOOS == "linux" || thisAddrs.Host == laddrs.Host) && + // However, binding to *different specific* interfaces + // (e.g. 127.0.0.2:80 and 127.0.0.3:80) IS allowed on Linux. + // The conflict only happens when mixing specific IPs with + // wildcards (0.0.0.0 or ::). + + // Hosts match exactly (e.g. 127.0.0.2 == 127.0.0.2) -> Conflict. + hostMatch := thisAddrs.Host == laddrs.Host + + // On Linux, specific IP vs Wildcard fails to bind. + // So if we are on Linux AND either host is empty (wildcard), we treat + // it as a match (conflict). But if both are specific and different + // (127.0.0.2 vs 127.0.0.3), this remains false (no conflict). + linuxWildcardConflict := runtime.GOOS == "linux" && (thisAddrs.Host == "" || laddrs.Host == "") + + if (hostMatch || linuxWildcardConflict) && (laddrs.StartPort <= thisAddrs.EndPort) && (laddrs.StartPort >= thisAddrs.StartPort) { return true From 7d24124430ed8a2bfc3aa0625a742082361e3ff8 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 30 Jan 2026 12:24:16 -0700 Subject: [PATCH 041/206] caddyhttp: Reject invalid Host header (fix #7449) --- modules/caddyhttp/server.go | 106 ++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 7d2f41a12..de318a953 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -18,6 +18,7 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "io" "net" @@ -297,6 +298,8 @@ var ( // ServeHTTP is the entry point for all HTTP requests. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + start := time.Now() + // If there are listener wrappers that process tls connections but don't return a *tls.Conn, this field will be nil. if r.TLS == nil { if tlsConnStateFunc, ok := r.Context().Value(tlsConnectionStateFuncCtxKey).(func() *tls.ConnectionState); ok { @@ -304,6 +307,17 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } + // enable full-duplex for HTTP/1, ensuring the entire + // request body gets consumed before writing the response + if s.EnableFullDuplex && r.ProtoMajor == 1 { + if err := http.NewResponseController(w).EnableFullDuplex(); err != nil { //nolint:bodyclose + if c := s.logger.Check(zapcore.WarnLevel, "failed to enable full duplex"); c != nil { + c.Write(zap.Error(err)) + } + } + } + + // set the Server header h := w.Header() h["Server"] = serverHeader @@ -316,39 +330,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - // reject very long methods; probably a mistake or an attack - if len(r.Method) > 32 { - if s.shouldLogRequest(r) { - if c := s.accessLogger.Check(zapcore.DebugLevel, "rejecting request with long method"); c != nil { - c.Write( - zap.String("method_trunc", r.Method[:32]), - zap.String("remote_addr", r.RemoteAddr), - ) - } - } - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - + // prepare internals of the request for the handler pipeline repl := caddy.NewReplacer() r = PrepareRequest(r, repl, w, s) - // enable full-duplex for HTTP/1, ensuring the entire - // request body gets consumed before writing the response - if s.EnableFullDuplex && r.ProtoMajor == 1 { - if err := http.NewResponseController(w).EnableFullDuplex(); err != nil { //nolint:bodyclose - if c := s.logger.Check(zapcore.WarnLevel, "failed to enable full duplex"); c != nil { - c.Write(zap.Error(err)) - } - } - } - - // clone the request for logging purposes before - // it enters any handler chain; this is necessary - // to capture the original request in case it gets - // modified during handling - // cloning the request and using .WithLazy is considerably faster - // than using .With, which will JSON encode the request immediately + // clone the request for logging purposes before it enters any handler chain; + // this is necessary to capture the original request in case it gets modified + // during handling (cloning the request and using .WithLazy is considerably + // faster than using .With, which will JSON-encode the request immediately) shouldLogCredentials := s.Logs != nil && s.Logs.ShouldLogCredentials loggableReq := zap.Object("request", LoggableHTTPRequest{ Request: r.Clone(r.Context()), @@ -376,36 +365,33 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // capture the original version of the request - accLog := s.accessLogger.With(loggableReq) + accLog := s.accessLogger.WithLazy(loggableReq) defer s.logRequest(accLog, r, wrec, &duration, repl, bodyReader, shouldLogCredentials) } - start := time.Now() - - // guarantee ACME HTTP challenges; handle them - // separately from any user-defined handlers + // guarantee ACME HTTP challenges; handle them separately from any user-defined handlers if s.tlsApp.HandleHTTPChallenge(w, r) { duration = time.Since(start) return } - // execute the primary handler chain - err := s.primaryHandlerChain.ServeHTTP(w, r) + err := s.serveHTTP(w, r) duration = time.Since(start) - // if no errors, we're done! if err == nil { return } // restore original request before invoking error handler chain (issue #3717) - // TODO: this does not restore original headers, if modified (for efficiency) - origReq := r.Context().Value(OriginalRequestCtxKey).(http.Request) - r.Method = origReq.Method - r.RemoteAddr = origReq.RemoteAddr - r.RequestURI = origReq.RequestURI - cloneURL(origReq.URL, r.URL) + // NOTE: this does not restore original headers if modified (for efficiency) + origReq, ok := r.Context().Value(OriginalRequestCtxKey).(http.Request) + if ok { + r.Method = origReq.Method + r.RemoteAddr = origReq.RemoteAddr + r.RequestURI = origReq.RequestURI + cloneURL(origReq.URL, r.URL) + } // prepare the error log errLog = errLog.With(zap.Duration("duration", duration)) @@ -424,8 +410,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if s.Errors != nil && len(s.Errors.Routes) > 0 { // execute user-defined error handling route if err2 := s.errorHandlerChain.ServeHTTP(w, r); err2 == nil { - // user's error route handled the error response - // successfully, so now just log the error + // user's error route handled the error response successfully, so now just log the error for _, logger := range errLoggers { if c := logger.Check(zapcore.DebugLevel, errMsg); c != nil { if fields == nil { @@ -473,6 +458,35 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error { + // reject very long methods; probably a mistake or an attack + if len(r.Method) > 32 { + if s.shouldLogRequest(r) { + if c := s.accessLogger.Check(zapcore.DebugLevel, "rejecting request with long method"); c != nil { + c.Write( + zap.String("method_trunc", r.Method[:32]), + zap.String("remote_addr", r.RemoteAddr), + ) + } + } + return HandlerError{StatusCode: http.StatusMethodNotAllowed} + } + + // RFC 9112 section 3.2: "A server MUST respond with a 400 (Bad Request) status + // code to any HTTP/1.1 request message that lacks a Host header field and to any + // request message that contains more than one Host header field line or a Host + // header field with an invalid field value." + if r.Host == "" { + return HandlerError{ + Err: errors.New("rfc9112 forbids empty Host"), + StatusCode: http.StatusBadRequest, + } + } + + // execute the primary handler chain + return s.primaryHandlerChain.ServeHTTP(w, r) +} + // wrapPrimaryRoute wraps stack (a compiled middleware handler chain) // in s.enforcementHandler which performs crucial security checks, etc. func (s *Server) wrapPrimaryRoute(stack Handler) Handler { From 935b09de836d5ce001632193ac21c19abf0a57ed Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 30 Jan 2026 12:24:59 -0700 Subject: [PATCH 042/206] caddtls: Skip .ts.net domains for ECH (#6971) As it is also a special case in our automatic HTTPS. --- modules/caddytls/ech.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index a5b70d17d..a53344202 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -392,6 +392,10 @@ func (t *TLS) publishECHConfigs(logger *zap.Logger) error { if publication.Domains == nil { serverNamesSet = make(map[string]struct{}, len(t.serverNames)) for name := range t.serverNames { + // skip Tailscale names, a special case we also handle differently in our auto-HTTPS + if strings.HasSuffix(name, ".ts.net") { + continue + } serverNamesSet[name] = struct{}{} } } else { From 3bb22672f910f679b421b91fac3c7ad700df5255 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 2 Feb 2026 11:25:51 -0700 Subject: [PATCH 043/206] reverseproxy: Customizable dial network for SRV upstreams By request of a sponsor --- modules/caddyhttp/reverseproxy/caddyfile.go | 1 + modules/caddyhttp/reverseproxy/upstreams.go | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 12d610800..7b0b052da 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -1528,6 +1528,7 @@ func (u *SRVUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.Errf("bad delay value '%s': %v", d.Val(), err) } u.FallbackDelay = caddy.Duration(dur) + case "grace_period": if !d.NextArg() { return d.ArgErr() diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go index e9eb7e60a..4f4a3dbc8 100644 --- a/modules/caddyhttp/reverseproxy/upstreams.go +++ b/modules/caddyhttp/reverseproxy/upstreams.go @@ -70,6 +70,11 @@ type SRVUpstreams struct { // A negative value disables this. FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"` + // Specific network to dial when connecting to the upstream(s) + // provided by SRV records upstream. See Go's net package for + // accepted values. For example, to restrict to IPv4, use "tcp4". + DialNetwork string `json:"dial_network,omitempty"` + resolver *net.Resolver logger *zap.Logger @@ -177,6 +182,9 @@ func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { ) } addr := net.JoinHostPort(rec.Target, strconv.Itoa(int(rec.Port))) + if su.DialNetwork != "" { + addr = su.DialNetwork + "/" + addr + } upstreams[i] = Upstream{Dial: addr} } From e0f8d9b2047af417d8faf354b675941f3dac9891 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 3 Feb 2026 13:59:53 -0700 Subject: [PATCH 044/206] caddytls: Check type assertion Fix https://github.com/mholt/caddy-l4/issues/378 --- modules/caddytls/capools.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/caddytls/capools.go b/modules/caddytls/capools.go index bcc9ec6e8..55abb7466 100644 --- a/modules/caddytls/capools.go +++ b/modules/caddytls/capools.go @@ -502,8 +502,8 @@ func (t *TLSConfig) unmarshalCaddyfile(d *caddyfile.Dispenser) error { // If there is no custom TLS configuration, a nil config may be returned. // copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go func (t *TLSConfig) makeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) { - repl, _ := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - if repl == nil { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok || repl == nil { repl = caddy.NewReplacer() } cfg := new(tls.Config) From 40927d2f75bd928f99155653b90a2762919a3f1c Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Thu, 5 Feb 2026 06:12:26 -0700 Subject: [PATCH 045/206] Require disclosure of LLM usage in security reports Added requirement to disclose the use of LLMs in security reports. --- .github/SECURITY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 6f13031b9..1ca84f55f 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -33,6 +33,8 @@ We get a lot of difficult reports that turn out to be invalid. Clear, obvious re First please ensure your report falls within the accepted scope of security bugs (above). +**YOU MUST DISCLOSE THE USE OF LLMs ("AI"), WHETHER FOR DISCOVERING SECURITY BUGS OR WRITING THE REPORT.** Even if you are using AI as part of writing the report or its replies, we require you to mention the extent of it. + We'll need enough information to verify the bug and make a patch. To speed things up, please include: - Most minimal possible config (without redactions!) From 42ca010e9d8da35e91edd69e724d3736678b5620 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 5 Feb 2026 09:39:11 -0700 Subject: [PATCH 046/206] admin: Reject requests with Sec-Fetch-Mode headers And buggy Origin: null headers. Resolves a low-risk security report by @1seal. --- admin.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/admin.go b/admin.go index 0b8aedf9f..ae9bae795 100644 --- a/admin.go +++ b/admin.go @@ -807,13 +807,38 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) { } } + // common mitigations in browser contexts if strings.Contains(r.Header.Get("Upgrade"), "websocket") { // I've never been able demonstrate a vulnerability myself, but apparently // WebSocket connections originating from browsers aren't subject to CORS // restrictions, so we'll just be on the safe side - h.handleError(w, r, fmt.Errorf("websocket connections aren't allowed")) + h.handleError(w, r, APIError{ + HTTPStatus: http.StatusBadRequest, + Err: errors.New("websocket connections aren't allowed"), + Message: "WebSocket connections aren't allowed.", + }) return } + if strings.Contains(r.Header.Get("Sec-Fetch-Mode"), "no-cors") { + // turns out web pages can just disable the same-origin policy (!???!?) + // but at least browsers let us know that's the case, holy heck + h.handleError(w, r, APIError{ + HTTPStatus: http.StatusBadRequest, + Err: errors.New("client attempted to make request by disabling same-origin policy using no-cors mode"), + Message: "Disabling same-origin restrictions is not allowed.", + }) + return + } + if r.Header.Get("Origin") == "null" { + // bug in Firefox in certain cross-origin situations (yikes?) + // (not strictly a security vuln on its own, but it's red flaggy, + // since it seems to manifest in cross-origin contexts) + h.handleError(w, r, APIError{ + HTTPStatus: http.StatusBadRequest, + Err: errors.New("invalid origin 'null'"), + Message: "Buggy browser is sending null Origin header.", + }) + } if h.enforceHost { // DNS rebinding mitigation From 58968b3fd38cacbf4b5e07cc8c8be27696dce60f Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 6 Feb 2026 08:45:09 -0700 Subject: [PATCH 047/206] Update detail in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bcbc9397..508352c56 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,6 @@ Matthew Holt began developing Caddy in 2014 while studying computer science at B - _Project on X: [@caddyserver](https://x.com/caddyserver)_ - _Author on X: [@mholt6](https://x.com/mholt6)_ -Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company. +Caddy is a project of [ZeroSSL](https://zerossl.com), an HID Global company. Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence. From 2ae0f7af69521581e4b7fc4fc1913204d0659ecb Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Mon, 9 Feb 2026 15:06:19 -0500 Subject: [PATCH 048/206] reverseproxy: Set `Host` to `{upstream_hostport}` automatically if TLS (#7454) --- .../caddyhttp/reverseproxy/httptransport.go | 23 ++++++++ .../reverseproxy/httptransport_test.go | 21 ++++++++ .../caddyhttp/reverseproxy/reverseproxy.go | 52 +++++++++++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 8edc585e7..dd01b6ef5 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -40,6 +40,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/internal/network" ) @@ -514,6 +515,28 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e return rt, nil } +// RequestHeaderOps implements TransportHeaderOpsProvider. It returns header +// operations for requests when the transport's configuration indicates they +// should be applied. In particular, when TLS is enabled for this transport, +// return an operation to set the Host header to the upstream host:port +// placeholder so HTTPS upstreams get the proper Host by default. +// +// Note: this is a provision-time hook; the Handler will call this during +// its Provision and cache the resulting HeaderOps. The HeaderOps are +// applied per-request (so placeholders are expanded at request time). +func (h *HTTPTransport) RequestHeaderOps() *headers.HeaderOps { + // If TLS is not configured for this transport, don't inject Host + // defaults. TLS being non-nil indicates HTTPS to the upstream. + if h.TLS == nil { + return nil + } + return &headers.HeaderOps{ + Set: http.Header{ + "Host": []string{"{http.reverse_proxy.upstream.hostport}"}, + }, + } +} + // RoundTrip implements http.RoundTripper. func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { h.SetScheme(req) diff --git a/modules/caddyhttp/reverseproxy/httptransport_test.go b/modules/caddyhttp/reverseproxy/httptransport_test.go index 46931c8b1..1fa4965f2 100644 --- a/modules/caddyhttp/reverseproxy/httptransport_test.go +++ b/modules/caddyhttp/reverseproxy/httptransport_test.go @@ -94,3 +94,24 @@ func TestHTTPTransportUnmarshalCaddyFileWithCaPools(t *testing.T) { }) } } + +func TestHTTPTransport_RequestHeaderOps_TLS(t *testing.T) { + var ht HTTPTransport + // When TLS is nil, expect no header ops + if ops := ht.RequestHeaderOps(); ops != nil { + t.Fatalf("expected nil HeaderOps when TLS is nil, got: %#v", ops) + } + + // When TLS is configured, expect a HeaderOps that sets Host + ht.TLS = &TLSConfig{} + ops := ht.RequestHeaderOps() + if ops == nil { + t.Fatal("expected non-nil HeaderOps when TLS is set") + } + if ops.Set == nil { + t.Fatalf("expected ops.Set to be non-nil, got nil") + } + if got := ops.Set.Get("Host"); got != "{http.reverse_proxy.upstream.hostport}" { + t.Fatalf("unexpected Host value; want placeholder, got: %s", got) + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 2ea17046a..6f6a0f9f2 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -192,6 +192,13 @@ type Handler struct { CB CircuitBreaker `json:"-"` DynamicUpstreams UpstreamSource `json:"-"` + // transportHeaderOps is a set of header operations provided + // by the transport at provision time, if the transport + // implements TransportHeaderOpsProvider. These ops are + // applied before any user-configured header ops so the + // user can override transport defaults. + transportHeaderOps *headers.HeaderOps + // Holds the parsed CIDR ranges from TrustedProxies trustedProxies []netip.Prefix @@ -322,6 +329,18 @@ func (h *Handler) Provision(ctx caddy.Context) error { h.Transport = t } + // If the transport can provide header ops, cache them now so we don't + // have to compute them per-request. Provision the HeaderOps if present + // so any runtime artifacts (like precompiled regex) are prepared. + if tph, ok := h.Transport.(RequestHeaderOpsTransport); ok { + h.transportHeaderOps = tph.RequestHeaderOps() + if h.transportHeaderOps != nil { + if err := h.transportHeaderOps.Provision(ctx); err != nil { + return fmt.Errorf("provisioning transport header ops: %v", err) + } + } + } + // set up load balancing if h.LoadBalancing == nil { h.LoadBalancing = new(LoadBalancing) @@ -575,14 +594,26 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h repl.Set("http.reverse_proxy.upstream.fails", upstream.Host.Fails()) // mutate request headers according to this upstream; - // because we're in a retry loop, we have to copy - // headers (and the r.Host value) from the original - // so that each retry is identical to the first - if h.Headers != nil && h.Headers.Request != nil { + // because we're in a retry loop, we have to copy headers + // (and the r.Host value) from the original so that each + // retry is identical to the first. If either transport or + // user ops exist, apply them in order (transport first, + // then user, so user's config wins). + var userOps *headers.HeaderOps + if h.Headers != nil { + userOps = h.Headers.Request + } + transportOps := h.transportHeaderOps + if transportOps != nil || userOps != nil { r.Header = make(http.Header) copyHeader(r.Header, reqHeader) r.Host = reqHost - h.Headers.Request.ApplyToRequest(r) + if transportOps != nil { + transportOps.ApplyToRequest(r) + } + if userOps != nil { + userOps.ApplyToRequest(r) + } } // proxy the request to that upstream @@ -1542,6 +1573,17 @@ type BufferedTransport interface { DefaultBufferSizes() (int64, int64) } +// RequestHeaderOpsTransport may be implemented by a transport to provide +// header operations to apply to requests immediately before the RoundTrip. +// For example, overriding the default Host when TLS is enabled. +type RequestHeaderOpsTransport interface { + // RequestHeaderOps allows a transport to provide header operations + // to apply to the request. The transport is asked at provision time + // to return a HeaderOps (or nil) that will be applied before + // user-configured header ops. + RequestHeaderOps() *headers.HeaderOps +} + // roundtripSucceededError is an error type that is returned if the // roundtrip succeeded, but an error occurred after-the-fact. type roundtripSucceededError struct{ error } From bd374ca9d72e296c9361aee76924b6540f22f0c0 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 9 Feb 2026 09:43:07 -0700 Subject: [PATCH 049/206] caddyhttp: Lowercase comparison when matching with escape sequence --- modules/caddyhttp/matchers.go | 4 ++-- modules/caddyhttp/matchers_test.go | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 22976cfbd..25666a481 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -632,8 +632,8 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b // we can now treat rawpath globs (%*) as regular globs (*) matchPath = strings.ReplaceAll(matchPath, "%*", "*") - // ignore error here because we can't handle it anyway= - matches, _ := path.Match(matchPath, sb.String()) + // ignore error here because we can't handle it anyway + matches, _ := path.Match(matchPath, strings.ToLower(sb.String())) return matches } diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index b15b6316d..b5e965b4a 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -412,6 +412,11 @@ func TestPathMatcher(t *testing.T) { input: "/foo%2fbar/baz", expect: true, }, + { + match: MatchPath{"/admin%2fpanel"}, + input: "/ADMIN%2fpanel", + expect: true, + }, } { err := tc.match.Provision(caddy.Context{}) if err == nil && tc.provisionErr { From 1f43e8566b4c1d66a00a138188f0defc5adc6d75 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 9 Feb 2026 14:18:55 -0700 Subject: [PATCH 050/206] caddyhttp: Use case-insensitive comparison for large Host lists --- modules/caddyhttp/matchers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 25666a481..afba1b36f 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -319,7 +319,7 @@ func (m MatchHost) MatchWithError(r *http.Request) (bool, error) { } return m[i] >= reqHost }) - if pos < len(m) && m[pos] == reqHost { + if pos < len(m) && strings.EqualFold(m[pos], reqHost) { return true, nil } } From 5ff50779ccf1d5698bba48d140b713cff1a09390 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Mon, 9 Feb 2026 14:40:41 -0700 Subject: [PATCH 051/206] Update LLM disclosure requirements in SECURITY.md Clarified disclosure requirements for LLMs in security reports. --- .github/SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 1ca84f55f..5da8fdf30 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -33,7 +33,7 @@ We get a lot of difficult reports that turn out to be invalid. Clear, obvious re First please ensure your report falls within the accepted scope of security bugs (above). -**YOU MUST DISCLOSE THE USE OF LLMs ("AI"), WHETHER FOR DISCOVERING SECURITY BUGS OR WRITING THE REPORT.** Even if you are using AI as part of writing the report or its replies, we require you to mention the extent of it. +**YOU MUST DISCLOSE THE USE OF LLMs ("AI") INVOLVED IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT.** We'll need enough information to verify the bug and make a patch. To speed things up, please include: From 96f142c2a66ac2f0f9168e18e8ee84e785a5fa1f Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Tue, 10 Feb 2026 11:44:40 -0700 Subject: [PATCH 052/206] Update SECURITY.md --- .github/SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 5da8fdf30..eb7437269 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -18,7 +18,7 @@ A security report must demonstrate a security bug in the source code from this r Some security problems are the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please only report vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing, BGP hijacks, or missing/misconfigured HTTP headers a vulnerability in the Caddy web server). -Vulnerabilities caused by misconfigurations are out of scope. Yes, it is entirely possible to craft and use a configuration that is unsafe, just like with every other web server; we recommend against doing that. +Vulnerabilities caused by misconfigurations are out of scope. Yes, it is entirely possible to craft and use a configuration that is unsafe, just like with every other web server; we recommend against doing that. Similarly, external misconfigurations are out of scope. For example, an open or forwarded port from a public network to a Caddy instance intended to serve only internal clients is not a vulnerability in Caddy. We do not accept reports if the steps imply or require a compromised system or third-party software, as we cannot control those. We expect that users secure their own systems and keep all their software patched. For example, if untrusted users are able to upload/write/host arbitrary files in the web root directory, it is NOT a security bug in Caddy if those files get served to clients; however, it _would_ be a valid report if a bug in Caddy's source code unintentionally gave unauthorized users the ability to upload unsafe files or delete files without relying on an unpatched system or piece of software. @@ -33,7 +33,7 @@ We get a lot of difficult reports that turn out to be invalid. Clear, obvious re First please ensure your report falls within the accepted scope of security bugs (above). -**YOU MUST DISCLOSE THE USE OF LLMs ("AI") INVOLVED IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT.** +**YOU MUST DISCLOSE THE USE OF LLMs ("AI") INVOLVED IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT AND POTENTIAL BLOCKLISTING.** We'll need enough information to verify the bug and make a patch. To speed things up, please include: From 7c28c0c07ac70a8960a166c7126150a408ba7464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 10 Feb 2026 19:52:36 +0100 Subject: [PATCH 053/206] Merge commit from fork * fix: FastCGI split SCRIPT_NAME/PATH_INFO confusion * fix comment --- .../caddyhttp/reverseproxy/fastcgi/fastcgi.go | 90 ++++++- .../reverseproxy/fastcgi/fastcgi_test.go | 246 ++++++++++++++++++ 2 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index 5c68c3ad5..c60da897b 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -16,6 +16,7 @@ package fastcgi import ( "crypto/tls" + "errors" "fmt" "net" "net/http" @@ -23,9 +24,12 @@ import ( "strconv" "strings" "time" + "unicode/utf8" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "golang.org/x/text/language" + "golang.org/x/text/search" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" @@ -33,7 +37,11 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddytls" ) -var noopLogger = zap.NewNop() +var ( + ErrInvalidSplitPath = errors.New("split path contains non-ASCII characters") + + noopLogger = zap.NewNop() +) func init() { caddy.RegisterModule(Transport{}) @@ -50,6 +58,9 @@ type Transport struct { // actual resource (CGI script) name, and the second piece will be set to // PATH_INFO for the CGI script to use. // + // Split paths can only contain ASCII characters. + // Comparison is case-insensitive. + // // Future enhancements should be careful to avoid CVE-2019-11043, // which can be mitigated with use of a try_files-like behavior // that 404s if the fastcgi path info is not found. @@ -109,6 +120,28 @@ func (t *Transport) Provision(ctx caddy.Context) error { t.DialTimeout = caddy.Duration(3 * time.Second) } + var b strings.Builder + + for i, split := range t.SplitPath { + b.Grow(len(split)) + + for j := 0; j < len(split); j++ { + c := split[j] + if c >= utf8.RuneSelf { + return ErrInvalidSplitPath + } + + if 'A' <= c && c <= 'Z' { + b.WriteByte(c + 'a' - 'A') + } else { + b.WriteByte(c) + } + } + + t.SplitPath[i] = b.String() + b.Reset() + } + return nil } @@ -385,8 +418,15 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) { return env, nil } +var splitSearchNonASCII = search.New(language.Und, search.IgnoreCase) + // splitPos returns the index where path should // be split based on t.SplitPath. +// +// example: if splitPath is [".php"] +// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path") +// +// Adapted from FrankenPHP's code (copyright 2026 Kévin Dunglas, MIT license) func (t Transport) splitPos(path string) int { // TODO: from v1... // if httpserver.CaseSensitivePath { @@ -396,12 +436,54 @@ func (t Transport) splitPos(path string) int { return 0 } - lowerPath := strings.ToLower(path) + pathLen := len(path) + + // We are sure that split strings are all ASCII-only and lower-case because of validation and normalization in Provision(). for _, split := range t.SplitPath { - if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 { - return idx + len(split) + splitLen := len(split) + + for i := 0; i < pathLen; i++ { + if path[i] >= utf8.RuneSelf { + if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 { + return end + } + + break + } + + if i+splitLen > pathLen { + continue + } + + match := true + for j := 0; j < splitLen; j++ { + c := path[i+j] + + if c >= utf8.RuneSelf { + if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 { + return end + } + + break + } + + if 'A' <= c && c <= 'Z' { + c += 'a' - 'A' + } + + if c != split[j] { + match = false + + break + } + } + + if match { + return i + splitLen + } } } + return -1 } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go new file mode 100644 index 000000000..7097ff790 --- /dev/null +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go @@ -0,0 +1,246 @@ +package fastcgi + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/caddyserver/caddy/v2" +) + +func TestProvisionSplitPath(t *testing.T) { + tests := []struct { + name string + splitPath []string + wantErr error + wantSplitPath []string + }{ + { + name: "valid lowercase split path", + splitPath: []string{".php"}, + wantErr: nil, + wantSplitPath: []string{".php"}, + }, + { + name: "valid uppercase split path normalized", + splitPath: []string{".PHP"}, + wantErr: nil, + wantSplitPath: []string{".php"}, + }, + { + name: "valid mixed case split path normalized", + splitPath: []string{".PhP", ".PHTML"}, + wantErr: nil, + wantSplitPath: []string{".php", ".phtml"}, + }, + { + name: "empty split path", + splitPath: []string{}, + wantErr: nil, + wantSplitPath: []string{}, + }, + { + name: "non-ASCII character in split path rejected", + splitPath: []string{".php", ".Ⱥphp"}, + wantErr: ErrInvalidSplitPath, + }, + { + name: "unicode character in split path rejected", + splitPath: []string{".phpⱥ"}, + wantErr: ErrInvalidSplitPath, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := Transport{SplitPath: tt.splitPath} + err := tr.Provision(caddy.Context{}) + + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + + return + } + + require.NoError(t, err) + assert.Equal(t, tt.wantSplitPath, tr.SplitPath) + }) + } +} + +func TestSplitPos(t *testing.T) { + tests := []struct { + name string + path string + splitPath []string + wantPos int + }{ + { + name: "simple php extension", + path: "/path/to/script.php", + splitPath: []string{".php"}, + wantPos: 19, + }, + { + name: "php extension with path info", + path: "/path/to/script.php/some/path", + splitPath: []string{".php"}, + wantPos: 19, + }, + { + name: "case insensitive match", + path: "/path/to/script.PHP", + splitPath: []string{".php"}, + wantPos: 19, + }, + { + name: "mixed case match", + path: "/path/to/script.PhP/info", + splitPath: []string{".php"}, + wantPos: 19, + }, + { + name: "no match", + path: "/path/to/script.txt", + splitPath: []string{".php"}, + wantPos: -1, + }, + { + name: "empty split path", + path: "/path/to/script.php", + splitPath: []string{}, + wantPos: 0, + }, + { + name: "multiple split paths first match", + path: "/path/to/script.php", + splitPath: []string{".php", ".phtml"}, + wantPos: 19, + }, + { + name: "multiple split paths second match", + path: "/path/to/script.phtml", + splitPath: []string{".php", ".phtml"}, + wantPos: 21, + }, + // Unicode case-folding tests (security fix for GHSA-g966-83w7-6w38) + // U+023A (Ⱥ) lowercases to U+2C65 (ⱥ), which has different UTF-8 byte length + // Ⱥ: 2 bytes (C8 BA), ⱥ: 3 bytes (E2 B1 A5) + { + name: "unicode path with case-folding length expansion", + path: "/ȺȺȺȺshell.php", + splitPath: []string{".php"}, + wantPos: 18, // correct position in original string + }, + { + name: "unicode path with extension after expansion chars", + path: "/ȺȺȺȺshell.php/path/info", + splitPath: []string{".php"}, + wantPos: 18, + }, + { + name: "unicode in filename with multiple php occurrences", + path: "/ȺȺȺȺshell.php.txt.php", + splitPath: []string{".php"}, + wantPos: 18, // should match first .php, not be confused by byte offset shift + }, + { + name: "unicode case insensitive extension", + path: "/ȺȺȺȺshell.PHP", + splitPath: []string{".php"}, + wantPos: 18, + }, + { + name: "unicode in middle of path", + path: "/path/Ⱥtest/script.php", + splitPath: []string{".php"}, + wantPos: 23, // Ⱥ is 2 bytes, so path is 23 bytes total, .php ends at byte 23 + }, + { + name: "unicode only in directory not filename", + path: "/Ⱥ/script.php", + splitPath: []string{".php"}, + wantPos: 14, + }, + // Additional Unicode characters that expand when lowercased + // U+0130 (İ - Turkish capital I with dot) lowercases to U+0069 + U+0307 + { + name: "turkish capital I with dot", + path: "/İtest.php", + splitPath: []string{".php"}, + wantPos: 11, + }, + // Ensure standard ASCII still works correctly + { + name: "ascii only path with case variation", + path: "/PATH/TO/SCRIPT.PHP/INFO", + splitPath: []string{".php"}, + wantPos: 19, + }, + { + name: "path at root", + path: "/index.php", + splitPath: []string{".php"}, + wantPos: 10, + }, + { + name: "extension in middle of filename", + path: "/test.php.bak", + splitPath: []string{".php"}, + wantPos: 9, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPos := Transport{SplitPath: tt.splitPath}.splitPos(tt.path) + assert.Equal(t, tt.wantPos, gotPos, "splitPos(%q, %v)", tt.path, tt.splitPath) + + // Verify that the split produces valid substrings + if gotPos > 0 && gotPos <= len(tt.path) { + scriptName := tt.path[:gotPos] + pathInfo := tt.path[gotPos:] + + // The script name should end with one of the split extensions (case-insensitive) + hasValidEnding := false + for _, split := range tt.splitPath { + if strings.HasSuffix(strings.ToLower(scriptName), split) { + hasValidEnding = true + break + } + } + assert.True(t, hasValidEnding, "script name %q should end with one of %v", scriptName, tt.splitPath) + + // Original path should be reconstructable + assert.Equal(t, tt.path, scriptName+pathInfo, "path should be reconstructable from split parts") + } + }) + } +} + +// TestSplitPosUnicodeSecurityRegression specifically tests the vulnerability +// described in GHSA-g966-83w7-6w38 where Unicode case-folding caused +// incorrect SCRIPT_NAME/PATH_INFO splitting +func TestSplitPosUnicodeSecurityRegression(t *testing.T) { + // U+023A: Ⱥ (UTF-8: C8 BA). Lowercase is ⱥ (UTF-8: E2 B1 A5), longer in bytes. + path := "/ȺȺȺȺshell.php.txt.php" + split := []string{".php"} + + pos := Transport{SplitPath: split}.splitPos(path) + + // The vulnerable code would return 22 (computed on lowercased string) + // The correct code should return 18 (position in original string) + expectedPos := strings.Index(path, ".php") + len(".php") + assert.Equal(t, expectedPos, pos, "split position should match first .php in original string") + assert.Equal(t, 18, pos, "split position should be 18, not 22") + + if pos > 0 && pos <= len(path) { + scriptName := path[:pos] + pathInfo := path[pos:] + + assert.Equal(t, "/ȺȺȺȺshell.php", scriptName, "script name should be the path up to first .php") + assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php") + } +} From 03e6e439dd07d048323cc39516146e0f04032186 Mon Sep 17 00:00:00 2001 From: XYenon Date: Wed, 11 Feb 2026 04:00:20 +0800 Subject: [PATCH 054/206] reverseproxy: fix X-Forwarded-* headers for Unix socket requests (#7463) When a request arrives via a Unix domain socket (RemoteAddr == "@"), net.SplitHostPort fails, causing addForwardedHeaders to strip all X-Forwarded-* headers even when the connection is trusted via trusted_proxies_unix. Handle Unix socket connections before parsing RemoteAddr: if untrusted, strip headers for security; if trusted, let clientIP remain empty (no peer IP for a Unix socket hop) and fall through to the shared header logic, preserving the existing XFF chain without appending a spurious entry. Amp-Thread-ID: https://ampcode.com/threads/T-019c4225-a0ad-7283-ac56-e2c01eae1103 Co-authored-by: Amp --- .../caddyhttp/reverseproxy/headers_test.go | 93 +++++++++++++++++++ .../caddyhttp/reverseproxy/reverseproxy.go | 86 ++++++++++------- 2 files changed, 146 insertions(+), 33 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/headers_test.go b/modules/caddyhttp/reverseproxy/headers_test.go index 22f589141..9385468f6 100644 --- a/modules/caddyhttp/reverseproxy/headers_test.go +++ b/modules/caddyhttp/reverseproxy/headers_test.go @@ -32,3 +32,96 @@ func TestAddForwardedHeadersNonIP(t *testing.T) { t.Errorf("expected no error for non-IP address, got: %v", err) } } + +func TestAddForwardedHeaders_UnixSocketTrusted(t *testing.T) { + h := Handler{} + + req := httptest.NewRequest("GET", "http://example.com/", nil) + req.RemoteAddr = "@" + req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.0.0.1") + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Forwarded-Host", "original.example.com") + + vars := map[string]interface{}{ + caddyhttp.TrustedProxyVarKey: true, + caddyhttp.ClientIPVarKey: "1.2.3.4", + } + ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars) + req = req.WithContext(ctx) + + err := h.addForwardedHeaders(req) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + if got := req.Header.Get("X-Forwarded-For"); got != "1.2.3.4, 10.0.0.1" { + t.Errorf("X-Forwarded-For = %q, want %q", got, "1.2.3.4, 10.0.0.1") + } + if got := req.Header.Get("X-Forwarded-Proto"); got != "https" { + t.Errorf("X-Forwarded-Proto = %q, want %q", got, "https") + } + if got := req.Header.Get("X-Forwarded-Host"); got != "original.example.com" { + t.Errorf("X-Forwarded-Host = %q, want %q", got, "original.example.com") + } +} + +func TestAddForwardedHeaders_UnixSocketUntrusted(t *testing.T) { + h := Handler{} + + req := httptest.NewRequest("GET", "http://example.com/", nil) + req.RemoteAddr = "@" + req.Header.Set("X-Forwarded-For", "1.2.3.4") + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Forwarded-Host", "spoofed.example.com") + + vars := map[string]interface{}{ + caddyhttp.TrustedProxyVarKey: false, + caddyhttp.ClientIPVarKey: "", + } + ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars) + req = req.WithContext(ctx) + + err := h.addForwardedHeaders(req) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + if got := req.Header.Get("X-Forwarded-For"); got != "" { + t.Errorf("X-Forwarded-For should be deleted, got %q", got) + } + if got := req.Header.Get("X-Forwarded-Proto"); got != "" { + t.Errorf("X-Forwarded-Proto should be deleted, got %q", got) + } + if got := req.Header.Get("X-Forwarded-Host"); got != "" { + t.Errorf("X-Forwarded-Host should be deleted, got %q", got) + } +} + +func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) { + h := Handler{} + + req := httptest.NewRequest("GET", "http://example.com/", nil) + req.RemoteAddr = "@" + + vars := map[string]interface{}{ + caddyhttp.TrustedProxyVarKey: true, + caddyhttp.ClientIPVarKey: "5.6.7.8", + } + ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars) + req = req.WithContext(ctx) + + err := h.addForwardedHeaders(req) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + if got := req.Header.Get("X-Forwarded-For"); got != "" { + t.Errorf("X-Forwarded-For should be empty when no prior XFF exists, got %q", got) + } + if got := req.Header.Get("X-Forwarded-Proto"); got != "http" { + t.Errorf("X-Forwarded-Proto = %q, want %q", got, "http") + } + if got := req.Header.Get("X-Forwarded-Host"); got != "example.com" { + t.Errorf("X-Forwarded-Host = %q, want %q", got, "example.com") + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 6f6a0f9f2..f9fdd164e 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -801,37 +801,53 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. // the headers at all, then they will be added with the values // that we can glean from the request. func (h Handler) addForwardedHeaders(req *http.Request) error { - // 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 - // though, unless some other module manipulated the request's - // remote address and used an invalid value. - clientIP, _, err := net.SplitHostPort(req.RemoteAddr) - if err != nil { - // Remove the `X-Forwarded-*` headers to avoid upstreams - // potentially trusting a header that came from the client - req.Header.Del("X-Forwarded-For") - req.Header.Del("X-Forwarded-Proto") - req.Header.Del("X-Forwarded-Host") - return nil - } - - // Client IP may contain a zone if IPv6, so we need - // to pull that out before parsing the IP - clientIP, _, _ = strings.Cut(clientIP, "%") - ipAddr, err := netip.ParseAddr(clientIP) - // Check if the client is a trusted proxy trusted := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool) - // If ParseAddr fails (e.g. non-IP network like SCION), we cannot check - // if it is a trusted proxy by IP range. In this case, we ignore the - // error and treat the connection as untrusted (or retain existing status). - if err == nil { - for _, ipRange := range h.trustedProxies { - if ipRange.Contains(ipAddr) { - trusted = true - break + var clientIP string + + if req.RemoteAddr == "@" { + // For Unix socket connections, RemoteAddr is "@" which cannot + // be parsed as host:port. If untrusted, strip forwarded headers + // for security. If trusted, there is no peer IP to append to + // X-Forwarded-For, so clientIP stays empty. + if !trusted { + req.Header.Del("X-Forwarded-For") + req.Header.Del("X-Forwarded-Proto") + req.Header.Del("X-Forwarded-Host") + return nil + } + } else { + // 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 + // though, unless some other module manipulated the request's + // remote address and used an invalid value. + var err error + clientIP, _, err = net.SplitHostPort(req.RemoteAddr) + if err != nil { + // Remove the `X-Forwarded-*` headers to avoid upstreams + // potentially trusting a header that came from the client + req.Header.Del("X-Forwarded-For") + req.Header.Del("X-Forwarded-Proto") + req.Header.Del("X-Forwarded-Host") + return nil + } + + // Client IP may contain a zone if IPv6, so we need + // to pull that out before parsing the IP + clientIP, _, _ = strings.Cut(clientIP, "%") + ipAddr, err := netip.ParseAddr(clientIP) + + // If ParseAddr fails (e.g. non-IP network like SCION), we cannot check + // if it is a trusted proxy by IP range. In this case, we ignore the + // error and treat the connection as untrusted (or retain existing status). + if err == nil { + for _, ipRange := range h.trustedProxies { + if ipRange.Contains(ipAddr) { + trusted = true + break + } } } } @@ -839,13 +855,17 @@ func (h Handler) addForwardedHeaders(req *http.Request) error { // If we aren't the first proxy, and the proxy is trusted, // retain prior X-Forwarded-For information as a comma+space // separated list and fold multiple headers into one. - clientXFF := clientIP prior, ok, omit := allHeaderValues(req.Header, "X-Forwarded-For") - if trusted && ok && prior != "" { - clientXFF = prior + ", " + clientXFF - } if !omit { - req.Header.Set("X-Forwarded-For", clientXFF) + if trusted && ok && prior != "" { + if clientIP != "" { + req.Header.Set("X-Forwarded-For", prior+", "+clientIP) + } else { + req.Header.Set("X-Forwarded-For", prior) + } + } else if clientIP != "" { + req.Header.Set("X-Forwarded-For", clientIP) + } } // Set X-Forwarded-Proto; many backend apps expect this, From 47f3e8f8dc5f27632599aa2ee9a00ec7a6fba903 Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Thu, 12 Feb 2026 00:15:51 +0800 Subject: [PATCH 055/206] use math/rand/v2 instead of math/rand (#7413) --- caddytest/integration/listener_test.go | 4 ++-- modules/caddyhttp/caddyauth/basicauth.go | 4 ++-- modules/caddyhttp/errors.go | 4 ++-- modules/caddyhttp/fileserver/staticfiles.go | 4 ++-- modules/caddyhttp/reverseproxy/fastcgi/client_test.go | 4 ++-- modules/caddyhttp/reverseproxy/httptransport.go | 4 ++-- modules/caddyhttp/reverseproxy/selectionpolicies.go | 10 +++++----- modules/caddyhttp/reverseproxy/streaming.go | 2 +- modules/caddyhttp/reverseproxy/upstreams.go | 6 +++--- modules/caddypki/acmeserver/acmeserver.go | 4 ++-- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/caddytest/integration/listener_test.go b/caddytest/integration/listener_test.go index 30642b1ae..bd2d94e1a 100644 --- a/caddytest/integration/listener_test.go +++ b/caddytest/integration/listener_test.go @@ -3,7 +3,7 @@ package integration import ( "bytes" "fmt" - "math/rand" + "math/rand/v2" "net" "net/http" "strings" @@ -54,7 +54,7 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) { const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte // 1 more than an MB body := make([]byte, uploadSize) - rand.New(rand.NewSource(0)).Read(body) + rand.NewChaCha8([32]byte{}).Read(body) tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) { buf := new(bytes.Buffer) diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go index 5a9e167e1..81b62d8eb 100644 --- a/modules/caddyhttp/caddyauth/basicauth.go +++ b/modules/caddyhttp/caddyauth/basicauth.go @@ -19,7 +19,7 @@ import ( "encoding/hex" "encoding/json" "fmt" - weakrand "math/rand" + weakrand "math/rand/v2" "net/http" "strings" "sync" @@ -244,7 +244,7 @@ func (c *Cache) makeRoom() { // strategy; generating random numbers is cheap and // ensures a much better distribution. //nolint:gosec - rnd := weakrand.Intn(len(c.cache)) + rnd := weakrand.IntN(len(c.cache)) i := 0 for key := range c.cache { if i == rnd { diff --git a/modules/caddyhttp/errors.go b/modules/caddyhttp/errors.go index fc8ffbfaa..673368e2e 100644 --- a/modules/caddyhttp/errors.go +++ b/modules/caddyhttp/errors.go @@ -17,7 +17,7 @@ package caddyhttp import ( "errors" "fmt" - weakrand "math/rand" + weakrand "math/rand/v2" "path" "runtime" "strings" @@ -98,7 +98,7 @@ func randString(n int, sameCase bool) string { b := make([]byte, n) for i := range b { //nolint:gosec - b[i] = dict[weakrand.Int63()%int64(len(dict))] + b[i] = dict[weakrand.IntN(len(dict))] } return string(b) } diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 3daf8daef..8a074f546 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -20,7 +20,7 @@ import ( "fmt" "io" "io/fs" - weakrand "math/rand" + weakrand "math/rand/v2" "mime" "net/http" "os" @@ -601,7 +601,7 @@ func (fsrv *FileServer) openFile(fileSystem fs.FS, filename string, w http.Respo // maybe the server is under load and ran out of file descriptors? // have client wait arbitrary seconds to help prevent a stampede //nolint:gosec - backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff + backoff := weakrand.IntN(maxBackoff-minBackoff) + minBackoff w.Header().Set("Retry-After", strconv.Itoa(backoff)) if c := fsrv.logger.Check(zapcore.DebugLevel, "retry after backoff"); c != nil { c.Write(zap.String("filename", filename), zap.Int("backoff", backoff), zap.Error(err)) diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go index f850cfb9d..798ee883e 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go @@ -27,7 +27,7 @@ import ( "fmt" "io" "log" - "math/rand" + "math/rand/v2" "net" "net/http" "net/http/fcgi" @@ -197,7 +197,7 @@ func generateRandFile(size int) (p string, m string) { h := md5.New() for i := 0; i < size/16; i++ { buf := make([]byte, 16) - binary.PutVarint(buf, rand.Int63()) + binary.PutVarint(buf, rand.Int64()) if _, err := fo.Write(buf); err != nil { log.Printf("[ERROR] failed to write buffer: %v\n", err) } diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index dd01b6ef5..8d06d3bd2 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -21,7 +21,7 @@ import ( "encoding/base64" "encoding/json" "fmt" - weakrand "math/rand" + weakrand "math/rand/v2" "net" "net/http" "net/url" @@ -266,7 +266,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e PreferGo: true, Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { //nolint:gosec - addr := h.Resolver.netAddrs[weakrand.Intn(len(h.Resolver.netAddrs))] + addr := h.Resolver.netAddrs[weakrand.IntN(len(h.Resolver.netAddrs))] return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0)) }, } diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index 585fc3400..2059c3ecf 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -20,7 +20,7 @@ import ( "encoding/hex" "encoding/json" "fmt" - weakrand "math/rand" + weakrand "math/rand/v2" "net" "net/http" "strconv" @@ -225,7 +225,7 @@ func (r RandomChoiceSelection) Select(pool UpstreamPool, _ *http.Request, _ http if !upstream.Available() { continue } - j := weakrand.Intn(i + 1) //nolint:gosec + j := weakrand.IntN(i + 1) //nolint:gosec if j < k { choices[j] = upstream } @@ -274,7 +274,7 @@ func (LeastConnSelection) Select(pool UpstreamPool, _ *http.Request, _ http.Resp // sample: https://en.wikipedia.org/wiki/Reservoir_sampling if numReqs == leastReqs { count++ - if count == 1 || (weakrand.Int()%count) == 0 { //nolint:gosec + if count == 1 || weakrand.IntN(count) == 0 { //nolint:gosec bestHost = host } } @@ -788,7 +788,7 @@ func selectRandomHost(pool []*Upstream) *Upstream { // upstream will always be chosen if there is at // least one available count++ - if (weakrand.Int() % count) == 0 { //nolint:gosec + if weakrand.IntN(count) == 0 { //nolint:gosec randomHost = upstream } } @@ -827,7 +827,7 @@ func leastRequests(upstreams []*Upstream) *Upstream { if len(best) == 1 { return best[0] } - return best[weakrand.Intn(len(best))] //nolint:gosec + return best[weakrand.IntN(len(best))] //nolint:gosec } // hostByHashing returns an available host from pool based on a hashable string s. diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index 99e3cd009..0a8118520 100644 --- a/modules/caddyhttp/reverseproxy/streaming.go +++ b/modules/caddyhttp/reverseproxy/streaming.go @@ -24,7 +24,7 @@ import ( "errors" "fmt" "io" - weakrand "math/rand" + weakrand "math/rand/v2" "mime" "net/http" "sync" diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go index 4f4a3dbc8..e9120725a 100644 --- a/modules/caddyhttp/reverseproxy/upstreams.go +++ b/modules/caddyhttp/reverseproxy/upstreams.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - weakrand "math/rand" + weakrand "math/rand/v2" "net" "net/http" "strconv" @@ -107,7 +107,7 @@ func (su *SRVUpstreams) Provision(ctx caddy.Context) error { PreferGo: true, Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { //nolint:gosec - addr := su.Resolver.netAddrs[weakrand.Intn(len(su.Resolver.netAddrs))] + addr := su.Resolver.netAddrs[weakrand.IntN(len(su.Resolver.netAddrs))] return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0)) }, } @@ -330,7 +330,7 @@ func (au *AUpstreams) Provision(ctx caddy.Context) error { PreferGo: true, Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { //nolint:gosec - addr := au.Resolver.netAddrs[weakrand.Intn(len(au.Resolver.netAddrs))] + addr := au.Resolver.netAddrs[weakrand.IntN(len(au.Resolver.netAddrs))] return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0)) }, } diff --git a/modules/caddypki/acmeserver/acmeserver.go b/modules/caddypki/acmeserver/acmeserver.go index aeb4eab8e..1a41671f2 100644 --- a/modules/caddypki/acmeserver/acmeserver.go +++ b/modules/caddypki/acmeserver/acmeserver.go @@ -17,7 +17,7 @@ package acmeserver import ( "context" "fmt" - weakrand "math/rand" + weakrand "math/rand/v2" "net" "net/http" "os" @@ -307,7 +307,7 @@ func (ash Handler) makeClient() (acme.Client, error) { PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { //nolint:gosec - addr := ash.resolvers[weakrand.Intn(len(ash.resolvers))] + addr := ash.resolvers[weakrand.IntN(len(ash.resolvers))] return dialer.DialContext(ctx, addr.Network, addr.JoinHostPort(0)) }, } From 72ac479f5d0472425fe150c4aacd03d1030b0077 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 11 Feb 2026 09:52:56 -0700 Subject: [PATCH 056/206] admin: Enforce origin implicitly based on request headers --- admin.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/admin.go b/admin.go index ae9bae795..46f1bbda3 100644 --- a/admin.go +++ b/admin.go @@ -849,7 +849,9 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) { } } - if h.enforceOrigin { + _, hasOriginHeader := r.Header["Origin"] + _, hasSecHeader := r.Header["Sec-Fetch-Mode"] + if h.enforceOrigin || hasOriginHeader || hasSecHeader { // cross-site mitigation origin, err := h.checkOrigin(r) if err != nil { From c0af7b665fe8a860efa8e8fa9c204675a59af39f Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Wed, 11 Feb 2026 13:21:10 -0500 Subject: [PATCH 057/206] chore: bump Go to v1.26 (#7466) --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/cross-build.yml | 6 +++--- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 6 +++--- go.mod | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50501a0f1..08a8cd60d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,13 +31,13 @@ jobs: - mac - windows go: - - '1.25' + - '1.26' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.25' - GO_SEMVER: '~1.25.0' + - go: '1.26' + GO_SEMVER: '~1.26.0' # Set some variables per OS, usable via ${{ matrix.VAR }} # OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories) @@ -235,7 +235,7 @@ jobs: - name: Install Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: - go-version: "~1.25" + go-version: "~1.26" check-latest: true - name: Install xcaddy run: | diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index 8aa9eaf59..b86bbdb7c 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -36,13 +36,13 @@ jobs: - 'darwin' - 'netbsd' go: - - '1.25' + - '1.26' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.25' - GO_SEMVER: '~1.25.0' + - go: '1.26' + GO_SEMVER: '~1.26.0' runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 849188c64..e94ad3f35 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -52,7 +52,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: - go-version: '~1.25' + go-version: '~1.26' check-latest: true - name: golangci-lint @@ -80,7 +80,7 @@ jobs: - name: govulncheck uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 with: - go-version-input: '~1.25.0' + go-version-input: '~1.26.0' check-latest: true dependency-review: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4880a64c..c975a4cf8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -334,13 +334,13 @@ jobs: os: - ubuntu-latest go: - - '1.25' + - '1.26' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.25' - GO_SEMVER: '~1.25.0' + - go: '1.26' + GO_SEMVER: '~1.26.0' runs-on: ${{ matrix.os }} # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233 diff --git a/go.mod b/go.mod index 8fc21ed50..cb0a295d8 100644 --- a/go.mod +++ b/go.mod @@ -170,7 +170,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/sys v0.39.0 - golang.org/x/text v0.32.0 // indirect + golang.org/x/text v0.32.0 golang.org/x/tools v0.39.0 // indirect google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect From 0188ef2e62e772be30ef01344e6103ecf7a57db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oleh=20Konko=20=7C=20trust=20infra=20security=20audit=20?= =?UTF-8?q?=26=20contribution=20=7C=20deterministic=20ai-augmented=20pipel?= =?UTF-8?q?ine=20=C2=B7=20human-verified?= Date: Wed, 11 Feb 2026 18:54:51 +0000 Subject: [PATCH 058/206] acmeserver: warn when policy rules unset (#7469) --- modules/caddypki/acmeserver/acmeserver.go | 17 ++++ .../caddypki/acmeserver/acmeserver_test.go | 94 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 modules/caddypki/acmeserver/acmeserver_test.go diff --git a/modules/caddypki/acmeserver/acmeserver.go b/modules/caddypki/acmeserver/acmeserver.go index 1a41671f2..4d158ed9f 100644 --- a/modules/caddypki/acmeserver/acmeserver.go +++ b/modules/caddypki/acmeserver/acmeserver.go @@ -140,6 +140,8 @@ func (ash *Handler) Provision(ctx caddy.Context) error { } } + ash.warnIfPolicyAllowsAll() + // get a reference to the configured CA appModule, err := ctx.App("pki") if err != nil { @@ -214,6 +216,21 @@ func (ash *Handler) Provision(ctx caddy.Context) error { return nil } +func (ash *Handler) warnIfPolicyAllowsAll() { + allow := ash.Policy.normalizeAllowRules() + deny := ash.Policy.normalizeDenyRules() + if allow != nil || deny != nil { + return + } + + allowWildcardNames := ash.Policy != nil && ash.Policy.AllowWildcardNames + ash.logger.Warn( + "acme_server policy has no allow/deny rules; order identifiers are unrestricted (allow-all)", + zap.String("ca", ash.CA), + zap.Bool("allow_wildcard_names", allowWildcardNames), + ) +} + func (ash Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { if strings.HasPrefix(r.URL.Path, ash.PathPrefix) { acmeCtx := acme.NewContext( diff --git a/modules/caddypki/acmeserver/acmeserver_test.go b/modules/caddypki/acmeserver/acmeserver_test.go new file mode 100644 index 000000000..ca54012eb --- /dev/null +++ b/modules/caddypki/acmeserver/acmeserver_test.go @@ -0,0 +1,94 @@ +package acmeserver + +import ( + "strings" + "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" +) + +func TestHandler_warnIfPolicyAllowsAll(t *testing.T) { + tests := []struct { + name string + policy *Policy + wantWarns int + wantAllowWildcard bool + }{ + { + name: "warns when policy is nil", + policy: nil, + wantWarns: 1, + wantAllowWildcard: false, + }, + { + name: "warns when allow/deny rules are empty", + policy: &Policy{}, + wantWarns: 1, + wantAllowWildcard: false, + }, + { + name: "warns when only allow_wildcard_names is true", + policy: &Policy{ + AllowWildcardNames: true, + }, + wantWarns: 1, + wantAllowWildcard: true, + }, + { + name: "does not warn when allow rules are configured", + policy: &Policy{ + Allow: &RuleSet{ + Domains: []string{"example.com"}, + }, + }, + wantWarns: 0, + wantAllowWildcard: false, + }, + { + name: "does not warn when deny rules are configured", + policy: &Policy{ + Deny: &RuleSet{ + Domains: []string{"bad.example.com"}, + }, + }, + wantWarns: 0, + wantAllowWildcard: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + core, logs := observer.New(zap.WarnLevel) + ash := &Handler{ + CA: "local", + Policy: tt.policy, + logger: zap.New(core), + } + + ash.warnIfPolicyAllowsAll() + if logs.Len() != tt.wantWarns { + t.Fatalf("expected %d warning logs, got %d", tt.wantWarns, logs.Len()) + } + + if tt.wantWarns == 0 { + return + } + + entry := logs.All()[0] + if entry.Level != zap.WarnLevel { + t.Fatalf("expected warn level, got %v", entry.Level) + } + if !strings.Contains(entry.Message, "policy has no allow/deny rules") { + t.Fatalf("unexpected log message: %q", entry.Message) + } + ctx := entry.ContextMap() + if ctx["ca"] != "local" { + t.Fatalf("expected ca=local, got %v", ctx["ca"]) + } + if ctx["allow_wildcard_names"] != tt.wantAllowWildcard { + t.Fatalf("expected allow_wildcard_names=%v, got %v", tt.wantAllowWildcard, ctx["allow_wildcard_names"]) + } + }) + } +} From d42d39b4bc237c628f9a95363b28044cb7a7fe72 Mon Sep 17 00:00:00 2001 From: moscowchill <72578879+moscowchill@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:42:54 +0800 Subject: [PATCH 059/206] caddytls: Return errors instead of nil in client auth provisioning (#7464) Two error returns in ClientAuthentication.provision() were returning nil instead of the actual error, silently swallowing failures when converting PEM files to DER and when provisioning the CA pool. This could cause mTLS client authentication to silently fall back to the system trust store, accepting any client certificate signed by a public CA instead of restricting to the configured trust anchors. --- modules/caddytls/connpolicy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 036c5fb92..6b6dc3636 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -784,7 +784,7 @@ func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error { for _, fpath := range clientauth.TrustedCACertPEMFiles { ders, err := convertPEMFilesToDER(fpath) if err != nil { - return nil + return err } clientauth.TrustedCACerts = append(clientauth.TrustedCACerts, ders...) } @@ -797,7 +797,7 @@ func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error { } err := caPool.Provision(ctx) if err != nil { - return nil + return err } clientauth.ca = caPool } From 80bf81839d90a134698308c871dc06cbc187cf02 Mon Sep 17 00:00:00 2001 From: Omer Cohen Date: Thu, 12 Feb 2026 17:54:48 +0200 Subject: [PATCH 060/206] go.mod: update nebula v1.10.3 to resolve cve (#7471) --- go.mod | 53 +++++++++-------- go.sum | 183 +++++++++++++++++++++++++++++---------------------------- 2 files changed, 119 insertions(+), 117 deletions(-) diff --git a/go.mod b/go.mod index cb0a295d8..e88589fac 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/caddyserver/zerossl v0.1.4 github.com/cloudflare/circl v1.6.2 github.com/dustin/go-humanize v1.0.1 - github.com/go-chi/chi/v5 v5.2.3 + github.com/go-chi/chi/v5 v5.2.4 github.com/google/cel-go v0.26.1 github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.18.2 @@ -21,7 +21,7 @@ require ( github.com/mholt/acmez/v3 v3.1.4 github.com/prometheus/client_golang v1.23.2 github.com/quic-go/quic-go v0.59.0 - github.com/smallstep/certificates v0.29.0 + github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c github.com/smallstep/nosql v0.7.0 github.com/smallstep/truststore v0.13.0 github.com/spf13/cobra v1.10.2 @@ -35,25 +35,26 @@ require ( go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 go.opentelemetry.io/otel v1.39.0 go.opentelemetry.io/otel/sdk v1.39.0 - go.step.sm/crypto v0.75.0 + go.step.sm/crypto v0.76.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.46.0 + golang.org/x/crypto v0.48.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 - golang.org/x/net v0.48.0 + golang.org/x/net v0.50.0 golang.org/x/sync v0.19.0 - golang.org/x/term v0.38.0 + golang.org/x/term v0.40.0 golang.org/x/time v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) require ( cel.dev/expr v0.24.0 // indirect - cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth v0.18.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.2 // indirect + filippo.io/bigmod v0.1.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -63,11 +64,11 @@ require ( github.com/go-jose/go-jose/v3 v3.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect - github.com/google/go-tpm v0.9.7 // indirect + github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect @@ -106,11 +107,11 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - google.golang.org/api v0.256.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + google.golang.org/api v0.265.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -144,21 +145,21 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/miekg/dns v1.1.69 // indirect + github.com/miekg/dns v1.1.70 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pires/go-proxyproto v0.8.1 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // 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 github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/slackhq/nebula v1.9.7 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/slackhq/nebula v1.10.3 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/urfave/cli v1.22.17 // indirect @@ -168,11 +169,11 @@ require ( go.opentelemetry.io/otel/trace v1.39.0 go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/sys v0.39.0 - golang.org/x/text v0.32.0 - golang.org/x/tools v0.39.0 // indirect - google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/sys v0.41.0 + golang.org/x/text v0.34.0 + golang.org/x/tools v0.42.0 // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index de5352c9c..0cfdf9fcb 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,23 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= -cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= +cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= -cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= -cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k= -cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= -cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= -cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ= +cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8= +filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= @@ -49,36 +51,36 @@ github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= -github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= -github.com/aws/aws-sdk-go-v2/config v1.32.1 h1:iODUDLgk3q8/flEC7ymhmxjfoAnBDwEEYEVyKZ9mzjU= -github.com/aws/aws-sdk-go-v2/config v1.32.1/go.mod h1:xoAgo17AGrPpJBSLg81W+ikM0cpOZG8ad04T2r+d5P0= -github.com/aws/aws-sdk-go-v2/credentials v1.19.1 h1:JeW+EwmtTE0yXFK8SmklrFh/cGTTXsQJumgMZNlbxfM= -github.com/aws/aws-sdk-go-v2/credentials v1.19.1/go.mod h1:BOoXiStwTF+fT2XufhO0Efssbi1CNIO/ZXpZu87N0pw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= +github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E= -github.com/aws/aws-sdk-go-v2/service/kms v1.48.0 h1:pQgVxqqNOacqb19+xaoih/wNLil4d8tgi+FxtBi/qQY= -github.com/aws/aws-sdk-go-v2/service/kms v1.48.0/go.mod h1:VJcNH6BLr+3VJwinRKdotLOMglHO8mIKlD3ea5c7hbw= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9 h1:LU8S9W/mPDAU9q0FjCLi0TrCheLMGwzbRpvUMwYspcA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso= -github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= -github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caddyserver/certmagic v0.25.1 h1:4sIKKbOt5pg6+sL7tEwymE1x2bj6CHr80da1CRRIPbY= @@ -143,8 +145,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= -github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= +github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= @@ -173,8 +175,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA= -github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws= github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= @@ -183,10 +185,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= +github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -234,8 +236,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 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/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= -github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= +github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA= +github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -264,8 +266,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 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.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= @@ -287,14 +289,14 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE= -github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU= +github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/certificates v0.29.0 h1:f90szTKYTW62bmCc+qE5doGqIGPVxTQb8Ba37e/K8Zs= -github.com/smallstep/certificates v0.29.0/go.mod h1:27WI0od6gu84mvE4mYQ/QZGyYwHXvhsiSRNC+y3t+mo= +github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c h1:XQpX0IPYUAoJ661YlgfOJmY48ZOhIbglw4E2gw9mcyc= +github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c/go.mod h1:75NRLmYJq6ZcCb8ApJc+W1eL4oMYwjeufMJDHpv4rx4= github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k= github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y= github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= @@ -426,8 +428,8 @@ go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6 go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.step.sm/crypto v0.75.0 h1:UAHYD6q6ggYyzLlIKHv1MCUVjZIesXRZpGTlRC/HSHw= -go.step.sm/crypto v0.75.0/go.mod h1:wwQ57+ajmDype9mrI/2hRyrvJd7yja5xVgWYqpUN3PE= +go.step.sm/crypto v0.76.0 h1:K23BSaeoiY7Y5dvvijTeYC9EduDBetNwQYMBwMhi1aA= +go.step.sm/crypto v0.76.0/go.mod h1:PXYJdKkK8s+GHLwLguFaLxHNAFsFL3tL1vSBrYfey5k= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -451,19 +453,19 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto/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-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= -golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -472,10 +474,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -493,7 +495,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -502,8 +503,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/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= @@ -513,8 +514,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -524,8 +525,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -534,25 +535,25 @@ 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= -google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU= +google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= 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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= From 6718bd470f23367e49ae9a3642f2e1ef8feb12d7 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 12 Feb 2026 11:35:28 -0700 Subject: [PATCH 061/206] caddytls: Finish removing prefer_wildcard Finish what should have been done a year ago in #6959) --- caddyconfig/httpcaddyfile/options.go | 3 +- caddyconfig/httpcaddyfile/tlsapp.go | 51 ---------------------------- 2 files changed, 1 insertion(+), 53 deletions(-) diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index 58a75e9a2..82d368db1 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -457,9 +457,8 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) { case "disable_redirects": case "disable_certs": case "ignore_loaded_certs": - case "prefer_wildcard": default: - return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'") + return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'") } } return val, nil diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 30948f84f..8b34cbc97 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -92,26 +92,8 @@ func (st ServerType) buildTLSApp( tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP) } - var wildcardHosts []string // collect all hosts that have a wildcard in them, and aren't HTTP forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard - for _, p := range pairings { - var addresses []string - for _, addressWithProtocols := range p.addressesWithProtocols { - addresses = append(addresses, addressWithProtocols.address) - } - if !listenersUseAnyPortOtherThan(addresses, httpPort) { - continue - } - for _, sblock := range p.serverBlocks { - for _, addr := range sblock.parsedKeys { - if strings.HasPrefix(addr.Host, "*.") { - wildcardHosts = append(wildcardHosts, addr.Host[2:]) - } - } - } - } - for _, p := range pairings { // avoid setting up TLS automation policies for a server that is HTTP-only var addresses []string @@ -135,12 +117,6 @@ func (st ServerType) buildTLSApp( return nil, warnings, err } - // make a plain copy so we can compare whether we made any changes - apCopy, err := newBaseAutomationPolicy(options, warnings, true) - if err != nil { - return nil, warnings, err - } - sblockHosts := sblock.hostsFromKeys(false) if len(sblockHosts) == 0 && catchAllAP != nil { ap = catchAllAP @@ -253,16 +229,6 @@ func (st ServerType) buildTLSApp( hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort) sort.Strings(hostsNotHTTP) // solely for deterministic test results - // if the we prefer wildcards and the AP is unchanged, - // then we can skip this AP because it should be covered - // by an AP with a wildcard - if slices.Contains(autoHTTPS, "prefer_wildcard") { - if hostsCoveredByWildcard(hostsNotHTTP, wildcardHosts) && - reflect.DeepEqual(ap, apCopy) { - continue - } - } - // associate our new automation policy with this server block's hosts ap.SubjectsRaw = hostsNotHTTP @@ -849,20 +815,3 @@ func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool { func isTailscaleDomain(name string) bool { return strings.HasSuffix(strings.ToLower(name), ".ts.net") } - -func hostsCoveredByWildcard(hosts []string, wildcards []string) bool { - if len(hosts) == 0 || len(wildcards) == 0 { - return false - } - for _, host := range hosts { - for _, wildcard := range wildcards { - if strings.HasPrefix(host, "*.") { - continue - } - if certmagic.MatchWildcard(host, "*."+wildcard) { - return true - } - } - } - return false -} From 929d0e502ad895737d7b0bafab18c63fb1d44cc1 Mon Sep 17 00:00:00 2001 From: mehrdadbn9 <80095851+mehrdadbn9@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:17:02 +0330 Subject: [PATCH 062/206] caddyfile: Add `renewal_window_ratio` global option and `tls` subdirective (#7473) * caddyfile: Add renewal_window_ratio global option Adds support for configuring the TLS certificate renewal window ratio directly in the Caddyfile global options block. This allows users to customize when certificates should be renewed without needing to use JSON configuration. Example usage: { renewal_window_ratio 0.1666 } Fixes #7467 * caddyfile: Add renewal_window_ratio to tls directive and tests Adds support for renewal_window_ratio in the tls directive (not just global options) and adds caddyfile adapt tests for both the global option and tls directive. * fix: inherit global renewal_window_ratio in site policies * fix: correct test expected output for policy consolidation * fix: properly inherit global renewal_window_ratio without removing other code --- caddyconfig/httpcaddyfile/builtins.go | 24 +++++++ caddyconfig/httpcaddyfile/options.go | 20 ++++++ caddyconfig/httpcaddyfile/tlsapp.go | 13 +++- .../renewal_window_ratio_global.caddyfiletest | 41 ++++++++++++ ...l_window_ratio_tls_directive.caddyfiletest | 63 +++++++++++++++++++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 caddytest/integration/caddyfile_adapt/renewal_window_ratio_global.caddyfiletest create mode 100644 caddytest/integration/caddyfile_adapt/renewal_window_ratio_tls_directive.caddyfiletest diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index cf8ad044f..a7bb3b1de 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -113,6 +113,7 @@ func parseBind(h Helper) ([]ConfigValue, error) { // issuer [...] // get_certificate [...] // insecure_secrets_log +// renewal_window_ratio // } func parseTLS(h Helper) ([]ConfigValue, error) { h.Next() // consume directive name @@ -129,6 +130,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { var onDemand bool var reusePrivateKeys bool var forceAutomate bool + var renewalWindowRatio float64 // Track which DNS challenge options are set var dnsOptionsSet []string @@ -473,6 +475,20 @@ func parseTLS(h Helper) ([]ConfigValue, error) { } cp.InsecureSecretsLog = h.Val() + case "renewal_window_ratio": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + ratio, err := strconv.ParseFloat(arg[0], 64) + if err != nil { + return nil, h.Errf("parsing renewal_window_ratio: %v", err) + } + if ratio <= 0 || ratio >= 1 { + return nil, h.Errf("renewal_window_ratio must be between 0 and 1 (exclusive)") + } + renewalWindowRatio = ratio + default: return nil, h.Errf("unknown subdirective: %s", h.Val()) } @@ -597,6 +613,14 @@ func parseTLS(h Helper) ([]ConfigValue, error) { }) } + // renewal window ratio + if renewalWindowRatio > 0 { + configVals = append(configVals, ConfigValue{ + Class: "tls.renewal_window_ratio", + Value: renewalWindowRatio, + }) + } + // if enabled, the names in the site addresses will be // added to the automation policies if forceAutomate { diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index 82d368db1..f985cff9e 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -65,6 +65,7 @@ func init() { RegisterGlobalOption("persist_config", parseOptPersistConfig) RegisterGlobalOption("dns", parseOptDNS) RegisterGlobalOption("ech", parseOptECH) + RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio) } func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil } @@ -624,3 +625,22 @@ func parseOptECH(d *caddyfile.Dispenser, _ any) (any, error) { return ech, nil } + +func parseOptRenewalWindowRatio(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + if !d.Next() { + return 0, d.ArgErr() + } + val := d.Val() + ratio, err := strconv.ParseFloat(val, 64) + if err != nil { + return 0, d.Errf("parsing renewal_window_ratio: %v", err) + } + if ratio <= 0 || ratio >= 1 { + return 0, d.Errf("renewal_window_ratio must be between 0 and 1 (exclusive)") + } + if d.Next() { + return 0, d.ArgErr() + } + return ratio, nil +} diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 8b34cbc97..e1e37a84b 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -143,6 +143,12 @@ func (st ServerType) buildTLSApp( ap.KeyType = keyTypeVals[0].Value.(string) } + if renewalWindowRatioVals, ok := sblock.pile["tls.renewal_window_ratio"]; ok { + ap.RenewalWindowRatio = renewalWindowRatioVals[0].Value.(float64) + } else if globalRenewalWindowRatio, ok := options["renewal_window_ratio"]; ok { + ap.RenewalWindowRatio = globalRenewalWindowRatio.(float64) + } + // certificate issuers if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok { var issuers []certmagic.Issuer @@ -607,7 +613,8 @@ func newBaseAutomationPolicy( _, hasLocalCerts := options["local_certs"] keyType, hasKeyType := options["key_type"] ocspStapling, hasOCSPStapling := options["ocsp_stapling"] - hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling + renewalWindowRatio, hasRenewalWindowRatio := options["renewal_window_ratio"] + hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling || hasRenewalWindowRatio globalACMECA := options["acme_ca"] globalACMECARoot := options["acme_ca_root"] @@ -654,6 +661,10 @@ func newBaseAutomationPolicy( ap.OCSPOverrides = ocspConfig.ResponderOverrides } + if hasRenewalWindowRatio { + ap.RenewalWindowRatio = renewalWindowRatio.(float64) + } + return ap, nil } diff --git a/caddytest/integration/caddyfile_adapt/renewal_window_ratio_global.caddyfiletest b/caddytest/integration/caddyfile_adapt/renewal_window_ratio_global.caddyfiletest new file mode 100644 index 000000000..f6af0ce72 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/renewal_window_ratio_global.caddyfiletest @@ -0,0 +1,41 @@ +{ + renewal_window_ratio 0.1666 +} + +example.com { +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "renewal_window_ratio": 0.1666 + } + ] + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/renewal_window_ratio_tls_directive.caddyfiletest b/caddytest/integration/caddyfile_adapt/renewal_window_ratio_tls_directive.caddyfiletest new file mode 100644 index 000000000..82c43f2a5 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/renewal_window_ratio_tls_directive.caddyfiletest @@ -0,0 +1,63 @@ +{ + renewal_window_ratio 0.1666 +} + +a.example.com { + tls { + renewal_window_ratio 0.25 + } +} + +b.example.com { +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "a.example.com" + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "b.example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "a.example.com" + ], + "renewal_window_ratio": 0.25 + }, + { + "renewal_window_ratio": 0.1666 + } + ] + } + } + } +} From d6a6b486db238b7fd2be850331687267aa6bc83a Mon Sep 17 00:00:00 2001 From: Aditya Bhargava Date: Sun, 15 Feb 2026 04:04:59 -0500 Subject: [PATCH 063/206] httpcaddyfile: Override global `dns` with `acme_dns` (fix #7294) (#7458) This brings the behaviour in line with what the documentation implies. --- caddyconfig/httpcaddyfile/tlsapp.go | 5 +- ...acme_dns_override_global_dns.caddyfiletest | 83 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/tls_acme_dns_override_global_dns.caddyfiletest diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index e1e37a84b..d14bd17fb 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -548,9 +548,8 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e 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 + if globalACMEDNS != nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil { + // Set a global DNS provider if `acme_dns` is set acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil) } } diff --git a/caddytest/integration/caddyfile_adapt/tls_acme_dns_override_global_dns.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_acme_dns_override_global_dns.caddyfiletest new file mode 100644 index 000000000..1267b6c78 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_acme_dns_override_global_dns.caddyfiletest @@ -0,0 +1,83 @@ +{ + dns mock foo + acme_dns mock bar +} + +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": { + "argument": "bar", + "name": "mock" + }, + "resolvers": [ + "8.8.8.8", + "8.8.4.4" + ] + } + }, + "module": "acme" + } + ] + }, + { + "issuers": [ + { + "challenges": { + "dns": { + "provider": { + "argument": "bar", + "name": "mock" + } + } + }, + "module": "acme" + } + ] + } + ] + }, + "dns": { + "argument": "foo", + "name": "mock" + } + } + } +} From affbb99275bc34aa1cfce0824d23aa8dcf7b1a79 Mon Sep 17 00:00:00 2001 From: Amirhf Date: Sun, 15 Feb 2026 17:40:12 +0330 Subject: [PATCH 064/206] pki: add per-CA configurable `maintenance_interval` and `renewal_window_ratio` (#7479) * pki: add per-CA configurable maintenance_interval and renewal_window_ratio - Add MaintenanceInterval and RenewalWindowRatio to CA struct (JSON + Caddyfile). - Run one maintenance goroutine per CA using its own interval. - needsRenewal uses per-CA RenewalWindowRatio; invalid/zero ratio falls back to defaults. - Caddyfile: maintenance_interval duration, renewal_window_ratio <0-1>. - Tests: TestCA_needsRenewal, TestParsePKIApp for new options. Fixes #7475 * fix codestyle --- caddyconfig/httpcaddyfile/pkiapp.go | 33 +++++++-- caddyconfig/httpcaddyfile/pkiapp_test.go | 86 ++++++++++++++++++++++++ modules/caddypki/ca.go | 17 +++++ modules/caddypki/maintain.go | 28 +++++--- modules/caddypki/maintain_test.go | 86 ++++++++++++++++++++++++ modules/caddypki/pki.go | 6 +- 6 files changed, 239 insertions(+), 17 deletions(-) create mode 100644 caddyconfig/httpcaddyfile/pkiapp_test.go create mode 100644 modules/caddypki/maintain_test.go diff --git a/caddyconfig/httpcaddyfile/pkiapp.go b/caddyconfig/httpcaddyfile/pkiapp.go index 25b6c221c..3f856ff36 100644 --- a/caddyconfig/httpcaddyfile/pkiapp.go +++ b/caddyconfig/httpcaddyfile/pkiapp.go @@ -16,6 +16,7 @@ package httpcaddyfile import ( "slices" + "strconv" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -27,14 +28,16 @@ func init() { RegisterGlobalOption("pki", parsePKIApp) } -// parsePKIApp parses the global log option. Syntax: +// parsePKIApp parses the global pki option. Syntax: // // pki { // ca [] { -// name -// root_cn -// intermediate_cn -// intermediate_lifetime +// name +// root_cn +// intermediate_cn +// intermediate_lifetime +// maintenance_interval +// renewal_window_ratio // root { // cert // key @@ -99,6 +102,26 @@ func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) { } pkiCa.IntermediateLifetime = caddy.Duration(dur) + case "maintenance_interval": + if !d.NextArg() { + return nil, d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return nil, err + } + pkiCa.MaintenanceInterval = caddy.Duration(dur) + + case "renewal_window_ratio": + if !d.NextArg() { + return nil, d.ArgErr() + } + ratio, err := strconv.ParseFloat(d.Val(), 64) + if err != nil || ratio <= 0 || ratio > 1 { + return nil, d.Errf("renewal_window_ratio must be a number in (0, 1], got %s", d.Val()) + } + pkiCa.RenewalWindowRatio = ratio + case "root": if pkiCa.Root == nil { pkiCa.Root = new(caddypki.KeyPair) diff --git a/caddyconfig/httpcaddyfile/pkiapp_test.go b/caddyconfig/httpcaddyfile/pkiapp_test.go new file mode 100644 index 000000000..57662f71e --- /dev/null +++ b/caddyconfig/httpcaddyfile/pkiapp_test.go @@ -0,0 +1,86 @@ +// 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 httpcaddyfile + +import ( + "encoding/json" + "testing" + "time" + + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +func TestParsePKIApp_maintenanceIntervalAndRenewalWindowRatio(t *testing.T) { + input := `{ + pki { + ca local { + maintenance_interval 5m + renewal_window_ratio 0.15 + } + } + } + :8080 { + } + ` + adapter := caddyfile.Adapter{ServerType: ServerType{}} + out, _, err := adapter.Adapt([]byte(input), nil) + if err != nil { + t.Fatalf("Adapt failed: %v", err) + } + + var cfg struct { + Apps struct { + PKI struct { + CertificateAuthorities map[string]struct { + MaintenanceInterval int64 `json:"maintenance_interval,omitempty"` + RenewalWindowRatio float64 `json:"renewal_window_ratio,omitempty"` + } `json:"certificate_authorities,omitempty"` + } `json:"pki,omitempty"` + } `json:"apps"` + } + if err := json.Unmarshal(out, &cfg); err != nil { + t.Fatalf("unmarshal config: %v", err) + } + + ca, ok := cfg.Apps.PKI.CertificateAuthorities["local"] + if !ok { + t.Fatal("expected certificate_authorities.local to exist") + } + wantInterval := 5 * time.Minute.Nanoseconds() + if ca.MaintenanceInterval != wantInterval { + t.Errorf("maintenance_interval = %d, want %d (5m)", ca.MaintenanceInterval, wantInterval) + } + if ca.RenewalWindowRatio != 0.15 { + t.Errorf("renewal_window_ratio = %v, want 0.15", ca.RenewalWindowRatio) + } +} + +func TestParsePKIApp_renewalWindowRatioInvalid(t *testing.T) { + input := `{ + pki { + ca local { + renewal_window_ratio 1.5 + } + } + } + :8080 { + } + ` + adapter := caddyfile.Adapter{ServerType: ServerType{}} + _, _, err := adapter.Adapt([]byte(input), nil) + if err == nil { + t.Error("expected error for renewal_window_ratio > 1") + } +} diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go index 8f6fd3afe..4b98244aa 100644 --- a/modules/caddypki/ca.go +++ b/modules/caddypki/ca.go @@ -63,6 +63,15 @@ type CA struct { // The intermediate (signing) certificate; if null, one will be generated. Intermediate *KeyPair `json:"intermediate,omitempty"` + // How often to check if intermediate (and root, when applicable) certificates need renewal. + // Default: 10m. + MaintenanceInterval caddy.Duration `json:"maintenance_interval,omitempty"` + + // The fraction of certificate lifetime (0.0–1.0) after which renewal is attempted. + // For example, 0.2 means renew when 20% of the lifetime remains (e.g. ~73 days for a 1-year cert). + // Default: 0.2. + RenewalWindowRatio float64 `json:"renewal_window_ratio,omitempty"` + // Optionally configure a separate storage module associated with this // issuer, instead of using Caddy's global/default-configured storage. // This can be useful if you want to keep your signing keys in a @@ -126,6 +135,12 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { if ca.IntermediateLifetime == 0 { ca.IntermediateLifetime = caddy.Duration(defaultIntermediateLifetime) } + if ca.MaintenanceInterval == 0 { + ca.MaintenanceInterval = caddy.Duration(defaultMaintenanceInterval) + } + if ca.RenewalWindowRatio <= 0 || ca.RenewalWindowRatio > 1 { + ca.RenewalWindowRatio = defaultRenewalWindowRatio + } // load the certs and key that will be used for signing var rootCert *x509.Certificate @@ -456,4 +471,6 @@ const ( defaultRootLifetime = 24 * time.Hour * 30 * 12 * 10 defaultIntermediateLifetime = 24 * time.Hour * 7 + defaultMaintenanceInterval = 10 * time.Minute + defaultRenewalWindowRatio = 0.2 ) diff --git a/modules/caddypki/maintain.go b/modules/caddypki/maintain.go index 091e71243..31e4c6a8b 100644 --- a/modules/caddypki/maintain.go +++ b/modules/caddypki/maintain.go @@ -24,20 +24,24 @@ import ( "go.uber.org/zap" ) -func (p *PKI) maintenance() { +func (p *PKI) maintenanceForCA(ca *CA) { defer func() { if err := recover(); err != nil { - log.Printf("[PANIC] PKI maintenance: %v\n%s", err, debug.Stack()) + log.Printf("[PANIC] PKI maintenance for CA %s: %v\n%s", ca.ID, err, debug.Stack()) } }() - ticker := time.NewTicker(10 * time.Minute) // TODO: make configurable + interval := time.Duration(ca.MaintenanceInterval) + if interval <= 0 { + interval = defaultMaintenanceInterval + } + ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: - p.renewCerts() + _ = p.renewCertsForCA(ca) case <-p.ctx.Done(): return } @@ -63,7 +67,7 @@ func (p *PKI) renewCertsForCA(ca *CA) error { // only maintain the root if it's not manually provided in the config if ca.Root == nil { - if needsRenewal(ca.root) { + if ca.needsRenewal(ca.root) { // TODO: implement root renewal (use same key) log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)", zap.Duration("time_remaining", time.Until(ca.interChain[0].NotAfter)), @@ -73,7 +77,7 @@ func (p *PKI) renewCertsForCA(ca *CA) error { // only maintain the intermediate if it's not manually provided in the config if ca.Intermediate == nil { - if needsRenewal(ca.interChain[0]) { + if ca.needsRenewal(ca.interChain[0]) { log.Info("intermediate expires soon; renewing", zap.Duration("time_remaining", time.Until(ca.interChain[0].NotAfter)), ) @@ -97,11 +101,15 @@ func (p *PKI) renewCertsForCA(ca *CA) error { return nil } -func needsRenewal(cert *x509.Certificate) bool { +// needsRenewal reports whether the certificate is within its renewal window +// (i.e. the fraction of lifetime remaining is less than or equal to RenewalWindowRatio). +func (ca *CA) needsRenewal(cert *x509.Certificate) bool { + ratio := ca.RenewalWindowRatio + if ratio <= 0 { + ratio = defaultRenewalWindowRatio + } lifetime := cert.NotAfter.Sub(cert.NotBefore) - renewalWindow := time.Duration(float64(lifetime) * renewalWindowRatio) + renewalWindow := time.Duration(float64(lifetime) * ratio) renewalWindowStart := cert.NotAfter.Add(-renewalWindow) return time.Now().After(renewalWindowStart) } - -const renewalWindowRatio = 0.2 // TODO: make configurable diff --git a/modules/caddypki/maintain_test.go b/modules/caddypki/maintain_test.go new file mode 100644 index 000000000..d20d1d8a5 --- /dev/null +++ b/modules/caddypki/maintain_test.go @@ -0,0 +1,86 @@ +// 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 caddypki + +import ( + "crypto/x509" + "testing" + "time" +) + +func TestCA_needsRenewal(t *testing.T) { + now := time.Now() + + // cert with 100 days lifetime; last 20% = 20 days before expiry + // So renewal window starts at (NotAfter - 20 days) + makeCert := func(daysUntilExpiry int, lifetimeDays int) *x509.Certificate { + notAfter := now.AddDate(0, 0, daysUntilExpiry) + notBefore := notAfter.AddDate(0, 0, -lifetimeDays) + return &x509.Certificate{NotBefore: notBefore, NotAfter: notAfter} + } + + tests := []struct { + name string + ca *CA + cert *x509.Certificate + expect bool + }{ + { + name: "inside renewal window with ratio 0.2", + ca: &CA{RenewalWindowRatio: 0.2}, + cert: makeCert(10, 100), + expect: true, + }, + { + name: "outside renewal window with ratio 0.2", + ca: &CA{RenewalWindowRatio: 0.2}, + cert: makeCert(50, 100), + expect: false, + }, + { + name: "outside renewal window with 21 days left", + ca: &CA{RenewalWindowRatio: 0.2}, + cert: makeCert(21, 100), + expect: false, + }, + { + name: "just inside renewal window with ratio 0.5", + ca: &CA{RenewalWindowRatio: 0.5}, + cert: makeCert(30, 100), + expect: true, + }, + { + name: "zero ratio uses default", + ca: &CA{RenewalWindowRatio: 0}, + cert: makeCert(10, 100), + expect: true, + }, + { + name: "invalid ratio uses default", + ca: &CA{RenewalWindowRatio: 1.5}, + cert: makeCert(10, 100), + expect: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.ca.needsRenewal(tt.cert) + if got != tt.expect { + t.Errorf("needsRenewal() = %v, want %v", got, tt.expect) + } + }) + } +} diff --git a/modules/caddypki/pki.go b/modules/caddypki/pki.go index 9f974a956..557df74fc 100644 --- a/modules/caddypki/pki.go +++ b/modules/caddypki/pki.go @@ -109,8 +109,10 @@ func (p *PKI) Start() error { // see if root/intermediates need renewal... p.renewCerts() - // ...and keep them renewed - go p.maintenance() + // ...and keep them renewed (one goroutine per CA with its own interval) + for _, ca := range p.CAs { + go p.maintenanceForCA(ca) + } return nil } From f2213e943e5e829c7f112c8f3e269fb11b7b80a3 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Mon, 16 Feb 2026 12:08:29 -0500 Subject: [PATCH 065/206] chore: Bump zerossl dependency to 0.1.5 (#7489) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e88589fac..b4f101252 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/alecthomas/chroma/v2 v2.21.1 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/caddyserver/certmagic v0.25.1 - github.com/caddyserver/zerossl v0.1.4 + github.com/caddyserver/zerossl v0.1.5 github.com/cloudflare/circl v1.6.2 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.2.4 diff --git a/go.sum b/go.sum index 0cfdf9fcb..1fac2e8bb 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caddyserver/certmagic v0.25.1 h1:4sIKKbOt5pg6+sL7tEwymE1x2bj6CHr80da1CRRIPbY= github.com/caddyserver/certmagic v0.25.1/go.mod h1:VhyvndxtVton/Fo/wKhRoC46Rbw1fmjvQ3GjHYSQTEY= -github.com/caddyserver/zerossl v0.1.4 h1:CVJOE3MZeFisCERZjkxIcsqIH4fnFdlYWnPYeFtBHRw= -github.com/caddyserver/zerossl v0.1.4/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= +github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0= github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= From ff4f79aebeb12bffeb6e2fcca7d6f2cd40c84c1b Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Mon, 16 Feb 2026 12:17:01 -0500 Subject: [PATCH 066/206] chore: Remove obsolete comment in `ech.go` (#7487) --- modules/caddytls/ech.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index a53344202..d06047cb1 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -50,12 +50,6 @@ func init() { // applied will automatically upgrade the minimum TLS version to 1.3, even if // configured to a lower version. // -// Note that, as of Caddy 2.10.0 (~March 2025), ECH keys are not automatically -// rotated due to a limitation in the Go standard library (see -// https://github.com/golang/go/issues/71920). This should be resolved when -// Go 1.25 is released (~Aug. 2025), and Caddy will be updated to automatically -// rotate ECH keys/configs at that point. -// // EXPERIMENTAL: Subject to change. type ECH struct { // The list of ECH configurations for which to automatically generate From d64c7e67a4c6453bcdb3519f1a64e198a286ad9c Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Mon, 16 Feb 2026 12:20:47 -0500 Subject: [PATCH 067/206] caddyhttp: Option to disable 0-RTT (#7485) --- caddyconfig/httpcaddyfile/serveroptions.go | 15 +++++++++++++++ .../global_server_options_single.caddyfiletest | 4 +++- listeners.go | 8 ++++++-- modules/caddyhttp/server.go | 12 +++++++++++- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go index 06ceea3c3..1febf4097 100644 --- a/caddyconfig/httpcaddyfile/serveroptions.go +++ b/caddyconfig/httpcaddyfile/serveroptions.go @@ -57,6 +57,9 @@ type serverOptions struct { ShouldLogCredentials bool Metrics *caddyhttp.Metrics Trace bool // TODO: EXPERIMENTAL + // If set, overrides whether QUIC listeners allow 0-RTT (early data). + // If nil, the default behavior is used (currently allowed). + Allow0RTT *bool } func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { @@ -309,6 +312,17 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { } serverOpts.Trace = true + case "0rtt": + // only supports "off" for now + if !d.NextArg() { + return nil, d.ArgErr() + } + if d.Val() != "off" { + return nil, d.Errf("unsupported 0rtt argument '%s' (only 'off' is supported)", d.Val()) + } + boolVal := false + serverOpts.Allow0RTT = &boolVal + default: return nil, d.Errf("unrecognized servers option '%s'", d.Val()) } @@ -373,6 +387,7 @@ func applyServerOptions( server.TrustedProxiesStrict = opts.TrustedProxiesStrict server.TrustedProxiesUnix = opts.TrustedProxiesUnix server.Metrics = opts.Metrics + server.Allow0RTT = opts.Allow0RTT if opts.ShouldLogCredentials { if server.Logs == nil { server.Logs = new(caddyhttp.ServerLogConfig) diff --git a/caddytest/integration/caddyfile_adapt/global_server_options_single.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_server_options_single.caddyfiletest index 6b2ffaec4..4991b308e 100644 --- a/caddytest/integration/caddyfile_adapt/global_server_options_single.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/global_server_options_single.caddyfiletest @@ -21,6 +21,7 @@ keepalive_interval 20s keepalive_idle 20s keepalive_count 10 + 0rtt off } } @@ -90,7 +91,8 @@ foo.com { "h2", "h2c", "h3" - ] + ], + "allow_0rtt": false } } } diff --git a/listeners.go b/listeners.go index b64011939..e214caf5c 100644 --- a/listeners.go +++ b/listeners.go @@ -511,7 +511,7 @@ func JoinNetworkAddress(network, host, port string) string { // // NOTE: This API is EXPERIMENTAL and may be changed or removed. // NOTE: user should close the returned listener twice, once to stop accepting new connections, the second time to free up the packet conn. -func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config, pcWrappers []PacketConnWrapper) (http3.QUICListener, error) { +func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config, pcWrappers []PacketConnWrapper, allow0rttconf *bool) (http3.QUICListener, error) { lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset)) sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { @@ -550,10 +550,14 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config Conn: h3ln, VerifySourceAddress: func(addr net.Addr) bool { return !limiter.Allow() }, } + allow0rtt := true + if allow0rttconf != nil { + allow0rtt = *allow0rttconf + } earlyLn, err := tr.ListenEarly( http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{ - Allow0RTT: true, + Allow0RTT: allow0rtt, Tracer: h3qlog.DefaultConnectionTracer, }, ) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index de318a953..70635d959 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -253,6 +253,16 @@ type Server struct { // A nil value or element indicates that Protocols will be used instead. ListenProtocols [][]string `json:"listen_protocols,omitempty"` + // If set, overrides whether QUIC listeners allow 0-RTT (early data). + // If nil, the default behavior is used (currently allowed). + // + // One reason to disable 0-RTT is if a remote IP matcher is used, + // which introduces a dependency on the remote address being verified + // if routing happens before the TLS handshake completes. An HTTP 425 + // response is written in that case, but some clients misbehave and + // don't perform a retry, so disabling 0-RTT can smooth it out. + Allow0RTT *bool `json:"allow_0rtt,omitempty"` + // If set, metrics observations will be enabled. // This setting is EXPERIMENTAL and subject to change. // DEPRECATED: Use the app-level `metrics` field. @@ -650,7 +660,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err) } addr.Network = h3net - h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg, s.packetConnWrappers) + h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg, s.packetConnWrappers, s.Allow0RTT) if err != nil { return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err) } From 23d07ac89dc3a28ff3114e1d8018d7beab479568 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Mon, 16 Feb 2026 21:25:49 +0300 Subject: [PATCH 068/206] dep: upgrade cel-go (#7478) * dep: upgrade cel-go Signed-off-by: Mohammed Al Sahaf * Try handling `map[any]any`, fix error messages --------- Signed-off-by: Mohammed Al Sahaf Co-authored-by: Francis Lavoie --- go.mod | 6 +++--- go.sum | 6 ++++++ modules/caddyhttp/celmatcher.go | 38 +++++++++++++++++++++++++++++---- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index b4f101252..b9ccb25ae 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/cloudflare/circl v1.6.2 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.2.4 - github.com/google/cel-go v0.26.1 + github.com/google/cel-go v0.27.0 github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.18.2 github.com/klauspost/cpuid/v2 v2.3.0 @@ -49,13 +49,13 @@ require ( ) require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect cloud.google.com/go/auth v0.18.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/bigmod v0.1.0 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect diff --git a/go.sum b/go.sum index 1fac2e8bb..fd3f68e54 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= @@ -48,6 +50,8 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= @@ -168,6 +172,8 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= +github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go index 66a60b817..b67cb9e92 100644 --- a/modules/caddyhttp/celmatcher.go +++ b/modules/caddyhttp/celmatcher.go @@ -665,12 +665,29 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander { // map literals containing heterogeneous values, in this case string and list // of string. func CELValueToMapStrList(data ref.Val) (map[string][]string, error) { + // Prefer map[string]any, but newer cel-go versions may return map[any]any mapStrType := reflect.TypeFor[map[string]any]() mapStrRaw, err := data.ConvertToNative(mapStrType) + var mapStrIface map[string]any if err != nil { - return nil, err + // Try map[any]any and convert keys to strings + mapAnyType := reflect.TypeFor[map[any]any]() + mapAnyRaw, err2 := data.ConvertToNative(mapAnyType) + if err2 != nil { + return nil, err + } + mapAnyIface := mapAnyRaw.(map[any]any) + mapStrIface = make(map[string]any, len(mapAnyIface)) + for k, v := range mapAnyIface { + ks, ok := k.(string) + if !ok { + return nil, fmt.Errorf("unsupported map key type in header match: %T", k) + } + mapStrIface[ks] = v + } + } else { + mapStrIface = mapStrRaw.(map[string]any) } - mapStrIface := mapStrRaw.(map[string]any) mapStrListStr := make(map[string][]string, len(mapStrIface)) for k, v := range mapStrIface { switch val := v.(type) { @@ -685,13 +702,26 @@ func CELValueToMapStrList(data ref.Val) (map[string][]string, error) { for i, elem := range val { strVal, ok := elem.(types.String) if !ok { - return nil, fmt.Errorf("unsupported value type in header match: %T", val) + return nil, fmt.Errorf("unsupported value type in matcher input: %T", val) } convVals[i] = string(strVal) } mapStrListStr[k] = convVals + case []any: + convVals := make([]string, len(val)) + for i, elem := range val { + switch e := elem.(type) { + case string: + convVals[i] = e + case types.String: + convVals[i] = string(e) + default: + return nil, fmt.Errorf("unsupported element type in matcher input list: %T", elem) + } + } + mapStrListStr[k] = convVals default: - return nil, fmt.Errorf("unsupported value type in header match: %T", val) + return nil, fmt.Errorf("unsupported value type in matcher input: %T", val) } } return mapStrListStr, nil From 8a18acc0251640b7979355997fc677fe8c60ec51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:38:55 -0500 Subject: [PATCH 069/206] build(deps): bump the all-updates group across 1 directory with 12 updates (#7490) Bumps the all-updates group with 9 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.21.1` | `2.23.1` | | [github.com/cloudflare/circl](https://github.com/cloudflare/circl) | `1.6.2` | `1.6.3` | | [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.4` | `5.2.5` | | [github.com/klauspost/compress](https://github.com/klauspost/compress) | `1.18.2` | `1.18.4` | | [github.com/yuin/goldmark](https://github.com/yuin/goldmark) | `1.7.15` | `1.7.16` | | [go.opentelemetry.io/contrib/exporters/autoexport](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.64.0` | `0.65.0` | | [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.64.0` | `0.65.0` | | [go.opentelemetry.io/contrib/propagators/autoprop](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.64.0` | `0.65.0` | | [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) | `0.8.1` | `0.11.0` | Updates `github.com/alecthomas/chroma/v2` from 2.21.1 to 2.23.1 - [Release notes](https://github.com/alecthomas/chroma/releases) - [Commits](https://github.com/alecthomas/chroma/compare/v2.21.1...v2.23.1) Updates `github.com/cloudflare/circl` from 1.6.2 to 1.6.3 - [Release notes](https://github.com/cloudflare/circl/releases) - [Commits](https://github.com/cloudflare/circl/compare/v1.6.2...v1.6.3) Updates `github.com/go-chi/chi/v5` from 5.2.4 to 5.2.5 - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.2.4...v5.2.5) Updates `github.com/klauspost/compress` from 1.18.2 to 1.18.4 - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.2...v1.18.4) Updates `github.com/yuin/goldmark` from 1.7.15 to 1.7.16 - [Release notes](https://github.com/yuin/goldmark/releases) - [Commits](https://github.com/yuin/goldmark/compare/v1.7.15...v1.7.16) Updates `go.opentelemetry.io/contrib/exporters/autoexport` from 0.64.0 to 0.65.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.64.0...zpages/v0.65.0) Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.64.0 to 0.65.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.64.0...zpages/v0.65.0) Updates `go.opentelemetry.io/contrib/propagators/autoprop` from 0.64.0 to 0.65.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.64.0...zpages/v0.65.0) Updates `go.opentelemetry.io/otel` from 1.39.0 to 1.40.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.40.0) Updates `go.opentelemetry.io/otel/sdk` from 1.39.0 to 1.40.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.40.0) Updates `github.com/pires/go-proxyproto` from 0.8.1 to 0.11.0 - [Release notes](https://github.com/pires/go-proxyproto/releases) - [Commits](https://github.com/pires/go-proxyproto/compare/v0.8.1...v0.11.0) Updates `go.opentelemetry.io/otel/trace` from 1.39.0 to 1.40.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.40.0) --- updated-dependencies: - dependency-name: github.com/alecthomas/chroma/v2 dependency-version: 2.23.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: github.com/cloudflare/circl dependency-version: 1.6.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: github.com/go-chi/chi/v5 dependency-version: 5.2.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: github.com/klauspost/compress dependency-version: 1.18.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: github.com/yuin/goldmark dependency-version: 1.7.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/exporters/autoexport dependency-version: 0.65.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp dependency-version: 0.65.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/propagators/autoprop dependency-version: 0.65.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/otel dependency-version: 1.40.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/otel/sdk dependency-version: 1.40.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: github.com/pires/go-proxyproto dependency-version: 0.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/otel/trace dependency-version: 1.40.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 67 +++++++++++++------------- go.sum | 145 +++++++++++++++++++++++++++------------------------------ 2 files changed, 101 insertions(+), 111 deletions(-) diff --git a/go.mod b/go.mod index b9ccb25ae..cb03b4acc 100644 --- a/go.mod +++ b/go.mod @@ -7,16 +7,16 @@ require ( github.com/DeRuina/timberjack v1.3.9 github.com/KimMachineGun/automemlimit v0.7.5 github.com/Masterminds/sprig/v3 v3.3.0 - github.com/alecthomas/chroma/v2 v2.21.1 + github.com/alecthomas/chroma/v2 v2.23.1 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/caddyserver/certmagic v0.25.1 github.com/caddyserver/zerossl v0.1.5 - github.com/cloudflare/circl v1.6.2 + github.com/cloudflare/circl v1.6.3 github.com/dustin/go-humanize v1.0.1 - github.com/go-chi/chi/v5 v5.2.4 + github.com/go-chi/chi/v5 v5.2.5 github.com/google/cel-go v0.27.0 github.com/google/uuid v1.6.0 - github.com/klauspost/compress v1.18.2 + github.com/klauspost/compress v1.18.4 github.com/klauspost/cpuid/v2 v2.3.0 github.com/mholt/acmez/v3 v3.1.4 github.com/prometheus/client_golang v1.23.2 @@ -28,13 +28,13 @@ require ( github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 - github.com/yuin/goldmark v1.7.15 + github.com/yuin/goldmark v1.7.16 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - go.opentelemetry.io/contrib/exporters/autoexport v0.64.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 - go.opentelemetry.io/otel v1.39.0 - go.opentelemetry.io/otel/sdk v1.39.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/sdk v1.40.0 go.step.sm/crypto v0.76.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 @@ -69,7 +69,7 @@ require ( github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -87,24 +87,24 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/bridges/prometheus v0.64.0 // indirect - go.opentelemetry.io/contrib/propagators/aws v1.39.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect - go.opentelemetry.io/contrib/propagators/ot v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.61.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect - go.opentelemetry.io/otel/log v0.15.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.15.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.40.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.40.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect + go.opentelemetry.io/otel/log v0.16.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect @@ -149,7 +149,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pires/go-proxyproto v0.8.1 + github.com/pires/go-proxyproto v0.11.0 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.67.5 // indirect @@ -161,12 +161,11 @@ require ( github.com/sirupsen/logrus v1.9.4 // indirect github.com/slackhq/nebula v1.10.3 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/stoewer/go-strcase v1.2.0 // indirect github.com/urfave/cli v1.22.17 // indirect go.etcd.io/bbolt v1.3.10 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.33.0 // indirect diff --git a/go.sum b/go.sum index fd3f68e54..0c79eeffb 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= @@ -43,13 +41,11 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA= -github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= +github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -108,8 +104,8 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= -github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= @@ -149,8 +145,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= -github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= @@ -170,8 +166,6 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= -github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= @@ -195,8 +189,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dq github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= @@ -215,8 +209,8 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -259,8 +253,8 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhM github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= -github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= -github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= +github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -333,8 +327,6 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -342,7 +334,6 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -362,8 +353,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.15 h1:xYJWgq3Qd8qsaZpj5pHKoEI4mosqVZi/qRpq/MdKyyk= -github.com/yuin/goldmark v1.7.15/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= @@ -376,62 +367,62 @@ go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/bridges/prometheus v0.64.0 h1:7TYhBCu6Xz6vDJGNtEslWZLuuX2IJ/aH50hBY4MVeUg= -go.opentelemetry.io/contrib/bridges/prometheus v0.64.0/go.mod h1:tHQctZfAe7e4PBPGyt3kae6mQFXNpj+iiDJa3ithM50= -go.opentelemetry.io/contrib/exporters/autoexport v0.64.0 h1:9pzPj3RFyKOxBAMkM2w84LpT+rdHam1XoFA+QhARiRw= -go.opentelemetry.io/contrib/exporters/autoexport v0.64.0/go.mod h1:hlVZx1btWH0XTfXpuGX9dsquB50s+tc3fYFOO5elo2M= +go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI= +go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac= +go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g= +go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= -go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 h1:VVrb1ErDD0Tlh/0K0rUqjky1e8AekjspTFN9sU2ekaA= -go.opentelemetry.io/contrib/propagators/autoprop v0.64.0/go.mod h1:QCsOQk+9Ep8Mkp4/aPtSzUT0dc8SaPYzBAE6o1jYuSE= -go.opentelemetry.io/contrib/propagators/aws v1.39.0 h1:IvNR8pAVGpkK1CHMjU/YE6B6TlnAPGFvogkMWRWU6wo= -go.opentelemetry.io/contrib/propagators/aws v1.39.0/go.mod h1:TUsFCERuGM4IGhJG9w+9l0nzmHUKHuaDYYNF6mtNgjY= -go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk= -go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY= -go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 h1:Gz3yKzfMSEFzF0Vy5eIpu9ndpo4DhXMCxsLMF0OOApo= -go.opentelemetry.io/contrib/propagators/jaeger v1.39.0/go.mod h1:2D/cxxCqTlrday0rZrPujjg5aoAdqk1NaNyoXn8FJn8= -go.opentelemetry.io/contrib/propagators/ot v1.39.0 h1:vKTve1W/WKPVp1fzJamhCDDECt+5upJJ65bPyWoddGg= -go.opentelemetry.io/contrib/propagators/ot v1.39.0/go.mod h1:FH5VB2N19duNzh1Q8ks6CsZFyu3LFhNLiA9lPxyEkvU= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= -go.opentelemetry.io/otel/exporters/prometheus v0.61.0 h1:cCyZS4dr67d30uDyh8etKM2QyDsQ4zC9ds3bdbrVoD0= -go.opentelemetry.io/otel/exporters/prometheus v0.61.0/go.mod h1:iivMuj3xpR2DkUrUya3TPS/Z9h3dz7h01GxU+fQBRNg= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 h1:0BSddrtQqLEylcErkeFrJBmwFzcqfQq9+/uxfTZq+HE= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0/go.mod h1:87sjYuAPzaRCtdd09GU5gM1U9wQLrrcYrm77mh5EBoc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g= -go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= -go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= -go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s= +go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE= +go.opentelemetry.io/contrib/propagators/aws v1.40.0 h1:4VIrh75jW4RTimUNx1DSk+6H9/nDr1FvmKoOVDh3K04= +go.opentelemetry.io/contrib/propagators/aws v1.40.0/go.mod h1:B0dCov9KNQGlut3T8wZZjDnLXEXdBroM7bFsHh/gRos= +go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg= +go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs= +go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 h1:aXl9uobjJs5vquMLt9ZkI/3zIuz8XQ3TqOKSWx0/xdU= +go.opentelemetry.io/contrib/propagators/jaeger v1.40.0/go.mod h1:ioMePqe6k6c/ovXSkmkMr1mbN5qRBGJxNTVop7/2XO0= +go.opentelemetry.io/contrib/propagators/ot v1.40.0 h1:Lon8J5SPmWaL1Ko2TIlCNHJ42/J1b5XbJlgJaE/9m7I= +go.opentelemetry.io/contrib/propagators/ot v1.40.0/go.mod h1:dKWtJTlp1Yj+8Cneye5idO46eRPIbi23qVuJYKjNnvY= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo= +go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= +go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= +go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= +go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.step.sm/crypto v0.76.0 h1:K23BSaeoiY7Y5dvvijTeYC9EduDBetNwQYMBwMhi1aA= From 68d50020eef0d4c3398b878f17c8092ca5b58ca0 Mon Sep 17 00:00:00 2001 From: zjumathcode Date: Tue, 17 Feb 2026 03:30:44 +0800 Subject: [PATCH 070/206] refactor: use strings.Builder to improve performance (#7364) * refactor: use strings.Builder to improve performance Signed-off-by: zjumathcode * refactor: small builder improvements per review (WriteByte / split writes) also revert builder change in client_test.go refactor(logging): build IP mask output via join of parts (more efficient) --------- Signed-off-by: zjumathcode Co-authored-by: Francis Lavoie --- modules/caddyhttp/routes.go | 19 ++++++++++++------- modules/logging/filters.go | 10 +++++----- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index 3dd770938..78bf12209 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "github.com/caddyserver/caddy/v2" ) @@ -110,14 +111,16 @@ func (r Route) Empty() bool { } func (r Route) String() string { - handlersRaw := "[" + var handlersRaw strings.Builder + handlersRaw.WriteByte('[') for _, hr := range r.HandlersRaw { - handlersRaw += " " + string(hr) + handlersRaw.WriteByte(' ') + handlersRaw.WriteString(string(hr)) } - handlersRaw += "]" + handlersRaw.WriteByte(']') return fmt.Sprintf(`{Group:"%s" MatcherSetsRaw:%s HandlersRaw:%s Terminal:%t}`, - r.Group, r.MatcherSetsRaw, handlersRaw, r.Terminal) + r.Group, r.MatcherSetsRaw, handlersRaw.String(), r.Terminal) } // Provision sets up both the matchers and handlers in the route. @@ -440,13 +443,15 @@ func (ms *MatcherSets) FromInterface(matcherSets any) error { // TODO: Is this used? func (ms MatcherSets) String() string { - result := "[" + var result strings.Builder + result.WriteByte('[') for _, matcherSet := range ms { for _, matcher := range matcherSet { - result += fmt.Sprintf(" %#v", matcher) + result.WriteString(fmt.Sprintf(" %#v", matcher)) } } - return result + " ]" + result.WriteByte(']') + return result.String() } var routeGroupCtxKey = caddy.CtxKey("route_group") diff --git a/modules/logging/filters.go b/modules/logging/filters.go index a2ce6502f..4574b7ca0 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -255,7 +255,7 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { } func (m IPMaskFilter) mask(s string) string { - output := "" + parts := make([]string, 0) for value := range strings.SplitSeq(s, ",") { value = strings.TrimSpace(value) host, port, err := net.SplitHostPort(value) @@ -264,7 +264,7 @@ func (m IPMaskFilter) mask(s string) string { } ipAddr := net.ParseIP(host) if ipAddr == nil { - output += value + ", " + parts = append(parts, value) continue } mask := m.v4Mask @@ -273,13 +273,13 @@ func (m IPMaskFilter) mask(s string) string { } masked := ipAddr.Mask(mask) if port == "" { - output += masked.String() + ", " + parts = append(parts, masked.String()) continue } - output += net.JoinHostPort(masked.String(), port) + ", " + parts = append(parts, net.JoinHostPort(masked.String(), port)) } - return strings.TrimSuffix(output, ", ") + return strings.Join(parts, ", ") } type filterAction string From b8b00d91606c897ad37dc96a165a25e6ba616b35 Mon Sep 17 00:00:00 2001 From: wangjingcun Date: Tue, 17 Feb 2026 03:41:21 +0800 Subject: [PATCH 071/206] chore: fix some comments to improve readability (#7395) Co-authored-by: Francis Lavoie --- caddy.go | 14 +++++++------- modules/logging/filewriter_test_windows.go | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/caddy.go b/caddy.go index 5f71d8e8b..7309d4471 100644 --- a/caddy.go +++ b/caddy.go @@ -147,8 +147,8 @@ func Load(cfgJSON []byte, forceReload bool) error { // the new value (if applicable; i.e. "DELETE" doesn't have an input). // If the resulting config is the same as the previous, no reload will // occur unless forceReload is true. If the config is unchanged and not -// forcefully reloaded, then errConfigUnchanged This function is safe for -// concurrent use. +// forcefully reloaded, then errConfigUnchanged is returned. This function +// is safe for concurrent use. // The ifMatchHeader can optionally be given a string of the format: // // " " @@ -1092,7 +1092,7 @@ type Event struct { } // NewEvent creates a new event, but does not emit the event. To emit an -// event, call Emit() on the current instance of the caddyevents app insteaad. +// event, call Emit() on the current instance of the caddyevents app instead. // // EXPERIMENTAL: Subject to change. func NewEvent(ctx Context, name string, data map[string]any) (Event, error) { @@ -1250,10 +1250,10 @@ func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) { // 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. +// 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() diff --git a/modules/logging/filewriter_test_windows.go b/modules/logging/filewriter_test_windows.go index d32a8d2c0..a032d1c28 100644 --- a/modules/logging/filewriter_test_windows.go +++ b/modules/logging/filewriter_test_windows.go @@ -23,9 +23,9 @@ import ( ) // Windows relies on ACLs instead of unix permissions model. -// Go allows to open files with a particular mode put it is limited to read or write. +// Go allows to open files with a particular mode but it is limited to read or write. // See https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/syscall/syscall_windows.go;l=708. -// This is pretty restrictive and has few interest for log files and thus we just test that log files are +// This is pretty restrictive and has little interest for log files and thus we just test that log files are // opened with R/W permissions by default on Windows too. func TestFileCreationMode(t *testing.T) { dir, err := os.MkdirTemp("", "caddytest") From 9fe694c79c17d9d7886675131c13ba7d3f783578 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Mon, 16 Feb 2026 17:38:56 -0500 Subject: [PATCH 072/206] caddytls: Enable debug logging for DNSManager (#7491) --- modules/caddytls/acmeissuer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 34bcfc0dc..41c0e20a1 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -178,6 +178,7 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout), Resolvers: iss.Challenges.DNS.Resolvers, OverrideDomain: iss.Challenges.DNS.OverrideDomain, + Logger: iss.logger.Named("dns_manager"), }, } } From bdcdaf77ba6276b5ead20fa2518e00391150523d Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 16 Feb 2026 15:59:10 -0700 Subject: [PATCH 073/206] encode: Implement Flush for legacy compatibility (By sponsor request) --- modules/caddyhttp/encode/encode.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index ac995c37b..e7f98a31a 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -307,6 +307,14 @@ func (rw *responseWriter) FlushError() error { return http.NewResponseController(rw.ResponseWriter).Flush() } +// Flush calls FlushError() and simply discards any error. It is only implemented for backwards +// compatibility with legacy code that does not use FlushError; we know at least one sponsor +// needs this. It should not be relied upon as a stable part of the exported API, as it may be +// removed in the future. +func (rw *responseWriter) Flush() { + _ = rw.FlushError() +} + // Write writes to the response. If the response qualifies, // it is encoded using the encoder, which is initialized // if not done so already. From 091add5ae3dc0ab3265e45b4417fc06fb8a05a39 Mon Sep 17 00:00:00 2001 From: Amirhf Date: Tue, 17 Feb 2026 15:11:38 +0330 Subject: [PATCH 074/206] caddytest: make TestReverseProxyHealthCheck deterministic with poll instead of sleep (#7474) Start lightweight backend servers before starting Caddy so active health checks probe a ready backend instead of the same Caddy instance during provisioning. This removes the startup race without fixed sleeps or polling. --- caddytest/integration/reverseproxy_test.go | 44 +++++++++++++++++----- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/caddytest/integration/reverseproxy_test.go b/caddytest/integration/reverseproxy_test.go index cbfe8433b..0b992d3e3 100644 --- a/caddytest/integration/reverseproxy_test.go +++ b/caddytest/integration/reverseproxy_test.go @@ -8,7 +8,6 @@ import ( "runtime" "strings" "testing" - "time" "github.com/caddyserver/caddy/v2/caddytest" ) @@ -327,6 +326,41 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) { } func TestReverseProxyHealthCheck(t *testing.T) { + // Start lightweight backend servers so they're ready before Caddy's + // active health checker runs; this avoids a startup race where the + // health checker probes backends that haven't yet begun accepting + // connections and marks them unhealthy. + // + // This mirrors how health checks are typically used in practice (to a separate + // backend service) and avoids probing the same Caddy instance while it's still + // provisioning and not ready to accept connections. + + // backend server that responds to proxied requests + helloSrv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte("Hello, World!")) + }), + } + ln0, err := net.Listen("tcp", "127.0.0.1:2020") + if err != nil { + t.Fatalf("failed to listen on 127.0.0.1:2020: %v", err) + } + go helloSrv.Serve(ln0) + t.Cleanup(func() { helloSrv.Close(); ln0.Close() }) + + // backend server that serves health checks + healthSrv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte("ok")) + }), + } + ln1, err := net.Listen("tcp", "127.0.0.1:2021") + if err != nil { + t.Fatalf("failed to listen on 127.0.0.1:2021: %v", err) + } + go healthSrv.Serve(ln1) + t.Cleanup(func() { healthSrv.Close(); ln1.Close() }) + tester := caddytest.NewTester(t) tester.InitServer(` { @@ -336,12 +370,6 @@ func TestReverseProxyHealthCheck(t *testing.T) { https_port 9443 grace_period 1ns } - http://localhost:2020 { - respond "Hello, World!" - } - http://localhost:2021 { - respond "ok" - } http://localhost:9080 { reverse_proxy { to localhost:2020 @@ -355,8 +383,6 @@ func TestReverseProxyHealthCheck(t *testing.T) { } } `, "caddyfile") - - time.Sleep(100 * time.Millisecond) // TODO: for some reason this test seems particularly flaky, getting 503 when it should be 200, unless we wait tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") } From 3adcafd4c1120f071e1c1b1407566780144e7fc9 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Tue, 17 Feb 2026 15:14:06 -0500 Subject: [PATCH 075/206] admin: Fix tests locally, properly isolate storage (#7486) * admin: Fix tests locally, properly isolate storage * Fix flaky pki_test * Drop testdata dir logic * Safer temp dir * Test handlers without a full server --- admin.go | 19 +++++++++++- admin_test.go | 49 +++++++++++++++++++++++++------ caddytest/integration/pki_test.go | 4 +-- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/admin.go b/admin.go index 46f1bbda3..2eb9c3b04 100644 --- a/admin.go +++ b/admin.go @@ -47,6 +47,12 @@ import ( "go.uber.org/zap/zapcore" ) +// testCertMagicStorageOverride is a package-level test hook. Tests may set +// this variable to provide a temporary certmagic.Storage so that cert +// management in tests does not hit the real default storage on disk. +// This must NOT be set in production code. +var testCertMagicStorageOverride certmagic.Storage + func init() { // The hard-coded default `DefaultAdminListen` can be overridden // by setting the `CADDY_ADMIN` environment variable. @@ -633,8 +639,19 @@ func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) // certmagic config, although it'll be mostly useless for remote management ident = new(IdentityConfig) } + // Choose storage: prefer the package-level test override when present, + // otherwise use the configured DefaultStorage. Tests may set an override + // to divert storage into a temporary location. Otherwise, in production + // we use the DefaultStorage since we don't want to act as part of a + // cluster; this storage is for the server's local identity only. + var storage certmagic.Storage + if testCertMagicStorageOverride != nil { + storage = testCertMagicStorageOverride + } else { + storage = DefaultStorage + } template := certmagic.Config{ - Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity) + Storage: storage, Logger: logger, Issuers: ident.issuers, } diff --git a/admin_test.go b/admin_test.go index 92dd43a5c..97dc76f4d 100644 --- a/admin_test.go +++ b/admin_test.go @@ -22,9 +22,11 @@ import ( "maps" "net/http" "net/http/httptest" + "os" "reflect" "sync" "testing" + "time" "github.com/caddyserver/certmagic" "github.com/prometheus/client_golang/prometheus" @@ -275,13 +277,12 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) { }, } - err := replaceLocalAdminServer(cfg, Context{}) + // Build the admin handler directly (no listener active) + addr, err := ParseNetworkAddress("localhost:2019") if err != nil { - t.Fatalf("setting up admin server: %v", err) + t.Fatalf("Failed to parse address: %v", err) } - defer func() { - stopAdminServer(localAdminServer) - }() + handler := cfg.Admin.newAdminHandler(addr, false, Context{}) tests := []struct { name string @@ -314,7 +315,7 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) { req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil) rr := httptest.NewRecorder() - localAdminServer.Handler.ServeHTTP(rr, req) + handler.ServeHTTP(rr, req) if rr.Code != test.expectedStatus { t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code) @@ -799,8 +800,24 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP ... -----END PRIVATE KEY-----`) - testStorage := certmagic.FileStorage{Path: t.TempDir()} - err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM) + tmpDir, err := os.MkdirTemp("", "TestManageIdentity-") + if err != nil { + t.Fatal(err) + } + testStorage := certmagic.FileStorage{Path: tmpDir} + // Clean up the temp dir after the test finishes. Ensure any background + // certificate maintenance is stopped first to avoid RemoveAll races. + t.Cleanup(func() { + if identityCertCache != nil { + identityCertCache.Stop() + identityCertCache = nil + } + // Give goroutines a moment to exit and release file handles. + time.Sleep(50 * time.Millisecond) + _ = os.RemoveAll(tmpDir) + }) + + err = testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM) if err != nil { t.Fatal(err) } @@ -862,7 +879,7 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP }, }, }, - storage: &certmagic.FileStorage{Path: "testdata"}, + storage: &testStorage, }, checkState: func(t *testing.T, cfg *Config) { if len(cfg.Admin.Identity.issuers) != 1 { @@ -900,6 +917,13 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP identityCertCache.Stop() identityCertCache = nil } + // Ensure any cache started by manageIdentity is stopped at the end + defer func() { + if identityCertCache != nil { + identityCertCache.Stop() + identityCertCache = nil + } + }() ctx := Context{ Context: context.Background(), @@ -907,6 +931,13 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP moduleInstances: make(map[string][]Module), } + // If this test provided a FileStorage, set the package-level + // testCertMagicStorageOverride so certmagicConfig will use it. + if test.cfg != nil && test.cfg.storage != nil { + testCertMagicStorageOverride = test.cfg.storage + defer func() { testCertMagicStorageOverride = nil }() + } + err := manageIdentity(ctx, test.cfg) if test.wantErr { diff --git a/caddytest/integration/pki_test.go b/caddytest/integration/pki_test.go index 846798209..3f1491e7e 100644 --- a/caddytest/integration/pki_test.go +++ b/caddytest/integration/pki_test.go @@ -53,7 +53,7 @@ func TestLeafCertLifetimeLessThanIntermediate(t *testing.T) { } } } - `, "json", "certificate lifetime (168h0m0s) should be less than intermediate certificate lifetime (168h0m0s)") + `, "json", "should be less than intermediate certificate lifetime") } func TestIntermediateLifetimeLessThanRoot(t *testing.T) { @@ -103,5 +103,5 @@ func TestIntermediateLifetimeLessThanRoot(t *testing.T) { } } } - `, "json", "intermediate certificate lifetime must be less than root certificate lifetime (86400h0m0s)") + `, "json", "intermediate certificate lifetime must be less than actual root certificate lifetime") } From 95941a71e87d6ccd6318bfd95c56929b01edfafd Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Tue, 17 Feb 2026 16:52:54 -0700 Subject: [PATCH 076/206] chore: Add nolints to work around haywire linters (#7493) * chore: Add nolints to work around haywire linters * More lint wrangling --- caddyconfig/httploader.go | 2 +- caddyconfig/load.go | 2 +- caddytest/caddytest.go | 6 +++--- caddytest/integration/acmeserver_test.go | 4 ++-- cmd/commandfuncs.go | 4 ++-- modules/caddyhttp/caddyauth/basicauth.go | 2 +- modules/caddyhttp/celmatcher.go | 2 ++ modules/caddyhttp/fileserver/browse.go | 1 + modules/caddyhttp/reverseproxy/healthchecks.go | 2 +- modules/caddyhttp/reverseproxy/selectionpolicies.go | 2 +- modules/caddyhttp/reverseproxy/streaming.go | 2 +- modules/caddyhttp/routes.go | 2 +- modules/caddyhttp/staticresp.go | 2 +- modules/caddypki/adminapi.go | 4 ++-- modules/caddypki/crypto.go | 2 +- modules/caddytls/acmeissuer.go | 2 +- modules/caddytls/capools.go | 2 +- modules/caddytls/certmanagers.go | 2 +- modules/caddytls/zerosslissuer.go | 2 +- 19 files changed, 25 insertions(+), 22 deletions(-) diff --git a/caddyconfig/httploader.go b/caddyconfig/httploader.go index a25041a34..e1a0fc85b 100644 --- a/caddyconfig/httploader.go +++ b/caddyconfig/httploader.go @@ -136,7 +136,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) { } func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) { - resp, err := client.Do(request) + resp, err := client.Do(request) //nolint:gosec // no SSRF; comes from trusted config if err != nil { return nil, fmt.Errorf("problem calling http loader url: %v", err) } else if resp.StatusCode < 200 || resp.StatusCode > 499 { diff --git a/caddyconfig/load.go b/caddyconfig/load.go index 9422d2fbb..d2498ed6f 100644 --- a/caddyconfig/load.go +++ b/caddyconfig/load.go @@ -106,7 +106,7 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error { if err != nil { caddy.Log().Named("admin.api.load").Error(err.Error()) } - _, _ = w.Write(respBody) + _, _ = w.Write(respBody) //nolint:gosec // false positive: no XSS here } body = result } diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index dfced29cf..26fa0533e 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -187,7 +187,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { req.Header.Add("Content-Type", "text/"+configType) } - res, err := client.Do(req) + res, err := client.Do(req) //nolint:gosec // no SSRF because URL is hard-coded to localhost, and port comes from config if err != nil { tc.t.Errorf("unable to contact caddy server. %s", err) return err @@ -279,7 +279,7 @@ func validateTestPrerequisites(tc *Tester) error { return err } tc.t.Cleanup(func() { - os.Remove(f.Name()) + os.Remove(f.Name()) //nolint:gosec // false positive, filename comes from std lib, no path traversal }) if _, err := fmt.Fprintf(f, initConfig, tc.config.AdminPort); err != nil { return err @@ -506,7 +506,7 @@ func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) { func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response { tc.t.Helper() - resp, err := tc.Client.Do(req) + resp, err := tc.Client.Do(req) //nolint:gosec // no SSRFs demonstrated if err != nil { tc.t.Fatalf("failed to call server %s", err) } diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index d6a9ba005..06deaa0ef 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -127,7 +127,7 @@ func TestACMEServerAllowPolicy(t *testing.T) { _, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"}) if err == nil { t.Errorf("obtaining certificate for 'not-matching.localhost' domain") - } else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") { + } else if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") { t.Logf("unexpected error: %v", err) } } @@ -200,7 +200,7 @@ func TestACMEServerDenyPolicy(t *testing.T) { _, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"}) if err == nil { t.Errorf("obtaining certificate for 'deny.localhost' domain") - } else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") { + } else if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") { t.Logf("unexpected error: %v", err) } } diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 8e46ba63b..3b458c2ba 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -74,7 +74,7 @@ func cmdStart(fl Flags) (int, error) { // ensure it's the process we're expecting - we can be // sure by giving it some random bytes and having it echo // them back to us) - cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) + cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) //nolint:gosec // no command injection that I can determine... // we should be able to run caddy in relative paths if errors.Is(cmd.Err, exec.ErrDot) { cmd.Err = nil @@ -820,7 +820,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io }, } - resp, err := client.Do(req) + resp, err := client.Do(req) //nolint:gosec // the only SSRF here would be self-sabatoge I think if err != nil { return nil, fmt.Errorf("performing request: %v", err) } diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go index 81b62d8eb..4152d7908 100644 --- a/modules/caddyhttp/caddyauth/basicauth.go +++ b/modules/caddyhttp/caddyauth/basicauth.go @@ -287,7 +287,7 @@ type Account struct { // The user's hashed password, in Modular Crypt Format (with `$` prefix) // or base64-encoded. - Password string `json:"password"` + Password string `json:"password"` //nolint:gosec // false positive, this is a hashed password password []byte } diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go index b67cb9e92..3038c8926 100644 --- a/modules/caddyhttp/celmatcher.go +++ b/modules/caddyhttp/celmatcher.go @@ -412,10 +412,12 @@ func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fa return nil, fmt.Errorf("unsupported matcher data type: %s, %s", matcherDataTypes[0], matcherDataTypes[1]) } case 3: + // nolint:gosec // false positive, impossible to be out of bounds; see: https://github.com/securego/gosec/issues/1525 if matcherDataTypes[0] == cel.StringType && matcherDataTypes[1] == cel.StringType && matcherDataTypes[2] == cel.StringType { macro = parser.NewGlobalMacro(macroName, 3, celMatcherStringListMacroExpander(funcName)) matcherDataTypes = []*cel.Type{cel.ListType(cel.StringType)} } else { + // nolint:gosec // false positive, impossible to be out of bounds; see: https://github.com/securego/gosec/issues/1525 return nil, fmt.Errorf("unsupported matcher data type: %s, %s, %s", matcherDataTypes[0], matcherDataTypes[1], matcherDataTypes[2]) } } diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go index 52aa7a9f8..304417009 100644 --- a/modules/caddyhttp/fileserver/browse.go +++ b/modules/caddyhttp/fileserver/browse.go @@ -169,6 +169,7 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht // Actual files for _, item := range listing.Items { + //nolint:gosec // not sure how this could be XSS unless you lose control of the file system (like aren't sanitizing) and client ignores Content-Type of text/plain if _, err := fmt.Fprintf(writer, "%s\t%s\t%s\n", item.Name, item.HumanSize(), item.HumanModTime("January 2, 2006 at 15:04:05"), ); err != nil { diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index b72e723e0..a194b88c8 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -500,7 +500,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ } // do the request, being careful to tame the response body - resp, err := h.HealthChecks.Active.httpClient.Do(req) + resp, err := h.HealthChecks.Active.httpClient.Do(req) //nolint:gosec // no SSRF if err != nil { if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "HTTP request failed"); c != nil { c.Write( diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index 2059c3ecf..3b68f504c 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -617,7 +617,7 @@ type CookieHashSelection struct { // The HTTP cookie name whose value is to be hashed and used for upstream selection. Name string `json:"name,omitempty"` // Secret to hash (Hmac256) chosen upstream in cookie - Secret string `json:"secret,omitempty"` + Secret string `json:"secret,omitempty"` //nolint:gosec // yes it's exported because it needs to encode to JSON // The cookie's Max-Age before it expires. Default is no expiry. MaxAge caddy.Duration `json:"max_age,omitempty"` diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index 0a8118520..38f056cd8 100644 --- a/modules/caddyhttp/reverseproxy/streaming.go +++ b/modules/caddyhttp/reverseproxy/streaming.go @@ -529,7 +529,7 @@ func maskBytes(key [4]byte, pos int, b []byte) int { // Create aligned word size key. var k [wordSize]byte for i := range k { - k[i] = key[(pos+i)&3] + k[i] = key[(pos+i)&3] // nolint:gosec // false positive, impossible to be out of bounds; see: https://github.com/securego/gosec/issues/1525 } kw := *(*uintptr)(unsafe.Pointer(&k)) diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index 78bf12209..d029d19b9 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -447,7 +447,7 @@ func (ms MatcherSets) String() string { result.WriteByte('[') for _, matcherSet := range ms { for _, matcher := range matcherSet { - result.WriteString(fmt.Sprintf(" %#v", matcher)) + fmt.Fprintf(&result, " %#v", matcher) } } result.WriteByte(']') diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index 1a5bbb9e1..439ba4f1f 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -246,7 +246,7 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, next H // write response body if statusCode != http.StatusEarlyHints && body != "" { - fmt.Fprint(w, body) + fmt.Fprint(w, body) //nolint:gosec // no XSS unless you sabatoge your own config } // continue handling after Early Hints as they are not the final response diff --git a/modules/caddypki/adminapi.go b/modules/caddypki/adminapi.go index c37b8d7b6..dcd3b5816 100644 --- a/modules/caddypki/adminapi.go +++ b/modules/caddypki/adminapi.go @@ -163,9 +163,9 @@ func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error { } w.Header().Set("Content-Type", "application/pem-certificate-chain") - _, err = w.Write(interCert) + _, err = w.Write(interCert) //nolint:gosec // false positive... no XSS in a PEM for cryin' out loud if err == nil { - _, _ = w.Write(rootCert) + _, _ = w.Write(rootCert) //nolint:gosec // false positive... no XSS in a PEM for cryin' out loud } return nil diff --git a/modules/caddypki/crypto.go b/modules/caddypki/crypto.go index 715155eb2..cfe46cd6d 100644 --- a/modules/caddypki/crypto.go +++ b/modules/caddypki/crypto.go @@ -77,7 +77,7 @@ type KeyPair struct { // The private key. By default, this should be the path to // a PEM file unless format is something else. - PrivateKey string `json:"private_key,omitempty"` + PrivateKey string `json:"private_key,omitempty"` //nolint:gosec // false positive: yes it's exported, since it needs to encode/decode as JSON; and is often just a filepath // The format in which the certificate and private // key are provided. Default: pem_file diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 41c0e20a1..f254f7b2b 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -337,7 +337,7 @@ func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct a req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("User-Agent", certmagic.UserAgent) - resp, err := http.DefaultClient.Do(req) + resp, err := http.DefaultClient.Do(req) //nolint:gosec // no SSRF since URL is from trusted config if err != nil { return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err) } diff --git a/modules/caddytls/capools.go b/modules/caddytls/capools.go index 55abb7466..97ce6af2b 100644 --- a/modules/caddytls/capools.go +++ b/modules/caddytls/capools.go @@ -588,7 +588,7 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error { if err != nil { return err } - res, err := httpClient.Do(req) + res, err := httpClient.Do(req) //nolint:gosec // SSRF false positive... uri comes from config if err != nil { return err } diff --git a/modules/caddytls/certmanagers.go b/modules/caddytls/certmanagers.go index 0a9d459df..68014635e 100644 --- a/modules/caddytls/certmanagers.go +++ b/modules/caddytls/certmanagers.go @@ -155,7 +155,7 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH return nil, err } - resp, err := http.DefaultClient.Do(req) + resp, err := http.DefaultClient.Do(req) //nolint:gosec // SSRF false positive... request URI comes from config if err != nil { return nil, err } diff --git a/modules/caddytls/zerosslissuer.go b/modules/caddytls/zerosslissuer.go index b8727ab66..3421e816a 100644 --- a/modules/caddytls/zerosslissuer.go +++ b/modules/caddytls/zerosslissuer.go @@ -40,7 +40,7 @@ func init() { type ZeroSSLIssuer struct { // The API key (or "access key") for using the ZeroSSL API. // REQUIRED. - APIKey string `json:"api_key,omitempty"` + APIKey string `json:"api_key,omitempty"` //nolint:gosec // false positive... yes this is exported, for JSON interop // How many days the certificate should be valid for. // Only certain values are accepted; see ZeroSSL docs. From 6772ffb805d688475369a697e3962fa48cbf6bd2 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 19 Feb 2026 11:32:26 -0700 Subject: [PATCH 077/206] Revert "listeners: Add support for named socket activation (#7243)" This reverts commit 156ce99d3a46be8cefe8502b2c30b757e4deb79f. --- listeners.go | 82 +------------ listeners_test.go | 284 ---------------------------------------------- 2 files changed, 1 insertion(+), 365 deletions(-) diff --git a/listeners.go b/listeners.go index e214caf5c..326c18573 100644 --- a/listeners.go +++ b/listeners.go @@ -38,10 +38,6 @@ import ( "github.com/caddyserver/caddy/v2/internal" ) -// listenFdsStart is the first file descriptor number for systemd socket activation. -// File descriptors 0, 1, 2 are reserved for stdin, stdout, stderr. -const listenFdsStart = 3 - // NetworkAddress represents one or more network addresses. // It contains the individual components for a parsed network // address of the form accepted by ParseNetworkAddress(). @@ -309,64 +305,6 @@ func IsFdNetwork(netw string) bool { return strings.HasPrefix(netw, "fd") } -// getFdByName returns the file descriptor number for the given -// socket name from systemd's LISTEN_FDNAMES environment variable. -// Socket names are provided by systemd via socket activation. -// -// The name can optionally include an index to handle multiple sockets -// with the same name: "web:0" for first, "web:1" for second, etc. -// If no index is specified, defaults to index 0 (first occurrence). -func getFdByName(nameWithIndex string) (int, error) { - if nameWithIndex == "" { - return 0, fmt.Errorf("socket name cannot be empty") - } - - fdNamesStr := os.Getenv("LISTEN_FDNAMES") - if fdNamesStr == "" { - return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set") - } - - // Parse name and optional index - parts := strings.Split(nameWithIndex, ":") - if len(parts) > 2 { - return 0, fmt.Errorf("invalid socket name format '%s': too many colons", nameWithIndex) - } - - name := parts[0] - targetIndex := 0 - - if len(parts) > 1 { - var err error - targetIndex, err = strconv.Atoi(parts[1]) - if err != nil { - return 0, fmt.Errorf("invalid socket index '%s': %v", parts[1], err) - } - if targetIndex < 0 { - return 0, fmt.Errorf("socket index cannot be negative: %d", targetIndex) - } - } - - // Parse the socket names - names := strings.Split(fdNamesStr, ":") - - // Find the Nth occurrence of the requested name - matchCount := 0 - for i, fdName := range names { - if fdName == name { - if matchCount == targetIndex { - return listenFdsStart + i, nil - } - matchCount++ - } - } - - if matchCount == 0 { - return 0, fmt.Errorf("socket name '%s' not found in LISTEN_FDNAMES", name) - } - - return 0, fmt.Errorf("socket name '%s' found %d times, but index %d requested", name, matchCount, targetIndex) -} - // ParseNetworkAddress parses addr into its individual // components. The input string is expected to be of // the form "network/host:port-range" where any part is @@ -398,27 +336,9 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui }, err } if IsFdNetwork(network) { - fdAddr := host - - // Handle named socket activation (fdname/name, fdgramname/name) - if strings.HasPrefix(network, "fdname") || strings.HasPrefix(network, "fdgramname") { - fdNum, err := getFdByName(host) - if err != nil { - return NetworkAddress{}, fmt.Errorf("named socket activation: %v", err) - } - fdAddr = strconv.Itoa(fdNum) - - // Normalize network to standard fd/fdgram - if strings.HasPrefix(network, "fdname") { - network = "fd" - } else { - network = "fdgram" - } - } - return NetworkAddress{ Network: network, - Host: fdAddr, + Host: host, }, nil } var start, end uint64 diff --git a/listeners_test.go b/listeners_test.go index c2cc255f2..a4cadd3aa 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -15,7 +15,6 @@ package caddy import ( - "os" "reflect" "testing" @@ -653,286 +652,3 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) { } } } - -// TestGetFdByName tests the getFdByName function for systemd socket activation. -func TestGetFdByName(t *testing.T) { - // Save original environment - originalFdNames := os.Getenv("LISTEN_FDNAMES") - - // Restore environment after test - defer func() { - if originalFdNames != "" { - os.Setenv("LISTEN_FDNAMES", originalFdNames) - } else { - os.Unsetenv("LISTEN_FDNAMES") - } - }() - - tests := []struct { - name string - fdNames string - socketName string - expectedFd int - expectError bool - }{ - { - name: "simple http socket", - fdNames: "http", - socketName: "http", - expectedFd: 3, - }, - { - name: "multiple different sockets - first", - fdNames: "http:https:dns", - socketName: "http", - expectedFd: 3, - }, - { - name: "multiple different sockets - second", - fdNames: "http:https:dns", - socketName: "https", - expectedFd: 4, - }, - { - name: "multiple different sockets - third", - fdNames: "http:https:dns", - socketName: "dns", - expectedFd: 5, - }, - { - name: "duplicate names - first occurrence (no index)", - fdNames: "web:web:api", - socketName: "web", - expectedFd: 3, - }, - { - name: "duplicate names - first occurrence (explicit index 0)", - fdNames: "web:web:api", - socketName: "web:0", - expectedFd: 3, - }, - { - name: "duplicate names - second occurrence (index 1)", - fdNames: "web:web:api", - socketName: "web:1", - expectedFd: 4, - }, - { - name: "complex duplicates - first api", - fdNames: "web:api:web:api:dns", - socketName: "api:0", - expectedFd: 4, - }, - { - name: "complex duplicates - second api", - fdNames: "web:api:web:api:dns", - socketName: "api:1", - expectedFd: 6, - }, - { - name: "complex duplicates - first web", - fdNames: "web:api:web:api:dns", - socketName: "web:0", - expectedFd: 3, - }, - { - name: "complex duplicates - second web", - fdNames: "web:api:web:api:dns", - socketName: "web:1", - expectedFd: 5, - }, - { - name: "socket not found", - fdNames: "http:https", - socketName: "missing", - expectError: true, - }, - { - name: "empty socket name", - fdNames: "http", - socketName: "", - expectError: true, - }, - { - name: "missing LISTEN_FDNAMES", - fdNames: "", - socketName: "http", - expectError: true, - }, - { - name: "index out of range", - fdNames: "web:web", - socketName: "web:2", - expectError: true, - }, - { - name: "negative index", - fdNames: "web", - socketName: "web:-1", - expectError: true, - }, - { - name: "invalid index format", - fdNames: "web", - socketName: "web:abc", - expectError: true, - }, - { - name: "too many colons", - fdNames: "web", - socketName: "web:0:extra", - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Set up environment - if tc.fdNames != "" { - os.Setenv("LISTEN_FDNAMES", tc.fdNames) - } else { - os.Unsetenv("LISTEN_FDNAMES") - } - - // Test the function - fd, err := getFdByName(tc.socketName) - - if tc.expectError { - if err == nil { - t.Errorf("Expected error but got none") - } - } else { - if err != nil { - t.Errorf("Expected no error but got: %v", err) - } - if fd != tc.expectedFd { - t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd) - } - } - }) - } -} - -// TestParseNetworkAddressFdName tests parsing of fdname and fdgramname addresses. -func TestParseNetworkAddressFdName(t *testing.T) { - // Save and restore environment - originalFdNames := os.Getenv("LISTEN_FDNAMES") - defer func() { - if originalFdNames != "" { - os.Setenv("LISTEN_FDNAMES", originalFdNames) - } else { - os.Unsetenv("LISTEN_FDNAMES") - } - }() - - // Set up test environment - os.Setenv("LISTEN_FDNAMES", "http:https:dns") - - tests := []struct { - input string - expectAddr NetworkAddress - expectErr bool - }{ - { - input: "fdname/http", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "3", - }, - }, - { - input: "fdname/https", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "4", - }, - }, - { - input: "fdname/dns", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "5", - }, - }, - { - input: "fdname/http:0", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "3", - }, - }, - { - input: "fdname/https:0", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "4", - }, - }, - { - input: "fdgramname/http", - expectAddr: NetworkAddress{ - Network: "fdgram", - Host: "3", - }, - }, - { - input: "fdgramname/https", - expectAddr: NetworkAddress{ - Network: "fdgram", - Host: "4", - }, - }, - { - input: "fdgramname/http:0", - expectAddr: NetworkAddress{ - Network: "fdgram", - Host: "3", - }, - }, - { - input: "fdname/nonexistent", - expectErr: true, - }, - { - input: "fdgramname/nonexistent", - expectErr: true, - }, - { - input: "fdname/http:99", - expectErr: true, - }, - { - input: "fdname/invalid:abc", - expectErr: true, - }, - // Test that old fd/N syntax still works - { - input: "fd/7", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "7", - }, - }, - { - input: "fdgram/8", - expectAddr: NetworkAddress{ - Network: "fdgram", - Host: "8", - }, - }, - } - - for i, tc := range tests { - actualAddr, err := ParseNetworkAddress(tc.input) - - if tc.expectErr && err == nil { - t.Errorf("Test %d (%s): Expected error but got none", i, tc.input) - } - if !tc.expectErr && err != nil { - t.Errorf("Test %d (%s): Expected no error but got: %v", i, tc.input, err) - } - if !tc.expectErr && !reflect.DeepEqual(tc.expectAddr, actualAddr) { - t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectAddr, actualAddr) - } - } -} From db256b53e517191f52eca4ea11d415d7a11e729f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:20:06 -0500 Subject: [PATCH 078/206] build(deps): bump filippo.io/edwards25519 from 1.1.0 to 1.1.1 (#7497) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cb03b4acc..5b0791dd3 100644 --- a/go.mod +++ b/go.mod @@ -116,7 +116,7 @@ require ( ) require ( - filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/edwards25519 v1.1.1 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect diff --git a/go.sum b/go.sum index 0c79eeffb..7c6449f08 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8= filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= From a2825c5dd952769f139a16448dc1ca1be61b6058 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 19 Feb 2026 13:17:19 -0700 Subject: [PATCH 079/206] fileserver: Replace \ with \\ in file matcher paths --- modules/caddyhttp/fileserver/matcher.go | 1 + modules/caddyhttp/fileserver/matcher_test.go | 6 ++++++ "modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" | 0 3 files changed, 7 insertions(+) create mode 100644 "modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 152f31430..8ced5a19d 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -720,6 +720,7 @@ var globSafeRepl = strings.NewReplacer( "*", "\\*", "[", "\\[", "?", "\\?", + "\\", "\\\\", ) const ( diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go index f0ec4b392..36483fcd9 100644 --- a/modules/caddyhttp/fileserver/matcher_test.go +++ b/modules/caddyhttp/fileserver/matcher_test.go @@ -115,6 +115,12 @@ func TestFileMatcher(t *testing.T) { expectedType: "file", matched: !isWindows, }, + { + path: "/foodir/secr%5Cet.txt", + expectedPath: "/foodir/secr\\et.txt", + expectedType: "file", + matched: true, + }, } { m := &MatchFile{ fsmap: &filesystems.FileSystemMap{}, diff --git "a/modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" "b/modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" new file mode 100644 index 000000000..e69de29bb From eec32a0bb5a11651c6a7b04ce82dc50610f2b27e Mon Sep 17 00:00:00 2001 From: Asim Viladi Oglu Manizada <257676403+manizada@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:19:42 -0800 Subject: [PATCH 080/206] Merge commit from fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Normalize exact hosts at provisioning and reqHost in the fast path so case-different Host variants can’t bypass host-gated routes. Co-authored-by: Asim Viladi Oglu Manizada --- modules/caddyhttp/matchers.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index afba1b36f..eda8cd3ae 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -262,13 +262,17 @@ func (m MatchHost) Provision(_ caddy.Context) error { if err != nil { return fmt.Errorf("converting hostname '%s' to ASCII: %v", host, err) } - if asciiHost != host { - m[i] = asciiHost - } normalizedHost := strings.ToLower(asciiHost) if firstI, ok := seen[normalizedHost]; ok { return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, host) } + // Normalize exact hosts for standardized comparison in large-list fastpath later on. + // Keep wildcards/placeholders untouched. + if m.fuzzy(asciiHost) { + m[i] = asciiHost + } else { + m[i] = normalizedHost + } seen[normalizedHost] = i } @@ -312,14 +316,15 @@ func (m MatchHost) MatchWithError(r *http.Request) (bool, error) { } if m.large() { + reqHostLower := strings.ToLower(reqHost) // fast path: locate exact match using binary search (about 100-1000x faster for large lists) pos := sort.Search(len(m), func(i int) bool { if m.fuzzy(m[i]) { return false } - return m[i] >= reqHost + return m[i] >= reqHostLower }) - if pos < len(m) && strings.EqualFold(m[pos], reqHost) { + if pos < len(m) && m[pos] == reqHostLower { return true, nil } } From a1081194bfae4e0d8c227ec44aecb95eded55d1e Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Fri, 20 Feb 2026 10:54:50 -0700 Subject: [PATCH 081/206] Merge commit from fork Necessary as otherwise the early-bail in `until = strings.IndexByte(remaining, nextCh) ... if until == -1` can cause a case-insensitive mismatch Co-authored-by: Asim Viladi Oglu Manizada --- modules/caddyhttp/matchers.go | 1 + modules/caddyhttp/matchers_test.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index eda8cd3ae..27e5c5ae6 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -538,6 +538,7 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) { } func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool { + escapedPath = strings.ToLower(escapedPath) // We would just compare the pattern against r.URL.Path, // but the pattern contains %, indicating that we should // compare at least some part of the path in raw/escaped diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index b5e965b4a..160aa424f 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -417,6 +417,11 @@ func TestPathMatcher(t *testing.T) { input: "/ADMIN%2fpanel", expect: true, }, + { + match: MatchPath{"/admin%2fpa*el"}, + input: "/ADMIN%2fPaAzZLm123NEL", + expect: true, + }, } { err := tc.match.Provision(caddy.Context{}) if err == nil && tc.provisionErr { From cb436f0a0ed62e0a5d4af158486bba39fc575fe1 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 20 Feb 2026 11:46:45 -0700 Subject: [PATCH 082/206] fileserver: Fix tests on Windows --- modules/caddyhttp/fileserver/matcher_test.go | 113 +++++++++++------- .../fileserver/testdata/foodir/secr\\et.txt" | 0 2 files changed, 69 insertions(+), 44 deletions(-) delete mode 100644 "modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go index 36483fcd9..4342d5d31 100644 --- a/modules/caddyhttp/fileserver/matcher_test.go +++ b/modules/caddyhttp/fileserver/matcher_test.go @@ -20,7 +20,9 @@ import ( "net/http/httptest" "net/url" "os" + "path/filepath" "runtime" + "strings" "testing" "github.com/caddyserver/caddy/v2" @@ -28,6 +30,13 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) +type testCase struct { + path string + expectedPath string + expectedType string + matched bool +} + func TestFileMatcher(t *testing.T) { // Windows doesn't like colons in files names isWindows := runtime.GOOS == "windows" @@ -45,12 +54,7 @@ func TestFileMatcher(t *testing.T) { f.Close() } - for i, tc := range []struct { - path string - expectedPath string - expectedType string - matched bool - }{ + for i, tc := range []testCase{ { path: "/foo.txt", expectedPath: "/foo.txt", @@ -115,51 +119,72 @@ func TestFileMatcher(t *testing.T) { expectedType: "file", matched: !isWindows, }, - { - path: "/foodir/secr%5Cet.txt", - expectedPath: "/foodir/secr\\et.txt", - expectedType: "file", - matched: true, - }, } { - m := &MatchFile{ - fsmap: &filesystems.FileSystemMap{}, - Root: "./testdata", - TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, - } + fileMatcherTest(t, i, tc) + } +} - u, err := url.Parse(tc.path) - if err != nil { - t.Errorf("Test %d: parsing path: %v", i, err) - } +func TestFileMatcherNonWindows(t *testing.T) { + if runtime.GOOS == "windows" { + return + } - req := &http.Request{URL: u} - repl := caddyhttp.NewTestReplacer(req) + // this is impossible to test on Windows, but tests a security patch for other platforms + tc := testCase{ + path: "/foodir/secr%5Cet.txt", + expectedPath: "/foodir/secr\\et.txt", + expectedType: "file", + matched: true, + } - result, err := m.MatchWithError(req) - if err != nil { - t.Errorf("Test %d: unexpected error: %v", i, err) - } - if result != tc.matched { - t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result) - } + f, err := os.Create(filepath.Join("testdata", strings.TrimPrefix(tc.expectedPath, "/"))) + if err != nil { + t.Fatalf("could not create test file: %v", err) + } + defer f.Close() + defer os.Remove(f.Name()) - rel, ok := repl.Get("http.matchers.file.relative") - if !ok && result { - t.Errorf("Test %d: expected replacer value", i) - } - if !result { - continue - } + fileMatcherTest(t, 0, tc) +} - if rel != tc.expectedPath { - t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath) - } +func fileMatcherTest(t *testing.T, i int, tc testCase) { + m := &MatchFile{ + fsmap: &filesystems.FileSystemMap{}, + Root: "./testdata", + TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, + } - fileType, _ := repl.Get("http.matchers.file.type") - if fileType != tc.expectedType { - t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) - } + u, err := url.Parse(tc.path) + if err != nil { + t.Errorf("Test %d: parsing path: %v", i, err) + } + + req := &http.Request{URL: u} + repl := caddyhttp.NewTestReplacer(req) + + result, err := m.MatchWithError(req) + if err != nil { + t.Errorf("Test %d: unexpected error: %v", i, err) + } + if result != tc.matched { + t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result) + } + + rel, ok := repl.Get("http.matchers.file.relative") + if !ok && result { + t.Errorf("Test %d: expected replacer value", i) + } + if !result { + return + } + + if rel != tc.expectedPath { + t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath) + } + + fileType, _ := repl.Get("http.matchers.file.type") + if fileType != tc.expectedType { + t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) } } diff --git "a/modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" "b/modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" deleted file mode 100644 index e69de29bb..000000000 From 03243e42fe46f6bf276cbbf5dcdc7e18fde461d7 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 20 Feb 2026 12:28:11 -0700 Subject: [PATCH 083/206] go.mod: Upgrade dependencies --- go.mod | 16 ++++++++-------- go.sum | 34 ++++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 5b0791dd3..f5015aa0f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/caddyserver/caddy/v2 -go 1.25 +go 1.25.0 require ( github.com/BurntSushi/toml v1.6.0 @@ -9,7 +9,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.23.1 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.25.1 + github.com/caddyserver/certmagic v0.25.2 github.com/caddyserver/zerossl v0.1.5 github.com/cloudflare/circl v1.6.3 github.com/dustin/go-humanize v1.0.1 @@ -18,7 +18,7 @@ require ( github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.18.4 github.com/klauspost/cpuid/v2 v2.3.0 - github.com/mholt/acmez/v3 v3.1.4 + github.com/mholt/acmez/v3 v3.1.6 github.com/prometheus/client_golang v1.23.2 github.com/quic-go/quic-go v0.59.0 github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c @@ -35,12 +35,12 @@ require ( go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/sdk v1.40.0 - go.step.sm/crypto v0.76.0 + go.step.sm/crypto v0.76.2 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 golang.org/x/crypto v0.48.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 + golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 golang.org/x/net v0.50.0 golang.org/x/sync v0.19.0 golang.org/x/term v0.40.0 @@ -116,10 +116,10 @@ require ( ) require ( - filippo.io/edwards25519 v1.1.1 // indirect + filippo.io/edwards25519 v1.2.0 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 @@ -145,7 +145,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/miekg/dns v1.1.70 // indirect + github.com/miekg/dns v1.1.72 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index 7c6449f08..8d7cdee58 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,14 @@ cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ= cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= +code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8= filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= -filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= -filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -32,8 +34,8 @@ github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43Dw github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= @@ -83,8 +85,8 @@ github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/caddyserver/certmagic v0.25.1 h1:4sIKKbOt5pg6+sL7tEwymE1x2bj6CHr80da1CRRIPbY= -github.com/caddyserver/certmagic v0.25.1/go.mod h1:VhyvndxtVton/Fo/wKhRoC46Rbw1fmjvQ3GjHYSQTEY= +github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= +github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0= @@ -223,6 +225,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU= +github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk= +github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U= +github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ= github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -234,10 +240,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ= -github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= -github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA= -github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= +github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -425,8 +431,8 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.step.sm/crypto v0.76.0 h1:K23BSaeoiY7Y5dvvijTeYC9EduDBetNwQYMBwMhi1aA= -go.step.sm/crypto v0.76.0/go.mod h1:PXYJdKkK8s+GHLwLguFaLxHNAFsFL3tL1vSBrYfey5k= +go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4= +go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -452,8 +458,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/crypto/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/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= From 6610e2f1bd8f54853006eefd3849c9965190e57f Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Fri, 20 Feb 2026 17:47:21 -0500 Subject: [PATCH 084/206] chore: Disable windows/arm build target (Go 1.26 disabled) (#7503) --- .goreleaser.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index c7ed4b365..3c87131bd 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -13,7 +13,7 @@ before: - cp cmd/caddy/main.go caddy-build/main.go - /bin/sh -c 'cd ./caddy-build && go mod init caddy' # prepare syso files for windows embedding - - /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done' + - /bin/sh -c 'for a in amd64 arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done' - /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build' # GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env # so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate @@ -67,6 +67,8 @@ builds: goarch: s390x - goos: windows goarch: riscv64 + - goos: windows + goarch: arm - goos: freebsd goarch: ppc64le - goos: freebsd From d7b21c610494e30e147de77a4783c18e5f206d99 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sun, 22 Feb 2026 05:37:10 +0300 Subject: [PATCH 085/206] reverseproxy: fix tls dialing w/ proxy protocol (#7508) --- .../caddyhttp/reverseproxy/httptransport.go | 15 +++- .../reverseproxy/httptransport_test.go | 80 +++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 8d06d3bd2..db806acbd 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -412,8 +412,13 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e return nil, fmt.Errorf("making TLS client config: %v", err) } - // servername has a placeholder, so we need to replace it - if strings.Contains(h.TLS.ServerName, "{") { + serverNameHasPlaceholder := strings.Contains(h.TLS.ServerName, "{") + + // We need to use custom DialTLSContext if: + // 1. ServerName has a placeholder that needs to be replaced at request-time, OR + // 2. ProxyProtocol is enabled, because req.URL.Host is modified to include + // client address info with "->" separator which breaks Go's address parsing + if serverNameHasPlaceholder || h.ProxyProtocol != "" { rt.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { // reuses the dialer from above to establish a plaintext connection conn, err := dialContext(ctx, network, addr) @@ -422,9 +427,11 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e } // but add our own handshake logic - repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) tlsConfig := rt.TLSClientConfig.Clone() - tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "") + if serverNameHasPlaceholder { + repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "") + } // h1 only if caddyhttp.GetVar(ctx, tlsH1OnlyVarKey) == true { diff --git a/modules/caddyhttp/reverseproxy/httptransport_test.go b/modules/caddyhttp/reverseproxy/httptransport_test.go index 1fa4965f2..88ac9d591 100644 --- a/modules/caddyhttp/reverseproxy/httptransport_test.go +++ b/modules/caddyhttp/reverseproxy/httptransport_test.go @@ -1,11 +1,13 @@ package reverseproxy import ( + "context" "encoding/json" "fmt" "reflect" "testing" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) @@ -115,3 +117,81 @@ func TestHTTPTransport_RequestHeaderOps_TLS(t *testing.T) { t.Fatalf("unexpected Host value; want placeholder, got: %s", got) } } + +// TestHTTPTransport_DialTLSContext_ProxyProtocol verifies that when TLS and +// ProxyProtocol are both enabled, DialTLSContext is set. This is critical because +// ProxyProtocol modifies req.URL.Host to include client info with "->" separator +// (e.g., "[2001:db8::1]:12345->127.0.0.1:443"), which breaks Go's address parsing. +// Without a custom DialTLSContext, Go's HTTP library would fail with +// "too many colons in address" when trying to parse the mangled host. +func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + tests := []struct { + name string + tls *TLSConfig + proxyProtocol string + serverNameHasPlaceholder bool + expectDialTLSContext bool + }{ + { + name: "no TLS, no proxy protocol", + tls: nil, + proxyProtocol: "", + expectDialTLSContext: false, + }, + { + name: "TLS without proxy protocol", + tls: &TLSConfig{}, + proxyProtocol: "", + expectDialTLSContext: false, + }, + { + name: "TLS with proxy protocol v1", + tls: &TLSConfig{}, + proxyProtocol: "v1", + expectDialTLSContext: true, + }, + { + name: "TLS with proxy protocol v2", + tls: &TLSConfig{}, + proxyProtocol: "v2", + expectDialTLSContext: true, + }, + { + name: "TLS with placeholder ServerName", + tls: &TLSConfig{ServerName: "{http.request.host}"}, + proxyProtocol: "", + serverNameHasPlaceholder: true, + expectDialTLSContext: true, + }, + { + name: "TLS with placeholder ServerName and proxy protocol", + tls: &TLSConfig{ServerName: "{http.request.host}"}, + proxyProtocol: "v2", + serverNameHasPlaceholder: true, + expectDialTLSContext: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ht := &HTTPTransport{ + TLS: tt.tls, + ProxyProtocol: tt.proxyProtocol, + } + + rt, err := ht.NewTransport(ctx) + if err != nil { + t.Fatalf("NewTransport() error = %v", err) + } + + hasDialTLSContext := rt.DialTLSContext != nil + if hasDialTLSContext != tt.expectDialTLSContext { + t.Errorf("DialTLSContext set = %v, want %v", hasDialTLSContext, tt.expectDialTLSContext) + } + }) + } +} + From 7ffb640a4da666203ab43396eaeb2b6b84bb983f Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sat, 21 Feb 2026 23:42:03 -0300 Subject: [PATCH 086/206] httpcaddyfile: Fix missing TLS connection policies when auto_https is default (#7325) (#7507) --- caddyconfig/httpcaddyfile/httptype.go | 2 +- caddyconfig/httpcaddyfile/httptype_test.go | 52 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 49cf40497..1b9c625fe 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -822,7 +822,7 @@ func (st *ServerType) serversFromPairings( // https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761 createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"] hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) || - (addr.Host != "" && srv.AutoHTTPS != nil && !slices.Contains(srv.AutoHTTPS.Skip, addr.Host)) + (addr.Host != "" && (srv.AutoHTTPS == nil || !slices.Contains(srv.AutoHTTPS.Skip, addr.Host))) // we'll need to remember if the address qualifies for auto-HTTPS, so we // can add a TLS conn policy if necessary diff --git a/caddyconfig/httpcaddyfile/httptype_test.go b/caddyconfig/httpcaddyfile/httptype_test.go index 69f55501c..2436efcd9 100644 --- a/caddyconfig/httpcaddyfile/httptype_test.go +++ b/caddyconfig/httpcaddyfile/httptype_test.go @@ -1,9 +1,11 @@ package httpcaddyfile import ( + "encoding/json" "testing" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func TestMatcherSyntax(t *testing.T) { @@ -209,3 +211,53 @@ func TestGlobalOptions(t *testing.T) { } } } + +func TestDefaultSNIWithoutHTTPS(t *testing.T) { + caddyfileStr := `{ + default_sni my-sni.com + } + example.com { + }` + + adapter := caddyfile.Adapter{ + ServerType: ServerType{}, + } + + result, _, err := adapter.Adapt([]byte(caddyfileStr), nil) + if err != nil { + t.Fatalf("Failed to adapt Caddyfile: %v", err) + } + + var config struct { + Apps struct { + HTTP struct { + Servers map[string]*caddyhttp.Server `json:"servers"` + } `json:"http"` + } `json:"apps"` + } + + if err := json.Unmarshal(result, &config); err != nil { + t.Fatalf("Failed to unmarshal JSON config: %v", err) + } + + server, ok := config.Apps.HTTP.Servers["srv0"] + if !ok { + t.Fatalf("Expected server 'srv0' to be created") + } + + if len(server.TLSConnPolicies) == 0 { + t.Fatalf("Expected TLS connection policies to be generated, got none") + } + + found := false + for _, policy := range server.TLSConnPolicies { + if policy.DefaultSNI == "my-sni.com" { + found = true + break + } + } + + if !found { + t.Errorf("Expected default_sni 'my-sni.com' in TLS connection policies, but it was missing. Generated JSON: %s", string(result)) + } +} From 76b198f586e4e2482a0278ba52c176cff70af8cf Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Sat, 21 Feb 2026 23:42:40 -0300 Subject: [PATCH 087/206] http: Sort auto-HTTPS redirect routes by host specificity (fixes #7390) (#7502) --- caddytest/integration/autohttps_test.go | 23 ++++++++++ modules/caddyhttp/autohttps.go | 57 +++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/caddytest/integration/autohttps_test.go b/caddytest/integration/autohttps_test.go index 1dbdbcee2..fdfb5a93e 100644 --- a/caddytest/integration/autohttps_test.go +++ b/caddytest/integration/autohttps_test.go @@ -143,3 +143,26 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo") tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo") } + +func TestAutoHTTPSRedirectSortingExactMatchOverWildcard(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + local_certs + } + *.localhost:10443 { + respond "Wildcard" + } + dev.localhost { + respond "Exact" + } + `, "caddyfile") + + tester.AssertRedirect("http://dev.localhost:9080/", "https://dev.localhost/", http.StatusPermanentRedirect) + + tester.AssertRedirect("http://foo.localhost:9080/", "https://foo.localhost:10443/", http.StatusPermanentRedirect) +} diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 8bcaebe69..2ae50f725 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -424,6 +424,40 @@ redirServersLoop: // we'll create a new server for all the listener addresses // that are unused and serve the remaining redirects from it + // Sort redirect routes by host specificity to ensure exact matches + // take precedence over wildcards, preventing ambiguous routing. + slices.SortFunc(routes, func(a, b Route) int { + hostA := getFirstHostFromRoute(a) + hostB := getFirstHostFromRoute(b) + + // Catch-all routes (empty host) have the lowest priority + if hostA == "" && hostB != "" { + return 1 + } + if hostB == "" && hostA != "" { + return -1 + } + + hasWildcardA := strings.Contains(hostA, "*") + hasWildcardB := strings.Contains(hostB, "*") + + // Exact domains take precedence over wildcards + if !hasWildcardA && hasWildcardB { + return -1 + } + if hasWildcardA && !hasWildcardB { + return 1 + } + + // If both are exact or both are wildcards, the longer one is more specific + if len(hostA) != len(hostB) { + return len(hostB) - len(hostA) + } + + // Tie-breaker: alphabetical order to ensure determinism + return strings.Compare(hostA, hostB) + }) + // Use the sorted srvNames to consistently find the target server for _, srvName := range srvNames { srv := app.Servers[srvName] @@ -793,3 +827,26 @@ func isTailscaleDomain(name string) bool { } type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer } + +// getFirstHostFromRoute traverses a route's matchers to find the Host rule. +// Since we are dealing with internally generated redirect routes, the host +// is typically the first string within the MatchHost. +func getFirstHostFromRoute(r Route) string { + for _, matcherSet := range r.MatcherSets { + for _, m := range matcherSet { + // Check if the matcher is of type MatchHost (value or pointer) + switch hm := m.(type) { + case MatchHost: + if len(hm) > 0 { + return hm[0] + } + case *MatchHost: + if len(*hm) > 0 { + return (*hm)[0] + } + } + } + } + // Return an empty string if it's a catch-all route (no specific host) + return "" +} From 294dfff4435c31b7c8d20d26067365b0a0016610 Mon Sep 17 00:00:00 2001 From: Dean Ruina <81315494+DeRuina@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:27:27 +0200 Subject: [PATCH 088/206] logging: add DirMode options and propagate FileMode to rotations (#7335) Co-authored-by: Francis Lavoie --- .../log_roll_days.caddyfiletest | 54 ++++- modules/logging/filewriter.go | 161 +++++++++++-- modules/logging/filewriter_test.go | 222 ++++++++++++++++++ modules/logging/filewriter_test_windows.go | 38 +++ 4 files changed, 450 insertions(+), 25 deletions(-) diff --git a/caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest index 3ead4ac18..3e2aa0d81 100644 --- a/caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest @@ -1,7 +1,9 @@ :80 -log { +log one { output file /var/log/access.log { + mode 0644 + dir_mode 0755 roll_size 1gb roll_uncompressed roll_local_time @@ -9,18 +11,33 @@ log { roll_keep_for 90d } } +log two { + output file /var/log/access-2.log { + mode 0777 + dir_mode from_file + roll_size 1gib + roll_interval 12h + roll_at 00:00 06:00 12:00,18:00 + roll_minutes 10 40 45,46 + roll_keep 10 + roll_keep_for 90d + } +} ---------- { "logging": { "logs": { "default": { "exclude": [ - "http.log.access.log0" + "http.log.access.one", + "http.log.access.two" ] }, - "log0": { + "one": { "writer": { + "dir_mode": "0755", "filename": "/var/log/access.log", + "mode": "0644", "output": "file", "roll_gzip": false, "roll_keep": 5, @@ -29,7 +46,34 @@ log { "roll_size_mb": 954 }, "include": [ - "http.log.access.log0" + "http.log.access.one" + ] + }, + "two": { + "writer": { + "dir_mode": "from_file", + "filename": "/var/log/access-2.log", + "mode": "0777", + "output": "file", + "roll_at": [ + "00:00", + "06:00", + "12:00", + "18:00" + ], + "roll_interval": 43200000000000, + "roll_keep": 10, + "roll_keep_days": 90, + "roll_minutes": [ + 10, + 40, + 45, + 46 + ], + "roll_size_mb": 1024 + }, + "include": [ + "http.log.access.two" ] } } @@ -42,7 +86,7 @@ log { ":80" ], "logs": { - "default_logger_name": "log0" + "default_logger_name": "two" } } } diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index c3df562cb..e9ca4013a 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -90,6 +90,15 @@ type FileWriter struct { // 0600 by default. Mode fileMode `json:"mode,omitempty"` + // DirMode controls permissions for any directories created to reach Filename. + // Default: 0700 (current behavior). + // + // Special values: + // - "inherit" → copy the nearest existing parent directory's perms (with r→x normalization) + // - "from_file" → derive from the file Mode (with r→x), e.g. 0644 → 0755, 0600 → 0700 + // Numeric octal strings (e.g. "0755") are also accepted. Subject to process umask. + DirMode string `json:"dir_mode,omitempty"` + // Roll toggles log rolling or rotation, which is // enabled by default. Roll *bool `json:"roll,omitempty"` @@ -177,11 +186,33 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { // roll log files as a sensible default to avoid disk space exhaustion 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. (timberjack will reuse the file mode across log rotation) - if err := os.MkdirAll(filepath.Dir(fw.Filename), 0o700); err != nil { - return nil, err + // Ensure directory exists before opening the file. + dirPath := filepath.Dir(fw.Filename) + switch strings.ToLower(strings.TrimSpace(fw.DirMode)) { + case "", "0": + // Preserve current behavior: locked-down directories by default. + if err := os.MkdirAll(dirPath, 0o700); err != nil { + return nil, err + } + case "inherit": + if err := mkdirAllInherit(dirPath); err != nil { + return nil, err + } + case "from_file": + if err := mkdirAllFromFile(dirPath, os.FileMode(fw.Mode)); err != nil { + return nil, err + } + default: + dm, err := parseFileMode(fw.DirMode) + if err != nil { + return nil, fmt.Errorf("dir_mode: %w", err) + } + if err := os.MkdirAll(dirPath, dm); err != nil { + return nil, err + } } + + // create/open the file file, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, modeIfCreating) if err != nil { return nil, err @@ -234,13 +265,70 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { RotateAtMinutes: fw.RollAtMinutes, RotateAt: fw.RollAt, BackupTimeFormat: fw.BackupTimeFormat, + FileMode: os.FileMode(fw.Mode), }, nil } +// normalizeDirPerm ensures that read bits also have execute bits set. +func normalizeDirPerm(p os.FileMode) os.FileMode { + if p&0o400 != 0 { + p |= 0o100 + } + if p&0o040 != 0 { + p |= 0o010 + } + if p&0o004 != 0 { + p |= 0o001 + } + return p +} + +// mkdirAllInherit creates missing dirs using the nearest existing parent's +// permissions, normalized with r→x. +func mkdirAllInherit(dir string) error { + if fi, err := os.Stat(dir); err == nil && fi.IsDir() { + return nil + } + cur := dir + var parent string + for { + next := filepath.Dir(cur) + if next == cur { + parent = next + break + } + if fi, err := os.Stat(next); err == nil { + if !fi.IsDir() { + return fmt.Errorf("path component %s exists and is not a directory", next) + } + parent = next + break + } + cur = next + } + perm := os.FileMode(0o700) + if fi, err := os.Stat(parent); err == nil && fi.IsDir() { + perm = fi.Mode().Perm() + } + perm = normalizeDirPerm(perm) + return os.MkdirAll(dir, perm) +} + +// mkdirAllFromFile creates missing dirs using the file's mode (with r→x) so +// 0644 → 0755, 0600 → 0700, etc. +func mkdirAllFromFile(dir string, fileMode os.FileMode) error { + if fi, err := os.Stat(dir); err == nil && fi.IsDir() { + return nil + } + perm := normalizeDirPerm(fileMode.Perm()) | 0o200 // ensure owner write on dir so files can be created + return os.MkdirAll(dir, perm) +} + // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: // // file { // mode +// dir_mode // roll_disabled // roll_size // roll_uncompressed @@ -284,6 +372,22 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } fw.Mode = fileMode(mode) + case "dir_mode": + var val string + if !d.AllArgs(&val) { + return d.ArgErr() + } + val = strings.TrimSpace(val) + switch strings.ToLower(val) { + case "inherit", "from_file": + fw.DirMode = val + default: + if _, err := parseFileMode(val); err != nil { + return d.Errf("parsing dir_mode: %v", err) + } + fw.DirMode = val + } + case "roll_disabled": var f bool fw.Roll = &f @@ -352,31 +456,48 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { fw.RollInterval = duration case "roll_minutes": - var minutesArrayStr string - if !d.AllArgs(&minutesArrayStr) { + // Accept either a single comma-separated argument or + // multiple space-separated arguments. Collect all + // remaining args on the line and split on commas. + args := d.RemainingArgs() + if len(args) == 0 { 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) + var minutes []int + for _, arg := range args { + parts := strings.SplitSeq(arg, ",") + for p := range parts { + ms := strings.TrimSpace(p) + if ms == "" { + return d.Errf("parsing roll_minutes: empty value") + } + m, err := strconv.Atoi(ms) + if err != nil { + return d.Errf("parsing roll_minutes number: %v", err) + } + minutes = append(minutes, m) } - minutes[i] = m } fw.RollAtMinutes = minutes case "roll_at": - var timeArrayStr string - if !d.AllArgs(&timeArrayStr) { + // Accept either a single comma-separated argument or + // multiple space-separated arguments. Collect all + // remaining args on the line and split on commas. + args := d.RemainingArgs() + if len(args) == 0 { return d.ArgErr() } - timeStr := strings.Split(timeArrayStr, ",") - times := make([]string, len(timeStr)) - for i := range timeStr { - times[i] = strings.Trim(timeStr[i], " ") + var times []string + for _, arg := range args { + parts := strings.SplitSeq(arg, ",") + for p := range parts { + ts := strings.TrimSpace(p) + if ts == "" { + return d.Errf("parsing roll_at: empty value") + } + times = append(times, ts) + } } fw.RollAt = times diff --git a/modules/logging/filewriter_test.go b/modules/logging/filewriter_test.go index 2a246156c..915784b53 100644 --- a/modules/logging/filewriter_test.go +++ b/modules/logging/filewriter_test.go @@ -385,3 +385,225 @@ func TestFileModeModification(t *testing.T) { t.Errorf("file mode is %v, want %v", st.Mode(), want) } } + +func TestDirMode_Inherit(t *testing.T) { + m := syscall.Umask(0) + defer syscall.Umask(m) + + parent := t.TempDir() + if err := os.Chmod(parent, 0o755); err != nil { + t.Fatal(err) + } + + targetDir := filepath.Join(parent, "a", "b") + fw := &FileWriter{ + Filename: filepath.Join(targetDir, "test.log"), + DirMode: "inherit", + Mode: 0o640, + Roll: func() *bool { f := false; return &f }(), + } + w, err := fw.OpenWriter() + if err != nil { + t.Fatal(err) + } + _ = w.Close() + + st, err := os.Stat(targetDir) + if err != nil { + t.Fatal(err) + } + if got := st.Mode().Perm(); got != 0o755 { + t.Fatalf("dir perm = %o, want 0755", got) + } +} + +func TestDirMode_FromFile(t *testing.T) { + m := syscall.Umask(0) + defer syscall.Umask(m) + + base := t.TempDir() + + dir1 := filepath.Join(base, "logs1") + fw1 := &FileWriter{ + Filename: filepath.Join(dir1, "app.log"), + DirMode: "from_file", + Mode: 0o644, // => dir 0755 + Roll: func() *bool { f := false; return &f }(), + } + w1, err := fw1.OpenWriter() + if err != nil { + t.Fatal(err) + } + _ = w1.Close() + + st1, err := os.Stat(dir1) + if err != nil { + t.Fatal(err) + } + if got := st1.Mode().Perm(); got != 0o755 { + t.Fatalf("dir perm = %o, want 0755", got) + } + + dir2 := filepath.Join(base, "logs2") + fw2 := &FileWriter{ + Filename: filepath.Join(dir2, "app.log"), + DirMode: "from_file", + Mode: 0o600, // => dir 0700 + Roll: func() *bool { f := false; return &f }(), + } + w2, err := fw2.OpenWriter() + if err != nil { + t.Fatal(err) + } + _ = w2.Close() + + st2, err := os.Stat(dir2) + if err != nil { + t.Fatal(err) + } + if got := st2.Mode().Perm(); got != 0o700 { + t.Fatalf("dir perm = %o, want 0700", got) + } +} + +func TestDirMode_ExplicitOctal(t *testing.T) { + m := syscall.Umask(0) + defer syscall.Umask(m) + + base := t.TempDir() + dest := filepath.Join(base, "logs3") + fw := &FileWriter{ + Filename: filepath.Join(dest, "app.log"), + DirMode: "0750", + Mode: 0o640, + Roll: func() *bool { f := false; return &f }(), + } + w, err := fw.OpenWriter() + if err != nil { + t.Fatal(err) + } + _ = w.Close() + + st, err := os.Stat(dest) + if err != nil { + t.Fatal(err) + } + if got := st.Mode().Perm(); got != 0o750 { + t.Fatalf("dir perm = %o, want 0750", got) + } +} + +func TestDirMode_Default0700(t *testing.T) { + m := syscall.Umask(0) + defer syscall.Umask(m) + + base := t.TempDir() + dest := filepath.Join(base, "logs4") + fw := &FileWriter{ + Filename: filepath.Join(dest, "app.log"), + Mode: 0o640, + Roll: func() *bool { f := false; return &f }(), + } + w, err := fw.OpenWriter() + if err != nil { + t.Fatal(err) + } + _ = w.Close() + + st, err := os.Stat(dest) + if err != nil { + t.Fatal(err) + } + if got := st.Mode().Perm(); got != 0o700 { + t.Fatalf("dir perm = %o, want 0700", got) + } +} + +func TestDirMode_UmaskInteraction(t *testing.T) { + _ = syscall.Umask(0o022) // typical umask; restore after + defer syscall.Umask(0) + + base := t.TempDir() + dest := filepath.Join(base, "logs5") + fw := &FileWriter{ + Filename: filepath.Join(dest, "app.log"), + DirMode: "0755", + Mode: 0o644, + Roll: func() *bool { f := false; return &f }(), + } + w, err := fw.OpenWriter() + if err != nil { + t.Fatal(err) + } + _ = w.Close() + + st, err := os.Stat(dest) + if err != nil { + t.Fatal(err) + } + // 0755 &^ 0022 still 0755 for dirs; this just sanity-checks we didn't get stricter unexpectedly + if got := st.Mode().Perm(); got != 0o755 { + t.Fatalf("dir perm = %o, want 0755 (considering umask)", got) + } +} + +func TestCaddyfile_DirMode_Inherit(t *testing.T) { + d := caddyfile.NewTestDispenser(` +file /var/log/app.log { + dir_mode inherit + mode 0640 +}`) + var fw FileWriter + if err := fw.UnmarshalCaddyfile(d); err != nil { + t.Fatal(err) + } + if fw.DirMode != "inherit" { + t.Fatalf("got %q", fw.DirMode) + } + if fw.Mode != 0o640 { + t.Fatalf("mode = %o", fw.Mode) + } +} + +func TestCaddyfile_DirMode_FromFile(t *testing.T) { + d := caddyfile.NewTestDispenser(` +file /var/log/app.log { + dir_mode from_file + mode 0600 +}`) + var fw FileWriter + if err := fw.UnmarshalCaddyfile(d); err != nil { + t.Fatal(err) + } + if fw.DirMode != "from_file" { + t.Fatalf("got %q", fw.DirMode) + } + if fw.Mode != 0o600 { + t.Fatalf("mode = %o", fw.Mode) + } +} + +func TestCaddyfile_DirMode_Octal(t *testing.T) { + d := caddyfile.NewTestDispenser(` +file /var/log/app.log { + dir_mode 0755 +}`) + var fw FileWriter + if err := fw.UnmarshalCaddyfile(d); err != nil { + t.Fatal(err) + } + if fw.DirMode != "0755" { + t.Fatalf("got %q", fw.DirMode) + } +} + +func TestCaddyfile_DirMode_Invalid(t *testing.T) { + d := caddyfile.NewTestDispenser(` +file /var/log/app.log { + dir_mode nope +}`) + var fw FileWriter + if err := fw.UnmarshalCaddyfile(d); err == nil { + t.Fatal("expected error for invalid dir_mode") + } +} diff --git a/modules/logging/filewriter_test_windows.go b/modules/logging/filewriter_test_windows.go index a032d1c28..254d5c30e 100644 --- a/modules/logging/filewriter_test_windows.go +++ b/modules/logging/filewriter_test_windows.go @@ -53,3 +53,41 @@ func TestFileCreationMode(t *testing.T) { t.Fatalf("file mode is %v, want rw for user", st.Mode().Perm()) } } + +func TestDirMode_Windows_CreateSucceeds(t *testing.T) { + dir, err := os.MkdirTemp("", "caddytest") + if err != nil { + t.Fatalf("failed to create tempdir: %v", err) + } + defer os.RemoveAll(dir) + + tests := []struct { + name string + dirMode string + }{ + {"inherit", "inherit"}, + {"from_file", "from_file"}, + {"octal", "0755"}, + {"default", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + subdir := path.Join(dir, "logs-"+tt.name) + fw := &FileWriter{ + Filename: path.Join(subdir, "test.log"), + DirMode: tt.dirMode, + Mode: 0o600, + } + w, err := fw.OpenWriter() + if err != nil { + t.Fatalf("failed to open writer: %v", err) + } + defer w.Close() + + if _, err := os.Stat(fw.Filename); err != nil { + t.Fatalf("expected file to exist: %v", err) + } + }) + } +} From 987375297862d9cd0a3fa33cfb199c25e504ad1b Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Mon, 23 Feb 2026 18:04:45 -0500 Subject: [PATCH 089/206] logging: Support `zstd` roll compression (#7515) --- .../log_roll_days.caddyfiletest | 4 ++ modules/logging/filewriter.go | 44 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest index 3e2aa0d81..9b15eb282 100644 --- a/caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest @@ -6,6 +6,7 @@ log one { dir_mode 0755 roll_size 1gb roll_uncompressed + roll_compression none roll_local_time roll_keep 5 roll_keep_for 90d @@ -16,6 +17,7 @@ log two { mode 0777 dir_mode from_file roll_size 1gib + roll_compression zstd roll_interval 12h roll_at 00:00 06:00 12:00,18:00 roll_minutes 10 40 45,46 @@ -39,6 +41,7 @@ log two { "filename": "/var/log/access.log", "mode": "0644", "output": "file", + "roll_compression": "none", "roll_gzip": false, "roll_keep": 5, "roll_keep_days": 90, @@ -61,6 +64,7 @@ log two { "12:00", "18:00" ], + "roll_compression": "zstd", "roll_interval": 43200000000000, "roll_keep": 10, "roll_keep_days": 90, diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index e9ca4013a..0ffcfa9bf 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -122,9 +122,16 @@ type FileWriter struct { // 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 + // Whether to compress rolled files. + // Default: true. + // Deprecated: Use RollCompression instead, setting it to "none". RollCompress *bool `json:"roll_gzip,omitempty"` + // RollCompression selects the compression algorithm for rolled files. + // Accepted values: "none", "gzip", "zstd". + // Default: gzip + RollCompression string `json:"roll_compression,omitempty"` + // Whether to use local timestamps in rolled filenames. // Default: false RollLocalTime bool `json:"roll_local_time,omitempty"` @@ -254,13 +261,32 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { if fw.RollKeepDays == 0 { fw.RollKeepDays = 90 } + + // Determine compression algorithm to use. Priority: + // 1) explicit RollCompression (none|gzip|zstd) + // 2) if RollCompress is unset or true -> gzip + // 3) if RollCompress is false -> none + var compression string + if fw.RollCompression != "" { + compression = strings.ToLower(strings.TrimSpace(fw.RollCompression)) + if compression != "none" && compression != "gzip" && compression != "zstd" { + return nil, fmt.Errorf("invalid roll_compression: %s", fw.RollCompression) + } + } else { + if fw.RollCompress == nil || *fw.RollCompress { + compression = "gzip" + } else { + compression = "none" + } + } + return &timberjack.Logger{ Filename: fw.Filename, MaxSize: fw.RollSizeMB, MaxAge: fw.RollKeepDays, MaxBackups: fw.RollKeep, LocalTime: fw.RollLocalTime, - Compress: *fw.RollCompress, + Compression: compression, RotationInterval: fw.RollInterval, RotateAtMinutes: fw.RollAtMinutes, RotateAt: fw.RollAt, @@ -332,6 +358,7 @@ func mkdirAllFromFile(dir string, fileMode os.FileMode) error { // roll_disabled // roll_size // roll_uncompressed +// roll_compression // roll_local_time // roll_keep // roll_keep_for @@ -413,6 +440,19 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } + case "roll_compression": + var comp string + if !d.AllArgs(&comp) { + return d.ArgErr() + } + comp = strings.ToLower(strings.TrimSpace(comp)) + switch comp { + case "none", "gzip", "zstd": + fw.RollCompression = comp + default: + return d.Errf("parsing roll_compression: must be 'none', 'gzip' or 'zstd'") + } + case "roll_local_time": fw.RollLocalTime = true if d.NextArg() { From 9798f6964d58eb5703d1498804962faca2dae4ea Mon Sep 17 00:00:00 2001 From: Fardjad Davari Date: Wed, 25 Feb 2026 10:08:41 +0100 Subject: [PATCH 090/206] caddyhttp: Avoid nil pointer dereference in proxyWrapper (#7521) --- modules/caddyhttp/reverseproxy/httptransport.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index db806acbd..8d2b99e9e 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -384,6 +384,9 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e } // we need to keep track if a proxy is used for a request proxyWrapper := func(req *http.Request) (*url.URL, error) { + if proxy == nil { + return nil, nil + } u, err := proxy(req) if u == nil || err != nil { return u, err From 72eaf2583ae70c352b6496598b0c3dc19820ace3 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Thu, 26 Feb 2026 23:01:35 +0200 Subject: [PATCH 091/206] chore: Enable modernize linter (#7519) --- .golangci.yml | 1 + caddyconfig/caddyfile/dispenser.go | 2 +- caddyconfig/caddyfile/parse.go | 2 +- caddyconfig/httploader.go | 2 +- listeners.go | 2 +- modules/caddyhttp/headers/headers.go | 6 +++--- modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go | 4 ++-- modules/caddyhttp/reverseproxy/selectionpolicies.go | 2 +- modules/caddyhttp/reverseproxy/streaming.go | 2 +- modules/logging/filewriter.go | 2 +- 10 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 4f4545054..e800788f5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -32,6 +32,7 @@ linters: - importas - ineffassign - misspell + - modernize - prealloc - promlinter - sloglint diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go index d95196e48..66b9b1087 100644 --- a/caddyconfig/caddyfile/dispenser.go +++ b/caddyconfig/caddyfile/dispenser.go @@ -270,7 +270,7 @@ func (d *Dispenser) File() string { // targets are left unchanged. If all the targets are filled, // then true is returned. func (d *Dispenser) Args(targets ...*string) bool { - for i := 0; i < len(targets); i++ { + for i := range targets { if !d.NextArg() { return false } diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index 5e6a21e2d..d11582392 100644 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -616,7 +616,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) { if err != nil { return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err) } - for i := 0; i < len(importedTokens); i++ { + for i := range importedTokens { importedTokens[i].File = filename } diff --git a/caddyconfig/httploader.go b/caddyconfig/httploader.go index e1a0fc85b..a0a46460a 100644 --- a/caddyconfig/httploader.go +++ b/caddyconfig/httploader.go @@ -151,7 +151,7 @@ func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http var err error const maxAttempts = 10 - for i := 0; i < maxAttempts; i++ { + for i := range maxAttempts { resp, err = attemptHttpCall(client, request) if err != nil && i < maxAttempts-1 { select { diff --git a/listeners.go b/listeners.go index 326c18573..0639b16b7 100644 --- a/listeners.go +++ b/listeners.go @@ -229,7 +229,7 @@ func (na NetworkAddress) JoinHostPort(offset uint) string { func (na NetworkAddress) Expand() []NetworkAddress { size := na.PortRangeSize() addrs := make([]NetworkAddress, size) - for portOffset := uint(0); portOffset < size; portOffset++ { + for portOffset := range size { addrs[portOffset] = na.At(portOffset) } return addrs diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index b8226ceec..97eee07ba 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -161,11 +161,11 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error { // containsPlaceholders checks if the string contains Caddy placeholder syntax {key} func containsPlaceholders(s string) bool { - openIdx := strings.Index(s, "{") - if openIdx == -1 { + _, after, ok := strings.Cut(s, "{") + if !ok { return false } - closeIdx := strings.Index(s[openIdx+1:], "}") + closeIdx := strings.Index(after, "}") if closeIdx == -1 { return false } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index c60da897b..c4279d9a0 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -442,7 +442,7 @@ func (t Transport) splitPos(path string) int { for _, split := range t.SplitPath { splitLen := len(split) - for i := 0; i < pathLen; i++ { + for i := range pathLen { if path[i] >= utf8.RuneSelf { if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 { return end @@ -456,7 +456,7 @@ func (t Transport) splitPos(path string) int { } match := true - for j := 0; j < splitLen; j++ { + for j := range splitLen { c := path[i+j] if c >= utf8.RuneSelf { diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index 3b68f504c..cd1e469f4 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -312,7 +312,7 @@ func (r *RoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http. if n == 0 { return nil } - for i := uint32(0); i < n; i++ { + for range n { robin := atomic.AddUint32(&r.robin, 1) host := pool[robin%n] if host.Available() { diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index 38f056cd8..64b6d39d1 100644 --- a/modules/caddyhttp/reverseproxy/streaming.go +++ b/modules/caddyhttp/reverseproxy/streaming.go @@ -536,7 +536,7 @@ func maskBytes(key [4]byte, pos int, b []byte) int { // Mask one word at a time. n := (len(b) / wordSize) * wordSize for i := 0; i < n; i += wordSize { - *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw + *(*uintptr)(unsafe.Add(unsafe.Pointer(&b[0]), i)) ^= kw } // Mask one byte at a time for remaining bytes. diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index 0ffcfa9bf..0445ef06f 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -63,7 +63,7 @@ func (m *fileMode) UnmarshalJSON(b []byte) error { // MarshalJSON satisfies json.Marshaler. func (m *fileMode) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf("\"%04o\"", *m)), nil + return fmt.Appendf(nil, "\"%04o\"", *m), nil } // parseFileMode parses a file mode string, From eac02ee98f9f61d79ae774aa49aa102ddf2076ba Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 27 Feb 2026 10:22:39 -0700 Subject: [PATCH 092/206] caddyhttp: Limit empty Host check to HTTP/1.1 --- modules/caddyhttp/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 70635d959..41a8e55b0 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -486,7 +486,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error { // code to any HTTP/1.1 request message that lacks a Host header field and to any // request message that contains more than one Host header field line or a Host // header field with an invalid field value." - if r.Host == "" { + if r.ProtoMajor == 1 && r.ProtoMinor == 1 && r.Host == "" { return HandlerError{ Err: errors.New("rfc9112 forbids empty Host"), StatusCode: http.StatusBadRequest, From ce203aa9e1ff2686777019f45c36c283debb7986 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 27 Feb 2026 10:35:24 -0700 Subject: [PATCH 093/206] go.mod: Upgrade x/net --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f5015aa0f..a3dbf86eb 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( go.uber.org/zap/exp v0.3.0 golang.org/x/crypto v0.48.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 - golang.org/x/net v0.50.0 + golang.org/x/net v0.51.0 golang.org/x/sync v0.19.0 golang.org/x/term v0.40.0 golang.org/x/time v0.14.0 diff --git a/go.sum b/go.sum index 8d7cdee58..cbce3127d 100644 --- a/go.sum +++ b/go.sum @@ -477,8 +477,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 06a05e383c8fd6d693e2b8f09db1b8fa55090026 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 27 Feb 2026 14:14:19 -0700 Subject: [PATCH 094/206] Revert "encode: Implement Flush for legacy compatibility" This reverts commit bdcdaf77ba6276b5ead20fa2518e00391150523d. --- modules/caddyhttp/encode/encode.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index e7f98a31a..ac995c37b 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -307,14 +307,6 @@ func (rw *responseWriter) FlushError() error { return http.NewResponseController(rw.ResponseWriter).Flush() } -// Flush calls FlushError() and simply discards any error. It is only implemented for backwards -// compatibility with legacy code that does not use FlushError; we know at least one sponsor -// needs this. It should not be relied upon as a stable part of the exported API, as it may be -// removed in the future. -func (rw *responseWriter) Flush() { - _ = rw.FlushError() -} - // Write writes to the response. If the response qualifies, // it is encoded using the encoder, which is initialized // if not done so already. From cd9e1660aa454e6536ad740c9d1aaecf3c3552ed Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Fri, 27 Feb 2026 15:24:05 -0700 Subject: [PATCH 095/206] cmd: Pass configFile, not configFlag, for reload command (#7532) * cmd: Pass configFile, not configFlag, for reload command This *should* fix #7528. * Remove debug log line --------- Co-authored-by: Francis Lavoie --- cmd/commandfuncs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 3b458c2ba..28ea20001 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -372,7 +372,7 @@ func cmdReload(fl Flags) (int, error) { return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load") } - adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag) + adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFile, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } From 174fa2ddb93f830370b05058d1ea51ad1512597d Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Sat, 28 Feb 2026 22:03:18 -0700 Subject: [PATCH 096/206] caddyhttp: Evaluate tls.client placeholders more accurately (fix #7530) (#7534) --- modules/caddyhttp/replacer.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 5d600c334..e7974a561 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -420,7 +420,16 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) { if strings.HasPrefix(field, "client.") { cert := getTLSPeerCert(req.TLS) if cert == nil { - return nil, false + // Instead of returning (nil, false) here, we set it to a dummy + // value to fix #7530. This way, even if there is no client cert, + // evaluating placeholders with ReplaceKnown() will still remove + // the placeholder, which would be expected. It is not expected + // for the placeholder to sometimes get removed based on whether + // the client presented a cert. We also do not return true here + // because we probably should remain accurate about whether a + // placeholder is, in fact, known or not. + // (This allocation may be slightly inefficient.) + cert = new(x509.Certificate) } // subject alternate names (SANs) From f145bce553a306ca6890fea782ca4e3b06787bbb Mon Sep 17 00:00:00 2001 From: Pavel Siomachkin Date: Sun, 1 Mar 2026 21:32:04 +0100 Subject: [PATCH 097/206] tls: Add `tls_resolvers` global option for DNS challenge configuration (#7297) Co-authored-by: Francis Lavoie --- caddyconfig/httpcaddyfile/options.go | 10 ++ caddyconfig/httpcaddyfile/options_test.go | 104 ++++++++++++++++ caddyconfig/httpcaddyfile/tlsapp.go | 14 +++ .../global_options_resolvers.caddyfiletest | 77 ++++++++++++ ...ons_resolvers_http_challenge.caddyfiletest | 38 ++++++ ..._resolvers_local_dns_inherit.caddyfiletest | 72 +++++++++++ ...ons_resolvers_local_override.caddyfiletest | 98 +++++++++++++++ ...obal_options_resolvers_mixed.caddyfiletest | 112 ++++++++++++++++++ modules/caddypki/acmeserver/acmeserver.go | 15 ++- modules/caddytls/tls.go | 9 +- 10 files changed, 547 insertions(+), 2 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/global_options_resolvers.caddyfiletest create mode 100644 caddytest/integration/caddyfile_adapt/global_options_resolvers_http_challenge.caddyfiletest create mode 100644 caddytest/integration/caddyfile_adapt/global_options_resolvers_local_dns_inherit.caddyfiletest create mode 100644 caddytest/integration/caddyfile_adapt/global_options_resolvers_local_override.caddyfiletest create mode 100644 caddytest/integration/caddyfile_adapt/global_options_resolvers_mixed.caddyfiletest diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index f985cff9e..ffe43ff7e 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -64,6 +64,7 @@ func init() { RegisterGlobalOption("preferred_chains", parseOptPreferredChains) RegisterGlobalOption("persist_config", parseOptPersistConfig) RegisterGlobalOption("dns", parseOptDNS) + RegisterGlobalOption("tls_resolvers", parseOptTLSResolvers) RegisterGlobalOption("ech", parseOptECH) RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio) } @@ -306,6 +307,15 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) { return val, nil } +func parseOptTLSResolvers(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + resolvers := d.RemainingArgs() + if len(resolvers) == 0 { + return nil, d.ArgErr() + } + return resolvers, nil +} + func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) { d.Next() // consume option name diff --git a/caddyconfig/httpcaddyfile/options_test.go b/caddyconfig/httpcaddyfile/options_test.go index bc9e88134..524187f30 100644 --- a/caddyconfig/httpcaddyfile/options_test.go +++ b/caddyconfig/httpcaddyfile/options_test.go @@ -1,9 +1,11 @@ package httpcaddyfile import ( + "encoding/json" "testing" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddytls" _ "github.com/caddyserver/caddy/v2/modules/logging" ) @@ -62,3 +64,105 @@ func TestGlobalLogOptionSyntax(t *testing.T) { } } } + +func TestGlobalResolversOption(t *testing.T) { + tests := []struct { + name string + input string + expectResolvers []string + expectError bool + }{ + { + name: "single resolver", + input: `{ + tls_resolvers 1.1.1.1 + } + example.com { + }`, + expectResolvers: []string{"1.1.1.1"}, + expectError: false, + }, + { + name: "two resolvers", + input: `{ + tls_resolvers 1.1.1.1 8.8.8.8 + } + example.com { + }`, + expectResolvers: []string{"1.1.1.1", "8.8.8.8"}, + expectError: false, + }, + { + name: "multiple resolvers", + input: `{ + tls_resolvers 1.1.1.1 8.8.8.8 9.9.9.9 + } + example.com { + }`, + expectResolvers: []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"}, + expectError: false, + }, + { + name: "no resolvers specified", + input: `{ + } + example.com { + }`, + expectResolvers: nil, + expectError: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + adapter := caddyfile.Adapter{ + ServerType: ServerType{}, + } + + out, _, err := adapter.Adapt([]byte(tc.input), nil) + + if (err != nil) != tc.expectError { + t.Errorf("error expectation failed. Expected error: %v, got: %v", tc.expectError, err) + return + } + + if tc.expectError { + return + } + + // Parse the output JSON to check resolvers + var config struct { + Apps struct { + TLS *caddytls.TLS `json:"tls"` + } `json:"apps"` + } + + if err := json.Unmarshal(out, &config); err != nil { + t.Errorf("failed to unmarshal output: %v", err) + return + } + + // Check if resolvers match expected + if config.Apps.TLS == nil { + if tc.expectResolvers != nil { + t.Errorf("Expected TLS config with resolvers %v, but TLS config is nil", tc.expectResolvers) + } + return + } + + actualResolvers := config.Apps.TLS.Resolvers + if len(tc.expectResolvers) == 0 && len(actualResolvers) == 0 { + return // Both empty, ok + } + if len(actualResolvers) != len(tc.expectResolvers) { + t.Errorf("Expected %d resolvers, got %d. Expected: %v, got: %v", len(tc.expectResolvers), len(actualResolvers), tc.expectResolvers, actualResolvers) + return + } + for j, expected := range tc.expectResolvers { + if actualResolvers[j] != expected { + t.Errorf("Resolver %d mismatch. Expected: %s, got: %s", j, expected, actualResolvers[j]) + } + } + }) + } +} diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index d14bd17fb..ddec0b941 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -334,6 +334,11 @@ func (st ServerType) buildTLSApp( tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil) } + // set up "global" (to the TLS app) DNS resolvers config + if globalResolvers, ok := options["tls_resolvers"]; ok && globalResolvers != nil { + tlsApp.Resolvers = globalResolvers.([]string) + } + // set up ECH from Caddyfile options if ech, ok := options["ech"].(*caddytls.ECH); ok { tlsApp.EncryptedClientHello = ech @@ -595,6 +600,15 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 { acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration) } + // apply global resolvers if DNS challenge is configured and resolvers are not already set + globalResolvers := options["tls_resolvers"] + if globalResolvers != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil { + // Check if DNS challenge is actually configured + hasDNSChallenge := globalACMEDNSok || acmeIssuer.Challenges.DNS.ProviderRaw != nil + if hasDNSChallenge && len(acmeIssuer.Challenges.DNS.Resolvers) == 0 { + acmeIssuer.Challenges.DNS.Resolvers = globalResolvers.([]string) + } + } return nil } diff --git a/caddytest/integration/caddyfile_adapt/global_options_resolvers.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_resolvers.caddyfiletest new file mode 100644 index 000000000..7043b5da3 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_resolvers.caddyfiletest @@ -0,0 +1,77 @@ +{ + email test@example.com + dns mock + tls_resolvers 1.1.1.1 8.8.8.8 + acme_dns +} + +example.com { +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "issuers": [ + { + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + }, + { + "ca": "https://acme.zerossl.com/v2/DV90", + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + } + ] + }, + "dns": { + "name": "mock" + }, + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/global_options_resolvers_http_challenge.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_resolvers_http_challenge.caddyfiletest new file mode 100644 index 000000000..d375dc711 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_resolvers_http_challenge.caddyfiletest @@ -0,0 +1,38 @@ +{ + tls_resolvers 1.1.1.1 8.8.8.8 +} + +example.com { +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_dns_inherit.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_dns_inherit.caddyfiletest new file mode 100644 index 000000000..20385f84b --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_dns_inherit.caddyfiletest @@ -0,0 +1,72 @@ +{ + email test@example.com + dns mock + tls_resolvers 1.1.1.1 8.8.8.8 +} + +example.com { + tls { + dns mock + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "example.com" + ], + "issuers": [ + { + "challenges": { + "dns": { + "provider": { + "name": "mock" + }, + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + } + ] + }, + "dns": { + "name": "mock" + }, + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_override.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_override.caddyfiletest new file mode 100644 index 000000000..27f7d09d3 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_override.caddyfiletest @@ -0,0 +1,98 @@ +{ + email test@example.com + dns mock + tls_resolvers 1.1.1.1 8.8.8.8 + acme_dns +} + +example.com { + tls { + resolvers 9.9.9.9 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "example.com" + ], + "issuers": [ + { + "challenges": { + "dns": { + "resolvers": [ + "9.9.9.9" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + }, + { + "issuers": [ + { + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + }, + { + "ca": "https://acme.zerossl.com/v2/DV90", + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + } + ] + }, + "dns": { + "name": "mock" + }, + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/global_options_resolvers_mixed.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_resolvers_mixed.caddyfiletest new file mode 100644 index 000000000..3a4b5571c --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_resolvers_mixed.caddyfiletest @@ -0,0 +1,112 @@ +{ + email test@example.com + dns mock + tls_resolvers 1.1.1.1 8.8.8.8 + acme_dns +} + +site1.example.com { +} + +site2.example.com { + tls { + resolvers 9.9.9.9 8.8.4.4 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "site1.example.com" + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "site2.example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "site2.example.com" + ], + "issuers": [ + { + "challenges": { + "dns": { + "resolvers": [ + "9.9.9.9", + "8.8.4.4" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + }, + { + "issuers": [ + { + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + }, + { + "ca": "https://acme.zerossl.com/v2/DV90", + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + } + ] + }, + "dns": { + "name": "mock" + }, + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + } +} diff --git a/modules/caddypki/acmeserver/acmeserver.go b/modules/caddypki/acmeserver/acmeserver.go index 4d158ed9f..446fd456f 100644 --- a/modules/caddypki/acmeserver/acmeserver.go +++ b/modules/caddypki/acmeserver/acmeserver.go @@ -40,6 +40,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddypki" + "github.com/caddyserver/caddy/v2/modules/caddytls" ) func init() { @@ -304,7 +305,19 @@ func (ash Handler) openDatabase() (*db.AuthDB, error) { // makeClient creates an ACME client which will use a custom // resolver instead of net.DefaultResolver. func (ash Handler) makeClient() (acme.Client, error) { - for _, v := range ash.Resolvers { + // If no local resolvers are configured, check for global resolvers from TLS app + resolversToUse := ash.Resolvers + if len(resolversToUse) == 0 { + tlsAppIface, err := ash.ctx.App("tls") + if err == nil { + tlsApp := tlsAppIface.(*caddytls.TLS) + if len(tlsApp.Resolvers) > 0 { + resolversToUse = tlsApp.Resolvers + } + } + } + + for _, v := range resolversToUse { addr, err := caddy.ParseNetworkAddressWithDefaults(v, "udp", 53) if err != nil { return nil, err diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 0d2dfcb6c..34ffbf62d 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -123,8 +123,15 @@ type TLS struct { // // EXPERIMENTAL: Subject to change. DNSRaw json.RawMessage `json:"dns,omitempty" caddy:"namespace=dns.providers inline_key=name"` - dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) + // The default DNS resolvers to use for TLS-related DNS operations, specifically + // for ACME DNS challenges and ACME server DNS validations. + // If not specified, the system default resolvers will be used. + // + // EXPERIMENTAL: Subject to change. + Resolvers []string `json:"resolvers,omitempty"` + + dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) certificateLoaders []CertificateLoader automateNames map[string]struct{} ctx caddy.Context From 2ab043b8903db4574b2fc7a625619018a072f082 Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Mon, 2 Mar 2026 15:04:06 +0800 Subject: [PATCH 098/206] reverseproxy: query escape request urls when proxy protocol is enabled (#7537) --- modules/caddyhttp/reverseproxy/reverseproxy.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index f9fdd164e..fb42828bf 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -1278,7 +1278,12 @@ func (h *Handler) directRequest(req *http.Request, di DialInfo) { // add client address to the host to let transport differentiate requests from different clients if ppt, ok := h.Transport.(ProxyProtocolTransport); ok && ppt.ProxyProtocolEnabled() { if proxyProtocolInfo, ok := caddyhttp.GetVar(req.Context(), proxyProtocolInfoVarKey).(ProxyProtocolInfo); ok { - reqHost = proxyProtocolInfo.AddrPort.String() + "->" + reqHost + // encode the request so it plays well with h2 transport, it's unnecessary for h1 but anyway + // The issue is that h2 transport will use the address to determine if new connections are needed + // to roundtrip requests but the without escaping, new connections are constantly created and closed until + // file descriptors are exhausted. + // see: https://github.com/caddyserver/caddy/issues/7529 + reqHost = url.QueryEscape(proxyProtocolInfo.AddrPort.String() + "->" + reqHost) } } From f283062d37c50627d53ca682ebae2ce219b35515 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:04:28 +0100 Subject: [PATCH 099/206] cmd: Custom binary names through `CustomBinaryName` and `CustomLongDescription` (#7513) --- caddy.go | 28 ++++++++++++++++++++++++++++ cmd/cobra.go | 18 ++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/caddy.go b/caddy.go index 7309d4471..59c410876 100644 --- a/caddy.go +++ b/caddy.go @@ -945,6 +945,34 @@ func InstanceID() (uuid.UUID, error) { // for example. var CustomVersion string +// CustomBinaryName is an optional string that overrides the root +// command name from the default of "caddy". This is useful for +// downstream projects that embed Caddy but use a different binary +// name. Shell completions and help text will use this name instead +// of "caddy". +// +// Set this variable during `go build` with `-ldflags`: +// +// -ldflags '-X github.com/caddyserver/caddy/v2.CustomBinaryName=my_custom_caddy' +// +// for example. +var CustomBinaryName string + +// CustomLongDescription is an optional string that overrides the +// long description of the root Cobra command. This is useful for +// downstream projects that embed Caddy but want different help +// output. +// +// Set this variable in an init() function of a package that is +// imported by your main: +// +// func init() { +// caddy.CustomLongDescription = "My custom server based on Caddy..." +// } +// +// for example. +var CustomLongDescription string + // Version returns the Caddy version in a simple/short form, and // a full version string. The short form will not have spaces and // is intended for User-Agent strings and similar, but may be diff --git a/cmd/cobra.go b/cmd/cobra.go index 9ecb389e2..14c8d2988 100644 --- a/cmd/cobra.go +++ b/cmd/cobra.go @@ -9,9 +9,14 @@ import ( ) var defaultFactory = newRootCommandFactory(func() *cobra.Command { - return &cobra.Command{ - Use: "caddy", - Long: `Caddy is an extensible server platform written in Go. + bin := caddy.CustomBinaryName + if bin == "" { + bin = "caddy" + } + + long := caddy.CustomLongDescription + if long == "" { + long = `Caddy is an extensible server platform written in Go. At its core, Caddy merely manages configuration. Modules are plugged in statically at compile-time to provide useful functionality. Caddy's @@ -91,7 +96,12 @@ package installers: https://caddyserver.com/docs/install Instructions for running Caddy in production are also available: https://caddyserver.com/docs/running -`, +` + } + + return &cobra.Command{ + Use: bin, + Long: long, Example: ` $ caddy run $ caddy run --config caddy.json $ caddy reload --config caddy.json From 11b56c6cfc25f8c814fa66cb02060548d12c4040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ak=C4=B1n=20Demirci?= Date: Tue, 3 Mar 2026 21:10:54 +0300 Subject: [PATCH 100/206] reverseproxy: Fix `health_port` being ignored in health checks (#7533) --- caddytest/integration/reverseproxy_test.go | 62 +++++++++++++++++++ .../caddyhttp/reverseproxy/healthchecks.go | 6 ++ 2 files changed, 68 insertions(+) diff --git a/caddytest/integration/reverseproxy_test.go b/caddytest/integration/reverseproxy_test.go index 0b992d3e3..6e0b3dcff 100644 --- a/caddytest/integration/reverseproxy_test.go +++ b/caddytest/integration/reverseproxy_test.go @@ -386,6 +386,68 @@ func TestReverseProxyHealthCheck(t *testing.T) { tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") } +// TestReverseProxyHealthCheckPortUsed verifies that health_port is actually +// used for active health checks and not the upstream's main port. This is a +// regression test for https://github.com/caddyserver/caddy/issues/7524. +func TestReverseProxyHealthCheckPortUsed(t *testing.T) { + // upstream server: serves proxied requests normally, but returns 503 for + // /health so that if health checks mistakenly hit this port the upstream + // gets marked unhealthy and the proxy returns 503. + upstreamSrv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/health" { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + _, _ = w.Write([]byte("Hello, World!")) + }), + } + ln0, err := net.Listen("tcp", "127.0.0.1:2022") + if err != nil { + t.Fatalf("failed to listen on 127.0.0.1:2022: %v", err) + } + go upstreamSrv.Serve(ln0) + t.Cleanup(func() { upstreamSrv.Close(); ln0.Close() }) + + // separate health check server on the configured health_port: returns 200 + // so the upstream is marked healthy only if health checks go to this port. + healthSrv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte("ok")) + }), + } + ln1, err := net.Listen("tcp", "127.0.0.1:2023") + if err != nil { + t.Fatalf("failed to listen on 127.0.0.1:2023: %v", err) + } + go healthSrv.Serve(ln1) + t.Cleanup(func() { healthSrv.Close(); ln1.Close() }) + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + grace_period 1ns + } + http://localhost:9080 { + reverse_proxy { + to localhost:2022 + + health_uri /health + health_port 2023 + health_interval 10ms + health_timeout 100ms + health_passes 1 + health_fails 1 + } + } + `, "caddyfile") + tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") +} + func TestReverseProxyHealthCheckUnixSocket(t *testing.T) { if runtime.GOOS == "windows" { t.SkipNow() diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index a194b88c8..73604f916 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -359,6 +359,12 @@ func (h *Handler) doActiveHealthCheckForAllHosts() { dialInfoUpstream = &Upstream{ Dial: h.HealthChecks.Active.Upstream, } + } else if upstream.activeHealthCheckPort != 0 { + // health_port overrides the port; addr has already been updated + // with the health port, so use its address for dialing + dialInfoUpstream = &Upstream{ + Dial: addr.JoinHostPort(0), + } } dialInfo, _ := dialInfoUpstream.fillDialInfo(repl) From 2dd3852416e2b04bc90b98643673ec9e131c32e6 Mon Sep 17 00:00:00 2001 From: prettysunflower Date: Tue, 3 Mar 2026 13:16:21 -0500 Subject: [PATCH 101/206] fix(caddyfile): Prevent parser to panic when no token were added by empty {block} (#7543) --- caddyconfig/caddyfile/parse.go | 2 +- ...ced_block_from_separate_file.caddyfiletest | 52 +++++++++++++++++++ ...ssue_7518_unused_block_panic_snippets.conf | 15 ++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 caddytest/integration/caddyfile_adapt/import_block_snippet_non_replaced_block_from_separate_file.caddyfiletest create mode 100644 caddytest/integration/testdata/issue_7518_unused_block_panic_snippets.conf diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index d11582392..e9f27dfbf 100644 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -507,7 +507,7 @@ func (p *parser) doImport(nesting int) error { // format, won't check for nesting correctness or any other error, that's what parser does. if !maybeSnippet && nesting == 0 { // first of the line - if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) { + if i == 0 || isNextOnNewLine(tokensCopy[len(tokensCopy)-1], token) { index = 0 } else { index++ diff --git a/caddytest/integration/caddyfile_adapt/import_block_snippet_non_replaced_block_from_separate_file.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_block_snippet_non_replaced_block_from_separate_file.caddyfiletest new file mode 100644 index 000000000..b42a84a09 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/import_block_snippet_non_replaced_block_from_separate_file.caddyfiletest @@ -0,0 +1,52 @@ +import testdata/issue_7518_unused_block_panic_snippets.conf + +example.com { + import snippet +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "headers", + "response": { + "set": { + "Reverse_proxy": [ + "localhost:3000" + ] + } + } + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/testdata/issue_7518_unused_block_panic_snippets.conf b/caddytest/integration/testdata/issue_7518_unused_block_panic_snippets.conf new file mode 100644 index 000000000..0f3e53a2b --- /dev/null +++ b/caddytest/integration/testdata/issue_7518_unused_block_panic_snippets.conf @@ -0,0 +1,15 @@ +# Used by import_block_snippet_non_replaced_block_from_separate_file.caddyfiletest + +(snippet) { + header { + reverse_proxy localhost:3000 + {block} + } +} + +# This snippet being unused by the test Caddyfile is intentional. +# This is to test that a panic runtime error triggered by an out-of-range slice index access +# will not happen again, please see issue #7518 and pull request #7543 for more information +(unused_snippet) { + header SomeHeader SomeValue +} \ No newline at end of file From d935a6956c16902623b8e8f6d1aafec4f6124f46 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Tue, 3 Mar 2026 16:44:06 -0500 Subject: [PATCH 102/206] autohttps: Ensure CertMagic config is recreated after autohttps runs (#7510) --- modules/caddyhttp/autohttps.go | 21 ++++++++++++ modules/caddytls/automation.go | 62 ++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 2ae50f725..32e9f106d 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -614,6 +614,27 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames, tails } } + // Ensure automation policies' CertMagic configs are rebuilt when + // ACME issuer templates may have been modified above (for example, + // alternate ports filled in by the HTTP app). If a policy is already + // provisioned, perform a lightweight rebuild of the CertMagic config + // so issuers receive SetConfig with the updated templates; otherwise + // run a normal Provision to initialize the policy. + for i, ap := range app.tlsApp.Automation.Policies { + // If the policy is already provisioned, rebuild only the CertMagic + // config so issuers get SetConfig with updated templates. Otherwise + // provision the policy normally (which may load modules). + if ap.IsProvisioned() { + if err := ap.RebuildCertMagic(app.tlsApp); err != nil { + return fmt.Errorf("rebuilding certmagic config for automation policy %d: %v", i, err) + } + } else { + if err := ap.Provision(app.tlsApp); err != nil { + return fmt.Errorf("provisioning automation policy %d after auto-HTTPS defaults: %v", i, err) + } + } + } + if basePolicy == nil { // no base policy found; we will make one basePolicy = new(caddytls.AutomationPolicy) diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index e69b5ad2f..74125a222 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -243,22 +243,49 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { } } + // build certmagic.Config and attach it to the policy + storage := ap.storage + if storage == nil { + storage = tlsApp.ctx.Storage() + } + cfg, err := ap.makeCertMagicConfig(tlsApp, issuers, storage) + if err != nil { + return err + } + certCacheMu.RLock() + ap.magic = certmagic.New(certCache, cfg) + certCacheMu.RUnlock() + + // give issuers a chance to see the config pointer + for _, issuer := range ap.magic.Issuers { + if annoying, ok := issuer.(ConfigSetter); ok { + annoying.SetConfig(ap.magic) + } + } + + return nil +} + +// makeCertMagicConfig constructs a certmagic.Config for this policy using the +// provided issuers and storage. It encapsulates common logic shared between +// Provision and RebuildCertMagic so we don't duplicate code. +func (ap *AutomationPolicy) makeCertMagicConfig(tlsApp *TLS, issuers []certmagic.Issuer, storage certmagic.Storage) (certmagic.Config, error) { + // key source keyType := ap.KeyType if keyType != "" { var err error keyType, err = caddy.NewReplacer().ReplaceOrErr(ap.KeyType, true, true) if err != nil { - return fmt.Errorf("invalid key type %s: %s", ap.KeyType, err) + return certmagic.Config{}, fmt.Errorf("invalid key type %s: %s", ap.KeyType, err) } if _, ok := supportedCertKeyTypes[keyType]; !ok { - return fmt.Errorf("unrecognized key type: %s", keyType) + return certmagic.Config{}, fmt.Errorf("unrecognized key type: %s", keyType) } } keySource := certmagic.StandardKeyGenerator{ KeyType: supportedCertKeyTypes[keyType], } - storage := ap.storage if storage == nil { storage = tlsApp.ctx.Storage() } @@ -277,7 +304,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { if noProtections { if !ap.hadExplicitManagers { // no managers, no explicitly-configured permission module, this is a config error - return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details") + return certmagic.Config{}, fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details") } // allow on-demand to be enabled but only for the purpose of the Managers; issuance won't be allowed from Issuers tlsApp.logger.Warn("on-demand TLS can only get certificates from the configured external manager(s) because no ask endpoint / permission module is specified") @@ -334,7 +361,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { } } - template := certmagic.Config{ + cfg := certmagic.Config{ MustStaple: ap.MustStaple, RenewalWindowRatio: ap.RenewalWindowRatio, KeySource: keySource, @@ -349,8 +376,31 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { Issuers: issuers, Logger: tlsApp.logger, } + + return cfg, nil +} + +// IsProvisioned reports whether the automation policy has been +// provisioned. A provisioned policy has an initialized CertMagic +// instance (i.e. ap.magic != nil). +func (ap *AutomationPolicy) IsProvisioned() bool { return ap.magic != nil } + +// RebuildCertMagic rebuilds the policy's CertMagic configuration from the +// policy's already-populated fields (Issuers, Managers, storage, etc.) and +// replaces the internal CertMagic instance. This is a lightweight +// alternative to calling Provision because it does not re-provision +// modules or re-run module Provision; instead, it constructs a new +// certmagic.Config and calls SetConfig on issuers so they receive updated +// templates (for example, alternate HTTP/TLS ports supplied by the HTTP +// app). RebuildCertMagic should only be called when the policy's required +// fields are already populated. +func (ap *AutomationPolicy) RebuildCertMagic(tlsApp *TLS) error { + cfg, err := ap.makeCertMagicConfig(tlsApp, ap.Issuers, ap.storage) + if err != nil { + return err + } certCacheMu.RLock() - ap.magic = certmagic.New(certCache, template) + ap.magic = certmagic.New(certCache, cfg) certCacheMu.RUnlock() // sometimes issuers may need the parent certmagic.Config in From 45cf61b1276b2cf79ce0147df8fa48c96e8e5356 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Tue, 3 Mar 2026 16:44:42 -0500 Subject: [PATCH 103/206] logging: Ensure `slog` error level logs don't print stack traces (#7512) --- caddytest/integration/acme_test.go | 4 ++-- cmd/main.go | 8 +++++++- context.go | 11 ++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go index f10aef6a8..5473f9a81 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -51,7 +51,7 @@ func TestACMEServerWithDefaults(t *testing.T) { Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", HTTPClient: tester.Client, - Logger: slog.New(zapslog.NewHandler(logger.Core())), + Logger: slog.New(zapslog.NewHandler(logger.Core(), zapslog.WithName("acmez"))), }, ChallengeSolvers: map[string]acmez.Solver{ acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, @@ -120,7 +120,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) { Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", HTTPClient: tester.Client, - Logger: slog.New(zapslog.NewHandler(logger.Core())), + Logger: slog.New(zapslog.NewHandler(logger.Core(), zapslog.WithName("acmez"))), }, ChallengeSolvers: map[string]acmez.Solver{ acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, diff --git a/cmd/main.go b/cmd/main.go index 4a969573e..07666072f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -484,7 +484,13 @@ func setResourceLimits(logger *zap.Logger) func() { // See https://pkg.go.dev/runtime/debug#SetMemoryLimit _, _ = memlimit.SetGoMemLimitWithOpts( memlimit.WithLogger( - slog.New(zapslog.NewHandler(logger.Core())), + slog.New(zapslog.NewHandler( + logger.Core(), + zapslog.WithName("memlimit"), + // the default enables traces at ERROR level, this disables + // them by setting it to a level higher than any other level + zapslog.AddStacktraceAt(slog.Level(127)), + )), ), memlimit.WithProvider( memlimit.ApplyFallback( diff --git a/context.go b/context.go index 095598682..a12cdcad4 100644 --- a/context.go +++ b/context.go @@ -608,6 +608,11 @@ func (ctx Context) Slogger() *slog.Logger { core zapcore.Core moduleID string ) + + // the default enables traces at ERROR level, this disables + // them by setting it to a level higher than any other level + tracesOpt := zapslog.AddStacktraceAt(slog.Level(127)) + if ctx.cfg == nil { // often the case in tests; just use a dev logger l, err := zap.NewDevelopment() @@ -616,16 +621,16 @@ func (ctx Context) Slogger() *slog.Logger { } core = l.Core() - handler = zapslog.NewHandler(core) + handler = zapslog.NewHandler(core, tracesOpt) } else { mod := ctx.Module() if mod == nil { core = Log().Core() - handler = zapslog.NewHandler(core) + handler = zapslog.NewHandler(core, tracesOpt) } else { moduleID = string(mod.CaddyModule().ID) core = ctx.cfg.Logging.Logger(mod).Core() - handler = zapslog.NewHandler(core, zapslog.WithName(moduleID)) + handler = zapslog.NewHandler(core, zapslog.WithName(moduleID), tracesOpt) } } From a6acb3902cb6453153db0738bd8210e093449ce1 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Tue, 3 Mar 2026 17:08:09 -0500 Subject: [PATCH 104/206] proxyproto: Generated test coverage (#7540) --- caddytest/integration/proxyprotocol_test.go | 595 ++++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 caddytest/integration/proxyprotocol_test.go diff --git a/caddytest/integration/proxyprotocol_test.go b/caddytest/integration/proxyprotocol_test.go new file mode 100644 index 000000000..e57c323bc --- /dev/null +++ b/caddytest/integration/proxyprotocol_test.go @@ -0,0 +1,595 @@ +// 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. + +// Integration tests for Caddy's PROXY protocol support, covering two distinct +// roles that Caddy can play: +// +// 1. As a PROXY protocol *sender* (reverse proxy outbound transport): +// Caddy receives an inbound request from a test client and the +// reverse_proxy handler forwards it to an upstream with a PROXY protocol +// header (v1 or v2) prepended to the connection. A lightweight backend +// built with go-proxyproto validates that the header was received and +// carries the correct client address. +// +// Transport versions tested: +// - "1.1" -> plain HTTP/1.1 to the upstream +// - "h2c" -> HTTP/2 cleartext (h2c) to the upstream (regression for #7529) +// - "2" -> HTTP/2 over TLS (h2) to the upstream +// +// For each transport version both PROXY protocol v1 and v2 are exercised. +// +// HTTP/3 (h3) is not included because it uses QUIC/UDP and therefore +// bypasses the TCP-level dialContext that injects PROXY protocol headers; +// there is no meaningful h3 + proxy protocol sender combination to test. +// +// 2. As a PROXY protocol *receiver* (server-side listener wrapper): +// A raw TCP client dials Caddy directly, injects a PROXY v2 header +// spoofing a source address, and sends a normal HTTP/1.1 request. The +// Caddy server is configured with the proxy_protocol listener wrapper and +// is expected to surface the spoofed address via the +// {http.request.remote.host} placeholder. + +package integration + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net" + "net/http" + "net/http/httptest" + "slices" + "strings" + "sync" + "testing" + + goproxy "github.com/pires/go-proxyproto" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + + "github.com/caddyserver/caddy/v2/caddytest" +) + +// proxyProtoBackend is a minimal HTTP server that sits behind a +// go-proxyproto listener and records the source address that was +// delivered in the PROXY header for each request. +type proxyProtoBackend struct { + mu sync.Mutex + headerAddrs []string // host:port strings extracted from each PROXY header + + ln net.Listener + srv *http.Server +} + +// newProxyProtoBackend starts a TCP listener wrapped with go-proxyproto on a +// random local port and serves requests with a simple "OK" body. The PROXY +// header source addresses are accumulated in headerAddrs so tests can +// inspect them. +func newProxyProtoBackend(t *testing.T) *proxyProtoBackend { + t.Helper() + + b := &proxyProtoBackend{} + + rawLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("backend: listen: %v", err) + } + + // Wrap with go-proxyproto so the PROXY header is stripped and parsed + // before the HTTP server sees the connection. We use REQUIRE so that a + // missing header returns an error instead of silently passing through. + pLn := &goproxy.Listener{ + Listener: rawLn, + Policy: func(_ net.Addr) (goproxy.Policy, error) { + return goproxy.REQUIRE, nil + }, + } + b.ln = pLn + + // Wrap the handler with h2c support so the backend can speak HTTP/2 + // cleartext (h2c) as well as plain HTTP/1.1. Without this, Caddy's + // reverse proxy would receive a 'frame too large' error when the + // upstream transport is configured to use h2c. + h2Server := &http2.Server{} + handlerFn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // go-proxyproto has already updated the net.Conn's remote + // address to the value from the PROXY header; the HTTP server + // surfaces it in r.RemoteAddr. + b.mu.Lock() + b.headerAddrs = append(b.headerAddrs, r.RemoteAddr) + b.mu.Unlock() + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, "OK") + }) + + b.srv = &http.Server{ + Handler: h2c.NewHandler(handlerFn, h2Server), + } + + go b.srv.Serve(pLn) //nolint:errcheck + t.Cleanup(func() { + _ = b.srv.Close() + _ = rawLn.Close() + }) + + return b +} + +// addr returns the listening address (host:port) of the backend. +func (b *proxyProtoBackend) addr() string { + return b.ln.Addr().String() +} + +// recordedAddrs returns a snapshot of all PROXY-header source addresses seen +// so far. +func (b *proxyProtoBackend) recordedAddrs() []string { + b.mu.Lock() + defer b.mu.Unlock() + cp := make([]string, len(b.headerAddrs)) + copy(cp, b.headerAddrs) + return cp +} + +// tlsProxyProtoBackend is a TLS-enabled backend that sits behind a +// go-proxyproto listener. The PROXY header is stripped before the TLS +// handshake so the layer order on a connection is: +// +// raw TCP → go-proxyproto (strips PROXY header) → TLS handshake → HTTP/2 +type tlsProxyProtoBackend struct { + mu sync.Mutex + headerAddrs []string + + srv *httptest.Server +} + +// newTLSProxyProtoBackend starts a TLS listener that first reads and strips +// PROXY protocol headers (go-proxyproto, REQUIRE policy) and then performs a +// TLS handshake. The backend speaks HTTP/2 over TLS (h2). +// +// The certificate is the standard self-signed certificate generated by +// httptest.Server; the Caddy transport must be configured with +// insecure_skip_verify: true to trust it. +func newTLSProxyProtoBackend(t *testing.T) *tlsProxyProtoBackend { + t.Helper() + + b := &tlsProxyProtoBackend{} + + handlerFn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b.mu.Lock() + b.headerAddrs = append(b.headerAddrs, r.RemoteAddr) + b.mu.Unlock() + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, "OK") + }) + + rawLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("tlsBackend: listen: %v", err) + } + + // Wrap with go-proxyproto so the PROXY header is consumed before TLS. + pLn := &goproxy.Listener{ + Listener: rawLn, + Policy: func(_ net.Addr) (goproxy.Policy, error) { + return goproxy.REQUIRE, nil + }, + } + + // httptest.NewUnstartedServer lets us replace the listener before + // calling StartTLS(), which wraps our proxyproto listener with + // tls.NewListener. This gives us the right layer order. + b.srv = httptest.NewUnstartedServer(handlerFn) + b.srv.Listener = pLn + + // StartTLS enables HTTP/2 on the server automatically. + b.srv.StartTLS() + + t.Cleanup(func() { + b.srv.Close() + }) + + return b +} + +// addr returns the listening address (host:port) of the TLS backend. +func (b *tlsProxyProtoBackend) addr() string { + return b.srv.Listener.Addr().String() +} + +// tlsConfig returns the *tls.Config used by the backend server. +// Tests can use it to verify cert details if needed. +func (b *tlsProxyProtoBackend) tlsConfig() *tls.Config { + return b.srv.TLS +} + +// recordedAddrs returns a snapshot of all PROXY-header source addresses. +func (b *tlsProxyProtoBackend) recordedAddrs() []string { + b.mu.Lock() + defer b.mu.Unlock() + cp := make([]string, len(b.headerAddrs)) + copy(cp, b.headerAddrs) + return cp +} + +// proxyProtoTLSConfig builds a Caddy JSON configuration that proxies to a TLS +// upstream with PROXY protocol. The transport uses insecure_skip_verify so +// the self-signed certificate generated by httptest.Server is accepted. +func proxyProtoTLSConfig(listenPort int, backendAddr, ppVersion string, transportVersions []string) string { + versionsJSON, _ := json.Marshal(transportVersions) + return fmt.Sprintf(`{ + "admin": { + "listen": "localhost:2999" + }, + "apps": { + "pki": { + "certificate_authorities": { + "local": { + "install_trust": false + } + } + }, + "http": { + "grace_period": 1, + "servers": { + "proxy": { + "listen": [":%d"], + "automatic_https": { + "disable": true + }, + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "upstreams": [{"dial": "%s"}], + "transport": { + "protocol": "http", + "proxy_protocol": "%s", + "versions": %s, + "tls": { + "insecure_skip_verify": true + } + } + } + ] + } + ] + } + } + } + } + }`, listenPort, backendAddr, ppVersion, string(versionsJSON)) +} + +// testTLSProxyProtocolMatrix is the shared implementation for TLS-based proxy +// protocol tests. It mirrors testProxyProtocolMatrix but uses a TLS backend. +func testTLSProxyProtocolMatrix(t *testing.T, ppVersion string, transportVersions []string, numRequests int) { + t.Helper() + + backend := newTLSProxyProtoBackend(t) + listenPort := freePort(t) + + tester := caddytest.NewTester(t) + tester.WithDefaultOverrides(caddytest.Config{ + AdminPort: 2999, + }) + cfg := proxyProtoTLSConfig(listenPort, backend.addr(), ppVersion, transportVersions) + tester.InitServer(cfg, "json") + + proxyURL := fmt.Sprintf("http://127.0.0.1:%d/", listenPort) + + for i := 0; i < numRequests; i++ { + resp, err := tester.Client.Get(proxyURL) + if err != nil { + t.Fatalf("request %d/%d: GET %s: %v", i+1, numRequests, proxyURL, err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Errorf("request %d/%d: expected status 200, got %d", i+1, numRequests, resp.StatusCode) + } + } + + addrs := backend.recordedAddrs() + if len(addrs) == 0 { + t.Fatalf("backend recorded no PROXY protocol addresses (expected at least 1)") + } + + for i, addr := range addrs { + host, _, err := net.SplitHostPort(addr) + if err != nil { + t.Errorf("addr[%d] %q: SplitHostPort: %v", i, addr, err) + continue + } + if host != "127.0.0.1" { + t.Errorf("addr[%d]: expected source 127.0.0.1, got %q", i, host) + } + } +} + +// proxyProtoConfig builds a Caddy JSON configuration that: +// - listens on listenPort for inbound HTTP requests +// - proxies them to backendAddr with PROXY protocol ppVersion ("v1"/"v2") +// - uses the given transport versions (e.g. ["1.1"] or ["h2c"]) +func proxyProtoConfig(listenPort int, backendAddr, ppVersion string, transportVersions []string) string { + versionsJSON, _ := json.Marshal(transportVersions) + return fmt.Sprintf(`{ + "admin": { + "listen": "localhost:2999" + }, + "apps": { + "pki": { + "certificate_authorities": { + "local": { + "install_trust": false + } + } + }, + "http": { + "grace_period": 1, + "servers": { + "proxy": { + "listen": [":%d"], + "automatic_https": { + "disable": true + }, + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "upstreams": [{"dial": "%s"}], + "transport": { + "protocol": "http", + "proxy_protocol": "%s", + "versions": %s + } + } + ] + } + ] + } + } + } + } + }`, listenPort, backendAddr, ppVersion, string(versionsJSON)) +} + +// freePort returns a free local TCP port by binding briefly and releasing it. +func freePort(t *testing.T) int { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("freePort: %v", err) + } + port := ln.Addr().(*net.TCPAddr).Port + _ = ln.Close() + return port +} + +// TestProxyProtocolV1WithH1 verifies that PROXY protocol v1 headers are sent +// correctly when the transport uses HTTP/1.1 to the upstream. +func TestProxyProtocolV1WithH1(t *testing.T) { + testProxyProtocolMatrix(t, "v1", []string{"1.1"}, 1) +} + +// TestProxyProtocolV2WithH1 verifies that PROXY protocol v2 headers are sent +// correctly when the transport uses HTTP/1.1 to the upstream. +func TestProxyProtocolV2WithH1(t *testing.T) { + testProxyProtocolMatrix(t, "v2", []string{"1.1"}, 1) +} + +// TestProxyProtocolV1WithH2C verifies that PROXY protocol v1 headers are sent +// correctly when the transport uses h2c (HTTP/2 cleartext) to the upstream. +func TestProxyProtocolV1WithH2C(t *testing.T) { + testProxyProtocolMatrix(t, "v1", []string{"h2c"}, 1) +} + +// TestProxyProtocolV2WithH2C verifies that PROXY protocol v2 headers are sent +// correctly when the transport uses h2c (HTTP/2 cleartext) to the upstream. +// This is the primary regression test for github.com/caddyserver/caddy/issues/7529: +// before the fix, the h2 transport opened a new TCP connection per request +// (because req.URL.Host was mangled differently for each request due to the +// varying client port), which caused file-descriptor exhaustion under load. +func TestProxyProtocolV2WithH2C(t *testing.T) { + testProxyProtocolMatrix(t, "v2", []string{"h2c"}, 1) +} + +// TestProxyProtocolV2WithH2CMultipleRequests sends several sequential requests +// through the h2c + PROXY-protocol path and confirms that: +// 1. Every request receives a 200 response (no connection exhaustion). +// 2. The backend received at least one PROXY header (connection was reused). +// +// This is the core regression guard for issue #7529: without the fix, a new +// TCP connection was opened per request, quickly exhausting file descriptors. +func TestProxyProtocolV2WithH2CMultipleRequests(t *testing.T) { + testProxyProtocolMatrix(t, "v2", []string{"h2c"}, 5) +} + +// TestProxyProtocolV1WithH2 verifies that PROXY protocol v1 headers are sent +// correctly when the transport uses HTTP/2 over TLS (h2) to the upstream. +func TestProxyProtocolV1WithH2(t *testing.T) { + testTLSProxyProtocolMatrix(t, "v1", []string{"2"}, 1) +} + +// TestProxyProtocolV2WithH2 verifies that PROXY protocol v2 headers are sent +// correctly when the transport uses HTTP/2 over TLS (h2) to the upstream. +func TestProxyProtocolV2WithH2(t *testing.T) { + testTLSProxyProtocolMatrix(t, "v2", []string{"2"}, 1) +} + +// TestProxyProtocolServerAndProxy is an end-to-end matrix test that exercises +// all combinations of PROXY protocol version x transport version. +func TestProxyProtocolServerAndProxy(t *testing.T) { + plainTests := []struct { + name string + ppVersion string + transportVersions []string + numRequests int + }{ + {"h1-v1", "v1", []string{"1.1"}, 3}, + {"h1-v2", "v2", []string{"1.1"}, 3}, + {"h2c-v1", "v1", []string{"h2c"}, 3}, + {"h2c-v2", "v2", []string{"h2c"}, 3}, + } + for _, tc := range plainTests { + t.Run(tc.name, func(t *testing.T) { + testProxyProtocolMatrix(t, tc.ppVersion, tc.transportVersions, tc.numRequests) + }) + } + + tlsTests := []struct { + name string + ppVersion string + transportVersions []string + numRequests int + }{ + {"h2-v1", "v1", []string{"2"}, 3}, + {"h2-v2", "v2", []string{"2"}, 3}, + } + for _, tc := range tlsTests { + t.Run(tc.name, func(t *testing.T) { + testTLSProxyProtocolMatrix(t, tc.ppVersion, tc.transportVersions, tc.numRequests) + }) + } +} + +// testProxyProtocolMatrix is the shared implementation for the proxy protocol +// tests. It: +// 1. Starts a go-proxyproto-wrapped backend. +// 2. Configures Caddy as a reverse proxy with the given PROXY protocol +// version and transport versions. +// 3. Sends numRequests GET requests through Caddy and asserts 200 OK each time. +// 4. Asserts the backend recorded at least one PROXY header whose source host +// is 127.0.0.1 (the loopback address used by the test client). +func testProxyProtocolMatrix(t *testing.T, ppVersion string, transportVersions []string, numRequests int) { + t.Helper() + + backend := newProxyProtoBackend(t) + listenPort := freePort(t) + + tester := caddytest.NewTester(t) + tester.WithDefaultOverrides(caddytest.Config{ + AdminPort: 2999, + }) + cfg := proxyProtoConfig(listenPort, backend.addr(), ppVersion, transportVersions) + tester.InitServer(cfg, "json") + + // If the test is h2c-only (no "1.1" in versions), reconfigure the test + // client transport to use unencrypted HTTP/2 so we actually exercise the + // h2c code path through Caddy. + if slices.Contains(transportVersions, "h2c") && !slices.Contains(transportVersions, "1.1") { + tr, ok := tester.Client.Transport.(*http.Transport) + if ok { + tr.Protocols = new(http.Protocols) + tr.Protocols.SetHTTP1(false) + tr.Protocols.SetUnencryptedHTTP2(true) + } + } + + proxyURL := fmt.Sprintf("http://127.0.0.1:%d/", listenPort) + + for i := 0; i < numRequests; i++ { + resp, err := tester.Client.Get(proxyURL) + if err != nil { + t.Fatalf("request %d/%d: GET %s: %v", i+1, numRequests, proxyURL, err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Errorf("request %d/%d: expected status 200, got %d", i+1, numRequests, resp.StatusCode) + } + } + + // The backend must have seen at least one PROXY header. For h1, there is + // one per request; for h2c, requests share the same connection so only one + // header is written at connection establishment. + addrs := backend.recordedAddrs() + if len(addrs) == 0 { + t.Fatalf("backend recorded no PROXY protocol addresses (expected at least 1)") + } + + // Every PROXY-decoded source address must be the loopback address since + // the test client always connects from 127.0.0.1. + for i, addr := range addrs { + host, _, err := net.SplitHostPort(addr) + if err != nil { + t.Errorf("addr[%d] %q: SplitHostPort: %v", i, addr, err) + continue + } + if host != "127.0.0.1" { + t.Errorf("addr[%d]: expected source 127.0.0.1, got %q", i, host) + } + } +} + +// TestProxyProtocolListenerWrapper verifies that Caddy's +// caddy.listeners.proxy_protocol listener wrapper can successfully parse +// incoming PROXY protocol headers. +// +// The test dials Caddy's listening port directly, injects a raw PROXY v2 +// header spoofing source address 10.0.0.1:1234, then sends a normal +// HTTP/1.1 GET request. The Caddy server is configured to echo back the +// remote address ({http.request.remote.host}). The test asserts that the +// echoed address is the spoofed 10.0.0.1. +func TestProxyProtocolListenerWrapper(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(`{ + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + grace_period 1ns + servers :9080 { + listener_wrappers { + proxy_protocol { + timeout 5s + allow 127.0.0.0/8 + } + } + } + } + http://localhost:9080 { + respond "{http.request.remote.host}" + }`, "caddyfile") + + // Dial the Caddy listener directly and inject a PROXY v2 header that + // claims the connection originates from 10.0.0.1:1234. + conn, err := net.Dial("tcp", "127.0.0.1:9080") + if err != nil { + t.Fatalf("dial: %v", err) + } + defer conn.Close() + + spoofedSrc := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 1234} + spoofedDst := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9080} + hdr := goproxy.HeaderProxyFromAddrs(2, spoofedSrc, spoofedDst) + if _, err := hdr.WriteTo(conn); err != nil { + t.Fatalf("write proxy header: %v", err) + } + + // Write a minimal HTTP/1.1 GET request. + _, err = fmt.Fprintf(conn, + "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") + if err != nil { + t.Fatalf("write HTTP request: %v", err) + } + + // Read the raw response and look for the spoofed address in the body. + buf := make([]byte, 4096) + n, _ := conn.Read(buf) + raw := string(buf[:n]) + + if !strings.Contains(raw, "10.0.0.1") { + t.Errorf("expected spoofed address 10.0.0.1 in response body; full response:\n%s", raw) + } +} From 7b34e3107eb6071fea3a3f94a5c913c32a55afd0 Mon Sep 17 00:00:00 2001 From: Salent Olivick <47511274+Chise1@users.noreply.github.com> Date: Wed, 4 Mar 2026 06:09:49 +0800 Subject: [PATCH 105/206] core: Check whether @id is unique (#7002) * caddy.go: Check whether @id is unique(#6991) * Alternate implementation, using Gemini 3.1 --------- Co-authored-by: Francis Lavoie --- caddy.go | 23 ++++++- caddytest/caddytest_test.go | 116 ++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/caddy.go b/caddy.go index 59c410876..1c08de8a8 100644 --- a/caddy.go +++ b/caddy.go @@ -227,8 +227,18 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force idx := make(map[string]string) err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx) if err != nil { + if len(rawCfgJSON) > 0 { + var oldCfg any + err2 := json.Unmarshal(rawCfgJSON, &oldCfg) + if err2 != nil { + err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2) + } + rawCfg[rawConfigKey] = oldCfg + } else { + rawCfg[rawConfigKey] = nil + } return APIError{ - HTTPStatus: http.StatusInternalServerError, + HTTPStatus: http.StatusBadRequest, Err: fmt.Errorf("indexing config: %v", err), } } @@ -248,6 +258,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2) } rawCfg[rawConfigKey] = oldCfg + } else { + rawCfg[rawConfigKey] = nil } return fmt.Errorf("loading new config: %v", err) @@ -281,14 +293,19 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err case map[string]any: for k, v := range val { if k == idKey { + var idStr string switch idVal := v.(type) { case string: - index[idVal] = configPath + idStr = idVal case float64: // all JSON numbers decode as float64 - index[fmt.Sprintf("%v", idVal)] = configPath + idStr = fmt.Sprintf("%v", idVal) default: return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey) } + if existingPath, ok := index[idStr]; ok { + return fmt.Errorf("duplicate ID '%s' found at %s and %s", idStr, existingPath, configPath) + } + index[idStr] = configPath continue } // traverse this object property recursively diff --git a/caddytest/caddytest_test.go b/caddytest/caddytest_test.go index a9d5da936..31266fa8f 100644 --- a/caddytest/caddytest_test.go +++ b/caddytest/caddytest_test.go @@ -1,6 +1,7 @@ package caddytest import ( + "bytes" "net/http" "strings" "testing" @@ -126,3 +127,118 @@ func TestLoadUnorderedJSON(t *testing.T) { } tester.AssertResponseCode(req, 200) } + +func TestCheckID(t *testing.T) { + tester := NewTester(t) + tester.InitServer(`{ + "admin": { + "listen": "localhost:2999" + }, + "apps": { + "http": { + "http_port": 9080, + "servers": { + "s_server": { + "@id": "s_server", + "listen": [ + ":9080" + ], + "routes": [ + { + "handle": [ + { + "handler": "static_response", + "body": "Hello" + } + ] + } + ] + } + } + } + } + } + `, "json") + headers := []string{"Content-Type:application/json"} + sServer1 := []byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello 2"}]}]}`) + + // PUT to an existing ID should fail with a 409 conflict + tester.AssertPutResponseBody( + "http://localhost:2999/id/s_server", + headers, + bytes.NewBuffer(sServer1), + 409, + `{"error":"[/config/apps/http/servers/s_server] key already exists: s_server"}`+"\n") + + // POST replaces the object fully + tester.AssertPostResponseBody( + "http://localhost:2999/id/s_server", + headers, + bytes.NewBuffer(sServer1), + 200, + "") + + // Verify the server is running the new route + tester.AssertGetResponse( + "http://localhost:9080/", + 200, + "Hello 2") + + // Update the existing route to ensure IDs are handled correctly when replaced + tester.AssertPostResponseBody( + "http://localhost:2999/id/s_server", + headers, + bytes.NewBuffer([]byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello2"}],"match":[{"path":["/route_1/*"]}]}]}`)), + 200, + "") + + sServer2 := []byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello2"}],"match":[{"path":["/route_1/*"]}]}]}`) + + // Identical patch should succeed and return 200 (config is unchanged branch) + tester.AssertPatchResponseBody( + "http://localhost:2999/id/s_server", + headers, + bytes.NewBuffer(sServer2), + 200, + "") + + route2 := []byte(`{"@id":"route2","handle": [{"handler": "static_response","body": "route2"}],"match":[{"path":["/route_2/*"]}]}`) + + // Put a new route2 object before the route1 object due to the path of /id/route1 + // Being translated to: /config/apps/http/servers/s_server/routes/0 + tester.AssertPutResponseBody( + "http://localhost:2999/id/route1", + headers, + bytes.NewBuffer(route2), + 200, + "") + + // Verify that the whole config looks correct, now containing both route1 and route2 + tester.AssertGetResponse( + "http://localhost:2999/config/", + 200, + `{"admin":{"listen":"localhost:2999"},"apps":{"http":{"http_port":9080,"servers":{"s_server":{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route2","handle":[{"body":"route2","handler":"static_response"}],"match":[{"path":["/route_2/*"]}]},{"@id":"route1","handle":[{"body":"Hello2","handler":"static_response"}],"match":[{"path":["/route_1/*"]}]}]}}}}}`+"\n") + + // Try to add another copy of route2 using POST to test duplicate ID handling + // Since the first route2 ended up at array index 0, and we are appending to the array, the index for the new element would be 2 + tester.AssertPostResponseBody( + "http://localhost:2999/id/route2", + headers, + bytes.NewBuffer(route2), + 400, + `{"error":"indexing config: duplicate ID 'route2' found at /config/apps/http/servers/s_server/routes/0 and /config/apps/http/servers/s_server/routes/2"}`+"\n") + + // Use PATCH to modify an existing object successfully + tester.AssertPatchResponseBody( + "http://localhost:2999/id/route1", + headers, + bytes.NewBuffer([]byte(`{"@id":"route1","handle":[{"handler":"static_response","body":"route1"}],"match":[{"path":["/route_1/*"]}]}`)), + 200, + "") + + // Verify the PATCH updated the server state + tester.AssertGetResponse( + "http://localhost:9080/route_1/", + 200, + "route1") +} From 88616e86e6e656738426bb86b4a42dcc20a59f77 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 3 Mar 2026 19:14:55 -0300 Subject: [PATCH 106/206] api: Add all in-flight requests /reverse_proxy/upstreams (Fixes #7277) (#7517) This refactors the initial approach in PR #7281, replacing the UsagePool with a dedicated package-level sync.Map and atomic.Int64 to track in-flight requests without global lock contention. It also introduces a lookup map in the admin API to fix a potential O(n^2) iteration over upstreams, ensuring that draining upstreams are correctly exposed across config reloads without leaking memory. Co-authored-by: Y.Horie reverseproxy: optimize in-flight tracking and admin API - Replaced sync.RWMutex with sync.Map and atomic.Int64 to avoid lock contention under high RPS. - Introduced a lookup map in the admin API to fix a potential O(n^2) iteration over upstreams. --- modules/caddyhttp/reverseproxy/admin.go | 15 +++++++- .../caddyhttp/reverseproxy/reverseproxy.go | 36 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/admin.go b/modules/caddyhttp/reverseproxy/admin.go index 7e72a4cdb..18215f0ae 100644 --- a/modules/caddyhttp/reverseproxy/admin.go +++ b/modules/caddyhttp/reverseproxy/admin.go @@ -73,6 +73,7 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er // Collect the results to respond with results := []upstreamStatus{} + knownHosts := make(map[string]struct{}) // Iterate over the upstream pool (needs to be fast) var rangeErr error @@ -95,6 +96,8 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er return false } + knownHosts[address] = struct{}{} + results = append(results, upstreamStatus{ Address: address, NumRequests: upstream.NumRequests(), @@ -103,7 +106,17 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er return true }) - // If an error happened during the range, return it + currentInFlight := getInFlightRequests() + for address, count := range currentInFlight { + if _, exists := knownHosts[address]; !exists && count > 0 { + results = append(results, upstreamStatus{ + Address: address, + NumRequests: int(count), + Fails: 0, + }) + } + } + if rangeErr != nil { return rangeErr } diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index fb42828bf..d83c3e709 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -32,6 +32,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "go.uber.org/zap" @@ -46,6 +47,31 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" ) +// inFlightRequests uses sync.Map with atomic.Int64 for lock-free updates on the hot path +var inFlightRequests sync.Map + +func incInFlightRequest(address string) { + v, _ := inFlightRequests.LoadOrStore(address, new(atomic.Int64)) + v.(*atomic.Int64).Add(1) +} + +func decInFlightRequest(address string) { + if v, ok := inFlightRequests.Load(address); ok { + if v.(*atomic.Int64).Add(-1) <= 0 { + inFlightRequests.Delete(address) + } + } +} + +func getInFlightRequests() map[string]int64 { + copyMap := make(map[string]int64) + inFlightRequests.Range(func(key, value any) bool { + copyMap[key.(string)] = value.(*atomic.Int64).Load() + return true + }) + return copyMap +} + func init() { caddy.RegisterModule(Handler{}) } @@ -904,8 +930,16 @@ func (h Handler) addForwardedHeaders(req *http.Request) error { // Go standard library which was used as the foundation.) func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origReq *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error { _ = di.Upstream.Host.countRequest(1) + + // Increment the in-flight request count + incInFlightRequest(di.Address) + //nolint:errcheck - defer di.Upstream.Host.countRequest(-1) + defer func() { + di.Upstream.Host.countRequest(-1) + // Decrement the in-flight request count + decInFlightRequest(di.Address) + }() // point the request to this upstream h.directRequest(req, di) From dc360828598440beb1dfdb21c0ba14e09bcd565b Mon Sep 17 00:00:00 2001 From: Varun Chawla <34209028+veeceey@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:15:55 -0800 Subject: [PATCH 107/206] caddyhttp: Collect metrics once per route instead of per handler (#7492) * perf: collect metrics once per route instead of per handler (#4644) Move Prometheus metrics instrumentation from the per-handler level to the per-route level. Previously, every middleware handler in a route was individually wrapped with metricsInstrumentedHandler, causing metrics to be collected N times per request (once per handler in the chain). Since all handlers in a route see the same request, these per-handler metrics were redundant and added significant CPU overhead (73% of request handling time per the original profiling). The fix introduces metricsInstrumentedRoute which wraps the entire compiled handler chain once in wrapRoute, collecting metrics only when the route actually matches. The handler label uses the first handler's module name, which is the most meaningful identifier for the route. Benchmark results (5 handlers per route): Old (per-handler): ~4650 ns/op, 4400 B/op, 45 allocs/op New (per-route): ~940 ns/op, 816 B/op, 8 allocs/op Improvement: ~5x faster, ~5.4x less memory, ~5.6x fewer allocs Signed-off-by: Varun Chawla * Remove unused metricsInstrumentedHandler code Delete the metricsInstrumentedHandler type, its constructor, and ServeHTTP method since they are no longer used after switching to route-level metrics collection via metricsInstrumentedRoute. Also remove the unused metrics parameter from wrapMiddleware and the middlewareHandlerFunc test helper, and convert existing tests to use the new route-level API. Co-Authored-By: Claude Opus 4.6 * Address review feedback: restore comments, move function to bottom - Move computeApproximateRequestSize back to bottom of file to minimize diff - Restore all useful comments that were accidentally dropped - Old metricsInstrumentedHandler already removed in previous commit --------- Signed-off-by: Varun Chawla Co-authored-by: Claude Opus 4.6 --- modules/caddyhttp/metrics.go | 19 ++-- modules/caddyhttp/metrics_test.go | 162 +++++++++++++++++++++++------- modules/caddyhttp/routes.go | 37 +++++-- 3 files changed, 161 insertions(+), 57 deletions(-) diff --git a/modules/caddyhttp/metrics.go b/modules/caddyhttp/metrics.go index 8b4d380f0..b212bbfb8 100644 --- a/modules/caddyhttp/metrics.go +++ b/modules/caddyhttp/metrics.go @@ -214,21 +214,24 @@ func serverNameFromContext(ctx context.Context) string { return srv.name } -type metricsInstrumentedHandler struct { +// metricsInstrumentedRoute wraps a compiled route Handler with metrics +// instrumentation. It wraps the entire compiled route chain once, +// collecting metrics only once per route match. +type metricsInstrumentedRoute struct { handler string - mh MiddlewareHandler + next Handler metrics *Metrics } -func newMetricsInstrumentedHandler(ctx caddy.Context, handler string, mh MiddlewareHandler, metrics *Metrics) *metricsInstrumentedHandler { - metrics.init.Do(func() { - initHTTPMetrics(ctx, metrics) +func newMetricsInstrumentedRoute(ctx caddy.Context, handler string, next Handler, m *Metrics) *metricsInstrumentedRoute { + m.init.Do(func() { + initHTTPMetrics(ctx, m) }) - return &metricsInstrumentedHandler{handler, mh, metrics} + return &metricsInstrumentedRoute{handler: handler, next: next, metrics: m} } -func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { +func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) error { server := serverNameFromContext(r.Context()) labels := prometheus.Labels{"server": server, "handler": h.handler} method := metrics.SanitizeMethod(r.Method) @@ -267,7 +270,7 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re return false }) wrec := NewResponseRecorder(w, nil, writeHeaderRecorder) - err := h.mh.ServeHTTP(wrec, r, next) + err := h.next.ServeHTTP(wrec, r) dur := time.Since(start).Seconds() h.metrics.httpMetrics.requestCount.With(labels).Inc() diff --git a/modules/caddyhttp/metrics_test.go b/modules/caddyhttp/metrics_test.go index 58b6a09ad..987b3f342 100644 --- a/modules/caddyhttp/metrics_test.go +++ b/modules/caddyhttp/metrics_test.go @@ -47,16 +47,12 @@ func TestMetricsInstrumentedHandler(t *testing.T) { return handlerErr }) - mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { - return h.ServeHTTP(w, r) - }) - - ih := newMetricsInstrumentedHandler(ctx, "bar", mh, metrics) + ih := newMetricsInstrumentedRoute(ctx, "bar", h, metrics) r := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() - if actual := ih.ServeHTTP(w, r, h); actual != handlerErr { + if actual := ih.ServeHTTP(w, r); actual != handlerErr { t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual) } if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 { @@ -64,19 +60,19 @@ func TestMetricsInstrumentedHandler(t *testing.T) { } handlerErr = nil - if err := ih.ServeHTTP(w, r, h); err != nil { + if err := ih.ServeHTTP(w, r); err != nil { t.Errorf("Received unexpected error: %v", err) } // an empty handler - no errors, no header written - mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + emptyHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }) - ih = newMetricsInstrumentedHandler(ctx, "empty", mh, metrics) + ih = newMetricsInstrumentedRoute(ctx, "empty", emptyHandler, metrics) r = httptest.NewRequest("GET", "/", nil) w = httptest.NewRecorder() - if err := ih.ServeHTTP(w, r, h); err != nil { + if err := ih.ServeHTTP(w, r); err != nil { t.Errorf("Received unexpected error: %v", err) } if actual := w.Result().StatusCode; actual != 200 { @@ -87,16 +83,16 @@ func TestMetricsInstrumentedHandler(t *testing.T) { } // handler returning an error with an HTTP status - mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + errHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return Error(http.StatusTooManyRequests, nil) }) - ih = newMetricsInstrumentedHandler(ctx, "foo", mh, metrics) + ih = newMetricsInstrumentedRoute(ctx, "foo", errHandler, metrics) r = httptest.NewRequest("GET", "/", nil) w = httptest.NewRecorder() - if err := ih.ServeHTTP(w, r, nil); err == nil { + if err := ih.ServeHTTP(w, r); err == nil { t.Errorf("expected error to be propagated") } @@ -225,16 +221,12 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) { return handlerErr }) - mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { - return h.ServeHTTP(w, r) - }) - - ih := newMetricsInstrumentedHandler(ctx, "bar", mh, metrics) + ih := newMetricsInstrumentedRoute(ctx, "bar", h, metrics) r := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() - if actual := ih.ServeHTTP(w, r, h); actual != handlerErr { + if actual := ih.ServeHTTP(w, r); actual != handlerErr { t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual) } if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 { @@ -242,19 +234,19 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) { } handlerErr = nil - if err := ih.ServeHTTP(w, r, h); err != nil { + if err := ih.ServeHTTP(w, r); err != nil { t.Errorf("Received unexpected error: %v", err) } // an empty handler - no errors, no header written - mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + emptyHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }) - ih = newMetricsInstrumentedHandler(ctx, "empty", mh, metrics) + ih = newMetricsInstrumentedRoute(ctx, "empty", emptyHandler, metrics) r = httptest.NewRequest("GET", "/", nil) w = httptest.NewRecorder() - if err := ih.ServeHTTP(w, r, h); err != nil { + if err := ih.ServeHTTP(w, r); err != nil { t.Errorf("Received unexpected error: %v", err) } if actual := w.Result().StatusCode; actual != 200 { @@ -265,16 +257,16 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) { } // handler returning an error with an HTTP status - mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + errHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return Error(http.StatusTooManyRequests, nil) }) - ih = newMetricsInstrumentedHandler(ctx, "foo", mh, metrics) + ih = newMetricsInstrumentedRoute(ctx, "foo", errHandler, metrics) r = httptest.NewRequest("GET", "/", nil) w = httptest.NewRecorder() - if err := ih.ServeHTTP(w, r, nil); err == nil { + if err := ih.ServeHTTP(w, r); err == nil { t.Errorf("expected error to be propagated") } @@ -397,30 +389,30 @@ func TestMetricsCardinalityProtection(t *testing.T) { // Add one allowed host metrics.allowedHosts["allowed.com"] = struct{}{} - mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { w.Write([]byte("hello")) return nil }) - ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics) + ih := newMetricsInstrumentedRoute(ctx, "test", h, metrics) // Test request to allowed host r1 := httptest.NewRequest("GET", "http://allowed.com/", nil) r1.Host = "allowed.com" w1 := httptest.NewRecorder() - ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil })) + ih.ServeHTTP(w1, r1) // Test request to unknown host (should be mapped to "_other") r2 := httptest.NewRequest("GET", "http://attacker.com/", nil) r2.Host = "attacker.com" w2 := httptest.NewRecorder() - ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil })) + ih.ServeHTTP(w2, r2) // Test request to another unknown host (should also be mapped to "_other") r3 := httptest.NewRequest("GET", "http://evil.com/", nil) r3.Host = "evil.com" w3 := httptest.NewRecorder() - ih.ServeHTTP(w3, r3, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil })) + ih.ServeHTTP(w3, r3) // Check that metrics contain: // - One entry for "allowed.com" @@ -452,26 +444,26 @@ func TestMetricsHTTPSCatchAll(t *testing.T) { allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts } - mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { w.Write([]byte("hello")) return nil }) - ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics) + ih := newMetricsInstrumentedRoute(ctx, "test", h, metrics) // Test HTTPS request (should be allowed even though not in allowedHosts) r1 := httptest.NewRequest("GET", "https://unknown.com/", nil) r1.Host = "unknown.com" r1.TLS = &tls.ConnectionState{} // Mark as TLS/HTTPS w1 := httptest.NewRecorder() - ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil })) + ih.ServeHTTP(w1, r1) // Test HTTP request (should be mapped to "_other") r2 := httptest.NewRequest("GET", "http://unknown.com/", nil) r2.Host = "unknown.com" // No TLS field = HTTP request w2 := httptest.NewRecorder() - ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil })) + ih.ServeHTTP(w2, r2) // Check that HTTPS request gets real host, HTTP gets "_other" expected := ` @@ -488,8 +480,102 @@ func TestMetricsHTTPSCatchAll(t *testing.T) { } } -type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error +func TestMetricsInstrumentedRoute(t *testing.T) { + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + m := &Metrics{ + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + } -func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error { - return f(w, r, h) + handlerErr := errors.New("oh noes") + response := []byte("hello world!") + innerHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + if actual := testutil.ToFloat64(m.httpMetrics.requestInFlight); actual != 1.0 { + t.Errorf("Expected requestInFlight to be 1.0, got %v", actual) + } + if handlerErr == nil { + w.Write(response) + } + return handlerErr + }) + + ih := newMetricsInstrumentedRoute(ctx, "test_handler", innerHandler, m) + + r := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + // Test with error + if actual := ih.ServeHTTP(w, r); actual != handlerErr { + t.Errorf("Expected error %v, got %v", handlerErr, actual) + } + if actual := testutil.ToFloat64(m.httpMetrics.requestInFlight); actual != 0.0 { + t.Errorf("Expected requestInFlight to be 0.0 after request, got %v", actual) + } + if actual := testutil.ToFloat64(m.httpMetrics.requestErrors); actual != 1.0 { + t.Errorf("Expected requestErrors to be 1.0, got %v", actual) + } + + // Test without error + handlerErr = nil + w = httptest.NewRecorder() + if err := ih.ServeHTTP(w, r); err != nil { + t.Errorf("Unexpected error: %v", err) + } +} + +func BenchmarkMetricsInstrumentedRoute(b *testing.B) { + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + m := &Metrics{ + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + } + + noopHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + w.Write([]byte("ok")) + return nil + }) + + ih := newMetricsInstrumentedRoute(ctx, "bench_handler", noopHandler, m) + + r := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + ih.ServeHTTP(w, r) + } +} + +// BenchmarkSingleRouteMetrics simulates the new behavior where metrics +// are collected once for the entire route. +func BenchmarkSingleRouteMetrics(b *testing.B) { + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + m := &Metrics{ + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + } + + // Build a chain of 5 plain middleware handlers (no per-handler metrics) + var next Handler = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + }) + for i := 0; i < 5; i++ { + capturedNext := next + next = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return capturedNext.ServeHTTP(w, r) + }) + } + + // Wrap the entire chain with a single route-level metrics handler + ih := newMetricsInstrumentedRoute(ctx, "handler", next, m) + + r := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + ih.ServeHTTP(w, r) + } } diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index d029d19b9..ce2287488 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -97,7 +97,10 @@ type Route struct { MatcherSets MatcherSets `json:"-"` Handlers []MiddlewareHandler `json:"-"` - middleware []Middleware + middleware []Middleware + metrics *Metrics + metricsCtx caddy.Context + handlerName string } // Empty returns true if the route has all zero/default values. @@ -162,12 +165,20 @@ func (r *Route) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error { r.Handlers = append(r.Handlers, handler.(MiddlewareHandler)) } + // Store metrics info for route-level instrumentation (applied once + // per route in wrapRoute, instead of per-handler which was redundant). + r.metrics = metrics + r.metricsCtx = ctx + if len(r.Handlers) > 0 { + r.handlerName = caddy.GetModuleName(r.Handlers[0]) + } + // Make ProvisionHandlers idempotent by clearing the middleware field r.middleware = []Middleware{} // pre-compile the middleware handler chain for _, midhandler := range r.Handlers { - r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler, metrics)) + r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler)) } return nil } @@ -298,6 +309,16 @@ func wrapRoute(route Route) Middleware { nextCopy = route.middleware[i](nextCopy) } + // Apply metrics instrumentation once for the entire route, + // rather than wrapping each individual handler. This avoids + // redundant metrics collection that caused significant CPU + // overhead (see issue #4644). + if route.metrics != nil { + nextCopy = newMetricsInstrumentedRoute( + route.metricsCtx, route.handlerName, nextCopy, route.metrics, + ) + } + return nextCopy.ServeHTTP(rw, req) }) } @@ -306,20 +327,14 @@ func wrapRoute(route Route) Middleware { // wrapMiddleware wraps mh such that it can be correctly // appended to a list of middleware in preparation for // compiling into a handler chain. -func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware { - handlerToUse := mh - if metrics != nil { - // wrap the middleware with metrics instrumentation - handlerToUse = newMetricsInstrumentedHandler(ctx, caddy.GetModuleName(mh), mh, metrics) - } - +func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler) Middleware { return func(next Handler) Handler { return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { // EXPERIMENTAL: Trace each module that gets invoked if server, ok := r.Context().Value(ServerCtxKey).(*Server); ok && server != nil { - server.logTrace(handlerToUse) + server.logTrace(mh) } - return handlerToUse.ServeHTTP(w, r, next) + return mh.ServeHTTP(w, r, next) }) } } From 2dbcdefbbee68e7b4a31ac66361a0f4e3bcd2eea Mon Sep 17 00:00:00 2001 From: newklei <105632119+NucleiAv@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:30:49 -0500 Subject: [PATCH 108/206] forward_auth: `copy_headers` does not strip client-supplied identity headers (Fixes GHSA-7r4p-vjf4-gxv4) (#7545) When using copy_headers in a forward_auth block, client-supplied headers with the same names were not being removed before being forwarded to the backend. This happens because PR #6608 added a MatchNot guard that skips the Set operation when the auth service does not return a given header. That guard prevents setting headers to empty strings, which is the correct behavior, but it also means a client can send X-User-Id: admin in their request and if the auth service validates the token without returning X-User-Id, Caddy skips the Set and the client value passes through unchanged to the backend. The fix adds an unconditional delete route for each copy_headers entry, placed just before the existing conditional set route. The delete always runs regardless of what the auth service returns. The conditional set still only runs when the auth service provides that header. The end result is: - Client-supplied headers are always removed - When the auth service returns the header, the backend gets that value - When the auth service does not return the header, the backend sees nothing Existing behavior is unchanged for any deployment where the auth service returns all of the configured copy_headers entries. Fixes GHSA-7r4p-vjf4-gxv4 --- .../forward_auth_authelia.caddyfiletest | 50 ++++- ...ward_auth_copy_headers_strip.caddyfiletest | 146 +++++++++++++ .../forward_auth_rename_headers.caddyfiletest | 62 +++++- caddytest/integration/forwardauth_test.go | 206 ++++++++++++++++++ .../reverseproxy/forwardauth/caddyfile.go | 18 ++ 5 files changed, 480 insertions(+), 2 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/forward_auth_copy_headers_strip.caddyfiletest create mode 100644 caddytest/integration/forwardauth_test.go diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest b/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest index 240bdc62f..831d7d2fb 100644 --- a/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest @@ -46,6 +46,18 @@ app.example.com { } ] }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "Remote-Email" + ] + } + } + ] + }, { "handle": [ { @@ -73,6 +85,18 @@ app.example.com { } ] }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "Remote-Groups" + ] + } + } + ] + }, { "handle": [ { @@ -100,6 +124,18 @@ app.example.com { } ] }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "Remote-Name" + ] + } + } + ] + }, { "handle": [ { @@ -127,6 +163,18 @@ app.example.com { } ] }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "Remote-User" + ] + } + } + ] + }, { "handle": [ { @@ -200,4 +248,4 @@ app.example.com { } } } -} \ No newline at end of file +} diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_copy_headers_strip.caddyfiletest b/caddytest/integration/caddyfile_adapt/forward_auth_copy_headers_strip.caddyfiletest new file mode 100644 index 000000000..887bef0ab --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/forward_auth_copy_headers_strip.caddyfiletest @@ -0,0 +1,146 @@ +:8080 + +forward_auth 127.0.0.1:9091 { + uri / + copy_headers X-User-Id X-User-Role +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8080" + ], + "routes": [ + { + "handle": [ + { + "handle_response": [ + { + "match": { + "status_code": [ + 2 + ] + }, + "routes": [ + { + "handle": [ + { + "handler": "vars" + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "X-User-Id" + ] + } + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "set": { + "X-User-Id": [ + "{http.reverse_proxy.header.X-User-Id}" + ] + } + } + } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.X-User-Id}": [ + "" + ] + } + } + ] + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "X-User-Role" + ] + } + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "set": { + "X-User-Role": [ + "{http.reverse_proxy.header.X-User-Role}" + ] + } + } + } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.X-User-Role}": [ + "" + ] + } + } + ] + } + ] + } + ] + } + ], + "handler": "reverse_proxy", + "headers": { + "request": { + "set": { + "X-Forwarded-Method": [ + "{http.request.method}" + ], + "X-Forwarded-Uri": [ + "{http.request.uri}" + ] + } + } + }, + "rewrite": { + "method": "GET", + "uri": "/" + }, + "upstreams": [ + { + "dial": "127.0.0.1:9091" + } + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest b/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest index c2be2ed43..5d61e5ff2 100644 --- a/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest @@ -35,6 +35,18 @@ forward_auth localhost:9000 { } ] }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "1" + ] + } + } + ] + }, { "handle": [ { @@ -62,6 +74,18 @@ forward_auth localhost:9000 { } ] }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "B" + ] + } + } + ] + }, { "handle": [ { @@ -89,6 +113,18 @@ forward_auth localhost:9000 { } ] }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "3" + ] + } + } + ] + }, { "handle": [ { @@ -116,6 +152,18 @@ forward_auth localhost:9000 { } ] }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "D" + ] + } + } + ] + }, { "handle": [ { @@ -143,6 +191,18 @@ forward_auth localhost:9000 { } ] }, + { + "handle": [ + { + "handler": "headers", + "request": { + "delete": [ + "5" + ] + } + } + ] + }, { "handle": [ { @@ -203,4 +263,4 @@ forward_auth localhost:9000 { } } } -} \ No newline at end of file +} diff --git a/caddytest/integration/forwardauth_test.go b/caddytest/integration/forwardauth_test.go new file mode 100644 index 000000000..d0ecc2be1 --- /dev/null +++ b/caddytest/integration/forwardauth_test.go @@ -0,0 +1,206 @@ +// 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 integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "github.com/caddyserver/caddy/v2/caddytest" +) + +// TestForwardAuthCopyHeadersStripsClientHeaders is a regression test for the +// header injection vulnerability in forward_auth copy_headers. +// +// When the auth service returns 200 OK without one of the copy_headers headers, +// the MatchNot guard skips the Set operation. Before this fix, the original +// client-supplied header survived unchanged into the backend request, allowing +// privilege escalation with only a valid (non-privileged) bearer token. After +// the fix, an unconditional delete route runs first, so the backend always +// sees an absent header rather than the attacker-supplied value. +func TestForwardAuthCopyHeadersStripsClientHeaders(t *testing.T) { + // Mock auth service: accepts any Bearer token, returns 200 OK with NO + // identity headers. This is the stateless JWT validator pattern that + // triggers the vulnerability. + authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusUnauthorized) + })) + defer authSrv.Close() + + // Mock backend: records the identity headers it receives. A real application + // would use X-User-Id / X-User-Role to make authorization decisions. + type received struct{ userID, userRole string } + var ( + mu sync.Mutex + last received + ) + backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + last = received{ + userID: r.Header.Get("X-User-Id"), + userRole: r.Header.Get("X-User-Role"), + } + mu.Unlock() + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "ok") + })) + defer backendSrv.Close() + + authAddr := strings.TrimPrefix(authSrv.URL, "http://") + backendAddr := strings.TrimPrefix(backendSrv.URL, "http://") + + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + grace_period 1ns + } + http://localhost:9080 { + forward_auth %s { + uri / + copy_headers X-User-Id X-User-Role + } + reverse_proxy %s + } + `, authAddr, backendAddr), "caddyfile") + + // Case 1: no token. Auth must still reject the request even when the client + // includes identity headers. This confirms the auth check is not bypassed. + req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) + req.Header.Set("X-User-Id", "injected") + req.Header.Set("X-User-Role", "injected") + resp := tester.AssertResponseCode(req, http.StatusUnauthorized) + resp.Body.Close() + + // Case 2: valid token, no injected headers. The backend should see absent + // identity headers (the auth service never returns them). + req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) + req.Header.Set("Authorization", "Bearer token123") + tester.AssertResponse(req, http.StatusOK, "ok") + mu.Lock() + gotID, gotRole := last.userID, last.userRole + mu.Unlock() + if gotID != "" { + t.Errorf("baseline: X-User-Id should be absent, got %q", gotID) + } + if gotRole != "" { + t.Errorf("baseline: X-User-Role should be absent, got %q", gotRole) + } + + // Case 3 (the security regression): valid token plus forged identity headers. + // The fix must strip those values so the backend never sees them. + req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) + req.Header.Set("Authorization", "Bearer token123") + req.Header.Set("X-User-Id", "admin") // forged + req.Header.Set("X-User-Role", "superadmin") // forged + tester.AssertResponse(req, http.StatusOK, "ok") + mu.Lock() + gotID, gotRole = last.userID, last.userRole + mu.Unlock() + if gotID != "" { + t.Errorf("injection: X-User-Id must be stripped, got %q", gotID) + } + if gotRole != "" { + t.Errorf("injection: X-User-Role must be stripped, got %q", gotRole) + } +} + +// TestForwardAuthCopyHeadersAuthResponseWins verifies that when the auth +// service does include a copy_headers header in its response, that value +// is forwarded to the backend and takes precedence over any client-supplied +// value for the same header. +func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) { + const wantUserID = "service-user-42" + const wantUserRole = "editor" + + // Auth service: accepts bearer token and sets identity headers. + authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { + w.Header().Set("X-User-Id", wantUserID) + w.Header().Set("X-User-Role", wantUserRole) + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusUnauthorized) + })) + defer authSrv.Close() + + type received struct{ userID, userRole string } + var ( + mu sync.Mutex + last received + ) + backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + last = received{ + userID: r.Header.Get("X-User-Id"), + userRole: r.Header.Get("X-User-Role"), + } + mu.Unlock() + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "ok") + })) + defer backendSrv.Close() + + authAddr := strings.TrimPrefix(authSrv.URL, "http://") + backendAddr := strings.TrimPrefix(backendSrv.URL, "http://") + + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + grace_period 1ns + } + http://localhost:9080 { + forward_auth %s { + uri / + copy_headers X-User-Id X-User-Role + } + reverse_proxy %s + } + `, authAddr, backendAddr), "caddyfile") + + // The client sends forged headers; the auth service overrides them with + // its own values. The backend must receive the auth service values. + req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) + req.Header.Set("Authorization", "Bearer token123") + req.Header.Set("X-User-Id", "forged-id") // must be overwritten + req.Header.Set("X-User-Role", "forged-role") // must be overwritten + tester.AssertResponse(req, http.StatusOK, "ok") + + mu.Lock() + gotID, gotRole := last.userID, last.userRole + mu.Unlock() + if gotID != wantUserID { + t.Errorf("X-User-Id: want %q, got %q", wantUserID, gotID) + } + if gotRole != wantUserRole { + t.Errorf("X-User-Role: want %q, got %q", wantUserRole, gotRole) + } +} diff --git a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go index f838c8702..1273e906c 100644 --- a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go @@ -208,6 +208,24 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) for _, from := range sortedHeadersToCopy { to := http.CanonicalHeaderKey(headersToCopy[from]) placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from) + + // Always delete the client-supplied header before conditionally setting + // it from the auth response. Without this, a client that pre-supplies a + // header listed in copy_headers can inject arbitrary values when the auth + // service does not return that header: the MatchNot guard below would + // skip the Set entirely, leaving the original client-controlled value + // intact and forwarding it to the backend. + copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{ + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject( + &headers.Handler{ + Request: &headers.HeaderOps{ + Delete: []string{to}, + }, + }, + "handler", "headers", nil, + )}, + }) + handler := &headers.Handler{ Request: &headers.HeaderOps{ Set: http.Header{ From 7e83775e3adea8b8da72fea3b159207bd71000dd Mon Sep 17 00:00:00 2001 From: "Sam.An" <56215891+sammiee5311@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:08:39 +0900 Subject: [PATCH 109/206] Merge commit from fork Only apply repl.ReplaceAll() on values from literal variable names (e.g. map outputs), not on values resolved from placeholder keys (e.g. {http.request.header.*}). The placeholder path already resolves the value via repl.Get(), so a second expansion allows user-controlled input containing {env.*} or {file.*} to be evaluated, leaking environment variables and file contents. Add regression test to verify placeholder-sourced values are not re-expanded. --- modules/caddyhttp/matchers_test.go | 11 ++++++++++- modules/caddyhttp/vars.go | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index 160aa424f..c3d8c405e 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -967,6 +967,7 @@ func TestVarREMatcher(t *testing.T) { desc string match MatchVarsRE input VarsMiddleware + headers http.Header expect bool expectRepl map[string]string }{ @@ -1001,6 +1002,14 @@ func TestVarREMatcher(t *testing.T) { input: VarsMiddleware{"Var1": "var1Value"}, expect: true, }, + { + desc: "placeholder key value containing braces is not double-expanded", + match: MatchVarsRE{"{http.request.header.X-Input}": &MatchRegexp{Pattern: ".+", Name: "val"}}, + input: VarsMiddleware{}, + headers: http.Header{"X-Input": []string{"{env.HOME}"}}, + expect: true, + expectRepl: map[string]string{"val.0": "{env.HOME}"}, + }, } { t.Run(tc.desc, func(t *testing.T) { t.Parallel() @@ -1017,7 +1026,7 @@ func TestVarREMatcher(t *testing.T) { } // set up the fake request and its Replacer - req := &http.Request{URL: new(url.URL), Method: http.MethodGet} + req := &http.Request{URL: new(url.URL), Method: http.MethodGet, Header: tc.headers} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any)) diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index d01f4a431..f19ca16fc 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -312,10 +312,12 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) for key, val := range m { var varValue any + var fromPlaceholder bool if strings.HasPrefix(key, "{") && strings.HasSuffix(key, "}") && strings.Count(key, "{") == 1 { varValue, _ = repl.Get(strings.Trim(key, "{}")) + fromPlaceholder = true } else { varValue = vars[key] } @@ -334,7 +336,14 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { varStr = fmt.Sprintf("%v", vv) } - valExpanded := repl.ReplaceAll(varStr, "") + // Only expand placeholders in values from literal variable names + // (e.g. map outputs). Values resolved from placeholder keys are + // already final and must not be re-expanded, as that would allow + // user input like {env.SECRET} to be evaluated. + valExpanded := varStr + if !fromPlaceholder { + valExpanded = repl.ReplaceAll(varStr, "") + } if match := val.Match(valExpanded, repl); match { return match, nil } From db2986028fc573ae3add0a9a3381268dd7599267 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Wed, 4 Mar 2026 15:05:26 -0500 Subject: [PATCH 110/206] reverseproxy: Track dynamic upstreams, enable passive healthchecking (#7539) * reverseproxy: Track dynamic upstreams, enable passive healthchecking * Add tests for dynamic upstream tracking, admin endpoint, health checks --- modules/caddyhttp/reverseproxy/admin.go | 13 +- modules/caddyhttp/reverseproxy/admin_test.go | 275 ++++++++++++ .../reverseproxy/dynamic_upstreams_test.go | 345 ++++++++++++++++ modules/caddyhttp/reverseproxy/hosts.go | 61 +++ .../reverseproxy/passive_health_test.go | 391 ++++++++++++++++++ .../caddyhttp/reverseproxy/reverseproxy.go | 24 +- 6 files changed, 1096 insertions(+), 13 deletions(-) create mode 100644 modules/caddyhttp/reverseproxy/admin_test.go create mode 100644 modules/caddyhttp/reverseproxy/dynamic_upstreams_test.go create mode 100644 modules/caddyhttp/reverseproxy/passive_health_test.go diff --git a/modules/caddyhttp/reverseproxy/admin.go b/modules/caddyhttp/reverseproxy/admin.go index 18215f0ae..97dd2827d 100644 --- a/modules/caddyhttp/reverseproxy/admin.go +++ b/modules/caddyhttp/reverseproxy/admin.go @@ -75,7 +75,7 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er results := []upstreamStatus{} knownHosts := make(map[string]struct{}) - // Iterate over the upstream pool (needs to be fast) + // Iterate over the static upstream pool (needs to be fast) var rangeErr error hosts.Range(func(key, val any) bool { address, ok := key.(string) @@ -121,6 +121,17 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er return rangeErr } + // Also include dynamic upstreams + dynamicHostsMu.RLock() + for address, entry := range dynamicHosts { + results = append(results, upstreamStatus{ + Address: address, + NumRequests: entry.host.NumRequests(), + Fails: entry.host.Fails(), + }) + } + dynamicHostsMu.RUnlock() + err := enc.Encode(results) if err != nil { return caddy.APIError{ diff --git a/modules/caddyhttp/reverseproxy/admin_test.go b/modules/caddyhttp/reverseproxy/admin_test.go new file mode 100644 index 000000000..de9ac967c --- /dev/null +++ b/modules/caddyhttp/reverseproxy/admin_test.go @@ -0,0 +1,275 @@ +// 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 reverseproxy + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +// adminHandlerFixture sets up the global host state for an admin endpoint test +// and returns a cleanup function that must be deferred by the caller. +// +// staticAddrs are inserted into the UsagePool (as a static upstream would be). +// dynamicAddrs are inserted into the dynamicHosts map (as a dynamic upstream would be). +func adminHandlerFixture(t *testing.T, staticAddrs, dynamicAddrs []string) func() { + t.Helper() + + for _, addr := range staticAddrs { + u := &Upstream{Dial: addr} + u.fillHost() + } + + dynamicHostsMu.Lock() + for _, addr := range dynamicAddrs { + dynamicHosts[addr] = dynamicHostEntry{host: new(Host), lastSeen: time.Now()} + } + dynamicHostsMu.Unlock() + + return func() { + // Remove static entries from the UsagePool. + for _, addr := range staticAddrs { + _, _ = hosts.Delete(addr) + } + // Remove dynamic entries. + dynamicHostsMu.Lock() + for _, addr := range dynamicAddrs { + delete(dynamicHosts, addr) + } + dynamicHostsMu.Unlock() + } +} + +// callAdminUpstreams fires a GET against handleUpstreams and returns the +// decoded response body. +func callAdminUpstreams(t *testing.T) []upstreamStatus { + t.Helper() + req := httptest.NewRequest(http.MethodGet, "/reverse_proxy/upstreams", nil) + w := httptest.NewRecorder() + + handler := adminUpstreams{} + if err := handler.handleUpstreams(w, req); err != nil { + t.Fatalf("handleUpstreams returned unexpected error: %v", err) + } + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + if ct := w.Header().Get("Content-Type"); ct != "application/json" { + t.Fatalf("expected Content-Type application/json, got %q", ct) + } + + var results []upstreamStatus + if err := json.NewDecoder(w.Body).Decode(&results); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + return results +} + +// resultsByAddress indexes a slice of upstreamStatus by address for easier +// lookup in assertions. +func resultsByAddress(statuses []upstreamStatus) map[string]upstreamStatus { + m := make(map[string]upstreamStatus, len(statuses)) + for _, s := range statuses { + m[s.Address] = s + } + return m +} + +// TestAdminUpstreamsMethodNotAllowed verifies that non-GET methods are rejected. +func TestAdminUpstreamsMethodNotAllowed(t *testing.T) { + for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodDelete} { + req := httptest.NewRequest(method, "/reverse_proxy/upstreams", nil) + w := httptest.NewRecorder() + err := (adminUpstreams{}).handleUpstreams(w, req) + if err == nil { + t.Errorf("method %s: expected an error, got nil", method) + continue + } + apiErr, ok := err.(interface{ HTTPStatus() int }) + if !ok { + // caddy.APIError stores the code in HTTPStatus field, access via the + // exported interface it satisfies indirectly; just check non-nil. + continue + } + if code := apiErr.HTTPStatus(); code != http.StatusMethodNotAllowed { + t.Errorf("method %s: expected 405, got %d", method, code) + } + } +} + +// TestAdminUpstreamsEmpty verifies that an empty response is valid JSON when +// no upstreams are registered. +func TestAdminUpstreamsEmpty(t *testing.T) { + resetDynamicHosts() + + results := callAdminUpstreams(t) + if results == nil { + t.Error("expected non-nil (empty) slice, got nil") + } + if len(results) != 0 { + t.Errorf("expected 0 results with empty pools, got %d", len(results)) + } +} + +// TestAdminUpstreamsStaticOnly verifies that static upstreams (from the +// UsagePool) appear in the response with correct addresses. +func TestAdminUpstreamsStaticOnly(t *testing.T) { + resetDynamicHosts() + cleanup := adminHandlerFixture(t, + []string{"10.0.0.1:80", "10.0.0.2:80"}, + nil, + ) + defer cleanup() + + results := callAdminUpstreams(t) + byAddr := resultsByAddress(results) + + for _, addr := range []string{"10.0.0.1:80", "10.0.0.2:80"} { + if _, ok := byAddr[addr]; !ok { + t.Errorf("expected static upstream %q in response", addr) + } + } + if len(results) != 2 { + t.Errorf("expected exactly 2 results, got %d", len(results)) + } +} + +// TestAdminUpstreamsDynamicOnly verifies that dynamic upstreams (from +// dynamicHosts) appear in the response with correct addresses. +func TestAdminUpstreamsDynamicOnly(t *testing.T) { + resetDynamicHosts() + cleanup := adminHandlerFixture(t, + nil, + []string{"10.0.1.1:80", "10.0.1.2:80"}, + ) + defer cleanup() + + results := callAdminUpstreams(t) + byAddr := resultsByAddress(results) + + for _, addr := range []string{"10.0.1.1:80", "10.0.1.2:80"} { + if _, ok := byAddr[addr]; !ok { + t.Errorf("expected dynamic upstream %q in response", addr) + } + } + if len(results) != 2 { + t.Errorf("expected exactly 2 results, got %d", len(results)) + } +} + +// TestAdminUpstreamsBothPools verifies that static and dynamic upstreams are +// both present in the same response and that there is no overlap or omission. +func TestAdminUpstreamsBothPools(t *testing.T) { + resetDynamicHosts() + cleanup := adminHandlerFixture(t, + []string{"10.0.2.1:80"}, + []string{"10.0.2.2:80"}, + ) + defer cleanup() + + results := callAdminUpstreams(t) + if len(results) != 2 { + t.Fatalf("expected 2 results (1 static + 1 dynamic), got %d", len(results)) + } + + byAddr := resultsByAddress(results) + if _, ok := byAddr["10.0.2.1:80"]; !ok { + t.Error("static upstream missing from response") + } + if _, ok := byAddr["10.0.2.2:80"]; !ok { + t.Error("dynamic upstream missing from response") + } +} + +// TestAdminUpstreamsNoOverlapBetweenPools verifies that an address registered +// only as a static upstream does not also appear as a dynamic entry, and +// vice-versa. +func TestAdminUpstreamsNoOverlapBetweenPools(t *testing.T) { + resetDynamicHosts() + cleanup := adminHandlerFixture(t, + []string{"10.0.3.1:80"}, + []string{"10.0.3.2:80"}, + ) + defer cleanup() + + results := callAdminUpstreams(t) + seen := make(map[string]int) + for _, r := range results { + seen[r.Address]++ + } + for addr, count := range seen { + if count > 1 { + t.Errorf("address %q appeared %d times; expected exactly once", addr, count) + } + } +} + +// TestAdminUpstreamsReportsFailCounts verifies that fail counts accumulated on +// a dynamic upstream's Host are reflected in the response. +func TestAdminUpstreamsReportsFailCounts(t *testing.T) { + resetDynamicHosts() + + const addr = "10.0.4.1:80" + h := new(Host) + _ = h.countFail(3) + + dynamicHostsMu.Lock() + dynamicHosts[addr] = dynamicHostEntry{host: h, lastSeen: time.Now()} + dynamicHostsMu.Unlock() + defer func() { + dynamicHostsMu.Lock() + delete(dynamicHosts, addr) + dynamicHostsMu.Unlock() + }() + + results := callAdminUpstreams(t) + byAddr := resultsByAddress(results) + + status, ok := byAddr[addr] + if !ok { + t.Fatalf("expected %q in response", addr) + } + if status.Fails != 3 { + t.Errorf("expected Fails=3, got %d", status.Fails) + } +} + +// TestAdminUpstreamsReportsNumRequests verifies that the active request count +// for a static upstream is reflected in the response. +func TestAdminUpstreamsReportsNumRequests(t *testing.T) { + resetDynamicHosts() + + const addr = "10.0.4.2:80" + u := &Upstream{Dial: addr} + u.fillHost() + defer func() { _, _ = hosts.Delete(addr) }() + + _ = u.Host.countRequest(2) + defer func() { _ = u.Host.countRequest(-2) }() + + results := callAdminUpstreams(t) + byAddr := resultsByAddress(results) + + status, ok := byAddr[addr] + if !ok { + t.Fatalf("expected %q in response", addr) + } + if status.NumRequests != 2 { + t.Errorf("expected NumRequests=2, got %d", status.NumRequests) + } +} diff --git a/modules/caddyhttp/reverseproxy/dynamic_upstreams_test.go b/modules/caddyhttp/reverseproxy/dynamic_upstreams_test.go new file mode 100644 index 000000000..577eccdb6 --- /dev/null +++ b/modules/caddyhttp/reverseproxy/dynamic_upstreams_test.go @@ -0,0 +1,345 @@ +// 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 reverseproxy + +import ( + "sync" + "testing" + "time" + + "github.com/caddyserver/caddy/v2" +) + +// resetDynamicHosts clears global dynamic host state between tests. +func resetDynamicHosts() { + dynamicHostsMu.Lock() + dynamicHosts = make(map[string]dynamicHostEntry) + dynamicHostsMu.Unlock() + // Reset the Once so cleanup goroutine tests can re-trigger if needed. + dynamicHostsCleanerOnce = sync.Once{} +} + +// TestFillDynamicHostCreatesEntry verifies that calling fillDynamicHost on a +// new address inserts an entry into dynamicHosts and assigns a non-nil Host. +func TestFillDynamicHostCreatesEntry(t *testing.T) { + resetDynamicHosts() + + u := &Upstream{Dial: "192.0.2.1:80"} + u.fillDynamicHost() + + if u.Host == nil { + t.Fatal("expected Host to be set after fillDynamicHost") + } + + dynamicHostsMu.RLock() + entry, ok := dynamicHosts["192.0.2.1:80"] + dynamicHostsMu.RUnlock() + + if !ok { + t.Fatal("expected entry in dynamicHosts map") + } + if entry.host != u.Host { + t.Error("dynamicHosts entry host should be the same pointer assigned to Upstream.Host") + } + if entry.lastSeen.IsZero() { + t.Error("expected lastSeen to be set") + } +} + +// TestFillDynamicHostReusesSameHost verifies that two calls for the same address +// return the exact same *Host pointer so that state (e.g. fail counts) is shared. +func TestFillDynamicHostReusesSameHost(t *testing.T) { + resetDynamicHosts() + + u1 := &Upstream{Dial: "192.0.2.2:80"} + u1.fillDynamicHost() + + u2 := &Upstream{Dial: "192.0.2.2:80"} + u2.fillDynamicHost() + + if u1.Host != u2.Host { + t.Error("expected both upstreams to share the same *Host pointer") + } +} + +// TestFillDynamicHostUpdatesLastSeen verifies that a second call for the same +// address advances the lastSeen timestamp. +func TestFillDynamicHostUpdatesLastSeen(t *testing.T) { + resetDynamicHosts() + + u := &Upstream{Dial: "192.0.2.3:80"} + u.fillDynamicHost() + + dynamicHostsMu.RLock() + first := dynamicHosts["192.0.2.3:80"].lastSeen + dynamicHostsMu.RUnlock() + + // Ensure measurable time passes. + time.Sleep(2 * time.Millisecond) + + u2 := &Upstream{Dial: "192.0.2.3:80"} + u2.fillDynamicHost() + + dynamicHostsMu.RLock() + second := dynamicHosts["192.0.2.3:80"].lastSeen + dynamicHostsMu.RUnlock() + + if !second.After(first) { + t.Error("expected lastSeen to be updated on second fillDynamicHost call") + } +} + +// TestFillDynamicHostIndependentAddresses verifies that different addresses get +// independent Host entries. +func TestFillDynamicHostIndependentAddresses(t *testing.T) { + resetDynamicHosts() + + u1 := &Upstream{Dial: "192.0.2.4:80"} + u1.fillDynamicHost() + + u2 := &Upstream{Dial: "192.0.2.5:80"} + u2.fillDynamicHost() + + if u1.Host == u2.Host { + t.Error("different addresses should have different *Host entries") + } +} + +// TestFillDynamicHostPreservesFailCount verifies that fail counts on a dynamic +// host survive across multiple fillDynamicHost calls (simulating sequential +// requests), which is the core behaviour fixed by this change. +func TestFillDynamicHostPreservesFailCount(t *testing.T) { + resetDynamicHosts() + + // First "request": provision and record a failure. + u1 := &Upstream{Dial: "192.0.2.6:80"} + u1.fillDynamicHost() + _ = u1.Host.countFail(1) + + if u1.Host.Fails() != 1 { + t.Fatalf("expected 1 fail, got %d", u1.Host.Fails()) + } + + // Second "request": provision the same address again (new *Upstream, same address). + u2 := &Upstream{Dial: "192.0.2.6:80"} + u2.fillDynamicHost() + + if u2.Host.Fails() != 1 { + t.Errorf("expected fail count to persist across fillDynamicHost calls, got %d", u2.Host.Fails()) + } +} + +// TestProvisionUpstreamDynamic verifies that provisionUpstream with dynamic=true +// uses fillDynamicHost (not the UsagePool) and sets healthCheckPolicy / +// MaxRequests correctly from handler config. +func TestProvisionUpstreamDynamic(t *testing.T) { + resetDynamicHosts() + + passive := &PassiveHealthChecks{ + FailDuration: caddy.Duration(10 * time.Second), + MaxFails: 3, + UnhealthyRequestCount: 5, + } + h := Handler{ + HealthChecks: &HealthChecks{ + Passive: passive, + }, + } + + u := &Upstream{Dial: "192.0.2.7:80"} + h.provisionUpstream(u, true) + + if u.Host == nil { + t.Fatal("Host should be set after provisionUpstream") + } + if u.healthCheckPolicy != passive { + t.Error("healthCheckPolicy should point to the handler's PassiveHealthChecks") + } + if u.MaxRequests != 5 { + t.Errorf("expected MaxRequests=5 from UnhealthyRequestCount, got %d", u.MaxRequests) + } + + // Must be in dynamicHosts, not in the static UsagePool. + dynamicHostsMu.RLock() + _, inDynamic := dynamicHosts["192.0.2.7:80"] + dynamicHostsMu.RUnlock() + if !inDynamic { + t.Error("dynamic upstream should be stored in dynamicHosts") + } + _, inPool := hosts.References("192.0.2.7:80") + if inPool { + t.Error("dynamic upstream should NOT be stored in the static UsagePool") + } +} + +// TestProvisionUpstreamStatic verifies that provisionUpstream with dynamic=false +// uses the UsagePool and does NOT insert into dynamicHosts. +func TestProvisionUpstreamStatic(t *testing.T) { + resetDynamicHosts() + + h := Handler{} + + u := &Upstream{Dial: "192.0.2.8:80"} + h.provisionUpstream(u, false) + + if u.Host == nil { + t.Fatal("Host should be set after provisionUpstream") + } + + refs, inPool := hosts.References("192.0.2.8:80") + if !inPool { + t.Error("static upstream should be in the UsagePool") + } + if refs != 1 { + t.Errorf("expected ref count 1, got %d", refs) + } + + dynamicHostsMu.RLock() + _, inDynamic := dynamicHosts["192.0.2.8:80"] + dynamicHostsMu.RUnlock() + if inDynamic { + t.Error("static upstream should NOT be in dynamicHosts") + } + + // Clean up the pool entry we just added. + _, _ = hosts.Delete("192.0.2.8:80") +} + +// TestDynamicHostHealthyConsultsFails verifies the end-to-end passive health +// check path: after enough failures are recorded against a dynamic upstream's +// shared *Host, Healthy() returns false for a newly provisioned *Upstream with +// the same address. +func TestDynamicHostHealthyConsultsFails(t *testing.T) { + resetDynamicHosts() + + passive := &PassiveHealthChecks{ + FailDuration: caddy.Duration(time.Minute), + MaxFails: 2, + } + h := Handler{ + HealthChecks: &HealthChecks{Passive: passive}, + } + + // First request: provision and record two failures. + u1 := &Upstream{Dial: "192.0.2.9:80"} + h.provisionUpstream(u1, true) + + _ = u1.Host.countFail(1) + _ = u1.Host.countFail(1) + + // Second request: fresh *Upstream, same address. + u2 := &Upstream{Dial: "192.0.2.9:80"} + h.provisionUpstream(u2, true) + + if u2.Healthy() { + t.Error("upstream should be unhealthy after MaxFails failures have been recorded against its shared Host") + } +} + +// TestDynamicHostCleanupEvictsStaleEntries verifies that the cleanup sweep +// removes entries whose lastSeen is older than dynamicHostIdleExpiry. +func TestDynamicHostCleanupEvictsStaleEntries(t *testing.T) { + resetDynamicHosts() + + const addr = "192.0.2.10:80" + + // Insert an entry directly with a lastSeen far in the past. + dynamicHostsMu.Lock() + dynamicHosts[addr] = dynamicHostEntry{ + host: new(Host), + lastSeen: time.Now().Add(-2 * dynamicHostIdleExpiry), + } + dynamicHostsMu.Unlock() + + // Run the cleanup logic inline (same logic as the goroutine). + dynamicHostsMu.Lock() + for a, entry := range dynamicHosts { + if time.Since(entry.lastSeen) > dynamicHostIdleExpiry { + delete(dynamicHosts, a) + } + } + dynamicHostsMu.Unlock() + + dynamicHostsMu.RLock() + _, stillPresent := dynamicHosts[addr] + dynamicHostsMu.RUnlock() + + if stillPresent { + t.Error("stale dynamic host entry should have been evicted by cleanup sweep") + } +} + +// TestDynamicHostCleanupRetainsFreshEntries verifies that the cleanup sweep +// keeps entries whose lastSeen is within dynamicHostIdleExpiry. +func TestDynamicHostCleanupRetainsFreshEntries(t *testing.T) { + resetDynamicHosts() + + const addr = "192.0.2.11:80" + + dynamicHostsMu.Lock() + dynamicHosts[addr] = dynamicHostEntry{ + host: new(Host), + lastSeen: time.Now(), + } + dynamicHostsMu.Unlock() + + // Run the cleanup logic inline. + dynamicHostsMu.Lock() + for a, entry := range dynamicHosts { + if time.Since(entry.lastSeen) > dynamicHostIdleExpiry { + delete(dynamicHosts, a) + } + } + dynamicHostsMu.Unlock() + + dynamicHostsMu.RLock() + _, stillPresent := dynamicHosts[addr] + dynamicHostsMu.RUnlock() + + if !stillPresent { + t.Error("fresh dynamic host entry should be retained by cleanup sweep") + } +} + +// TestDynamicHostConcurrentFillHost verifies that concurrent calls to +// fillDynamicHost for the same address all get the same *Host pointer and +// don't race (run with -race). +func TestDynamicHostConcurrentFillHost(t *testing.T) { + resetDynamicHosts() + + const addr = "192.0.2.12:80" + const goroutines = 50 + + var wg sync.WaitGroup + hosts := make([]*Host, goroutines) + + for i := range goroutines { + wg.Add(1) + go func(idx int) { + defer wg.Done() + u := &Upstream{Dial: addr} + u.fillDynamicHost() + hosts[idx] = u.Host + }(i) + } + wg.Wait() + + first := hosts[0] + for i, h := range hosts { + if h != first { + t.Errorf("goroutine %d got a different *Host pointer; expected all to share the same entry", i) + } + } +} diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go index fea85946d..8139a7b50 100644 --- a/modules/caddyhttp/reverseproxy/hosts.go +++ b/modules/caddyhttp/reverseproxy/hosts.go @@ -19,7 +19,9 @@ import ( "fmt" "net/netip" "strconv" + "sync" "sync/atomic" + "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" @@ -132,6 +134,43 @@ func (u *Upstream) fillHost() { u.Host = host } +// fillDynamicHost is like fillHost, but stores the host in the separate +// dynamicHosts map rather than the reference-counted UsagePool. Dynamic +// hosts are not reference-counted; instead, they are retained as long as +// they are actively seen and are evicted by a background cleanup goroutine +// after dynamicHostIdleExpiry of inactivity. This preserves health state +// (e.g. passive fail counts) across sequential requests. +func (u *Upstream) fillDynamicHost() { + dynamicHostsMu.Lock() + entry, ok := dynamicHosts[u.String()] + if ok { + entry.lastSeen = time.Now() + dynamicHosts[u.String()] = entry + u.Host = entry.host + } else { + h := new(Host) + dynamicHosts[u.String()] = dynamicHostEntry{host: h, lastSeen: time.Now()} + u.Host = h + } + dynamicHostsMu.Unlock() + + // ensure the cleanup goroutine is running + dynamicHostsCleanerOnce.Do(func() { + go func() { + for { + time.Sleep(dynamicHostCleanupInterval) + dynamicHostsMu.Lock() + for addr, entry := range dynamicHosts { + if time.Since(entry.lastSeen) > dynamicHostIdleExpiry { + delete(dynamicHosts, addr) + } + } + dynamicHostsMu.Unlock() + } + }() + }) +} + // Host is the basic, in-memory representation of the state of a remote host. // Its fields are accessed atomically and Host values must not be copied. type Host struct { @@ -268,6 +307,28 @@ func GetDialInfo(ctx context.Context) (DialInfo, bool) { // through config reloads. var hosts = caddy.NewUsagePool() +// dynamicHosts tracks hosts that were provisioned from dynamic upstream +// sources. Unlike static upstreams which are reference-counted via the +// UsagePool, dynamic upstream hosts are not reference-counted. Instead, +// their last-seen time is updated on each request, and a background +// goroutine evicts entries that have been idle for dynamicHostIdleExpiry. +// This preserves health state (e.g. passive fail counts) across requests +// to the same dynamic backend. +var ( + dynamicHosts = make(map[string]dynamicHostEntry) + dynamicHostsMu sync.RWMutex + dynamicHostsCleanerOnce sync.Once + dynamicHostCleanupInterval = 5 * time.Minute + dynamicHostIdleExpiry = time.Hour +) + +// dynamicHostEntry holds a Host and the last time it was seen +// in a set of dynamic upstreams returned for a request. +type dynamicHostEntry struct { + host *Host + lastSeen time.Time +} + // dialInfoVarKey is the key used for the variable that holds // the dial info for the upstream connection. const dialInfoVarKey = "reverse_proxy.dial_info" diff --git a/modules/caddyhttp/reverseproxy/passive_health_test.go b/modules/caddyhttp/reverseproxy/passive_health_test.go new file mode 100644 index 000000000..0bd6da181 --- /dev/null +++ b/modules/caddyhttp/reverseproxy/passive_health_test.go @@ -0,0 +1,391 @@ +// 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 reverseproxy + +import ( + "context" + "testing" + "time" + + "github.com/caddyserver/caddy/v2" +) + +// newPassiveHandler builds a minimal Handler with passive health checks +// configured and a live caddy.Context so the fail-forgetter goroutine can +// be cancelled cleanly. The caller must call cancel() when done. +func newPassiveHandler(t *testing.T, maxFails int, failDuration time.Duration) (*Handler, context.CancelFunc) { + t.Helper() + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + h := &Handler{ + ctx: caddyCtx, + HealthChecks: &HealthChecks{ + Passive: &PassiveHealthChecks{ + MaxFails: maxFails, + FailDuration: caddy.Duration(failDuration), + }, + }, + } + return h, cancel +} + +// provisionedStaticUpstream creates a static upstream, registers it in the +// UsagePool, and returns a cleanup func that removes it from the pool. +func provisionedStaticUpstream(t *testing.T, h *Handler, addr string) (*Upstream, func()) { + t.Helper() + u := &Upstream{Dial: addr} + h.provisionUpstream(u, false) + return u, func() { _, _ = hosts.Delete(addr) } +} + +// provisionedDynamicUpstream creates a dynamic upstream, registers it in +// dynamicHosts, and returns a cleanup func that removes it. +func provisionedDynamicUpstream(t *testing.T, h *Handler, addr string) (*Upstream, func()) { + t.Helper() + u := &Upstream{Dial: addr} + h.provisionUpstream(u, true) + return u, func() { + dynamicHostsMu.Lock() + delete(dynamicHosts, addr) + dynamicHostsMu.Unlock() + } +} + +// --- countFailure behaviour --- + +// TestCountFailureNoopWhenNoHealthChecks verifies that countFailure is a no-op +// when HealthChecks is nil. +func TestCountFailureNoopWhenNoHealthChecks(t *testing.T) { + resetDynamicHosts() + h := &Handler{} + u := &Upstream{Dial: "10.1.0.1:80", Host: new(Host)} + + h.countFailure(u) + + if u.Host.Fails() != 0 { + t.Errorf("expected 0 fails with no HealthChecks config, got %d", u.Host.Fails()) + } +} + +// TestCountFailureNoopWhenZeroDuration verifies that countFailure is a no-op +// when FailDuration is 0 (the zero value disables passive checks). +func TestCountFailureNoopWhenZeroDuration(t *testing.T) { + resetDynamicHosts() + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + h := &Handler{ + ctx: caddyCtx, + HealthChecks: &HealthChecks{ + Passive: &PassiveHealthChecks{MaxFails: 1, FailDuration: 0}, + }, + } + u := &Upstream{Dial: "10.1.0.2:80", Host: new(Host)} + + h.countFailure(u) + + if u.Host.Fails() != 0 { + t.Errorf("expected 0 fails with zero FailDuration, got %d", u.Host.Fails()) + } +} + +// TestCountFailureIncrementsCount verifies that countFailure increments the +// fail count on the upstream's Host. +func TestCountFailureIncrementsCount(t *testing.T) { + resetDynamicHosts() + h, cancel := newPassiveHandler(t, 2, time.Minute) + defer cancel() + u := &Upstream{Dial: "10.1.0.3:80", Host: new(Host)} + + h.countFailure(u) + + if u.Host.Fails() != 1 { + t.Errorf("expected 1 fail after countFailure, got %d", u.Host.Fails()) + } +} + +// TestCountFailureDecrementsAfterDuration verifies that the fail count is +// decremented back after FailDuration elapses. +func TestCountFailureDecrementsAfterDuration(t *testing.T) { + resetDynamicHosts() + const failDuration = 50 * time.Millisecond + h, cancel := newPassiveHandler(t, 2, failDuration) + defer cancel() + u := &Upstream{Dial: "10.1.0.4:80", Host: new(Host)} + + h.countFailure(u) + if u.Host.Fails() != 1 { + t.Fatalf("expected 1 fail immediately after countFailure, got %d", u.Host.Fails()) + } + + // Wait long enough for the forgetter goroutine to fire. + time.Sleep(3 * failDuration) + + if u.Host.Fails() != 0 { + t.Errorf("expected fail count to return to 0 after FailDuration, got %d", u.Host.Fails()) + } +} + +// TestCountFailureCancelledContextForgets verifies that cancelling the handler +// context (simulating a config unload) also triggers the forgetter to run, +// decrementing the fail count. +func TestCountFailureCancelledContextForgets(t *testing.T) { + resetDynamicHosts() + h, cancel := newPassiveHandler(t, 2, time.Hour) // very long duration + u := &Upstream{Dial: "10.1.0.5:80", Host: new(Host)} + + h.countFailure(u) + if u.Host.Fails() != 1 { + t.Fatalf("expected 1 fail immediately after countFailure, got %d", u.Host.Fails()) + } + + // Cancelling the context should cause the forgetter goroutine to exit and + // decrement the count. + cancel() + time.Sleep(50 * time.Millisecond) + + if u.Host.Fails() != 0 { + t.Errorf("expected fail count to be decremented after context cancel, got %d", u.Host.Fails()) + } +} + +// --- static upstream passive health check --- + +// TestStaticUpstreamHealthyWithNoFailures verifies that a static upstream with +// no recorded failures is considered healthy. +func TestStaticUpstreamHealthyWithNoFailures(t *testing.T) { + resetDynamicHosts() + h, cancel := newPassiveHandler(t, 2, time.Minute) + defer cancel() + + u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.1:80") + defer cleanup() + + if !u.Healthy() { + t.Error("upstream with no failures should be healthy") + } +} + +// TestStaticUpstreamUnhealthyAtMaxFails verifies that a static upstream is +// marked unhealthy once its fail count reaches MaxFails. +func TestStaticUpstreamUnhealthyAtMaxFails(t *testing.T) { + resetDynamicHosts() + h, cancel := newPassiveHandler(t, 2, time.Minute) + defer cancel() + + u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.2:80") + defer cleanup() + + h.countFailure(u) + if !u.Healthy() { + t.Error("upstream should still be healthy after 1 of 2 allowed failures") + } + + h.countFailure(u) + if u.Healthy() { + t.Error("upstream should be unhealthy after reaching MaxFails=2") + } +} + +// TestStaticUpstreamRecoversAfterFailDuration verifies that a static upstream +// returns to healthy once its failures expire. +func TestStaticUpstreamRecoversAfterFailDuration(t *testing.T) { + resetDynamicHosts() + const failDuration = 50 * time.Millisecond + h, cancel := newPassiveHandler(t, 1, failDuration) + defer cancel() + + u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.3:80") + defer cleanup() + + h.countFailure(u) + if u.Healthy() { + t.Fatal("upstream should be unhealthy immediately after MaxFails failure") + } + + time.Sleep(3 * failDuration) + + if !u.Healthy() { + t.Errorf("upstream should recover to healthy after FailDuration, Fails=%d", u.Host.Fails()) + } +} + +// TestStaticUpstreamHealthPersistedAcrossReprovisioning verifies that static +// upstreams share a Host via the UsagePool, so a second call to provisionUpstream +// for the same address (as happens on config reload) sees the accumulated state. +func TestStaticUpstreamHealthPersistedAcrossReprovisioning(t *testing.T) { + resetDynamicHosts() + h, cancel := newPassiveHandler(t, 2, time.Minute) + defer cancel() + + u1, cleanup1 := provisionedStaticUpstream(t, h, "10.2.0.4:80") + defer cleanup1() + + h.countFailure(u1) + h.countFailure(u1) + + // Simulate a second handler instance referencing the same upstream + // (e.g. after a config reload that keeps the same backend address). + u2, cleanup2 := provisionedStaticUpstream(t, h, "10.2.0.4:80") + defer cleanup2() + + if u1.Host != u2.Host { + t.Fatal("expected both Upstream structs to share the same *Host via UsagePool") + } + if u2.Healthy() { + t.Error("re-provisioned upstream should still see the prior fail count and be unhealthy") + } +} + +// --- dynamic upstream passive health check --- + +// TestDynamicUpstreamHealthyWithNoFailures verifies that a freshly provisioned +// dynamic upstream is healthy. +func TestDynamicUpstreamHealthyWithNoFailures(t *testing.T) { + resetDynamicHosts() + h, cancel := newPassiveHandler(t, 2, time.Minute) + defer cancel() + + u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.1:80") + defer cleanup() + + if !u.Healthy() { + t.Error("dynamic upstream with no failures should be healthy") + } +} + +// TestDynamicUpstreamUnhealthyAtMaxFails verifies that a dynamic upstream is +// marked unhealthy once its fail count reaches MaxFails. +func TestDynamicUpstreamUnhealthyAtMaxFails(t *testing.T) { + resetDynamicHosts() + h, cancel := newPassiveHandler(t, 2, time.Minute) + defer cancel() + + u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.2:80") + defer cleanup() + + h.countFailure(u) + if !u.Healthy() { + t.Error("dynamic upstream should still be healthy after 1 of 2 allowed failures") + } + + h.countFailure(u) + if u.Healthy() { + t.Error("dynamic upstream should be unhealthy after reaching MaxFails=2") + } +} + +// TestDynamicUpstreamFailCountPersistedBetweenRequests is the core regression +// test: it simulates two sequential (non-concurrent) requests to the same +// dynamic upstream. Before the fix, the UsagePool entry would be deleted +// between requests, wiping the fail count. Now it should survive. +func TestDynamicUpstreamFailCountPersistedBetweenRequests(t *testing.T) { + resetDynamicHosts() + h, cancel := newPassiveHandler(t, 2, time.Minute) + defer cancel() + + // --- first request --- + u1 := &Upstream{Dial: "10.3.0.3:80"} + h.provisionUpstream(u1, true) + h.countFailure(u1) + + if u1.Host.Fails() != 1 { + t.Fatalf("expected 1 fail after first request, got %d", u1.Host.Fails()) + } + + // Simulate end of first request: no delete from any pool (key difference + // vs. the old behaviour where hosts.Delete was deferred). + + // --- second request: brand-new *Upstream struct, same dial address --- + u2 := &Upstream{Dial: "10.3.0.3:80"} + h.provisionUpstream(u2, true) + + if u1.Host != u2.Host { + t.Fatal("expected both requests to share the same *Host pointer from dynamicHosts") + } + if u2.Host.Fails() != 1 { + t.Errorf("expected fail count to persist across requests, got %d", u2.Host.Fails()) + } + + // A second failure now tips it over MaxFails=2. + h.countFailure(u2) + if u2.Healthy() { + t.Error("upstream should be unhealthy after accumulated failures across requests") + } + + // Cleanup. + dynamicHostsMu.Lock() + delete(dynamicHosts, "10.3.0.3:80") + dynamicHostsMu.Unlock() +} + +// TestDynamicUpstreamRecoveryAfterFailDuration verifies that a dynamic +// upstream's fail count expires and it returns to healthy. +func TestDynamicUpstreamRecoveryAfterFailDuration(t *testing.T) { + resetDynamicHosts() + const failDuration = 50 * time.Millisecond + h, cancel := newPassiveHandler(t, 1, failDuration) + defer cancel() + + u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.4:80") + defer cleanup() + + h.countFailure(u) + if u.Healthy() { + t.Fatal("upstream should be unhealthy immediately after MaxFails failure") + } + + time.Sleep(3 * failDuration) + + // Re-provision (as a new request would) to get fresh *Upstream with policy set. + u2 := &Upstream{Dial: "10.3.0.4:80"} + h.provisionUpstream(u2, true) + + if !u2.Healthy() { + t.Errorf("dynamic upstream should recover to healthy after FailDuration, Fails=%d", u2.Host.Fails()) + } +} + +// TestDynamicUpstreamMaxRequestsFromUnhealthyRequestCount verifies that +// UnhealthyRequestCount is copied into MaxRequests so Full() works correctly. +func TestDynamicUpstreamMaxRequestsFromUnhealthyRequestCount(t *testing.T) { + resetDynamicHosts() + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + h := &Handler{ + ctx: caddyCtx, + HealthChecks: &HealthChecks{ + Passive: &PassiveHealthChecks{ + UnhealthyRequestCount: 3, + }, + }, + } + + u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.5:80") + defer cleanup() + + if u.MaxRequests != 3 { + t.Errorf("expected MaxRequests=3 from UnhealthyRequestCount, got %d", u.MaxRequests) + } + + // Should not be full with fewer requests than the limit. + _ = u.Host.countRequest(2) + if u.Full() { + t.Error("upstream should not be full with 2 of 3 allowed requests") + } + + _ = u.Host.countRequest(1) + if !u.Full() { + t.Error("upstream should be full at UnhealthyRequestCount concurrent requests") + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index d83c3e709..2ea063bd7 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -392,7 +392,7 @@ func (h *Handler) Provision(ctx caddy.Context) error { // set up upstreams for _, u := range h.Upstreams { - h.provisionUpstream(u) + h.provisionUpstream(u, false) } if h.HealthChecks != nil { @@ -563,18 +563,11 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h } else { upstreams = dUpstreams for _, dUp := range dUpstreams { - h.provisionUpstream(dUp) + h.provisionUpstream(dUp, true) } if c := h.logger.Check(zapcore.DebugLevel, "provisioned dynamic upstreams"); c != nil { c.Write(zap.Int("count", len(dUpstreams))) } - defer func() { - // these upstreams are dynamic, so they are only used for this iteration - // of the proxy loop; be sure to let them go away when we're done with them - for _, upstream := range dUpstreams { - _, _ = hosts.Delete(upstream.String()) - } - }() } } @@ -1324,9 +1317,16 @@ func (h *Handler) directRequest(req *http.Request, di DialInfo) { req.URL.Host = reqHost } -func (h Handler) provisionUpstream(upstream *Upstream) { - // create or get the host representation for this upstream - upstream.fillHost() +func (h Handler) provisionUpstream(upstream *Upstream, dynamic bool) { + // create or get the host representation for this upstream; + // dynamic upstreams are tracked in a separate map with last-seen + // timestamps so their health state persists across requests without + // being reference-counted (and thus discarded between requests). + if dynamic { + upstream.fillDynamicHost() + } else { + upstream.fillHost() + } // give it the circuit breaker, if any upstream.cb = h.CB From a5e7c6e232573b8a8df2946914a84a36070d4b9f Mon Sep 17 00:00:00 2001 From: Tom Paulus Date: Wed, 4 Mar 2026 12:17:02 -0800 Subject: [PATCH 111/206] reverseproxy: prevent body close on dial-error retries (#7547) --- .../caddyhttp/reverseproxy/retries_test.go | 257 ++++++++++++++++++ .../caddyhttp/reverseproxy/reverseproxy.go | 35 ++- 2 files changed, 281 insertions(+), 11 deletions(-) create mode 100644 modules/caddyhttp/reverseproxy/retries_test.go diff --git a/modules/caddyhttp/reverseproxy/retries_test.go b/modules/caddyhttp/reverseproxy/retries_test.go new file mode 100644 index 000000000..056223d4c --- /dev/null +++ b/modules/caddyhttp/reverseproxy/retries_test.go @@ -0,0 +1,257 @@ +package reverseproxy + +import ( + "errors" + "io" + "net" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +// prepareTestRequest injects the context values that ServeHTTP and +// proxyLoopIteration require (caddy.ReplacerCtxKey, VarsCtxKey, etc.) using +// the same helper that the real HTTP server uses. +// +// A zero-value Server is passed so that caddyhttp.ServerCtxKey is set to a +// non-nil pointer; reverseProxy dereferences it to check ShouldLogCredentials. +func prepareTestRequest(req *http.Request) *http.Request { + repl := caddy.NewReplacer() + return caddyhttp.PrepareRequest(req, repl, nil, &caddyhttp.Server{}) +} + +// closeOnCloseReader is an io.ReadCloser whose Close method actually makes +// subsequent reads fail, mimicking the behaviour of a real HTTP request body +// (as opposed to io.NopCloser, whose Close is a no-op and would mask the bug +// we are testing). +type closeOnCloseReader struct { + mu sync.Mutex + r *strings.Reader + closed bool +} + +func newCloseOnCloseReader(s string) *closeOnCloseReader { + return &closeOnCloseReader{r: strings.NewReader(s)} +} + +func (c *closeOnCloseReader) Read(p []byte) (int, error) { + c.mu.Lock() + defer c.mu.Unlock() + if c.closed { + return 0, errors.New("http: invalid Read on closed Body") + } + return c.r.Read(p) +} + +func (c *closeOnCloseReader) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + c.closed = true + return nil +} + +// deadUpstreamAddr returns a TCP address that is guaranteed to refuse +// connections: we bind a listener, note its address, close it immediately, +// and return the address. Any dial to that address will get ECONNREFUSED. +func deadUpstreamAddr(t *testing.T) string { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to create dead upstream listener: %v", err) + } + addr := ln.Addr().String() + ln.Close() + return addr +} + +// testTransport wraps http.Transport to: +// 1. Set the URL scheme to "http" when it is empty (matching what +// HTTPTransport.SetScheme does in production; cloneRequest strips the +// scheme intentionally so a plain *http.Transport would fail with +// "unsupported protocol scheme"). +// 2. Wrap dial errors as DialError so that tryAgain correctly identifies them +// as safe-to-retry regardless of request method (as HTTPTransport does in +// production via its custom dialer). +type testTransport struct{ *http.Transport } + +func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if req.URL.Scheme == "" { + req.URL.Scheme = "http" + } + resp, err := t.Transport.RoundTrip(req) + if err != nil { + // Wrap dial errors as DialError to match production behaviour. + // Without this wrapping, tryAgain treats ECONNREFUSED on a POST + // request as non-retryable (only GET is retried by default when + // the error is not a DialError). + var opErr *net.OpError + if errors.As(err, &opErr) && opErr.Op == "dial" { + return nil, DialError{err} + } + } + return resp, err +} + +// minimalHandler returns a Handler with only the fields required by ServeHTTP +// set directly, bypassing Provision (which requires a full Caddy runtime). +// RoundRobinSelection is used so that successive iterations of the proxy loop +// advance through the upstream pool in a predictable order. +func minimalHandler(retries int, upstreams ...*Upstream) *Handler { + return &Handler{ + logger: zap.NewNop(), + Transport: testTransport{&http.Transport{}}, + Upstreams: upstreams, + LoadBalancing: &LoadBalancing{ + Retries: retries, + SelectionPolicy: &RoundRobinSelection{}, + // RetryMatch intentionally nil: dial errors are always retried + // regardless of RetryMatch or request method. + }, + // ctx, connections, connectionsMu, events: zero/nil values are safe + // for the code paths exercised by these tests (TryInterval=0 so + // ctx.Done() is never consulted; no WebSocket hijacking; no passive + // health-check event emission). + } +} + +// TestDialErrorBodyRetry verifies that a POST request whose body has NOT been +// pre-buffered via request_buffers can still be retried after a dial error. +// +// Before the fix, a dial error caused Go's transport to close the shared body +// (via cloneRequest's shallow copy), so the retry attempt would read from an +// already-closed io.ReadCloser and produce: +// +// http: invalid Read on closed Body → HTTP 502 +// +// After the fix the handler wraps the body in noCloseBody when retries are +// configured, preventing the transport's Close() from propagating to the +// shared body. Since dial errors never read any bytes, the body remains at +// position 0 for the retry. +func TestDialErrorBodyRetry(t *testing.T) { + // Good upstream: echoes the request body with 200 OK. + goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "read body: "+err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write(body) + })) + t.Cleanup(goodServer.Close) + + const requestBody = "hello, retry" + + tests := []struct { + name string + method string + body string + retries int + wantStatus int + wantBody string + }{ + { + // Core regression case: POST with a body, no request_buffers, + // dial error on first upstream → retry to second upstream succeeds. + name: "POST body retried after dial error", + method: http.MethodPost, + body: requestBody, + retries: 1, + wantStatus: http.StatusOK, + wantBody: requestBody, + }, + { + // Dial errors are always retried regardless of method, but there + // is no body to re-read, so GET has always worked. Keep it as a + // sanity check that we did not break the no-body path. + name: "GET without body retried after dial error", + method: http.MethodGet, + body: "", + retries: 1, + wantStatus: http.StatusOK, + wantBody: "", + }, + { + // Without any retry configuration the handler must give up on the + // first dial error and return a 502. Confirms no wrapping occurs + // in the no-retry path. + name: "no retries configured returns 502 on dial error", + method: http.MethodPost, + body: requestBody, + retries: 0, + wantStatus: http.StatusBadGateway, + wantBody: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + dead := deadUpstreamAddr(t) + + // Build the upstream pool. RoundRobinSelection starts its + // counter at 0 and increments before returning, so with a + // two-element pool it picks index 1 first, then index 0. + // Put the good upstream at index 0 and the dead one at + // index 1 so that: + // attempt 1 → pool[1] = dead → DialError (ECONNREFUSED) + // attempt 2 → pool[0] = good → 200 + upstreams := []*Upstream{ + {Host: new(Host), Dial: goodServer.Listener.Addr().String()}, + {Host: new(Host), Dial: dead}, + } + if tc.retries == 0 { + // For the "no retries" case use only the dead upstream so + // there is nowhere to retry to. + upstreams = []*Upstream{ + {Host: new(Host), Dial: dead}, + } + } + + h := minimalHandler(tc.retries, upstreams...) + + // Use closeOnCloseReader so that Close() truly prevents further + // reads, matching real http.body semantics. io.NopCloser would + // mask the bug because its Close is a no-op. + var bodyReader io.ReadCloser + if tc.body != "" { + bodyReader = newCloseOnCloseReader(tc.body) + } + req := httptest.NewRequest(tc.method, "http://example.com/", bodyReader) + if bodyReader != nil { + // httptest.NewRequest wraps the reader in NopCloser; replace + // it with our close-aware reader so Close() is propagated. + req.Body = bodyReader + req.ContentLength = int64(len(tc.body)) + } + req = prepareTestRequest(req) + + rec := httptest.NewRecorder() + err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + + // For error cases (e.g. 502) ServeHTTP returns a HandlerError + // rather than writing the status itself. + gotStatus := rec.Code + if err != nil { + if herr, ok := err.(caddyhttp.HandlerError); ok { + gotStatus = herr.StatusCode + } + } + + if gotStatus != tc.wantStatus { + t.Errorf("status: got %d, want %d (err=%v)", gotStatus, tc.wantStatus, err) + } + if tc.wantBody != "" && rec.Body.String() != tc.wantBody { + t.Errorf("body: got %q, want %q", rec.Body.String(), tc.wantBody) + } + }) + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 2ea063bd7..2169d1717 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -482,18 +482,31 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht reqHost := clonedReq.Host reqHeader := clonedReq.Header - // If the cloned request body was fully buffered, keep a reference to its - // buffer so we can reuse it across retries and return it to the pool - // once we’re done. + // When retries are configured and there is a body, wrap it in + // io.NopCloser to prevent Go's transport from closing it on dial + // errors. cloneRequest does a shallow copy, so clonedReq.Body and + // r.Body share the same io.ReadCloser — a dial-failure Close() + // would kill the original body for all subsequent retry attempts. + // The real body is closed by the HTTP server when the handler + // returns. + // + // If the body was already fully buffered (via request_buffers), + // we also extract the buffer so the retry loop can replay it + // from the beginning on each attempt. (see #6259, #7546) var bufferedReqBody *bytes.Buffer - if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil { - bufferedReqBody = reqBodyBuf.buf - reqBodyBuf.buf = nil - - defer func() { - bufferedReqBody.Reset() - bufPool.Put(bufferedReqBody) - }() + if clonedReq.Body != nil && h.LoadBalancing != nil && + (h.LoadBalancing.Retries > 0 || h.LoadBalancing.TryDuration > 0) { + if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil { + bufferedReqBody = reqBodyBuf.buf + reqBodyBuf.buf = nil + clonedReq.Body = io.NopCloser(bytes.NewReader(bufferedReqBody.Bytes())) + defer func() { + bufferedReqBody.Reset() + bufPool.Put(bufferedReqBody) + }() + } else { + clonedReq.Body = io.NopCloser(clonedReq.Body) + } } start := time.Now() From 566e710991a6b752933178e2c6126181efc563aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oleh=20Konko=20=7C=20semantic=20verification=20for=20trust?= =?UTF-8?q?=20infra=20=7C=20=20LLM-augmented=20operations=20pipeline=20=28?= =?UTF-8?q?precision-first=2C=20claim=E2=89=A4evidence=2C=20submit-human?= =?UTF-8?q?=29=20=7C=20verify=20the=20payload=2C=20not=20the=20signer?= Date: Wed, 4 Mar 2026 23:00:10 +0100 Subject: [PATCH 112/206] fileserver: document hide case-sensitivity (F-CADDY-FILESERVER-HIDE-CASE-001) (#7548) --- modules/caddyhttp/fileserver/staticfiles.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 8a074f546..dce40302d 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -125,6 +125,11 @@ type FileServer struct { // When possible, all paths are resolved to their absolute form before // comparisons are made. For maximum clarity and explictness, use complete, // absolute paths; or, for greater portability, use relative paths instead. + // + // Note that hide comparisons are case-sensitive. On case-insensitive + // filesystems, requests with different path casing may still resolve to the + // same file or directory on disk, so hide should not be treated as a + // security boundary for sensitive paths. Hide []string `json:"hide,omitempty"` // The names of files to try as index files if a folder is requested. From e06dfcf6ed3a8e7b07540751ce4391cb1743337d Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Wed, 4 Mar 2026 16:16:24 -0700 Subject: [PATCH 113/206] Update SECURITY.md Simplify what versions are supported, clarify our policy for unreleased code (or beta code), and expand our AI policy to require a disclosure in ALL cases, even if AI is not used. As well as an invitation to share in some chocolate milk with us if you're human. --- .github/SECURITY.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index eb7437269..2b72b95b6 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,15 +1,14 @@ # Security Policy -The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities. +The Caddy project would like to make sure that it stays on top of all relevant and practically-exploitable vulnerabilities. ## Supported Versions -| Version | Supported | -| -------- | ----------| -| 2.latest | ✔️ | -| 1.x | :x: | -| < 1.x | :x: | +| Version | Supported | +| ----------- | ----------| +| 2.latest | ✔️ | +| <= 2.latest | :x: | ## Acceptable Scope @@ -26,6 +25,8 @@ Client-side exploits are out of scope. In other words, it is not a bug in Caddy Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code. +We accept security reports and patches, but do not assign CVEs, for code that has not been released with a non-prerelease tag. + ## Reporting a Vulnerability @@ -33,7 +34,7 @@ We get a lot of difficult reports that turn out to be invalid. Clear, obvious re First please ensure your report falls within the accepted scope of security bugs (above). -**YOU MUST DISCLOSE THE USE OF LLMs ("AI") INVOLVED IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT AND POTENTIAL BLOCKLISTING.** +:warning: **YOU MUST DISCLOSE WHETHER YOU USED LLMs ("AI") IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE EVEN IF YOU DO NOT USE AI MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT AND POTENTIAL BLOCKLISTING.** We will not waste our time chatting with bots. But if you're a human, pull up a chair and we'll drink some chocolate milk. We'll need enough information to verify the bug and make a patch. To speed things up, please include: From fbfb8fc517728f85890cd46559c01bec9495bf17 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 4 Mar 2026 16:18:33 -0700 Subject: [PATCH 114/206] rewrite: Force recomputing path when escaped path matches rewrite target Thank you for the report by @MaherAzzouzi, and the suggested fix! --- modules/caddyhttp/rewrite/rewrite.go | 1 + modules/caddyhttp/rewrite/rewrite_test.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index 2b18744db..ca5f63bac 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -247,6 +247,7 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool { } else { r.URL.Path = path } + r.URL.RawPath = "" // force recomputing when EscapedPath() is called } if qsStart >= 0 { r.URL.RawQuery = newQuery diff --git a/modules/caddyhttp/rewrite/rewrite_test.go b/modules/caddyhttp/rewrite/rewrite_test.go index 81360baee..c3b4c1f6c 100644 --- a/modules/caddyhttp/rewrite/rewrite_test.go +++ b/modules/caddyhttp/rewrite/rewrite_test.go @@ -224,6 +224,11 @@ func TestRewrite(t *testing.T) { input: newRequest(t, "GET", "/foo#fragFirst?c=d"), expect: newRequest(t, "GET", "/bar#fragFirst?c=d"), }, + { + rule: Rewrite{URI: "/api/admin/panel"}, + input: newRequest(t, "GET", "/api/admin%2Fpanel"), + expect: newRequest(t, "GET", "/api/admin/panel"), + }, { rule: Rewrite{StripPathPrefix: "/prefix"}, From 6e5e08cf582649b4eb46fa201ea05e6131870363 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Wed, 4 Mar 2026 19:14:52 -0500 Subject: [PATCH 115/206] Wire up Cause for most context cancels (#7538) --- admin.go | 6 +++++- caddy.go | 14 +++++++------- context.go | 13 ++++++++++--- listeners.go | 18 +++++++++--------- modules/caddyhttp/app.go | 12 ++++++++++-- .../caddyhttp/reverseproxy/httptransport.go | 2 +- 6 files changed, 42 insertions(+), 23 deletions(-) diff --git a/admin.go b/admin.go index 2eb9c3b04..5ceb3daeb 100644 --- a/admin.go +++ b/admin.go @@ -749,10 +749,14 @@ func stopAdminServer(srv *http.Server) error { if srv == nil { return fmt.Errorf("no admin server") } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + timeout := 10 * time.Second + ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("stopping admin server: %ds timeout", int(timeout.Seconds()))) defer cancel() err := srv.Shutdown(ctx) if err != nil { + if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) { + err = cause + } return fmt.Errorf("shutting down admin server: %v", err) } Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr)) diff --git a/caddy.go b/caddy.go index 1c08de8a8..c27ae4a68 100644 --- a/caddy.go +++ b/caddy.go @@ -88,7 +88,7 @@ type Config struct { storage certmagic.Storage eventEmitter eventEmitter - cancelFunc context.CancelFunc + cancelFunc context.CancelCauseFunc // fileSystems is a dict of fileSystems that will later be loaded from and added to. fileSystems FileSystems @@ -433,7 +433,7 @@ func run(newCfg *Config, start bool) (Context, error) { // partially copied from provisionContext if err != nil { globalMetrics.configSuccess.Set(0) - ctx.cfg.cancelFunc() + ctx.cfg.cancelFunc(fmt.Errorf("configuration start error: %w", err)) if currentCtx.cfg != nil { certmagic.Default.Storage = currentCtx.cfg.storage @@ -509,7 +509,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) // cleanup occurs when we return if there // was an error; if no error, it will get // cleaned up on next config cycle - ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg}) + ctx, cancelCause := NewContextWithCause(Context{Context: context.Background(), cfg: newCfg}) defer func() { if err != nil { globalMetrics.configSuccess.Set(0) @@ -518,7 +518,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) // since the associated config won't be used; // this will cause all modules that were newly // provisioned to clean themselves up - cancel() + cancelCause(fmt.Errorf("configuration error: %w", err)) // also undo any other state changes we made if currentCtx.cfg != nil { @@ -526,7 +526,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) } } }() - newCfg.cancelFunc = cancel // clean up later + newCfg.cancelFunc = cancelCause // clean up later // set up logging before anything bad happens if newCfg.Logging == nil { @@ -746,7 +746,7 @@ func unsyncedStop(ctx Context) { } // clean up all modules - ctx.cfg.cancelFunc() + ctx.cfg.cancelFunc(fmt.Errorf("stopping apps")) } // Validate loads, provisions, and validates @@ -754,7 +754,7 @@ func unsyncedStop(ctx Context) { func Validate(cfg *Config) error { _, err := run(cfg, false) if err == nil { - cfg.cancelFunc() // call Cleanup on all modules + cfg.cancelFunc(fmt.Errorf("validation complete")) // call Cleanup on all modules } return err } diff --git a/context.go b/context.go index a12cdcad4..980027275 100644 --- a/context.go +++ b/context.go @@ -63,10 +63,17 @@ type Context struct { // modules which are loaded will be properly unloaded. // See standard library context package's documentation. func NewContext(ctx Context) (Context, context.CancelFunc) { + newCtx, cancelCause := NewContextWithCause(ctx) + return newCtx, func() { cancelCause(nil) } +} + +// NewContextWithCause is like NewContext but returns a context.CancelCauseFunc. +// EXPERIMENTAL: This API is subject to change. +func NewContextWithCause(ctx Context) (Context, context.CancelCauseFunc) { newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg, metricsRegistry: prometheus.NewPedanticRegistry()} - c, cancel := context.WithCancel(ctx.Context) - wrappedCancel := func() { - cancel() + c, cancel := context.WithCancelCause(ctx.Context) + wrappedCancel := func(cause error) { + cancel(cause) for _, f := range ctx.cleanupFuncs { f() diff --git a/listeners.go b/listeners.go index 0639b16b7..84ebaaaba 100644 --- a/listeners.go +++ b/listeners.go @@ -512,7 +512,7 @@ func ListenerUsage(network, addr string) int { // contextAndCancelFunc groups context and its cancelFunc type contextAndCancelFunc struct { context.Context - context.CancelFunc + context.CancelCauseFunc } // sharedQUICState manages GetConfigForClient @@ -542,17 +542,17 @@ func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Co // addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc // so that when cancelled, the active tls.Config will change -func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelFunc) { +func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) { sqs.rmu.Lock() defer sqs.rmu.Unlock() if cacc, ok := sqs.tlsConfs[tlsConfig]; ok { - return cacc.Context, cacc.CancelFunc + return cacc.Context, cacc.CancelCauseFunc } - ctx, cancel := context.WithCancel(context.Background()) - wrappedCancel := func() { - cancel() + ctx, cancel := context.WithCancelCause(context.Background()) + wrappedCancel := func(cause error) { + cancel(cause) sqs.rmu.Lock() defer sqs.rmu.Unlock() @@ -608,13 +608,13 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error { // indicating that it is pretending to be closed so that the // server using it can terminate, while the underlying // socket is actually left open. -var errFakeClosed = fmt.Errorf("listener 'closed' 😉") +var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉") type fakeCloseQuicListener struct { closed int32 // accessed atomically; belongs to this struct only *sharedQuicListener // embedded, so we also become a quic.EarlyListener context context.Context - contextCancel context.CancelFunc + contextCancel context.CancelCauseFunc } // Currently Accept ignores the passed context, however a situation where @@ -637,7 +637,7 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error) func (fcql *fakeCloseQuicListener) Close() error { if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) { - fcql.contextCancel() + fcql.contextCancel(errFakeClosed) } else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) { _, _ = listenerPool.Delete(fcql.sharedQuicListener.key) } diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 8058dbf33..74f1466be 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -18,6 +18,7 @@ import ( "cmp" "context" "crypto/tls" + "errors" "fmt" "maps" "net" @@ -711,9 +712,10 @@ func (app *App) Stop() error { // enforce grace period if configured if app.GracePeriod > 0 { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod)) + timeout := time.Duration(app.GracePeriod) + ctx, cancel = context.WithTimeoutCause(ctx, timeout, fmt.Errorf("server graceful shutdown %ds timeout", int(timeout.Seconds()))) defer cancel() - app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", time.Duration(app.GracePeriod))) + app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", timeout)) } else { app.logger.Info("servers shutting down with eternal grace period") } @@ -739,6 +741,9 @@ func (app *App) Stop() error { } if err := server.server.Shutdown(ctx); err != nil { + if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) { + err = cause + } app.logger.Error("server shutdown", zap.Error(err), zap.Strings("addresses", server.Listen)) @@ -762,6 +767,9 @@ func (app *App) Stop() error { } if err := server.h3server.Shutdown(ctx); err != nil { + if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) { + err = cause + } app.logger.Error("HTTP/3 server shutdown", zap.Error(err), zap.Strings("addresses", server.Listen)) diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 8d2b99e9e..c65bd6185 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -448,7 +448,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e // complete the handshake before returning the connection if rt.TLSHandshakeTimeout != 0 { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, rt.TLSHandshakeTimeout) + ctx, cancel = context.WithTimeoutCause(ctx, rt.TLSHandshakeTimeout, fmt.Errorf("HTTP transport TLS handshake %ds timeout", int(rt.TLSHandshakeTimeout.Seconds()))) defer cancel() } err = tlsConn.HandshakeContext(ctx) From 5d20adc7a97f70d6fe722099356cca2027908576 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:26:18 -0700 Subject: [PATCH 116/206] build(deps): bump github.com/smallstep/certificates (#7535) Bumps the all-updates group with 1 update: [github.com/smallstep/certificates](https://github.com/smallstep/certificates). Updates `github.com/smallstep/certificates` from 0.30.0-rc2.0.20260211214201-20608299c29c to 0.30.0-rc3 - [Release notes](https://github.com/smallstep/certificates/releases) - [Changelog](https://github.com/smallstep/certificates/blob/master/CHANGELOG.md) - [Commits](https://github.com/smallstep/certificates/commits/v0.30.0-rc3) --- updated-dependencies: - dependency-name: github.com/smallstep/certificates dependency-version: 0.30.0-rc3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index a3dbf86eb..034174c85 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/mholt/acmez/v3 v3.1.6 github.com/prometheus/client_golang v1.23.2 github.com/quic-go/quic-go v0.59.0 - github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c + github.com/smallstep/certificates v0.30.0-rc3 github.com/smallstep/nosql v0.7.0 github.com/smallstep/truststore v0.13.0 github.com/spf13/cobra v1.10.2 @@ -108,10 +108,10 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/oauth2 v0.34.0 // indirect - google.golang.org/api v0.265.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + google.golang.org/api v0.266.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -172,7 +172,7 @@ require ( golang.org/x/sys v0.41.0 golang.org/x/text v0.34.0 golang.org/x/tools v0.42.0 // indirect - google.golang.org/grpc v1.78.0 // indirect + google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index cbce3127d..47017562d 100644 --- a/go.sum +++ b/go.sum @@ -301,8 +301,8 @@ github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c h1:XQpX0IPYUAoJ661YlgfOJmY48ZOhIbglw4E2gw9mcyc= -github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c/go.mod h1:75NRLmYJq6ZcCb8ApJc+W1eL4oMYwjeufMJDHpv4rx4= +github.com/smallstep/certificates v0.30.0-rc3 h1:Lx/NNJ4n+L3Pyx5NtVRGXeqviPPXTFFGLRiC1fCwU50= +github.com/smallstep/certificates v0.30.0-rc3/go.mod h1:e5/ylYYpvnjCVZz6RpyOkpTe73EGPYoL+8TZZ5EtLjI= github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k= github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y= github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= @@ -479,8 +479,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -543,16 +543,16 @@ golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU= -google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY= +google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk= +google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From 9371ee67c64d2d2c81f9530be0d9749ecdbd2b00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:29:44 -0700 Subject: [PATCH 117/206] build(deps): bump the actions-deps group across 1 directory with 12 updates (#7536) Bumps the actions-deps group with 12 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `5.0.0` | `6.0.2` | | [github/ai-moderator](https://github.com/github/ai-moderator) | `1.1.2` | `1.1.4` | | [step-security/harden-runner](https://github.com/step-security/harden-runner) | `2.13.1` | `2.15.0` | | [actions/setup-go](https://github.com/actions/setup-go) | `6.0.0` | `6.3.0` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.2` | `7.0.0` | | [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) | `6.4.0` | `7.0.0` | | [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) | `8.0.0` | `9.2.0` | | [actions/dependency-review-action](https://github.com/actions/dependency-review-action) | `4.8.0` | `4.8.3` | | [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) | `3.10.0` | `4.0.0` | | [anchore/sbom-action](https://github.com/anchore/sbom-action) | `0.20.6` | `0.23.0` | | [peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch) | `4.0.0` | `4.0.1` | | [github/codeql-action](https://github.com/github/codeql-action) | `3.30.5` | `4.32.4` | Updates `actions/checkout` from 5.0.0 to 6.0.2 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...de0fac2e4500dabe0009e67214ff5f5447ce83dd) Updates `github/ai-moderator` from 1.1.2 to 1.1.4 - [Release notes](https://github.com/github/ai-moderator/releases) - [Commits](https://github.com/github/ai-moderator/compare/6bcdb2a79c2e564db8d76d7d4439d91a044c4eb6...81159c370785e295c97461ade67d7c33576e9319) Updates `step-security/harden-runner` from 2.13.1 to 2.15.0 - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a...a90bcbc6539c36a85cdfeb73f7e2f433735f215b) Updates `actions/setup-go` from 6.0.0 to 6.3.0 - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/44694675825211faa026b3c33043df3e48a5fa00...4b73464bb391d4059bd26b0524d20df3927bd417) Updates `actions/upload-artifact` from 4.6.2 to 7.0.0 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f) Updates `goreleaser/goreleaser-action` from 6.4.0 to 7.0.0 - [Release notes](https://github.com/goreleaser/goreleaser-action/releases) - [Commits](https://github.com/goreleaser/goreleaser-action/compare/e435ccd777264be153ace6237001ef4d979d3a7a...ec59f474b9834571250b370d4735c50f8e2d1e29) Updates `golangci/golangci-lint-action` from 8.0.0 to 9.2.0 - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/4afd733a84b1f43292c63897423277bb7f4313a9...1e7e51e771db61008b38414a730f564565cf7c20) Updates `actions/dependency-review-action` from 4.8.0 to 4.8.3 - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/56339e523c0409420f6c2c9a2f4292bbb3c07dd3...05fe4576374b728f0c523d6a13d64c25081e0803) Updates `sigstore/cosign-installer` from 3.10.0 to 4.0.0 - [Release notes](https://github.com/sigstore/cosign-installer/releases) - [Commits](https://github.com/sigstore/cosign-installer/compare/d7543c93d881b35a8faa02e8e3605f69b7a1ce62...faadad0cce49287aee09b3a48701e75088a2c6ad) Updates `anchore/sbom-action` from 0.20.6 to 0.23.0 - [Release notes](https://github.com/anchore/sbom-action/releases) - [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md) - [Commits](https://github.com/anchore/sbom-action/compare/f8bdd1d8ac5e901a77a92f111440fdb1b593736b...17ae1740179002c89186b61233e0f892c3118b11) Updates `peter-evans/repository-dispatch` from 4.0.0 to 4.0.1 - [Release notes](https://github.com/peter-evans/repository-dispatch/releases) - [Commits](https://github.com/peter-evans/repository-dispatch/compare/5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f...28959ce8df70de7be546dd1250a005dd32156697) Updates `github/codeql-action` from 3.30.5 to 4.32.4 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/3599b3baa15b485a2e49ef411a7a4bb2452e7f93...89a39a4e59826350b863aa6b6252a07ad50cf83e) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.2 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: github/ai-moderator dependency-version: 1.1.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: step-security/harden-runner dependency-version: 2.15.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-deps - dependency-name: actions/setup-go dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-deps - dependency-name: actions/upload-artifact dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: goreleaser/goreleaser-action dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: golangci/golangci-lint-action dependency-version: 9.2.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/dependency-review-action dependency-version: 4.8.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: sigstore/cosign-installer dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: anchore/sbom-action dependency-version: 0.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-deps - dependency-name: peter-evans/repository-dispatch dependency-version: 4.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps - dependency-name: github/codeql-action dependency-version: 4.32.4 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ai.yml | 4 ++-- .github/workflows/ci.yml | 22 +++++++++++----------- .github/workflows/cross-build.yml | 6 +++--- .github/workflows/lint.yml | 16 ++++++++-------- .github/workflows/release-proposal.yml | 4 ++-- .github/workflows/release.yml | 16 ++++++++-------- .github/workflows/release_published.yml | 6 +++--- .github/workflows/scorecard.yml | 8 ++++---- 8 files changed, 41 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ai.yml b/.github/workflows/ai.yml index 0008febba..458f8d537 100644 --- a/.github/workflows/ai.yml +++ b/.github/workflows/ai.yml @@ -16,8 +16,8 @@ jobs: models: read contents: read steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - - uses: github/ai-moderator@6bcdb2a79c2e564db8d76d7d4439d91a044c4eb6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: github/ai-moderator@81159c370785e295c97461ade67d7c33576e9319 with: token: ${{ secrets.GITHUB_TOKEN }} spam-label: 'spam' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08a8cd60d..2c5052723 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,15 +65,15 @@ jobs: actions: write # to allow uploading artifacts and cache steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true @@ -120,7 +120,7 @@ jobs: ./caddy stop - name: Publish Build Artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }} path: ${{ matrix.CADDY_BIN_PATH }} @@ -162,13 +162,13 @@ 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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit allowed-endpoints: ci-s390x.caddyserver.com:22 - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Tests run: | set +e @@ -221,19 +221,19 @@ 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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + - uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 with: version: latest args: check - name: Install Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "~1.26" check-latest: true @@ -241,7 +241,7 @@ jobs: run: | go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest xcaddy version - - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + - uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 with: version: latest args: build --single-target --snapshot diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index b86bbdb7c..018a46d15 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -51,15 +51,15 @@ jobs: continue-on-error: true steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e94ad3f35..5a33c5399 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -45,18 +45,18 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: '~1.26' check-latest: true - name: golangci-lint - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: version: latest @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: 'Checkout Repository' - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0 + uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3 with: comment-summary-in-pr: on-failure # https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566 diff --git a/.github/workflows/release-proposal.yml b/.github/workflows/release-proposal.yml index 8cfb63cb6..0b9a09136 100644 --- a/.github/workflows/release-proposal.yml +++ b/.github/workflows/release-proposal.yml @@ -28,11 +28,11 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c975a4cf8..5cb5456ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # Force fetch upstream tags -- because 65 minutes @@ -355,23 +355,23 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Install Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true # Force fetch upstream tags -- because 65 minutes - # tl;dr: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2 runs this line: + # tl;dr: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 runs this line: # git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/ # which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran: # git fetch --prune --unshallow @@ -415,11 +415,11 @@ jobs: run: pip install --upgrade cloudsmith-cli - name: Install Cosign - uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main + uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # main - name: Cosign version run: cosign version - name: Install Syft - uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main + uses: anchore/sbom-action/download-syft@17ae1740179002c89186b61233e0f892c3118b11 # main - name: Syft version run: syft version - name: Install xcaddy @@ -428,7 +428,7 @@ jobs: xcaddy version # GoReleaser will take care of publishing those artifacts into the release - name: Run GoReleaser - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 with: version: latest args: release --clean --timeout 60m diff --git a/.github/workflows/release_published.yml b/.github/workflows/release_published.yml index 8afc5c35e..10a90cb9b 100644 --- a/.github/workflows/release_published.yml +++ b/.github/workflows/release_published.yml @@ -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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: Trigger event on caddyserver/dist - uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0 + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1 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@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0 + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1 with: token: ${{ secrets.REPO_DISPATCH_TOKEN }} repository: caddyserver/caddy-docker diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index bb49f935d..132803f0e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -37,12 +37,12 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: "Checkout code" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -72,7 +72,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: SARIF file path: results.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@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5 + uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5 with: sarif_file: results.sarif From ffb6ab0644f24c5ee6542aca6bd59b7a1b0a8f91 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 5 Mar 2026 08:41:54 -0700 Subject: [PATCH 118/206] Revert cosign (see #7536) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5cb5456ad..2cddde610 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -415,7 +415,7 @@ jobs: run: pip install --upgrade cloudsmith-cli - name: Install Cosign - uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # main + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main - name: Cosign version run: cosign version - name: Install Syft From 1fbb28720b5d20d54074593b3c5d12f3549f371b Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 11 Mar 2026 13:33:59 -0600 Subject: [PATCH 119/206] Fix lint errors Use VerifyConnection instead of VerifyPeerCertificate; the other 2 fixes are "meh" not really a big deal or an issue at all. --- cmd/commandfuncs.go | 2 +- modules/caddytls/connpolicy.go | 23 ++++++++++++++--------- modules/caddytls/folderloader.go | 22 ++++++++++++++++------ 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 28ea20001..faa275b03 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -697,7 +697,7 @@ func cmdFmt(fl Flags) (int, error) { output := caddyfile.Format(input) if fl.Bool("overwrite") { - if err := os.WriteFile(configFile, output, 0o600); err != nil { + if err := os.WriteFile(configFile, output, 0o600); err != nil { //nolint:gosec // path traversal is not really a thing here, this is either "Caddyfile" or admin-controlled return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err) } return caddy.ExitCodeSuccess, nil diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 6b6dc3636..c9258da48 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -885,24 +885,29 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro // if a custom verification function already exists, wrap it clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate - cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate + cfg.VerifyConnection = clientauth.verifyConnection return nil } -// verifyPeerCertificate is for use as a tls.Config.VerifyPeerCertificate -// callback to do custom client certificate verification. It is intended -// for installation only by clientauth.ConfigureTLSConfig(). -func (clientauth *ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { +// verifyConnection is for use as a tls.Config.VerifyConnection callback +// to do custom client certificate verification. It is intended for +// installation only by clientauth.ConfigureTLSConfig(). +// +// Unlike VerifyPeerCertificate, VerifyConnection is called on every +// connection including resumed sessions, preventing session-resumption bypass. +func (clientauth *ClientAuthentication) verifyConnection(cs tls.ConnectionState) error { // first use any pre-existing custom verification function if clientauth.existingVerifyPeerCert != nil { - err := clientauth.existingVerifyPeerCert(rawCerts, verifiedChains) - if err != nil { + rawCerts := make([][]byte, len(cs.PeerCertificates)) + for i, cert := range cs.PeerCertificates { + rawCerts[i] = cert.Raw + } + if err := clientauth.existingVerifyPeerCert(rawCerts, cs.VerifiedChains); err != nil { return err } } for _, verifier := range clientauth.verifiers { - err := verifier.VerifyClientCertificate(rawCerts, verifiedChains) - if err != nil { + if err := verifier.VerifyClientCertificate(nil, cs.VerifiedChains); err != nil { return err } } diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go index 2df6f4cee..b86d3b6ae 100644 --- a/modules/caddytls/folderloader.go +++ b/modules/caddytls/folderloader.go @@ -19,6 +19,7 @@ import ( "crypto/tls" "encoding/pem" "fmt" + "io/fs" "os" "path/filepath" "strings" @@ -62,18 +63,27 @@ func (fl FolderLoader) Provision(ctx caddy.Context) error { func (fl FolderLoader) LoadCertificates() ([]Certificate, error) { var certs []Certificate for _, dir := range fl { - err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error { + root, err := os.OpenRoot(dir) + if err != nil { + return nil, fmt.Errorf("unable to open root directory %s: %w", dir, err) + } + err = filepath.WalkDir(dir, func(fpath string, d fs.DirEntry, err error) error { if err != nil { return fmt.Errorf("unable to traverse into path: %s", fpath) } - if info.IsDir() { + if d.IsDir() { return nil } - if !strings.HasSuffix(strings.ToLower(info.Name()), ".pem") { + if !strings.HasSuffix(strings.ToLower(d.Name()), ".pem") { return nil } - bundle, err := os.ReadFile(fpath) + rel, err := filepath.Rel(dir, fpath) + if err != nil { + return fmt.Errorf("unable to get relative path for %s: %w", fpath, err) + } + + bundle, err := root.ReadFile(rel) if err != nil { return err } @@ -83,11 +93,11 @@ func (fl FolderLoader) LoadCertificates() ([]Certificate, error) { } certs = append(certs, Certificate{Certificate: cert}) - return nil }) + _ = root.Close() if err != nil { - return nil, err + return nil, fmt.Errorf("walking certificates directory %s: %w", dir, err) } } return certs, nil From 8499e34e10c4f5d8445d8d3cca3338cbb13bc2e1 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 16 Mar 2026 16:21:47 -0600 Subject: [PATCH 120/206] caddytls: Ensure key list always gets set (fix #7555) --- modules/caddytls/ech.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index d06047cb1..b915fcfbe 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -132,7 +132,10 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { } } - // ensure old keys are rotated out + // convert the configs into a structure ready for the std lib to use + ech.updateKeyList() + + // ensure any old keys are rotated out if err = ech.rotateECHKeys(ctx, logger, true); err != nil { return nil, fmt.Errorf("rotating ECH configs: %w", err) } @@ -179,9 +182,11 @@ func (ech *ECH) setConfigsFromStorage(ctx caddy.Context, logger *zap.Logger) ([] return outerNames, nil } -// rotateECHKeys updates the ECH keys/configs that are outdated. It should be called -// in a write lock on ech.configsMu. If a lock is already obtained in storage, then -// pass true for storageSynced. +// rotateECHKeys updates the ECH keys/configs that are outdated if rotation is needed. +// It should be called in a write lock on ech.configsMu. If a lock is already obtained +// in storage, then pass true for storageSynced. +// +// This function sets/updates the stdlib-ready key list only if a rotation occurs. func (ech *ECH) rotateECHKeys(ctx caddy.Context, logger *zap.Logger, storageSynced bool) error { storage := ctx.Storage() From df65455b1f0d230a34d4bd7111213fe6917abe98 Mon Sep 17 00:00:00 2001 From: vnxme <46669194+vnxme@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:08:47 +0300 Subject: [PATCH 121/206] caddyhttp: Sync placeholder expansion in `vars` and `vars_regexp` (#7573) * vars: Expand placeholders in custom variables like in `vars_regexp` * vars: Reuse variables inside match loops --- modules/caddyhttp/vars.go | 59 ++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index f19ca16fc..68aaca331 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -181,33 +181,46 @@ func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) { vars := r.Context().Value(VarsCtxKey).(map[string]any) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + var fromPlaceholder bool + var matcherValExpanded, valExpanded, varStr, v string + var varValue any for key, vals := range m { - var varValue any if strings.HasPrefix(key, "{") && strings.HasSuffix(key, "}") && strings.Count(key, "{") == 1 { varValue, _ = repl.Get(strings.Trim(key, "{}")) + fromPlaceholder = true } else { varValue = vars[key] + fromPlaceholder = false + } + + switch vv := varValue.(type) { + case string: + varStr = vv + case fmt.Stringer: + varStr = vv.String() + case error: + varStr = vv.Error() + case nil: + varStr = "" + default: + varStr = fmt.Sprintf("%v", vv) + } + + // Only expand placeholders in values from literal variable names + // (e.g. map outputs). Values resolved from placeholder keys are + // already final and must not be re-expanded, as that would allow + // user input like {env.SECRET} to be evaluated. + valExpanded = varStr + if !fromPlaceholder { + valExpanded = repl.ReplaceAll(varStr, "") } // see if any of the values given in the matcher match the actual value - for _, v := range vals { - matcherValExpanded := repl.ReplaceAll(v, "") - var varStr string - switch vv := varValue.(type) { - case string: - varStr = vv - case fmt.Stringer: - varStr = vv.String() - case error: - varStr = vv.Error() - case nil: - varStr = "" - default: - varStr = fmt.Sprintf("%v", vv) - } - if varStr == matcherValExpanded { + for _, v = range vals { + matcherValExpanded = repl.ReplaceAll(v, "") + if valExpanded == matcherValExpanded { return true, nil } } @@ -310,9 +323,11 @@ func (m MatchVarsRE) Match(r *http.Request) bool { func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { vars := r.Context().Value(VarsCtxKey).(map[string]any) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + + var fromPlaceholder, match bool + var valExpanded, varStr string + var varValue any for key, val := range m { - var varValue any - var fromPlaceholder bool if strings.HasPrefix(key, "{") && strings.HasSuffix(key, "}") && strings.Count(key, "{") == 1 { @@ -320,9 +335,9 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { fromPlaceholder = true } else { varValue = vars[key] + fromPlaceholder = false } - var varStr string switch vv := varValue.(type) { case string: varStr = vv @@ -340,11 +355,11 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { // (e.g. map outputs). Values resolved from placeholder keys are // already final and must not be re-expanded, as that would allow // user input like {env.SECRET} to be evaluated. - valExpanded := varStr + valExpanded = varStr if !fromPlaceholder { valExpanded = repl.ReplaceAll(varStr, "") } - if match := val.Match(valExpanded, repl); match { + if match = val.Match(valExpanded, repl); match { return match, nil } } From 5d189aff40dcfd6e8245310eb696e0de3775a690 Mon Sep 17 00:00:00 2001 From: Tao Date: Sat, 21 Mar 2026 01:36:03 +1000 Subject: [PATCH 122/206] caddytls: Avoid default issuers for implicit tailscale policies (#7577) --- modules/caddytls/automation.go | 25 ++++++++++++++++++- modules/caddytls/automation_test.go | 37 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 modules/caddytls/automation_test.go diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 74125a222..5b7a4ed5d 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -235,7 +235,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { } issuers := ap.Issuers - if len(issuers) == 0 { + if len(issuers) == 0 && !ap.implicitTailscaleManagersOnly() { var err error issuers, err = DefaultIssuersProvisioned(tlsApp.ctx) if err != nil { @@ -429,6 +429,29 @@ func (ap *AutomationPolicy) AllInternalSubjects() bool { }) } +// implicitTailscaleManagersOnly returns true if this policy is configured to +// serve only Tailscale names from the Tailscale manager at handshake-time. +func (ap *AutomationPolicy) implicitTailscaleManagersOnly() bool { + if len(ap.subjects) == 0 { + return false + } + + for _, subject := range ap.subjects { + if !strings.HasSuffix(strings.ToLower(subject), tailscaleDomainAliasEnding) { + return false + } + } + + for _, manager := range ap.Managers { + switch manager.(type) { + case Tailscale, *Tailscale: + return true + } + } + + return false +} + func (ap *AutomationPolicy) onlyInternalIssuer() bool { if len(ap.Issuers) != 1 { return false diff --git a/modules/caddytls/automation_test.go b/modules/caddytls/automation_test.go new file mode 100644 index 000000000..d991b9cf6 --- /dev/null +++ b/modules/caddytls/automation_test.go @@ -0,0 +1,37 @@ +package caddytls + +import ( + "testing" + + "github.com/caddyserver/certmagic" + "go.uber.org/zap" +) + +func TestAutomationPolicyMakeCertMagicConfigImplicitTailscaleManagersOnly(t *testing.T) { + ap := AutomationPolicy{ + Managers: []certmagic.Manager{Tailscale{}}, + subjects: []string{"test-node.example.ts.net"}, + } + + cfg, err := ap.makeCertMagicConfig(&TLS{ + logger: zap.NewNop(), + }, nil, &certmagic.FileStorage{Path: t.TempDir()}) + if err != nil { + t.Fatalf("making certmagic config: %v", err) + } + if cfg.OnDemand == nil { + t.Fatal("expected on-demand config to be set") + } + if len(cfg.Issuers) != 0 { + t.Fatalf("expected no issuers for tailscale-managed ts.net policy, got %d", len(cfg.Issuers)) + } +} + +func TestAutomationPolicyImplicitTailscaleManagersOnlyCatchAll(t *testing.T) { + ap := AutomationPolicy{ + Managers: []certmagic.Manager{Tailscale{}}, + } + if ap.implicitTailscaleManagersOnly() { + t.Fatal("expected catch-all manager policy to remain outside tailscale-only special case") + } +} From c35ba5588d2ccf85bb3767811a65eac819dd277b Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 25 Mar 2026 16:33:24 -0600 Subject: [PATCH 123/206] Add missing return to` handleError` in admin server Thanks to @Wernerina's LLM for finding this bug --- admin.go | 1 + 1 file changed, 1 insertion(+) diff --git a/admin.go b/admin.go index 5ceb3daeb..9c9102120 100644 --- a/admin.go +++ b/admin.go @@ -859,6 +859,7 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) { Err: errors.New("invalid origin 'null'"), Message: "Buggy browser is sending null Origin header.", }) + return } if h.enforceHost { From e98ed6232d65790d27bacd13fb49fa5474b9ec93 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Wed, 25 Mar 2026 23:21:27 -0600 Subject: [PATCH 124/206] chore: Resolve recent CI failures (#7593) --- go.mod | 110 +++---- go.sum | 292 +++++++++--------- modules/caddyhttp/reverseproxy/hosts.go | 35 ++- .../reverseproxy/selectionpolicies.go | 16 +- 4 files changed, 227 insertions(+), 226 deletions(-) diff --git a/go.mod b/go.mod index 034174c85..69a754dba 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( github.com/BurntSushi/toml v1.6.0 - github.com/DeRuina/timberjack v1.3.9 + github.com/DeRuina/timberjack v1.4.0 github.com/KimMachineGun/automemlimit v0.7.5 github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.23.1 @@ -16,41 +16,41 @@ require ( github.com/go-chi/chi/v5 v5.2.5 github.com/google/cel-go v0.27.0 github.com/google/uuid v1.6.0 - github.com/klauspost/compress v1.18.4 + github.com/klauspost/compress v1.18.5 github.com/klauspost/cpuid/v2 v2.3.0 github.com/mholt/acmez/v3 v3.1.6 github.com/prometheus/client_golang v1.23.2 github.com/quic-go/quic-go v0.59.0 - github.com/smallstep/certificates v0.30.0-rc3 - github.com/smallstep/nosql v0.7.0 + github.com/smallstep/certificates v0.30.2 + github.com/smallstep/nosql v0.8.0 github.com/smallstep/truststore v0.13.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 - github.com/yuin/goldmark v1.7.16 + github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/sdk v1.40.0 - go.step.sm/crypto v0.76.2 + go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.67.0 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/sdk v1.42.0 + go.step.sm/crypto v0.77.1 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.48.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 - golang.org/x/net v0.51.0 - golang.org/x/sync v0.19.0 - golang.org/x/term v0.40.0 - golang.org/x/time v0.14.0 + golang.org/x/crypto v0.49.0 + golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 + golang.org/x/net v0.52.0 + golang.org/x/sync v0.20.0 + golang.org/x/term v0.41.0 + golang.org/x/time v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) require ( cel.dev/expr v0.25.1 // indirect - cloud.google.com/go/auth v0.18.1 // indirect + cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect @@ -67,11 +67,11 @@ require ( github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect - github.com/googleapis/gax-go/v2 v2.17.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.18.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/jackc/pgx/v5 v5.8.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect @@ -87,31 +87,31 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect - go.opentelemetry.io/contrib/propagators/aws v1.40.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.40.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 // indirect - go.opentelemetry.io/contrib/propagators/ot v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect - go.opentelemetry.io/otel/log v0.16.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.42.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.42.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.42.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect + go.opentelemetry.io/otel/log v0.18.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.18.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/oauth2 v0.35.0 // indirect - google.golang.org/api v0.266.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + google.golang.org/api v0.271.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -133,13 +133,13 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/libdns/libdns v1.1.1 github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -153,7 +153,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect @@ -162,17 +162,17 @@ require ( github.com/slackhq/nebula v1.10.3 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/urfave/cli v1.22.17 // indirect - go.etcd.io/bbolt v1.3.10 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 + go.etcd.io/bbolt v1.4.3 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.33.0 // indirect - golang.org/x/sys v0.41.0 - golang.org/x/text v0.34.0 + golang.org/x/sys v0.42.0 + golang.org/x/text v0.35.0 golang.org/x/tools v0.42.0 // indirect - google.golang.org/grpc v1.79.1 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 47017562d..da3692ec3 100644 --- a/go.sum +++ b/go.sum @@ -2,16 +2,16 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= -cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ= -cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= +cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= +cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= @@ -28,8 +28,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo= -github.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= +github.com/DeRuina/timberjack v1.4.0 h1:Ipw9KjS/6K6A9D1xdhWebYJFqdQez5gXwfzmeKOroqE= +github.com/DeRuina/timberjack v1.4.0/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk= github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -53,36 +53,36 @@ github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmO github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= -github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= -github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= -github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= -github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= -github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= -github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= -github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= @@ -158,8 +158,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= @@ -187,12 +187,12 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= -github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= -github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= -github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= +github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= @@ -203,16 +203,16 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -276,8 +276,8 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= @@ -301,16 +301,16 @@ github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/certificates v0.30.0-rc3 h1:Lx/NNJ4n+L3Pyx5NtVRGXeqviPPXTFFGLRiC1fCwU50= -github.com/smallstep/certificates v0.30.0-rc3/go.mod h1:e5/ylYYpvnjCVZz6RpyOkpTe73EGPYoL+8TZZ5EtLjI= +github.com/smallstep/certificates v0.30.2 h1:1G3xBi8sJ740iA1mMPW2Svv7EIZKJ4Zf/iQtA5QlN0Y= +github.com/smallstep/certificates v0.30.2/go.mod h1:oyaE/aEYUGDr+YiCZLAxxP22bOQqcSHTeDgp8Vv2rlY= github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k= github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y= github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo= github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc= -github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE= -github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU= +github.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A= +github.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo= github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA= @@ -359,8 +359,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= -github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= @@ -369,70 +369,70 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= -go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI= -go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac= -go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g= -go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0= +go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk= +go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4= +go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8= +go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s= -go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE= -go.opentelemetry.io/contrib/propagators/aws v1.40.0 h1:4VIrh75jW4RTimUNx1DSk+6H9/nDr1FvmKoOVDh3K04= -go.opentelemetry.io/contrib/propagators/aws v1.40.0/go.mod h1:B0dCov9KNQGlut3T8wZZjDnLXEXdBroM7bFsHh/gRos= -go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg= -go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs= -go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 h1:aXl9uobjJs5vquMLt9ZkI/3zIuz8XQ3TqOKSWx0/xdU= -go.opentelemetry.io/contrib/propagators/jaeger v1.40.0/go.mod h1:ioMePqe6k6c/ovXSkmkMr1mbN5qRBGJxNTVop7/2XO0= -go.opentelemetry.io/contrib/propagators/ot v1.40.0 h1:Lon8J5SPmWaL1Ko2TIlCNHJ42/J1b5XbJlgJaE/9m7I= -go.opentelemetry.io/contrib/propagators/ot v1.40.0/go.mod h1:dKWtJTlp1Yj+8Cneye5idO46eRPIbi23qVuJYKjNnvY= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= -go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo= -go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= -go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= -go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= -go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= -go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= -go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/contrib/propagators/autoprop v0.67.0 h1:XhcQRf4MeqwQw96FcnatDAj6gwE19SUrWZ1VwNg77iE= +go.opentelemetry.io/contrib/propagators/autoprop v0.67.0/go.mod h1:7OK06SuNIBIlc5Uq3JGQEsKHuXw29t9OJemvDYyP1dk= +go.opentelemetry.io/contrib/propagators/aws v1.42.0 h1:Kbr3xDxs6kcxp5ThXTKWK2OtwLhNoXBVtqguNYcsZL0= +go.opentelemetry.io/contrib/propagators/aws v1.42.0/go.mod h1:Jzw9hZHtxdpCN7x8S17UH59X/EiFivp6VXLs9bdM1OQ= +go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU= +go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc= +go.opentelemetry.io/contrib/propagators/jaeger v1.42.0 h1:jP8unWI6q5kcb3gpGLjKDGaUa+JW+nHKWvpS/q+YuWA= +go.opentelemetry.io/contrib/propagators/jaeger v1.42.0/go.mod h1:xd89e/pUyPatUP1C4z1UknD9jHptESO99tWyvd4mWD4= +go.opentelemetry.io/contrib/propagators/ot v1.42.0 h1:uQjD1NNqX1+DfcAoWParPt1egNg9vC9gH4xarJ9Khxo= +go.opentelemetry.io/contrib/propagators/ot v1.42.0/go.mod h1:yw/c2TCmQLIv109HBOCn6NlJ8Dp7MNfjMcqQZRnAMmg= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0/go.mod h1:W2m8P+d5Wn5kipj4/xmbt9uMqezEKfBjzVJadfABSBE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= +go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs= +go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= +go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg= +go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw= +go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk= +go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA= +go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4= -go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q= +go.step.sm/crypto v0.77.1 h1:4EEqfKdv0egQ1lqz2RhnU8Jv6QgXZfrgoxWMqJF9aDs= +go.step.sm/crypto v0.77.1/go.mod h1:U/SsmEm80mNnfD5WIkbhuW/B1eFp3fgFvdXyDLpU1AQ= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -456,10 +456,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8= -golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 h1:sQVhWLXbNsa8CTzHOX3IHc7C4Q2JyxI5AweuMQZ/5H0= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -477,10 +477,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -488,8 +488,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -506,8 +506,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -517,8 +517,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -528,10 +528,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -543,16 +543,16 @@ golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk= -google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= -google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= -google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= +google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= +google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc= +google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go index 8139a7b50..a5406e04e 100644 --- a/modules/caddyhttp/reverseproxy/hosts.go +++ b/modules/caddyhttp/reverseproxy/hosts.go @@ -62,7 +62,7 @@ type Upstream struct { activeHealthCheckUpstream string healthCheckPolicy *PassiveHealthChecks cb CircuitBreaker - unhealthy int32 // accessed atomically; status from active health checker + unhealthy atomic.Int32 // status from active health checker } // (pointer receiver necessary to avoid a race condition, since @@ -174,36 +174,36 @@ func (u *Upstream) fillDynamicHost() { // Host is the basic, in-memory representation of the state of a remote host. // Its fields are accessed atomically and Host values must not be copied. type Host struct { - numRequests int64 // must be 64-bit aligned on 32-bit systems (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG) - fails int64 - activePasses int64 - activeFails int64 + numRequests atomic.Int64 // atomic.Int64 is automatically aligned for us (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG) + fails atomic.Int64 + activePasses atomic.Int64 + activeFails atomic.Int64 } // NumRequests returns the number of active requests to the upstream. func (h *Host) NumRequests() int { - return int(atomic.LoadInt64(&h.numRequests)) + return int(h.numRequests.Load()) } // Fails returns the number of recent failures with the upstream. func (h *Host) Fails() int { - return int(atomic.LoadInt64(&h.fails)) + return int(h.fails.Load()) } // activeHealthPasses returns the number of consecutive active health check passes with the upstream. func (h *Host) activeHealthPasses() int { - return int(atomic.LoadInt64(&h.activePasses)) + return int(h.activePasses.Load()) } // activeHealthFails returns the number of consecutive active health check failures with the upstream. func (h *Host) activeHealthFails() int { - return int(atomic.LoadInt64(&h.activeFails)) + return int(h.activeFails.Load()) } // countRequest mutates the active request count by // delta. It returns an error if the adjustment fails. func (h *Host) countRequest(delta int) error { - result := atomic.AddInt64(&h.numRequests, int64(delta)) + result := h.numRequests.Add(int64(delta)) if result < 0 { return fmt.Errorf("count below 0: %d", result) } @@ -213,7 +213,7 @@ func (h *Host) countRequest(delta int) error { // countFail mutates the recent failures count by // delta. It returns an error if the adjustment fails. func (h *Host) countFail(delta int) error { - result := atomic.AddInt64(&h.fails, int64(delta)) + result := h.fails.Add(int64(delta)) if result < 0 { return fmt.Errorf("count below 0: %d", result) } @@ -223,7 +223,7 @@ func (h *Host) countFail(delta int) error { // countHealthPass mutates the recent passes count by // delta. It returns an error if the adjustment fails. func (h *Host) countHealthPass(delta int) error { - result := atomic.AddInt64(&h.activePasses, int64(delta)) + result := h.activePasses.Add(int64(delta)) if result < 0 { return fmt.Errorf("count below 0: %d", result) } @@ -233,7 +233,7 @@ func (h *Host) countHealthPass(delta int) error { // countHealthFail mutates the recent failures count by // delta. It returns an error if the adjustment fails. func (h *Host) countHealthFail(delta int) error { - result := atomic.AddInt64(&h.activeFails, int64(delta)) + result := h.activeFails.Add(int64(delta)) if result < 0 { return fmt.Errorf("count below 0: %d", result) } @@ -242,14 +242,15 @@ func (h *Host) countHealthFail(delta int) error { // resetHealth resets the health check counters. func (h *Host) resetHealth() { - atomic.StoreInt64(&h.activePasses, 0) - atomic.StoreInt64(&h.activeFails, 0) + h.activePasses.Store(0) + h.activeFails.Store(0) } // healthy returns true if the upstream is not actively marked as unhealthy. // (This returns the status only from the "active" health checks.) func (u *Upstream) healthy() bool { - return atomic.LoadInt32(&u.unhealthy) == 0 + return u.unhealthy.Load() == 0 + // return atomic.LoadInt32(&u.unhealthy) == 0 } // SetHealthy sets the upstream has healthy or unhealthy @@ -260,7 +261,7 @@ func (u *Upstream) setHealthy(healthy bool) bool { if healthy { unhealthy, compare = 0, 1 } - return atomic.CompareAndSwapInt32(&u.unhealthy, compare, unhealthy) + return u.unhealthy.CompareAndSwap(compare, unhealthy) } // DialInfo contains information needed to dial a diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index cd1e469f4..050a4f671 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -40,8 +40,8 @@ func init() { caddy.RegisterModule(RandomSelection{}) caddy.RegisterModule(RandomChoiceSelection{}) caddy.RegisterModule(LeastConnSelection{}) - caddy.RegisterModule(RoundRobinSelection{}) - caddy.RegisterModule(WeightedRoundRobinSelection{}) + caddy.RegisterModule(new(RoundRobinSelection)) + caddy.RegisterModule(new(WeightedRoundRobinSelection)) caddy.RegisterModule(FirstSelection{}) caddy.RegisterModule(IPHashSelection{}) caddy.RegisterModule(ClientIPHashSelection{}) @@ -83,12 +83,12 @@ type WeightedRoundRobinSelection struct { // The weight of each upstream in order, // corresponding with the list of upstreams configured. Weights []int `json:"weights,omitempty"` - index uint32 + index atomic.Uint32 totalWeight int } // CaddyModule returns the Caddy module information. -func (WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo { +func (*WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.weighted_round_robin", New: func() caddy.Module { @@ -143,7 +143,7 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, weights = append(weights, w) } } - currentWeight := int(atomic.AddUint32(&r.index, 1)) % r.totalWeight + currentWeight := int(r.index.Add(1)) % r.totalWeight for i, weight := range weights { totalWeight += weight if currentWeight < totalWeight { @@ -295,11 +295,11 @@ func (r *LeastConnSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // RoundRobinSelection is a policy that selects // a host based on round-robin ordering. type RoundRobinSelection struct { - robin uint32 + robin atomic.Uint32 } // CaddyModule returns the Caddy module information. -func (RoundRobinSelection) CaddyModule() caddy.ModuleInfo { +func (*RoundRobinSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.round_robin", New: func() caddy.Module { return new(RoundRobinSelection) }, @@ -313,7 +313,7 @@ func (r *RoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http. return nil } for range n { - robin := atomic.AddUint32(&r.robin, 1) + robin := r.robin.Add(1) host := pool[robin%n] if host.Available() { return host From acf8d6a1ae933df6ae8cda037050a51041c1d67e Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Thu, 26 Mar 2026 14:41:34 -0600 Subject: [PATCH 125/206] caddytls: Consolidate empty APs more smartly (#7567) * caddytls: Consoldate empty APs more smartly (fix #7559) * Revise consolidation logic --- caddyconfig/httpcaddyfile/tlsapp.go | 23 ++++- .../tls_automation_policies_11.caddyfiletest | 5 - .../tls_automation_policies_12.caddyfiletest | 96 +++++++++++++++++++ 3 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/tls_automation_policies_12.caddyfiletest diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index ddec0b941..22bc22816 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -698,14 +698,31 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls emptyAPCount := 0 origLenAPs := len(aps) // compute the number of empty policies (disregarding subjects) - see #4128 + // while we're at it, emptyAP := new(caddytls.AutomationPolicy) for i := 0; i < len(aps); i++ { emptyAP.SubjectsRaw = aps[i].SubjectsRaw + emptyAP.ManagersRaw = nil if reflect.DeepEqual(aps[i], emptyAP) { + // AP is empty emptyAPCount++ - if !automationPolicyHasAllPublicNames(aps[i]) { - // if this automation policy has internal names, we might as well remove it - // so auto-https can implicitly use the internal issuer + + // see if this AP shadows something later + shadowIdx := automationPolicyShadows(i, aps) + emptyAP.SubjectsRaw = nil + if shadowIdx >= 0 { + emptyAP.SubjectsRaw = aps[shadowIdx].SubjectsRaw + // allow the later policy, which is likely for a wildcard, to have cert + // managers ("get_certificate"), since wildcards now cover specific + // subdomains by default, when configured (see discussion in #7559) + emptyAP.ManagersRaw = aps[shadowIdx].ManagersRaw + } + + // if this is the last AP, we can delete it, since auto-https should + // pick it up; if it shadows something later that is also empty, we + // can similarly delete this; but if it shadows something that is NOT + // empty, we must not delete it since the shadowing has a purpose + if i == len(aps)-1 || (shadowIdx >= 0 && reflect.DeepEqual(aps[shadowIdx], emptyAP)) { aps = slices.Delete(aps, i, i+1) i-- } diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest index 9cdfd1200..75a9deb2c 100644 --- a/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest @@ -54,11 +54,6 @@ b.com { "via": "http" } ] - }, - { - "subjects": [ - "b.com" - ] } ] } diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_12.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_12.caddyfiletest new file mode 100644 index 000000000..b79d29746 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_12.caddyfiletest @@ -0,0 +1,96 @@ +# example from https://github.com/caddyserver/caddy/issues/7559 +*.test.local { + tls { + get_certificate http http://cert-server:9000/certs + } + respond "wildcard" +} + +# certificate for this subdomain is covered by wildcard above +subdomain.test.local { + respond "subdomain" +} + +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "subdomain.test.local" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "subdomain", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "*.test.local" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "wildcard", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "*.test.local" + ], + "get_certificate": [ + { + "url": "http://cert-server:9000/certs", + "via": "http" + } + ] + } + ] + } + } + } +} \ No newline at end of file From 6f6771aa1deecd39405991d9ae4e69d4d93b1e5f Mon Sep 17 00:00:00 2001 From: Tao Date: Sun, 29 Mar 2026 03:10:34 +1000 Subject: [PATCH 126/206] rewrite: skip query rename when source key is absent (#7599) --- modules/caddyhttp/rewrite/rewrite.go | 9 +++- modules/caddyhttp/rewrite/rewrite_test.go | 50 +++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index ca5f63bac..ba2ea5407 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -529,7 +529,14 @@ func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) { if key == "" || val == "" { continue } - query[val] = query[key] + if key == val { + continue + } + originalValues, ok := query[key] + if !ok { + continue + } + query[val] = originalValues delete(query, key) } diff --git a/modules/caddyhttp/rewrite/rewrite_test.go b/modules/caddyhttp/rewrite/rewrite_test.go index c3b4c1f6c..602e31084 100644 --- a/modules/caddyhttp/rewrite/rewrite_test.go +++ b/modules/caddyhttp/rewrite/rewrite_test.go @@ -16,6 +16,7 @@ package rewrite import ( "net/http" + "reflect" "regexp" "testing" @@ -397,6 +398,55 @@ func TestRewrite(t *testing.T) { } } +func TestQueryOpsRenameNoOpCases(t *testing.T) { + repl := caddy.NewReplacer() + + for i, tc := range []struct { + input *http.Request + expect map[string][]string + ops *queryOps + }{ + { + ops: &queryOps{ + Rename: []queryOpsArguments{{Key: "ID", Val: "id"}}, + }, + input: newRequest(t, "GET", "/?page=test&id=5&test=100"), + expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}}, + }, + { + ops: &queryOps{ + Rename: []queryOpsArguments{{Key: "id", Val: "id"}}, + }, + input: newRequest(t, "GET", "/?page=test&id=5&test=100"), + expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}}, + }, + { + ops: &queryOps{ + Rename: []queryOpsArguments{{Key: "ID", Val: "id"}}, + }, + input: newRequest(t, "GET", "/?page=test&ID=5&test=100"), + expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}}, + }, + { + ops: &queryOps{ + Rename: []queryOpsArguments{{Key: "ID", Val: "id"}}, + }, + input: newRequest(t, "GET", "/?page=test&ID=5&id=7&test=100"), + expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}}, + }, + } { + repl.Set("http.request.uri", tc.input.RequestURI) + repl.Set("http.request.uri.path", tc.input.URL.Path) + repl.Set("http.request.uri.query", tc.input.URL.RawQuery) + + tc.ops.do(tc.input, repl) + + if actual := tc.input.URL.Query(); !reflect.DeepEqual(tc.expect, map[string][]string(actual)) { + t.Errorf("Test %d: Expected query=%v but got %v", i, tc.expect, actual) + } + } +} + func newRequest(t *testing.T, method, uri string) *http.Request { req, err := http.NewRequest(method, uri, nil) if err != nil { From 62e9c052648f36a34d7d26400ee7118534ae0ff3 Mon Sep 17 00:00:00 2001 From: Marc Date: Sun, 29 Mar 2026 00:44:42 +0700 Subject: [PATCH 127/206] root: introduce down-propagating Helper.BlockState for other directives/plugins to use (#7594) * add 'root' key to Helper.State for access in frankenphp's `php_server` directive * clone state before passing it to child directives, but keep sharing it among sibling directives * propagate named route state from children to parent * use BlockState to set "root" instead * gofmt -w . * go fmt ./... * here we go --- caddyconfig/httpcaddyfile/builtins.go | 6 ++++++ caddyconfig/httpcaddyfile/directives.go | 11 ++++++++++- caddyconfig/httpcaddyfile/httptype.go | 2 ++ caddytest/integration/forwardauth_test.go | 2 +- modules/caddyhttp/reverseproxy/httptransport_test.go | 9 ++++----- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index a7bb3b1de..6d6b71fa8 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -668,6 +668,8 @@ func parseRoot(h Helper) ([]ConfigValue, error) { if !h.NextArg() { return nil, h.ArgErr() } + // store the unmatched root in block state so sibling directives can access it + h.BlockState["root"] = h.Val() return h.NewRoute(nil, caddyhttp.VarsMiddleware{"root": h.Val()}), nil } @@ -682,6 +684,10 @@ func parseRoot(h Helper) ([]ConfigValue, error) { if !h.NextArg() { return nil, h.ArgErr() } + // store the unmatched root in state so sibling/child directives can access it + if userMatcherSet == nil { + h.BlockState["root"] = h.Val() + } // make the route with the matcher return h.NewRoute(userMatcherSet, caddyhttp.VarsMiddleware{"root": h.Val()}), nil } diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index eac7f5dc2..fa911b09e 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -202,7 +202,10 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) { type Helper struct { *caddyfile.Dispenser // State stores intermediate variables during caddyfile adaptation. - State map[string]any + State map[string]any + // BlockState stores intermediate variables scoped to the current block. + // It propagates down, but unlike state not back up from child to parent. + BlockState map[string]any options map[string]any warnings *[]caddyconfig.Warning matcherDefs map[string]caddy.ModuleMap @@ -385,6 +388,11 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) { } } + // clone BlockState once for the entire block so sibling directives + // can share state, but changes don't leak to the parent scope + subBlockState := make(map[string]any, len(h.BlockState)) + maps.Copy(subBlockState, h.BlockState) + // with matchers ready to go, evaluate each directive's segment for _, seg := range segments { dir := seg.Directive() @@ -396,6 +404,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) { subHelper := h subHelper.Dispenser = caddyfile.NewDispenser(seg) subHelper.matcherDefs = matcherDefs + subHelper.BlockState = subBlockState results, err := dirFunc(subHelper) if err != nil { diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 1b9c625fe..c6979e56d 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -143,6 +143,7 @@ func (st ServerType) Setup( parentBlock: sb.block, groupCounter: gc, State: state, + BlockState: state, } results, err := dirFunc(h) @@ -504,6 +505,7 @@ func (ServerType) extractNamedRoutes( parentBlock: sb.block, groupCounter: gc, State: state, + BlockState: state, } handler, err := ParseSegmentAsSubroute(h) diff --git a/caddytest/integration/forwardauth_test.go b/caddytest/integration/forwardauth_test.go index d0ecc2be1..513c80906 100644 --- a/caddytest/integration/forwardauth_test.go +++ b/caddytest/integration/forwardauth_test.go @@ -190,7 +190,7 @@ func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) { // its own values. The backend must receive the auth service values. req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) req.Header.Set("Authorization", "Bearer token123") - req.Header.Set("X-User-Id", "forged-id") // must be overwritten + req.Header.Set("X-User-Id", "forged-id") // must be overwritten req.Header.Set("X-User-Role", "forged-role") // must be overwritten tester.AssertResponse(req, http.StatusOK, "ok") diff --git a/modules/caddyhttp/reverseproxy/httptransport_test.go b/modules/caddyhttp/reverseproxy/httptransport_test.go index 88ac9d591..55ca3fd33 100644 --- a/modules/caddyhttp/reverseproxy/httptransport_test.go +++ b/modules/caddyhttp/reverseproxy/httptransport_test.go @@ -129,11 +129,11 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) { defer cancel() tests := []struct { - name string - tls *TLSConfig - proxyProtocol string + name string + tls *TLSConfig + proxyProtocol string serverNameHasPlaceholder bool - expectDialTLSContext bool + expectDialTLSContext bool }{ { name: "no TLS, no proxy protocol", @@ -194,4 +194,3 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) { }) } } - From 7a630f29103e0141ab293eb8dd76eb4f38bd4d96 Mon Sep 17 00:00:00 2001 From: Sam Ottenhoff Date: Sat, 28 Mar 2026 15:07:21 -0400 Subject: [PATCH 128/206] encode: make zstd checksum configurable (#7586) * http: make zstd checksum configurable * disable_checksum --- .../encode_options.caddyfiletest | 8 +- modules/caddyhttp/encode/caddyfile.go | 5 +- modules/caddyhttp/encode/zstd/zstd.go | 108 ++++++++++++++---- 3 files changed, 94 insertions(+), 27 deletions(-) diff --git a/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest index ea9038ef8..89e897631 100644 --- a/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest @@ -18,7 +18,9 @@ encode gzip zstd { # Long way with a block for each encoding encode { - zstd + zstd { + disable_checksum + } gzip 5 } @@ -71,7 +73,9 @@ encode "gzip": { "level": 5 }, - "zstd": {} + "zstd": { + "checksum": false + } }, "handler": "encode", "prefer": [ diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go index 8b8657080..04b1a3c77 100644 --- a/modules/caddyhttp/encode/caddyfile.go +++ b/modules/caddyhttp/encode/caddyfile.go @@ -41,7 +41,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // // encode [] { // gzip [] -// zstd +// zstd [] { +// level +// disable_checksum +// } // minimum_length // # response matcher block // match { diff --git a/modules/caddyhttp/encode/zstd/zstd.go b/modules/caddyhttp/encode/zstd/zstd.go index 1706de89d..ece935974 100644 --- a/modules/caddyhttp/encode/zstd/zstd.go +++ b/modules/caddyhttp/encode/zstd/zstd.go @@ -33,6 +33,10 @@ type Zstd struct { // The compression level. Accepted values: fastest, better, best, default. Level string `json:"level,omitempty"` + // Whether to include the optional 4-byte zstd frame checksum trailer. + // If unset, the upstream zstd library default is preserved. + Checksum *bool `json:"checksum,omitempty"` + // Compression level refer to type constants value from zstd.SpeedFastest to zstd.SpeedBestCompression level zstd.EncoderLevel } @@ -48,19 +52,48 @@ func (Zstd) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { d.Next() // consume option name - if !d.NextArg() { - return nil + args := d.RemainingArgs() + switch len(args) { + case 0: + case 1: + if _, err := parseEncoderLevel(args[0]); err != nil { + return d.Err(err.Error()) + } + z.Level = args[0] + default: + return d.ArgErr() } - levelStr := d.Val() - if ok, _ := zstd.EncoderLevelFromString(levelStr); !ok { - return d.Errf("unexpected compression level, use one of '%s', '%s', '%s', '%s'", - zstd.SpeedFastest, - zstd.SpeedBetterCompression, - zstd.SpeedBestCompression, - zstd.SpeedDefault, - ) + + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "level": + args := d.RemainingArgs() + if len(args) != 1 { + return d.ArgErr() + } + if z.Level != "" { + return d.Err("compression level already specified") + } + if _, err := parseEncoderLevel(args[0]); err != nil { + return d.Err(err.Error()) + } + z.Level = args[0] + + case "disable_checksum": + if d.NextArg() { + return d.ArgErr() + } + if z.Checksum != nil { + return d.Err("checksum already specified") + } + disabled := false + z.Checksum = &disabled + + default: + return d.Errf("unknown subdirective '%s'", d.Val()) + } } - z.Level = levelStr + return nil } @@ -69,15 +102,11 @@ func (z *Zstd) Provision(ctx caddy.Context) error { if z.Level == "" { z.Level = zstd.SpeedDefault.String() } - var ok bool - if ok, z.level = zstd.EncoderLevelFromString(z.Level); !ok { - return fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'", - zstd.SpeedFastest, - zstd.SpeedDefault, - zstd.SpeedBetterCompression, - zstd.SpeedBestCompression, - ) + level, err := parseEncoderLevel(z.Level) + if err != nil { + return err } + z.level = level return nil } @@ -90,14 +119,45 @@ func (z Zstd) NewEncoder() encode.Encoder { // The default of 8MB for the window is // too large for many clients, so we limit // it to 128K to lighten their load. - writer, _ := zstd.NewWriter( - nil, - zstd.WithWindowSize(128<<10), + writer, _ := zstd.NewWriter(nil, z.writerOptions(128<<10)...) + return writer +} + +func (z Zstd) writerOptions(windowSize int) []zstd.EOption { + opts := []zstd.EOption{ + zstd.WithWindowSize(windowSize), zstd.WithEncoderConcurrency(1), zstd.WithZeroFrames(true), - zstd.WithEncoderLevel(z.level), + zstd.WithEncoderLevel(z.encoderLevel()), + } + if z.Checksum != nil { + opts = append(opts, zstd.WithEncoderCRC(*z.Checksum)) + } + return opts +} + +func (z Zstd) encoderLevel() zstd.EncoderLevel { + if z.level != 0 { + return z.level + } + if z.Level != "" { + if level, err := parseEncoderLevel(z.Level); err == nil { + return level + } + } + return zstd.SpeedDefault +} + +func parseEncoderLevel(level string) (zstd.EncoderLevel, error) { + if ok, encLevel := zstd.EncoderLevelFromString(level); ok { + return encLevel, nil + } + return 0, fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'", + zstd.SpeedFastest, + zstd.SpeedBetterCompression, + zstd.SpeedBestCompression, + zstd.SpeedDefault, ) - return writer } // Interface guards From 30b80bece82822d03302a0260465e5c80da920b1 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Sat, 28 Mar 2026 15:08:34 -0400 Subject: [PATCH 129/206] notify: Always send "READY=1" even after an error (#7597) --- caddy.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/caddy.go b/caddy.go index c27ae4a68..2b4b9087b 100644 --- a/caddy.go +++ b/caddy.go @@ -127,10 +127,9 @@ func Load(cfgJSON []byte, forceReload bool) error { zap.Error(notifyErr), zap.String("reload_err", err.Error())) } - return } - if err := notify.Ready(); err != nil { - Log().Error("unable to notify to service manager of ready state", zap.Error(err)) + if notifyErr := notify.Ready(); notifyErr != nil { + Log().Error("unable to notify to service manager of ready state", zap.Error(notifyErr)) } }() From ea4ee3ae5d5f0e0548e3e90683ba321c02d94afc Mon Sep 17 00:00:00 2001 From: yubiuser Date: Mon, 30 Mar 2026 18:56:10 +0200 Subject: [PATCH 130/206] reverseproxy: Fix check for `header_up Host {upstream_hostport}` redundancy (#7564) * Fix check for header_up Signed-off-by: yubiuser * Onyl check in case commonScheme == "https" Signed-off-by: yubiuser * Move check after TLS transport is enabled Signed-off-by: yubiuser --------- Signed-off-by: yubiuser --- modules/caddyhttp/reverseproxy/caddyfile.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 7b0b052da..777bc06ac 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -725,9 +725,6 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], "", nil) case 2: // some lint checks, I guess - if strings.EqualFold(args[0], "host") && (args[1] == "{hostport}" || args[1] == "{http.request.hostport}") { - caddy.Log().Named("caddyfile").Warn("Unnecessary header_up Host: the reverse proxy's default behavior is to pass headers to the upstream") - } if strings.EqualFold(args[0], "x-forwarded-for") && (args[1] == "{remote}" || args[1] == "{http.request.remote}" || args[1] == "{remote_host}" || args[1] == "{http.request.remote.host}") { caddy.Log().Named("caddyfile").Warn("Unnecessary header_up X-Forwarded-For: the reverse proxy's default behavior is to pass headers to the upstream") } @@ -885,6 +882,14 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return err } } + // check if the user set 'header_up host upstream_hostport' when proxying to HTTPS + // this is unnecessary because it's the default behavior already + if te.TLSEnabled() && h.Headers != nil && h.Headers.Request != nil { + hostVal := h.Headers.Request.Set.Get("Host") + if hostVal == "{upstream_hostport}" || hostVal == "{http.reverse_proxy.upstream.hostport}" { + caddy.Log().Named("caddyfile").Warn("Unnecessary header_up Host: the reverse proxy's default behavior is to pass the configured upstream address to the upstream when proxying to HTTPS") + } + } if commonScheme == "http" && te.TLSEnabled() { return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)") } From 4f504588669e28373f455b694539475ffe4d2926 Mon Sep 17 00:00:00 2001 From: Pieter Berkel Date: Tue, 31 Mar 2026 16:46:32 +1100 Subject: [PATCH 131/206] tls: expand placeholders in dns_challenge override_domain (#7609) --- modules/caddytls/acmeissuer.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index f254f7b2b..b511346b5 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -149,6 +149,15 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { iss.AccountKey = accountKey } + // expand DNS override domain, if non-empty + if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.OverrideDomain != "" { + overrideDomain, err := repl.ReplaceOrErr(iss.Challenges.DNS.OverrideDomain, true, true) + if err != nil { + return fmt.Errorf("expanding DNS override domain '%s': %v", iss.Challenges.DNS.OverrideDomain, err) + } + iss.Challenges.DNS.OverrideDomain = overrideDomain + } + // DNS challenge provider, if not already established if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.solver == nil { var prov certmagic.DNSProvider From d7834676aac1c9718ca78ac4bab421f261fa789e Mon Sep 17 00:00:00 2001 From: Harsh Patel Date: Mon, 6 Apr 2026 03:43:34 +0530 Subject: [PATCH 132/206] tls: add `system` and `combined` CA pool modules (#7406) * feat: add system and combined CA pool modules * fix: combining pools using `CertificateProvider` * fix: lint issue * chore: caddyfiletests * doing it for first time, so not sure if its right. * fix: use `x509` native addCert * chore: explicit err handling * Apply suggestion from @mohammed90 --------- Co-authored-by: Mohammed Al Sahaf --- ...ent_auth_combined_trust_pool.caddyfiletest | 87 +++++ ...auth_combined_trust_pool_pki.caddyfiletest | 87 +++++ ...lient_auth_system_trust_pool.caddyfiletest | 66 ++++ modules/caddytls/capools.go | 322 +++++++++++++++++- modules/caddytls/capools_test.go | 217 ++++++++++++ 5 files changed, 764 insertions(+), 15 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/tls_client_auth_combined_trust_pool.caddyfiletest create mode 100644 caddytest/integration/caddyfile_adapt/tls_client_auth_combined_trust_pool_pki.caddyfiletest create mode 100644 caddytest/integration/caddyfile_adapt/tls_client_auth_system_trust_pool.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_combined_trust_pool.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_combined_trust_pool.caddyfiletest new file mode 100644 index 000000000..1a705f231 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_combined_trust_pool.caddyfiletest @@ -0,0 +1,87 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trust_pool combined { + source inline { + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ== + } + source file { + pem_file ../caddy.ca.cer + } + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "ca": { + "provider": "combined", + "sources": [ + { + "provider": "inline", + "trusted_ca_certs": [ + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==" + ] + }, + { + "pem_files": [ + "../caddy.ca.cer" + ], + "provider": "file" + } + ] + }, + "mode": "request" + } + }, + {} + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_combined_trust_pool_pki.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_combined_trust_pool_pki.caddyfiletest new file mode 100644 index 000000000..a6ba6427c --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_combined_trust_pool_pki.caddyfiletest @@ -0,0 +1,87 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode require_and_verify + trust_pool combined { + source inline { + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ== + } + source pki_root { + authority local + } + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "ca": { + "provider": "combined", + "sources": [ + { + "provider": "inline", + "trusted_ca_certs": [ + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==" + ] + }, + { + "authority": [ + "local" + ], + "provider": "pki_root" + } + ] + }, + "mode": "require_and_verify" + } + }, + {} + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_system_trust_pool.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_system_trust_pool.caddyfiletest new file mode 100644 index 000000000..03384d583 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_system_trust_pool.caddyfiletest @@ -0,0 +1,66 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trust_pool system + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "ca": { + "provider": "system" + }, + "mode": "request" + } + }, + {} + ] + } + } + } + } +} diff --git a/modules/caddytls/capools.go b/modules/caddytls/capools.go index 97ce6af2b..c275f7d6f 100644 --- a/modules/caddytls/capools.go +++ b/modules/caddytls/capools.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" "io" @@ -27,6 +28,8 @@ func init() { caddy.RegisterModule(PKIIntermediateCAPool{}) caddy.RegisterModule(StoragePool{}) caddy.RegisterModule(HTTPCertPool{}) + caddy.RegisterModule(SystemCAPool{}) + caddy.RegisterModule(CombinedCAPool{}) } // The interface to be implemented by all guest modules part of @@ -35,6 +38,12 @@ type CA interface { CertPool() *x509.CertPool } +// CertificateProvider is an optional interface that CA pool sources +// can implement to expose their underlying certificates for combining. +type CertificateProvider interface { + Certificates() []*x509.Certificate +} + // InlineCAPool is a certificate authority pool provider coming from // a DER-encoded certificates in the config type InlineCAPool struct { @@ -44,7 +53,8 @@ type InlineCAPool struct { // these CAs will be rejected. TrustedCACerts []string `json:"trusted_ca_certs,omitempty"` - pool *x509.CertPool + pool *x509.CertPool + certs []*x509.Certificate } // CaddyModule implements caddy.Module. @@ -60,14 +70,17 @@ func (icp InlineCAPool) CaddyModule() caddy.ModuleInfo { // Provision implements caddy.Provisioner. func (icp *InlineCAPool) Provision(ctx caddy.Context) error { caPool := x509.NewCertPool() + var certs []*x509.Certificate for i, clientCAString := range icp.TrustedCACerts { clientCA, err := decodeBase64DERCert(clientCAString) if err != nil { return fmt.Errorf("parsing certificate at index %d: %v", i, err) } caPool.AddCert(clientCA) + certs = append(certs, clientCA) } icp.pool = caPool + icp.certs = certs return nil } @@ -103,6 +116,11 @@ func (icp InlineCAPool) CertPool() *x509.CertPool { return icp.pool } +// Certificates implements CertificateProvider. +func (icp InlineCAPool) Certificates() []*x509.Certificate { + return icp.certs +} + // FileCAPool generates trusted root certificates pool from the designated DER and PEM file type FileCAPool struct { // TrustedCACertPEMFiles is a list of PEM file names @@ -111,7 +129,8 @@ type FileCAPool struct { // these CA certificates will be rejected. TrustedCACertPEMFiles []string `json:"pem_files,omitempty"` - pool *x509.CertPool + pool *x509.CertPool + certs []*x509.Certificate } // CaddyModule implements caddy.Module. @@ -127,14 +146,32 @@ func (FileCAPool) CaddyModule() caddy.ModuleInfo { // Loads and decodes the DER and pem files to generate the certificate pool func (f *FileCAPool) Provision(ctx caddy.Context) error { caPool := x509.NewCertPool() + var certs []*x509.Certificate for _, pemFile := range f.TrustedCACertPEMFiles { pemContents, err := os.ReadFile(pemFile) if err != nil { return fmt.Errorf("reading %s: %v", pemFile, err) } - caPool.AppendCertsFromPEM(pemContents) + // Parse PEM to extract certificates + for len(pemContents) > 0 { + var block *pem.Block + block, pemContents = pem.Decode(pemContents) + if block == nil { + break + } + if block.Type != "CERTIFICATE" { + continue + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("parsing certificate in %s: %v", pemFile, err) + } + caPool.AddCert(cert) + certs = append(certs, cert) + } } f.pool = caPool + f.certs = certs return nil } @@ -166,13 +203,19 @@ func (f FileCAPool) CertPool() *x509.CertPool { return f.pool } +// Certificates implements CertificateProvider. +func (f FileCAPool) Certificates() []*x509.Certificate { + return f.certs +} + // PKIRootCAPool extracts the trusted root certificates from Caddy's native 'pki' app type PKIRootCAPool struct { // List of the Authority names that are configured in the `pki` app whose root certificates are trusted Authority []string `json:"authority,omitempty"` - ca []*caddypki.CA - pool *x509.CertPool + ca []*caddypki.CA + pool *x509.CertPool + certs []*x509.Certificate } // CaddyModule implements caddy.Module. @@ -201,10 +244,17 @@ func (p *PKIRootCAPool) Provision(ctx caddy.Context) error { } caPool := x509.NewCertPool() + var certs []*x509.Certificate for _, ca := range p.ca { - caPool.AddCert(ca.RootCertificate()) + rootCert := ca.RootCertificate() + if rootCert == nil { + return fmt.Errorf("CA %s has no root certificate", ca.ID) + } + caPool.AddCert(rootCert) + certs = append(certs, rootCert) } p.pool = caPool + p.certs = certs return nil } @@ -238,13 +288,19 @@ func (p PKIRootCAPool) CertPool() *x509.CertPool { return p.pool } +// Certificates implements CertificateProvider. +func (p PKIRootCAPool) Certificates() []*x509.Certificate { + return p.certs +} + // PKIIntermediateCAPool extracts the trusted intermediate certificates from Caddy's native 'pki' app type PKIIntermediateCAPool struct { // List of the Authority names that are configured in the `pki` app whose intermediate certificates are trusted Authority []string `json:"authority,omitempty"` - ca []*caddypki.CA - pool *x509.CertPool + ca []*caddypki.CA + pool *x509.CertPool + certs []*x509.Certificate } // CaddyModule implements caddy.Module. @@ -273,12 +329,18 @@ func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error { } caPool := x509.NewCertPool() + var certs []*x509.Certificate for _, ca := range p.ca { for _, c := range ca.IntermediateCertificateChain() { + if c == nil { + return fmt.Errorf("CA %s has a nil certificate in its intermediate chain", ca.ID) + } caPool.AddCert(c) + certs = append(certs, c) } } p.pool = caPool + p.certs = certs return nil } @@ -311,6 +373,11 @@ func (p PKIIntermediateCAPool) CertPool() *x509.CertPool { return p.pool } +// Certificates implements CertificateProvider. +func (p PKIIntermediateCAPool) Certificates() []*x509.Certificate { + return p.certs +} + // StoragePool extracts the trusted certificates root from Caddy storage type StoragePool struct { // The storage module where the trusted root certificates are stored. Absent @@ -322,6 +389,7 @@ type StoragePool struct { storage certmagic.Storage pool *x509.CertPool + certs []*x509.Certificate } // CaddyModule implements caddy.Module. @@ -354,16 +422,33 @@ func (ca *StoragePool) Provision(ctx caddy.Context) error { return fmt.Errorf("no PEM keys specified") } caPool := x509.NewCertPool() + var certs []*x509.Certificate for _, caID := range ca.PEMKeys { bs, err := ca.storage.Load(ctx, caID) if err != nil { return fmt.Errorf("error loading cert '%s' from storage: %s", caID, err) } - if !caPool.AppendCertsFromPEM(bs) { - return fmt.Errorf("failed to add certificate '%s' to pool", caID) + // Parse PEM to extract certificates + pemData := bs + for len(pemData) > 0 { + var block *pem.Block + block, pemData = pem.Decode(pemData) + if block == nil { + break + } + if block.Type != "CERTIFICATE" { + continue + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("parsing certificate '%s': %v", caID, err) + } + caPool.AddCert(cert) + certs = append(certs, cert) } } ca.pool = caPool + ca.certs = certs return nil } @@ -413,9 +498,13 @@ func (p StoragePool) CertPool() *x509.CertPool { return p.pool } +// Certificates implements CertificateProvider. +func (p StoragePool) Certificates() []*x509.Certificate { + return p.certs +} + // TLSConfig holds configuration related to the TLS configuration for the // transport/client. -// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go type TLSConfig struct { // Provides the guest module that provides the trusted certificate authority (CA) certificates CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` @@ -500,7 +589,6 @@ func (t *TLSConfig) unmarshalCaddyfile(d *caddyfile.Dispenser) error { // MakeTLSClientConfig returns a tls.Config usable by a client to a backend. // If there is no custom TLS configuration, a nil config may be returned. -// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go func (t *TLSConfig) makeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) { repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) if !ok || repl == nil { @@ -554,7 +642,8 @@ type HTTPCertPool struct { // Customize the TLS connection knobs to used during the HTTP call TLS *TLSConfig `json:"tls,omitempty"` - pool *x509.CertPool + pool *x509.CertPool + certs []*x509.Certificate } // CaddyModule implements caddy.Module. @@ -570,6 +659,7 @@ func (HTTPCertPool) CaddyModule() caddy.ModuleInfo { // Provision implements caddy.Provisioner. func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error { caPool := x509.NewCertPool() + var certs []*x509.Certificate customTransport := http.DefaultTransport.(*http.Transport).Clone() if hcp.TLS != nil { @@ -597,11 +687,30 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error { if err != nil { return err } - if !caPool.AppendCertsFromPEM(pembs) { - return fmt.Errorf("failed to add certs from URL: %s", uri) + if res.StatusCode < 200 || res.StatusCode >= 300 { + return fmt.Errorf("HTTP %d fetching CA certificate bundle from %s", res.StatusCode, uri) + } + // Parse PEM to extract certificates + pemData := pembs + for len(pemData) > 0 { + var block *pem.Block + block, pemData = pem.Decode(pemData) + if block == nil { + break + } + if block.Type != "CERTIFICATE" { + continue + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("parsing certificate from URL %s: %v", uri, err) + } + caPool.AddCert(cert) + certs = append(certs, cert) } } hcp.pool = caPool + hcp.certs = certs return nil } @@ -665,6 +774,179 @@ func (hcp HTTPCertPool) CertPool() *x509.CertPool { return hcp.pool } +// Certificates implements CertificateProvider. +func (hcp HTTPCertPool) Certificates() []*x509.Certificate { + return hcp.certs +} + +// SystemCAPool obtains the trusted root certificates from the system's +// certificate pool using x509.SystemCertPool() +type SystemCAPool struct { + pool *x509.CertPool +} + +// CaddyModule implements caddy.Module. +func (SystemCAPool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.system", + New: func() caddy.Module { + return new(SystemCAPool) + }, + } +} + +// Provision implements caddy.Provisioner. +func (scp *SystemCAPool) Provision(ctx caddy.Context) error { + pool, err := x509.SystemCertPool() + if err != nil { + return fmt.Errorf("failed to load system cert pool: %v", err) + } + scp.pool = pool + return nil +} + +func (scp *SystemCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume module name + if d.CountRemainingArgs() > 0 { + return d.ArgErr() + } + if d.NextBlock(0) { + return d.Err("system trust pool does not support any configuration") + } + return nil +} + +// CertPool implements CA. +func (scp SystemCAPool) CertPool() *x509.CertPool { + return scp.pool +} + +// The `combined` pool type merges multiple pools. The `sources` pools must implement the +// `CertificateProvider` interface, which allows them to export their certificate set. +// +// Note: SystemCAPool does not implement CertificateProvider because +// x509.SystemCertPool() doesn't expose its certificates, so it cannot +// be used as a source in CombinedCAPool. +type CombinedCAPool struct { + // The CA pool sources to combine. Each source is a CA pool provider module. + SourcesRaw []json.RawMessage `json:"sources,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` + + sources []CA + pool *x509.CertPool + certs []*x509.Certificate +} + +// CaddyModule implements caddy.Module. +func (CombinedCAPool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.combined", + New: func() caddy.Module { + return new(CombinedCAPool) + }, + } +} + +// Provision implements caddy.Provisioner. +func (ccp *CombinedCAPool) Provision(ctx caddy.Context) error { + if len(ccp.SourcesRaw) == 0 { + return fmt.Errorf("no sources specified for combined CA pool") + } + + // Load all source modules + sources, err := ctx.LoadModule(ccp, "SourcesRaw") + if err != nil { + return fmt.Errorf("loading CA pool sources: %v", err) + } + + caPool := x509.NewCertPool() + var allCerts []*x509.Certificate + + for _, src := range sources.([]any) { + ca, ok := src.(CA) + if !ok { + return fmt.Errorf("source module is not a CA pool provider") + } + ccp.sources = append(ccp.sources, ca) + + certProvider, ok := ca.(CertificateProvider) + if !ok { + return fmt.Errorf("source %T does not implement CertificateProvider (required for combining)", ca) + } + + certs := certProvider.Certificates() + if certs == nil { + return fmt.Errorf("source %T returned nil certificates", ca) + } + for _, cert := range certs { + if cert == nil { + return fmt.Errorf("source %T returned a nil certificate", ca) + } + caPool.AddCert(cert) + allCerts = append(allCerts, cert) + } + } + + ccp.pool = caPool + ccp.certs = allCerts + + return nil +} + +// Syntax: +// +// trust_pool combined { +// source { +// +// } +// } +// +// The 'source' directive can be specified multiple times. Sources that +// don't implement CertificateProvider (like 'system') cannot be combined. +func (ccp *CombinedCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume module name + if d.CountRemainingArgs() > 0 { + return d.ArgErr() + } + + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "source": + if !d.NextArg() { + return d.ArgErr() + } + modStem := d.Val() + modID := "tls.ca_pool.source." + modStem + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + ca, ok := unm.(CA) + if !ok { + return d.Errf("module %s is not a CA pool provider", modID) + } + ccp.SourcesRaw = append(ccp.SourcesRaw, caddyconfig.JSONModuleObject(ca, "provider", modStem, nil)) + default: + return d.Errf("unrecognized directive: %s", d.Val()) + } + } + + if len(ccp.SourcesRaw) == 0 { + return d.Err("no sources specified") + } + + return nil +} + +// CertPool implements CA. +func (ccp CombinedCAPool) CertPool() *x509.CertPool { + return ccp.pool +} + +// Certificates implements CertificateProvider. +func (ccp CombinedCAPool) Certificates() []*x509.Certificate { + return ccp.certs +} + var ( _ caddy.Module = (*InlineCAPool)(nil) _ caddy.Provisioner = (*InlineCAPool)(nil) @@ -696,4 +978,14 @@ var ( _ caddy.Validator = (*HTTPCertPool)(nil) _ CA = (*HTTPCertPool)(nil) _ caddyfile.Unmarshaler = (*HTTPCertPool)(nil) + + _ caddy.Module = (*SystemCAPool)(nil) + _ caddy.Provisioner = (*SystemCAPool)(nil) + _ CA = (*SystemCAPool)(nil) + _ caddyfile.Unmarshaler = (*SystemCAPool)(nil) + + _ caddy.Module = (*CombinedCAPool)(nil) + _ caddy.Provisioner = (*CombinedCAPool)(nil) + _ CA = (*CombinedCAPool)(nil) + _ caddyfile.Unmarshaler = (*CombinedCAPool)(nil) ) diff --git a/modules/caddytls/capools_test.go b/modules/caddytls/capools_test.go index b355792d1..881eeb384 100644 --- a/modules/caddytls/capools_test.go +++ b/modules/caddytls/capools_test.go @@ -1,6 +1,7 @@ package caddytls import ( + "context" "encoding/json" "fmt" "reflect" @@ -776,3 +777,219 @@ func TestHTTPCertPoolUnmarshalCaddyfile(t *testing.T) { }) } } + +func TestSystemCAPoolUnmarshalCaddyfile(t *testing.T) { + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "basic system pool configuration", + args: args{ + d: caddyfile.NewTestDispenser(`system`), + }, + wantErr: false, + }, + { + name: "system pool with arguments produces error", + args: args{ + d: caddyfile.NewTestDispenser(`system foo`), + }, + wantErr: true, + }, + { + name: "system pool with block produces error", + args: args{ + d: caddyfile.NewTestDispenser(`system { + foo bar + }`), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scp := &SystemCAPool{} + if err := scp.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("SystemCAPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCombinedCAPoolUnmarshalCaddyfile(t *testing.T) { + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty block produces error", + args: args{ + d: caddyfile.NewTestDispenser(`combined { + }`), + }, + wantErr: true, + }, + { + name: "arguments on same line as module name produces error", + args: args{ + d: caddyfile.NewTestDispenser(`combined foo`), + }, + wantErr: true, + }, + { + name: "single source - system", + args: args{ + d: caddyfile.NewTestDispenser(`combined { + source system + }`), + }, + wantErr: false, + }, + { + name: "single source - inline with config", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(`combined { + source inline { + trust_der %s + } + }`, test_der_1)), + }, + wantErr: false, + }, + { + name: "multiple sources produces error due to limitation", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(`combined { + source system + source inline { + trust_der %s + } + }`, test_der_1)), + }, + wantErr: false, // UnmarshalCaddyfile succeeds, but Provision will fail + }, + { + name: "source without module name produces error", + args: args{ + d: caddyfile.NewTestDispenser(`combined { + source + }`), + }, + wantErr: true, + }, + { + name: "invalid directive produces error", + args: args{ + d: caddyfile.NewTestDispenser(`combined { + invalid_directive foo + }`), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ccp := &CombinedCAPool{} + if err := ccp.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("CombinedCAPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && len(ccp.SourcesRaw) == 0 { + t.Errorf("CombinedCAPool.UnmarshalCaddyfile() produced no sources") + } + }) + } +} + +func TestSystemCAPoolProvision(t *testing.T) { + scp := &SystemCAPool{} + ctx := caddy.Context{Context: context.Background()} + + err := scp.Provision(ctx) + if err != nil { + t.Errorf("SystemCAPool.Provision() error = %v", err) + } + + if scp.pool == nil { + t.Error("SystemCAPool.Provision() did not create a cert pool") + } + + pool := scp.CertPool() + if pool == nil { + t.Error("SystemCAPool.CertPool() returned nil") + } +} + +func TestCombinedCAPoolProvisionWithSystemFails(t *testing.T) { + // Test that combining system pool fails during Provision + // because SystemCAPool doesn't implement CertificateProvider + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + // Create a combined pool with system source + ccp := &CombinedCAPool{ + SourcesRaw: []json.RawMessage{ + json.RawMessage(`{"provider":"system"}`), + }, + } + + err := ccp.Provision(ctx) + if err == nil { + t.Error("CombinedCAPool.Provision() with system source should fail, but succeeded") + } + + // Verify error message mentions CertificateProvider + if err != nil && !contains(err.Error(), "CertificateProvider") { + t.Errorf("Expected error to mention CertificateProvider, got: %v", err) + } +} + +func TestCombinedCAPoolProvisionWithInlineSucceeds(t *testing.T) { + // Test that combining inline pools works + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + // Create a combined pool with inline source + ccp := &CombinedCAPool{ + SourcesRaw: []json.RawMessage{ + json.RawMessage(fmt.Sprintf(`{"provider":"inline","trusted_ca_certs":["%s"]}`, test_der_1)), + }, + } + + err := ccp.Provision(ctx) + if err != nil { + t.Errorf("CombinedCAPool.Provision() with inline source failed: %v", err) + } + + if ccp.pool == nil { + t.Error("CombinedCAPool.Provision() did not create a cert pool") + } + + pool := ccp.CertPool() + if pool == nil { + t.Error("CombinedCAPool.CertPool() returned nil") + } +} + +// Helper function for string contains check +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(substr) == 0 || + (len(s) > 0 && len(substr) > 0 && findSubstring(s, substr))) +} + +func findSubstring(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} From 5de1565ff6182fbb191b5a2d3949b977ed536d57 Mon Sep 17 00:00:00 2001 From: vnxme <46669194+vnxme@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:37:43 +0300 Subject: [PATCH 133/206] vars: Don't expand placeholders in values (#7629) --- modules/caddyhttp/vars.go | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index 68aaca331..6c17fe9bb 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -181,18 +181,15 @@ func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) { vars := r.Context().Value(VarsCtxKey).(map[string]any) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - var fromPlaceholder bool - var matcherValExpanded, valExpanded, varStr, v string + var matcherValExpanded, varStr, v string var varValue any for key, vals := range m { if strings.HasPrefix(key, "{") && strings.HasSuffix(key, "}") && strings.Count(key, "{") == 1 { varValue, _ = repl.Get(strings.Trim(key, "{}")) - fromPlaceholder = true } else { varValue = vars[key] - fromPlaceholder = false } switch vv := varValue.(type) { @@ -208,19 +205,15 @@ func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) { varStr = fmt.Sprintf("%v", vv) } - // Only expand placeholders in values from literal variable names - // (e.g. map outputs). Values resolved from placeholder keys are + // Don't expand placeholders in values from literal variable names + // (e.g. map outputs) or other placeholders. These values are // already final and must not be re-expanded, as that would allow // user input like {env.SECRET} to be evaluated. - valExpanded = varStr - if !fromPlaceholder { - valExpanded = repl.ReplaceAll(varStr, "") - } // see if any of the values given in the matcher match the actual value for _, v = range vals { matcherValExpanded = repl.ReplaceAll(v, "") - if valExpanded == matcherValExpanded { + if varStr == matcherValExpanded { return true, nil } } @@ -324,18 +317,16 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { vars := r.Context().Value(VarsCtxKey).(map[string]any) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - var fromPlaceholder, match bool - var valExpanded, varStr string + var match bool + var varStr string var varValue any for key, val := range m { if strings.HasPrefix(key, "{") && strings.HasSuffix(key, "}") && strings.Count(key, "{") == 1 { varValue, _ = repl.Get(strings.Trim(key, "{}")) - fromPlaceholder = true } else { varValue = vars[key] - fromPlaceholder = false } switch vv := varValue.(type) { @@ -351,15 +342,12 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { varStr = fmt.Sprintf("%v", vv) } - // Only expand placeholders in values from literal variable names - // (e.g. map outputs). Values resolved from placeholder keys are + // Don't expand placeholders in values from literal variable names + // (e.g. map outputs) or other placeholders. These values are // already final and must not be re-expanded, as that would allow // user input like {env.SECRET} to be evaluated. - valExpanded = varStr - if !fromPlaceholder { - valExpanded = repl.ReplaceAll(varStr, "") - } - if match = val.Match(valExpanded, repl); match { + + if match = val.Match(varStr, repl); match { return match, nil } } From 6c23ec2f3c91503df40e6d6d0f29d53ee93b33dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:31:08 -0600 Subject: [PATCH 134/206] build(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp (#7637) Bumps [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp](https://github.com/open-telemetry/opentelemetry-go) from 1.42.0 to 1.43.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.42.0...v1.43.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp dependency-version: 1.43.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 20 ++++++++++---------- go.sum | 44 ++++++++++++++++++++++---------------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index 69a754dba..048612cb6 100644 --- a/go.mod +++ b/go.mod @@ -33,8 +33,8 @@ require ( go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 go.opentelemetry.io/contrib/propagators/autoprop v0.67.0 - go.opentelemetry.io/otel v1.42.0 - go.opentelemetry.io/otel/sdk v1.42.0 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/sdk v1.43.0 go.step.sm/crypto v0.77.1 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 @@ -95,7 +95,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect @@ -104,14 +104,14 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect go.opentelemetry.io/otel/log v0.18.0 // indirect go.opentelemetry.io/otel/sdk/log v0.18.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/oauth2 v0.36.0 // indirect google.golang.org/api v0.271.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -164,15 +164,15 @@ require ( github.com/urfave/cli v1.22.17 // indirect go.etcd.io/bbolt v1.4.3 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 - go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/sys v0.42.0 golang.org/x/text v0.35.0 golang.org/x/tools v0.42.0 // indirect - google.golang.org/grpc v1.79.3 // indirect + google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index da3692ec3..e959cc949 100644 --- a/go.sum +++ b/go.sum @@ -391,16 +391,16 @@ go.opentelemetry.io/contrib/propagators/jaeger v1.42.0 h1:jP8unWI6q5kcb3gpGLjKDG go.opentelemetry.io/contrib/propagators/jaeger v1.42.0/go.mod h1:xd89e/pUyPatUP1C4z1UknD9jHptESO99tWyvd4mWD4= go.opentelemetry.io/contrib/propagators/ot v1.42.0 h1:uQjD1NNqX1+DfcAoWParPt1egNg9vC9gH4xarJ9Khxo= go.opentelemetry.io/contrib/propagators/ot v1.42.0/go.mod h1:yw/c2TCmQLIv109HBOCn6NlJ8Dp7MNfjMcqQZRnAMmg= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0/go.mod h1:W2m8P+d5Wn5kipj4/xmbt9uMqezEKfBjzVJadfABSBE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= @@ -417,20 +417,20 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1J go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg= go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= -go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw= go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk= go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA= go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw= -go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.step.sm/crypto v0.77.1 h1:4EEqfKdv0egQ1lqz2RhnU8Jv6QgXZfrgoxWMqJF9aDs= go.step.sm/crypto v0.77.1/go.mod h1:U/SsmEm80mNnfD5WIkbhuW/B1eFp3fgFvdXyDLpU1AQ= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= @@ -541,18 +541,18 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc= google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From 92b62004eb93440b0b234222224f99678382eb24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 20:39:00 +0000 Subject: [PATCH 135/206] build(deps): bump the all-updates group across 1 directory with 11 updates (#7641) Bumps the all-updates group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/DeRuina/timberjack](https://github.com/DeRuina/timberjack) | `1.4.0` | `1.4.1` | | [github.com/google/cel-go](https://github.com/google/cel-go) | `0.27.0` | `0.28.0` | | [go.opentelemetry.io/contrib/exporters/autoexport](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.67.0` | `0.68.0` | | [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.67.0` | `0.68.0` | | [go.opentelemetry.io/contrib/propagators/autoprop](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.67.0` | `0.68.0` | | [go.step.sm/crypto](https://github.com/smallstep/crypto) | `0.77.1` | `0.77.2` | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.49.0` | `0.50.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.52.0` | `0.53.0` | Updates `github.com/DeRuina/timberjack` from 1.4.0 to 1.4.1 - [Release notes](https://github.com/DeRuina/timberjack/releases) - [Changelog](https://github.com/DeRuina/timberjack/blob/main/CHANGELOG.md) - [Commits](https://github.com/DeRuina/timberjack/compare/v1.4.0...v1.4.1) Updates `github.com/google/cel-go` from 0.27.0 to 0.28.0 - [Release notes](https://github.com/google/cel-go/releases) - [Commits](https://github.com/google/cel-go/compare/v0.27.0...v0.28.0) Updates `go.opentelemetry.io/contrib/exporters/autoexport` from 0.67.0 to 0.68.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.67.0...zpages/v0.68.0) Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.67.0 to 0.68.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.67.0...zpages/v0.68.0) Updates `go.opentelemetry.io/contrib/propagators/autoprop` from 0.67.0 to 0.68.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.67.0...zpages/v0.68.0) Updates `go.step.sm/crypto` from 0.77.1 to 0.77.2 - [Release notes](https://github.com/smallstep/crypto/releases) - [Commits](https://github.com/smallstep/crypto/compare/v0.77.1...v0.77.2) Updates `golang.org/x/crypto` from 0.49.0 to 0.50.0 - [Commits](https://github.com/golang/crypto/compare/v0.49.0...v0.50.0) Updates `golang.org/x/net` from 0.52.0 to 0.53.0 - [Commits](https://github.com/golang/net/compare/v0.52.0...v0.53.0) Updates `golang.org/x/term` from 0.41.0 to 0.42.0 - [Commits](https://github.com/golang/term/compare/v0.41.0...v0.42.0) Updates `golang.org/x/sys` from 0.42.0 to 0.43.0 - [Commits](https://github.com/golang/sys/compare/v0.42.0...v0.43.0) Updates `golang.org/x/text` from 0.35.0 to 0.36.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.35.0...v0.36.0) --- updated-dependencies: - dependency-name: github.com/DeRuina/timberjack dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: github.com/google/cel-go dependency-version: 0.28.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/exporters/autoexport dependency-version: 0.68.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp dependency-version: 0.68.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/propagators/autoprop dependency-version: 0.68.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.step.sm/crypto dependency-version: 0.77.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: golang.org/x/crypto dependency-version: 0.50.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: golang.org/x/net dependency-version: 0.53.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: golang.org/x/term dependency-version: 0.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: golang.org/x/sys dependency-version: 0.43.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: golang.org/x/text dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 70 +++++++++++++------------- go.sum | 152 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 111 insertions(+), 111 deletions(-) diff --git a/go.mod b/go.mod index 048612cb6..090ab1925 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( github.com/BurntSushi/toml v1.6.0 - github.com/DeRuina/timberjack v1.4.0 + github.com/DeRuina/timberjack v1.4.1 github.com/KimMachineGun/automemlimit v0.7.5 github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.23.1 @@ -14,7 +14,7 @@ require ( github.com/cloudflare/circl v1.6.3 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.2.5 - github.com/google/cel-go v0.27.0 + github.com/google/cel-go v0.28.0 github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.18.5 github.com/klauspost/cpuid/v2 v2.3.0 @@ -30,20 +30,20 @@ require ( github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.67.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 - go.step.sm/crypto v0.77.1 + go.step.sm/crypto v0.77.2 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.49.0 + golang.org/x/crypto v0.50.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 - golang.org/x/net v0.52.0 + golang.org/x/net v0.53.0 golang.org/x/sync v0.20.0 - golang.org/x/term v0.41.0 + golang.org/x/term v0.42.0 golang.org/x/time v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -68,7 +68,7 @@ require ( github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.18.0 // indirect + github.com/googleapis/gax-go/v2 v2.19.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/jackc/pgx/v5 v5.8.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -87,31 +87,31 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect - go.opentelemetry.io/contrib/propagators/aws v1.42.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.42.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.42.0 // indirect - go.opentelemetry.io/contrib/propagators/ot v1.42.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.43.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.43.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect - go.opentelemetry.io/otel/log v0.18.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.18.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.65.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/oauth2 v0.36.0 // indirect - google.golang.org/api v0.271.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/api v0.272.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -163,15 +163,15 @@ require ( github.com/spf13/cast v1.7.0 // indirect github.com/urfave/cli v1.22.17 // indirect go.etcd.io/bbolt v1.4.3 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/sys v0.42.0 - golang.org/x/text v0.35.0 - golang.org/x/tools v0.42.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/sys v0.43.0 + golang.org/x/text v0.36.0 + golang.org/x/tools v0.43.0 // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect howett.net/plist v1.0.0 // indirect diff --git a/go.sum b/go.sum index e959cc949..8d77fbd82 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/DeRuina/timberjack v1.4.0 h1:Ipw9KjS/6K6A9D1xdhWebYJFqdQez5gXwfzmeKOroqE= -github.com/DeRuina/timberjack v1.4.0/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= +github.com/DeRuina/timberjack v1.4.1 h1:JftM5HN/ITKehAXjtdbGqN5XZIS1biHm7VSjU0Qbtqg= +github.com/DeRuina/timberjack v1.4.1/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk= github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -168,8 +168,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= -github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= +github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc= +github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= @@ -179,8 +179,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws= -github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ= +github.com/google/go-tpm-tools v0.4.8 h1:V4oIYyAD3BykOycwYQzO29WefDouQMTsYZqmG3HxOfM= +github.com/google/go-tpm-tools v0.4.8/go.mod h1:4DfiOtiS1KppJjwf1+tqtW4K3PrCJjAAqFKj/TYTJKg= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -189,8 +189,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= -github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= +github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= +github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -373,66 +373,66 @@ go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk= -go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4= -go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8= -go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ= +go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= +go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= +go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 h1:0D3GFvELGIwQGfC6agLsbrEYSGWZTRTxIXxcQUqrOuk= +go.opentelemetry.io/contrib/exporters/autoexport v0.68.0/go.mod h1:DM2NV7Zb8CcGeVPt6glouY0FAiwZQ/iqgcWExhgWeN8= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= -go.opentelemetry.io/contrib/propagators/autoprop v0.67.0 h1:XhcQRf4MeqwQw96FcnatDAj6gwE19SUrWZ1VwNg77iE= -go.opentelemetry.io/contrib/propagators/autoprop v0.67.0/go.mod h1:7OK06SuNIBIlc5Uq3JGQEsKHuXw29t9OJemvDYyP1dk= -go.opentelemetry.io/contrib/propagators/aws v1.42.0 h1:Kbr3xDxs6kcxp5ThXTKWK2OtwLhNoXBVtqguNYcsZL0= -go.opentelemetry.io/contrib/propagators/aws v1.42.0/go.mod h1:Jzw9hZHtxdpCN7x8S17UH59X/EiFivp6VXLs9bdM1OQ= -go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU= -go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc= -go.opentelemetry.io/contrib/propagators/jaeger v1.42.0 h1:jP8unWI6q5kcb3gpGLjKDGaUa+JW+nHKWvpS/q+YuWA= -go.opentelemetry.io/contrib/propagators/jaeger v1.42.0/go.mod h1:xd89e/pUyPatUP1C4z1UknD9jHptESO99tWyvd4mWD4= -go.opentelemetry.io/contrib/propagators/ot v1.42.0 h1:uQjD1NNqX1+DfcAoWParPt1egNg9vC9gH4xarJ9Khxo= -go.opentelemetry.io/contrib/propagators/ot v1.42.0/go.mod h1:yw/c2TCmQLIv109HBOCn6NlJ8Dp7MNfjMcqQZRnAMmg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 h1:wLGFvNBPqQhzBn0QRBZjrriH8lZ9gqtTz8ufHEjLg7k= +go.opentelemetry.io/contrib/propagators/autoprop v0.68.0/go.mod h1:evWK9nCqCzH8nhclTlpkdUzmxrmJQ2mrWCdKIvyOYec= +go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg= +go.opentelemetry.io/contrib/propagators/aws v1.43.0/go.mod h1:CJjTym6F87tEdm61Qvnz5xrV8vKlH4C92djiqcn62k8= +go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A= +go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw= +go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 h1:peiLMz1+aqJE+3L4mOVtR9wlmv+yh/JVYXCBjqmzJJE= +go.opentelemetry.io/contrib/propagators/jaeger v1.43.0/go.mod h1:Agvif+4A8p/3UtZzJ0MCcDEuQwgtrzM71DueU41DCs8= +go.opentelemetry.io/contrib/propagators/ot v1.43.0 h1:Hh1HahlGc81AOE7siqi1tVOlbanY/UxMMWedpb0d5oQ= +go.opentelemetry.io/contrib/propagators/ot v1.43.0/go.mod h1:58MlyS7lghzYvAm5LN9gGmZpCMQEMB5vpZp9SRgOyE4= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0/go.mod h1:W2m8P+d5Wn5kipj4/xmbt9uMqezEKfBjzVJadfABSBE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= -go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs= -go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= -go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg= -go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE= +go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw= -go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk= -go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA= -go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.step.sm/crypto v0.77.1 h1:4EEqfKdv0egQ1lqz2RhnU8Jv6QgXZfrgoxWMqJF9aDs= -go.step.sm/crypto v0.77.1/go.mod h1:U/SsmEm80mNnfD5WIkbhuW/B1eFp3fgFvdXyDLpU1AQ= +go.step.sm/crypto v0.77.2 h1:qFjjei+RHc5kP5R7NW9OUWT7SqWIuAOvOkXqg4fNWj8= +go.step.sm/crypto v0.77.2/go.mod h1:W0YJb9onM5l78qgkXIJ2Up6grnwW8EtpCKIza/NCg0o= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -445,8 +445,8 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -456,8 +456,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 h1:sQVhWLXbNsa8CTzHOX3IHc7C4Q2JyxI5AweuMQZ/5H0= golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= @@ -467,8 +467,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -477,8 +477,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -506,8 +506,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -517,8 +517,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -528,8 +528,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -538,19 +538,19 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= -google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= -google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc= -google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= +google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE= +google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= +google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= +google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= From ca0ca67fbdb831c026d334dfd77ecc653f321876 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Sat, 11 Apr 2026 06:49:32 +1000 Subject: [PATCH 136/206] reverseproxy: make stream copy buffer size configurable (#7627) --- ...rse_proxy_stream_buffer_size.caddyfiletest | 56 +++++++++++++++++++ modules/caddyhttp/reverseproxy/caddyfile.go | 5 +- .../caddyhttp/reverseproxy/reverseproxy.go | 6 ++ modules/caddyhttp/reverseproxy/streaming.go | 20 ++++++- .../caddyhttp/reverseproxy/streaming_test.go | 46 +++++++++++++++ 5 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/reverse_proxy_stream_buffer_size.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_stream_buffer_size.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_stream_buffer_size.caddyfiletest new file mode 100644 index 000000000..5320b0529 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_stream_buffer_size.caddyfiletest @@ -0,0 +1,56 @@ +https://example.com { + reverse_proxy https://localhost:54321 { + stream_buffer_size 8KB + } +} + +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "stream_buffer_size": 8000, + "transport": { + "protocol": "http", + "tls": {} + }, + "upstreams": [ + { + "dial": "localhost:54321" + } + ] + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 777bc06ac..a370a2873 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -96,6 +96,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // flush_interval // request_buffers // response_buffers +// stream_buffer_size // stream_timeout // stream_close_delay // verbose_logs @@ -646,7 +647,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { h.FlushInterval = caddy.Duration(dur) } - case "request_buffers", "response_buffers": + case "request_buffers", "response_buffers", "stream_buffer_size": subdir := d.Val() if !d.NextArg() { return d.ArgErr() @@ -670,6 +671,8 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { h.RequestBuffers = size case "response_buffers": h.ResponseBuffers = size + case "stream_buffer_size": + h.StreamBufferSize = int(size) } case "stream_timeout": diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 2169d1717..3b9b56a05 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -171,6 +171,12 @@ type Handler struct { // forcibly closed at the end of the timeout. Default: no timeout. StreamTimeout caddy.Duration `json:"stream_timeout,omitempty"` + // The size of the buffer used for each direction of streaming + // requests such as WebSockets. If zero, the default size is 32 KiB. + // This only affects upgraded bidirectional streams, not normal + // request or response buffering. + StreamBufferSize int `json:"stream_buffer_size,omitempty"` + // If nonzero, streaming requests such as WebSockets will not be // closed when the proxy config is unloaded, and instead the stream // will remain open until the delay is complete. In other words, diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index 64b6d39d1..e454ee655 100644 --- a/modules/caddyhttp/reverseproxy/streaming.go +++ b/modules/caddyhttp/reverseproxy/streaming.go @@ -204,7 +204,12 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, defer deleteFrontConn() defer deleteBackConn() - spc := switchProtocolCopier{user: conn, backend: backConn, wg: wg} + spc := switchProtocolCopier{ + user: conn, + backend: backConn, + wg: wg, + bufferSize: h.StreamBufferSize, + } // setup the timeout if requested var timeoutc <-chan time.Time @@ -636,20 +641,29 @@ func (m *maxLatencyWriter) stop() { type switchProtocolCopier struct { user, backend io.ReadWriteCloser wg *sync.WaitGroup + bufferSize int } func (c switchProtocolCopier) copyFromBackend(errc chan<- error) { - _, err := io.Copy(c.user, c.backend) + _, err := io.CopyBuffer(c.user, c.backend, c.buffer()) errc <- err c.wg.Done() } func (c switchProtocolCopier) copyToBackend(errc chan<- error) { - _, err := io.Copy(c.backend, c.user) + _, err := io.CopyBuffer(c.backend, c.user, c.buffer()) errc <- err c.wg.Done() } +func (c switchProtocolCopier) buffer() []byte { + size := c.bufferSize + if size <= 0 { + size = defaultBufferSize + } + return make([]byte, size) +} + var streamingBufPool = sync.Pool{ New: func() any { // The Pool's New function should generally only return pointer diff --git a/modules/caddyhttp/reverseproxy/streaming_test.go b/modules/caddyhttp/reverseproxy/streaming_test.go index 3f6da2ffa..ce0db65a0 100644 --- a/modules/caddyhttp/reverseproxy/streaming_test.go +++ b/modules/caddyhttp/reverseproxy/streaming_test.go @@ -2,8 +2,10 @@ package reverseproxy import ( "bytes" + "io" "net/http/httptest" "strings" + "sync" "testing" "github.com/caddyserver/caddy/v2" @@ -34,3 +36,47 @@ func TestHandlerCopyResponse(t *testing.T) { } } } + +func TestSwitchProtocolCopierBufferSize(t *testing.T) { + var wg sync.WaitGroup + var errc = make(chan error, 1) + var dst bytes.Buffer + + copier := switchProtocolCopier{ + user: nopReadWriteCloser{Reader: strings.NewReader("hello")}, + backend: nopReadWriteCloser{Writer: &dst}, + wg: &wg, + bufferSize: 7, + } + + buf := copier.buffer() + if got := len(buf); got != 7 { + t.Fatalf("buffer len = %d, want 7", got) + } + + wg.Add(1) + go copier.copyToBackend(errc) + wg.Wait() + + if err := <-errc; err != nil { + t.Fatalf("copyToBackend() error = %v", err) + } + if got := dst.String(); got != "hello" { + t.Fatalf("copied data = %q, want %q", got, "hello") + } +} + +func TestSwitchProtocolCopierDefaultBufferSize(t *testing.T) { + copier := switchProtocolCopier{} + buf := copier.buffer() + if got := len(buf); got != defaultBufferSize { + t.Fatalf("buffer len = %d, want %d", got, defaultBufferSize) + } +} + +type nopReadWriteCloser struct { + io.Reader + io.Writer +} + +func (nopReadWriteCloser) Close() error { return nil } From 7dcc041eec7e6221e99b8f13225b7afba6cacb8b Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Sat, 11 Apr 2026 08:27:52 +1000 Subject: [PATCH 137/206] vars: Add matcher placeholder handling tests (#7640) * vars: add matcher placeholder handling tests * vars: add query placeholder matcher coverage --- modules/caddyhttp/vars_test.go | 159 +++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 modules/caddyhttp/vars_test.go diff --git a/modules/caddyhttp/vars_test.go b/modules/caddyhttp/vars_test.go new file mode 100644 index 000000000..7cbe583e5 --- /dev/null +++ b/modules/caddyhttp/vars_test.go @@ -0,0 +1,159 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyhttp + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func newVarsTestRequest(t *testing.T, target string, headers http.Header, vars map[string]any) (*http.Request, *caddy.Replacer) { + t.Helper() + + if target == "" { + target = "https://example.com/test" + } + + req := httptest.NewRequest(http.MethodGet, target, nil) + req.Header = headers + + repl := caddy.NewReplacer() + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + if vars == nil { + vars = make(map[string]any) + } + // Inject vars directly so these tests exercise matcher-side handling of + // already-resolved values, not VarsMiddleware placeholder expansion. + ctx = context.WithValue(ctx, VarsCtxKey, vars) + req = req.WithContext(ctx) + + addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) + + return req, repl +} + +func TestVarsMatcherDoesNotExpandResolvedValues(t *testing.T) { + t.Setenv("CADDY_VARS_TEST_SECRET", "topsecret") + + for _, tc := range []struct { + name string + target string + match VarsMatcher + headers http.Header + vars map[string]any + expect bool + }{ + { + name: "literal variable value containing placeholder syntax is not re-expanded", + match: VarsMatcher{"secret": []string{"topsecret"}}, + vars: map[string]any{"secret": "{env.CADDY_VARS_TEST_SECRET}"}, + expect: false, + }, + { + name: "placeholder key value containing placeholder syntax is not re-expanded", + match: VarsMatcher{"{http.request.header.X-Input}": []string{"topsecret"}}, + headers: http.Header{"X-Input": []string{"{env.CADDY_VARS_TEST_SECRET}"}}, + expect: false, + }, + { + name: "query placeholder value containing placeholder syntax is not re-expanded", + target: "https://example.com/test?foo=%7Benv.CADDY_VARS_TEST_SECRET%7D", + match: VarsMatcher{"{http.request.uri.query.foo}": []string{"topsecret"}}, + expect: false, + }, + { + name: "matcher values still expand placeholders", + match: VarsMatcher{"secret": []string{"{env.CADDY_VARS_TEST_SECRET}"}}, + vars: map[string]any{"secret": "topsecret"}, + expect: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + req, _ := newVarsTestRequest(t, tc.target, tc.headers, tc.vars) + + actual, err := tc.match.MatchWithError(req) + if err != nil { + t.Fatalf("MatchWithError() error = %v", err) + } + + if actual != tc.expect { + t.Fatalf("MatchWithError() = %t, want %t", actual, tc.expect) + } + }) + } +} + +func TestMatchVarsREDoesNotExpandResolvedValues(t *testing.T) { + t.Setenv("CADDY_VARS_TEST_SECRET", "topsecret") + + for _, tc := range []struct { + name string + target string + match MatchVarsRE + headers http.Header + vars map[string]any + expect bool + }{ + { + name: "literal variable value containing placeholder syntax is not re-expanded", + match: MatchVarsRE{"secret": &MatchRegexp{Pattern: "^topsecret$"}}, + vars: map[string]any{"secret": "{env.CADDY_VARS_TEST_SECRET}"}, + expect: false, + }, + { + name: "placeholder key value containing placeholder syntax is not re-expanded", + match: MatchVarsRE{"{http.request.header.X-Input}": &MatchRegexp{Pattern: "^topsecret$"}}, + headers: http.Header{"X-Input": []string{"{env.CADDY_VARS_TEST_SECRET}"}}, + expect: false, + }, + { + name: "query placeholder value containing placeholder syntax is not re-expanded", + target: "https://example.com/test?foo=%7Benv.CADDY_VARS_TEST_SECRET%7D", + match: MatchVarsRE{"{http.request.uri.query.foo}": &MatchRegexp{Pattern: "^topsecret$"}}, + expect: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := tc.match.Provision(caddy.Context{}) + if err != nil { + t.Fatalf("Provision() error = %v", err) + } + + err = tc.match.Validate() + if err != nil { + t.Fatalf("Validate() error = %v", err) + } + + req, _ := newVarsTestRequest(t, tc.target, tc.headers, tc.vars) + + actual, err := tc.match.MatchWithError(req) + if err != nil { + t.Fatalf("MatchWithError() error = %v", err) + } + + if actual != tc.expect { + t.Fatalf("MatchWithError() = %t, want %t", actual, tc.expect) + } + }) + } +} From c8e4ac2c8c9bc8c4a6b63bc3618dd4ba64e9c7ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:33:07 -0600 Subject: [PATCH 138/206] build(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4 (#7621) Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.1.3 to 4.1.4. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Commits](https://github.com/go-jose/go-jose/compare/v4.1.3...v4.1.4) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v4 dependency-version: 4.1.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 090ab1925..de2031c97 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tspi v0.3.0 // indirect diff --git a/go.sum b/go.sum index 8d77fbd82..53018c2c1 100644 --- a/go.sum +++ b/go.sum @@ -151,8 +151,8 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From 5f44ea0748b9612bc09d274f5553c3cc9e624296 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Sat, 11 Apr 2026 09:09:12 +1000 Subject: [PATCH 139/206] logging: add journald encoder wrapper (#7623) --- .../log_journald_encoder.caddyfiletest | 47 ++++ modules/logging/journaldencoder.go | 221 ++++++++++++++++++ modules/logging/journaldencoder_test.go | 155 ++++++++++++ 3 files changed, 423 insertions(+) create mode 100644 caddytest/integration/caddyfile_adapt/log_journald_encoder.caddyfiletest create mode 100644 modules/logging/journaldencoder.go create mode 100644 modules/logging/journaldencoder_test.go diff --git a/caddytest/integration/caddyfile_adapt/log_journald_encoder.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_journald_encoder.caddyfiletest new file mode 100644 index 000000000..89ac3ed2b --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/log_journald_encoder.caddyfiletest @@ -0,0 +1,47 @@ +{ + log { + format journald { + wrap console + } + } +} + +:80 { + respond "Hello, World!" +} +---------- +{ + "logging": { + "logs": { + "default": { + "encoder": { + "format": "journald", + "wrap": { + "format": "console" + } + } + } + } + }, + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "handle": [ + { + "body": "Hello, World!", + "handler": "static_response" + } + ] + } + ] + } + } + } + } +} diff --git a/modules/logging/journaldencoder.go b/modules/logging/journaldencoder.go new file mode 100644 index 000000000..5826c6950 --- /dev/null +++ b/modules/logging/journaldencoder.go @@ -0,0 +1,221 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logging + +import ( + "encoding/json" + "fmt" + "os" + + "go.uber.org/zap/buffer" + "go.uber.org/zap/zapcore" + "golang.org/x/term" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +func init() { + caddy.RegisterModule(JournaldEncoder{}) +} + +// JournaldEncoder wraps another encoder and prepends a systemd/journald +// priority prefix to each emitted log line. This lets journald classify +// stdout/stderr log lines by severity while leaving the underlying log +// structure to the wrapped encoder. +// +// This encoder does not write directly to journald; it only changes the +// encoded output by adding the priority marker that journald understands. +// The wrapped encoder still controls the actual log format, such as JSON +// or console output. +type JournaldEncoder struct { + zapcore.Encoder `json:"-"` + + // The underlying encoder that actually encodes the log entries. + // If not specified, defaults to "json", unless the output is a + // terminal, in which case it defaults to "console". + WrappedRaw json.RawMessage `json:"wrap,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"` + + wrappedIsDefault bool + ctx caddy.Context +} + +// CaddyModule returns the Caddy module information. +func (JournaldEncoder) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.logging.encoders.journald", + New: func() caddy.Module { return new(JournaldEncoder) }, + } +} + +// Provision sets up the encoder. +func (je *JournaldEncoder) Provision(ctx caddy.Context) error { + je.ctx = ctx + + if je.WrappedRaw == nil { + je.Encoder = &JSONEncoder{} + if p, ok := je.Encoder.(caddy.Provisioner); ok { + if err := p.Provision(ctx); err != nil { + return fmt.Errorf("provisioning fallback encoder module: %v", err) + } + } + je.wrappedIsDefault = true + } else { + val, err := ctx.LoadModule(je, "WrappedRaw") + if err != nil { + return fmt.Errorf("loading wrapped encoder module: %v", err) + } + je.Encoder = val.(zapcore.Encoder) + } + + suppressEncoderTimestamp(je.Encoder) + + return nil +} + +// ConfigureDefaultFormat will set the default wrapped format to "console" +// if the writer is a terminal. If already configured, it passes through +// the writer so a deeply nested encoder can configure its own default format. +func (je *JournaldEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error { + if !je.wrappedIsDefault { + if cfd, ok := je.Encoder.(caddy.ConfiguresFormatterDefault); ok { + return cfd.ConfigureDefaultFormat(wo) + } + return nil + } + + if caddy.IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) { + je.Encoder = &ConsoleEncoder{} + if p, ok := je.Encoder.(caddy.Provisioner); ok { + if err := p.Provision(je.ctx); err != nil { + return fmt.Errorf("provisioning fallback encoder module: %v", err) + } + } + } + + suppressEncoderTimestamp(je.Encoder) + + return nil +} + +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// journald { +// wrap +// } +// +// Example: +// +// log { +// format journald { +// wrap json +// } +// } +func (je *JournaldEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume encoder name + if d.NextArg() { + return d.ArgErr() + } + + for d.NextBlock(0) { + if d.Val() != "wrap" { + return d.Errf("unrecognized subdirective %s", d.Val()) + } + if !d.NextArg() { + return d.ArgErr() + } + moduleName := d.Val() + moduleID := "caddy.logging.encoders." + moduleName + unm, err := caddyfile.UnmarshalModule(d, moduleID) + if err != nil { + return err + } + enc, ok := unm.(zapcore.Encoder) + if !ok { + return d.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm) + } + je.WrappedRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, nil) + } + + return nil +} + +// Clone implements zapcore.Encoder. +func (je JournaldEncoder) Clone() zapcore.Encoder { + return JournaldEncoder{ + Encoder: je.Encoder.Clone(), + } +} + +// EncodeEntry implements zapcore.Encoder. +func (je JournaldEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { + encoded, err := je.Encoder.Clone().EncodeEntry(ent, fields) + if err != nil { + return nil, err + } + + out := bufferpool.Get() + out.AppendString(journaldPriorityPrefix(ent.Level)) + out.AppendBytes(encoded.Bytes()) + encoded.Free() + + return out, nil +} + +func journaldPriorityPrefix(level zapcore.Level) string { + switch level { + case zapcore.InvalidLevel: + return "<6>" + case zapcore.DebugLevel: + return "<7>" + case zapcore.InfoLevel: + return "<6>" + case zapcore.WarnLevel: + return "<4>" + case zapcore.ErrorLevel: + return "<3>" + case zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel: + return "<2>" + default: + return "<6>" + } +} + +func suppressEncoderTimestamp(enc zapcore.Encoder) { + empty := "" + + switch e := enc.(type) { + case *ConsoleEncoder: + e.TimeKey = &empty + _ = e.Provision(caddy.Context{}) + case *JSONEncoder: + e.TimeKey = &empty + _ = e.Provision(caddy.Context{}) + case *AppendEncoder: + suppressEncoderTimestamp(e.wrapped) + case *FilterEncoder: + suppressEncoderTimestamp(e.wrapped) + case *JournaldEncoder: + suppressEncoderTimestamp(e.Encoder) + } +} + +// Interface guards +var ( + _ zapcore.Encoder = (*JournaldEncoder)(nil) + _ caddyfile.Unmarshaler = (*JournaldEncoder)(nil) + _ caddy.ConfiguresFormatterDefault = (*JournaldEncoder)(nil) +) diff --git a/modules/logging/journaldencoder_test.go b/modules/logging/journaldencoder_test.go new file mode 100644 index 000000000..7b9a17d77 --- /dev/null +++ b/modules/logging/journaldencoder_test.go @@ -0,0 +1,155 @@ +package logging + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/caddyserver/caddy/v2" + "go.uber.org/zap/buffer" + "go.uber.org/zap/zapcore" + + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +func TestJournaldPriorityPrefix(t *testing.T) { + tests := []struct { + level zapcore.Level + want string + }{ + {level: zapcore.InvalidLevel, want: "<6>"}, + {level: zapcore.DebugLevel, want: "<7>"}, + {level: zapcore.InfoLevel, want: "<6>"}, + {level: zapcore.WarnLevel, want: "<4>"}, + {level: zapcore.ErrorLevel, want: "<3>"}, + {level: zapcore.DPanicLevel, want: "<2>"}, + {level: zapcore.PanicLevel, want: "<2>"}, + {level: zapcore.FatalLevel, want: "<2>"}, + } + + for _, tt := range tests { + t.Run(tt.level.String(), func(t *testing.T) { + if got := journaldPriorityPrefix(tt.level); got != tt.want { + t.Fatalf("got %s, want %s", got, tt.want) + } + }) + } +} + +func TestJournaldEncoderEncodeEntry(t *testing.T) { + tests := []struct { + name string + level zapcore.Level + want string + }{ + {name: "debug", level: zapcore.DebugLevel, want: "<7>wrapped\n"}, + {name: "info", level: zapcore.InfoLevel, want: "<6>wrapped\n"}, + {name: "warn", level: zapcore.WarnLevel, want: "<4>wrapped\n"}, + {name: "error", level: zapcore.ErrorLevel, want: "<3>wrapped\n"}, + {name: "panic", level: zapcore.PanicLevel, want: "<2>wrapped\n"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enc := JournaldEncoder{Encoder: staticEncoder{output: "wrapped\n"}} + buf, err := enc.EncodeEntry(zapcore.Entry{Level: tt.level}, nil) + if err != nil { + t.Fatalf("EncodeEntry() error = %v", err) + } + defer buf.Free() + + if got := buf.String(); got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestJournaldEncoderUnmarshalCaddyfile(t *testing.T) { + d := caddyfile.NewTestDispenser(` +journald { + wrap console +} +`) + + var enc JournaldEncoder + if err := enc.UnmarshalCaddyfile(d); err != nil { + t.Fatalf("UnmarshalCaddyfile() error = %v", err) + } + + var got map[string]any + if err := json.Unmarshal(enc.WrappedRaw, &got); err != nil { + t.Fatalf("unmarshal wrapped encoder: %v", err) + } + + if got["format"] != "console" { + t.Fatalf("wrapped format = %v, want console", got["format"]) + } +} + +func TestJournaldEncoderSuppressesJSONTimestamp(t *testing.T) { + enc := &JournaldEncoder{ + Encoder: &JSONEncoder{}, + } + if err := enc.Provision(caddy.Context{Context: context.Background()}); err != nil { + t.Fatalf("Provision() error = %v", err) + } + + buf, err := enc.EncodeEntry(zapcore.Entry{ + Level: zapcore.InfoLevel, + Time: fixedEntryTime(), + Message: "hello", + }, nil) + if err != nil { + t.Fatalf("EncodeEntry() error = %v", err) + } + defer buf.Free() + + got := buf.String() + if strings.Contains(got, `"ts"`) { + t.Fatalf("got JSON output with ts field: %q", got) + } +} + +func TestJournaldEncoderSuppressesConsoleTimestamp(t *testing.T) { + enc := &JournaldEncoder{ + Encoder: &ConsoleEncoder{}, + } + if err := enc.Provision(caddy.Context{Context: context.Background()}); err != nil { + t.Fatalf("Provision() error = %v", err) + } + + buf, err := enc.EncodeEntry(zapcore.Entry{ + Level: zapcore.InfoLevel, + Time: fixedEntryTime(), + Message: "hello", + }, nil) + if err != nil { + t.Fatalf("EncodeEntry() error = %v", err) + } + defer buf.Free() + + got := buf.String() + if strings.Contains(got, "2001/02/03") { + t.Fatalf("got console output with timestamp: %q", got) + } +} + +type staticEncoder struct { + nopEncoder + output string +} + +func (se staticEncoder) Clone() zapcore.Encoder { return se } + +func (se staticEncoder) EncodeEntry(zapcore.Entry, []zapcore.Field) (*buffer.Buffer, error) { + buf := bufferpool.Get() + buf.AppendString(se.output) + return buf, nil +} + +func fixedEntryTime() (ts time.Time) { + return time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC) +} From 8e2dd5079c27af50a3288502b3a82f683e3737fe Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Sat, 11 Apr 2026 09:17:55 +1000 Subject: [PATCH 140/206] caddyfile: Improve import/global options UX for imports before global options (#7642) * caddyfile: improve import/global options UX Keep standalone global-options braces stable in fmt when they follow import lines. Also improve validate output for imports before the global options block with a clearer error message. Add focused formatter and parser regression coverage * caddyfile: satisfy staticcheck in formatter --- caddyconfig/caddyfile/formatter.go | 40 ++++++++++++++++++++++++- caddyconfig/caddyfile/formatter_test.go | 15 ++++++++++ caddyconfig/caddyfile/parse.go | 17 +++++++++++ caddyconfig/caddyfile/parse_test.go | 30 +++++++++++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) diff --git a/caddyconfig/caddyfile/formatter.go b/caddyconfig/caddyfile/formatter.go index dfd316b16..14315a3f1 100644 --- a/caddyconfig/caddyfile/formatter.go +++ b/caddyconfig/caddyfile/formatter.go @@ -63,8 +63,33 @@ func Format(input []byte) []byte { heredocClosingMarker []rune nesting int // indentation level + + currentToken strings.Builder + currentLineFirstToken string + previousLineWasTopLevelImport bool + openBraceOwnLine bool ) + finishToken := func() { + if currentToken.Len() == 0 { + return + } + if currentLineFirstToken == "" { + currentLineFirstToken = currentToken.String() + } + currentToken.Reset() + } + + finishLine := func() { + finishToken() + if currentLineFirstToken != "" { + previousLineWasTopLevelImport = nesting == 0 && currentLineFirstToken == "import" + } else if !openBrace || !openBraceOwnLine || openBraceWritten { + previousLineWasTopLevelImport = false + } + currentLineFirstToken = "" + } + write := func(ch rune) { out.WriteRune(ch) last = ch @@ -220,9 +245,11 @@ func Format(input []byte) []byte { } if unicode.IsSpace(ch) { + finishToken() space = true heredocEscaped = false if ch == '\n' { + finishLine() newLines++ } continue @@ -249,13 +276,19 @@ func Format(input []byte) []byte { } openBrace = false - if beginningOfLine { + if openBraceOwnLine && previousLineWasTopLevelImport { + if last != '\n' { + nextLine() + } + indent() + } else if beginningOfLine { indent() } else if !openBraceSpace || !unicode.IsSpace(last) { write(' ') } write('{') openBraceWritten = true + openBraceOwnLine = false nextLine() newLines = 0 // prevent infinite nesting from ridiculous inputs (issue #4169) @@ -266,8 +299,10 @@ func Format(input []byte) []byte { switch { case ch == '{': + finishToken() openBrace = true openBraceSpace = spacePrior && !beginningOfLine + openBraceOwnLine = newLines > 0 if openBraceSpace && newLines == 0 { write(' ') } @@ -275,11 +310,13 @@ func Format(input []byte) []byte { if quotes == "`" { write('{') openBraceWritten = true + openBraceOwnLine = false continue } continue case ch == '}' && (spacePrior || !openBrace): + finishToken() if quotes == "`" { write('}') continue @@ -324,6 +361,7 @@ func Format(input []byte) []byte { space = true } + currentToken.WriteRune(ch) write(ch) beginningOfLine = false diff --git a/caddyconfig/caddyfile/formatter_test.go b/caddyconfig/caddyfile/formatter_test.go index 6ab293615..3d586f813 100644 --- a/caddyconfig/caddyfile/formatter_test.go +++ b/caddyconfig/caddyfile/formatter_test.go @@ -475,6 +475,21 @@ Hope this helps.` + "`" + ` }`, expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}", }, + { + description: "imports before global options block keep standalone brace", + input: `import ./conf.d/matcher_my_subnet.caddy +import ./conf.d/matcher_not_my_subnet.caddy +{ + order crowdsec first + order appsec after crowdsec +}`, + expect: `import ./conf.d/matcher_my_subnet.caddy +import ./conf.d/matcher_not_my_subnet.caddy +{ + order crowdsec first + order appsec after crowdsec +}`, + }, } { // the formatter should output a trailing newline, // even if the tests aren't written to expect that diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index e9f27dfbf..6a4db5bbb 100644 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -682,11 +682,28 @@ func (p *parser) directive() error { // a opening curly brace. It does NOT advance the token. func (p *parser) openCurlyBrace() error { if p.Val() != "{" { + if p.valLooksLikeGlobalOptionsAfterImportedSnippets() { + return p.Err("global options block must appear before import directives; move the global options block to the top of the Caddyfile") + } return p.SyntaxErr("{") } return nil } +func (p *parser) valLooksLikeGlobalOptionsAfterImportedSnippets() bool { + if p.Val() != "import" || len(p.block.Keys) == 0 { + return false + } + + for _, key := range p.block.Keys { + if !strings.HasPrefix(key.Text, "(") || !strings.HasSuffix(key.Text, ")") { + return false + } + } + + return true +} + // closeCurlyBrace expects the current token to be // a closing curly brace. This acts like an assertion // because it returns an error if the token is not diff --git a/caddyconfig/caddyfile/parse_test.go b/caddyconfig/caddyfile/parse_test.go index bf149e635..516b7dfd9 100644 --- a/caddyconfig/caddyfile/parse_test.go +++ b/caddyconfig/caddyfile/parse_test.go @@ -930,6 +930,36 @@ func TestAcceptSiteImportWithBraces(t *testing.T) { } } +func TestGlobalOptionsAfterImportedSnippetsGivesHelpfulError(t *testing.T) { + tempDir := t.TempDir() + importFile1 := filepath.Join(tempDir, "matcher_snippet_1.caddy") + importFile2 := filepath.Join(tempDir, "matcher_snippet_2.caddy") + + err := os.WriteFile(importFile1, []byte(`(matcher1)`), 0o644) + if err != nil { + t.Fatalf("writing first import file: %v", err) + } + + err = os.WriteFile(importFile2, []byte(`(matcher2)`), 0o644) + if err != nil { + t.Fatalf("writing second import file: %v", err) + } + + _, err = Parse("Testfile", []byte(`import `+importFile1+` +import `+importFile2+` +{ + debug +}`)) + if err == nil { + t.Fatal("Expected an error, but got nil") + } + + expected := "global options block must appear before import directives; move the global options block to the top of the Caddyfile" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("Expected error to start with '%s' but got '%v'", expected, err) + } +} + func testParser(input string) parser { return parser{Dispenser: NewTestDispenser(input)} } From 0722cf6fd8e999d137aa1fb520d4c3bbdcc8b63e Mon Sep 17 00:00:00 2001 From: tsinglua Date: Sun, 12 Apr 2026 00:53:12 +0800 Subject: [PATCH 141/206] chore: replace `interface{}` with `any` for modernization (#7571) Signed-off-by: tsinglua --- modules/caddyhttp/reverseproxy/headers_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/headers_test.go b/modules/caddyhttp/reverseproxy/headers_test.go index 9385468f6..cb4a664f8 100644 --- a/modules/caddyhttp/reverseproxy/headers_test.go +++ b/modules/caddyhttp/reverseproxy/headers_test.go @@ -17,7 +17,7 @@ func TestAddForwardedHeadersNonIP(t *testing.T) { // Mock the context variables required by Caddy. // We need to inject the variable map manually since we aren't running the full server. - vars := map[string]interface{}{ + vars := map[string]any{ caddyhttp.TrustedProxyVarKey: false, } ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars) @@ -42,7 +42,7 @@ func TestAddForwardedHeaders_UnixSocketTrusted(t *testing.T) { req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Host", "original.example.com") - vars := map[string]interface{}{ + vars := map[string]any{ caddyhttp.TrustedProxyVarKey: true, caddyhttp.ClientIPVarKey: "1.2.3.4", } @@ -74,7 +74,7 @@ func TestAddForwardedHeaders_UnixSocketUntrusted(t *testing.T) { req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Host", "spoofed.example.com") - vars := map[string]interface{}{ + vars := map[string]any{ caddyhttp.TrustedProxyVarKey: false, caddyhttp.ClientIPVarKey: "", } @@ -103,7 +103,7 @@ func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com/", nil) req.RemoteAddr = "@" - vars := map[string]interface{}{ + vars := map[string]any{ caddyhttp.TrustedProxyVarKey: true, caddyhttp.ClientIPVarKey: "5.6.7.8", } From 1a3e900b35b78a3ea945eafa1ef6834e3c2bf0b6 Mon Sep 17 00:00:00 2001 From: Dean Ruina <81315494+DeRuina@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:31:59 +0300 Subject: [PATCH 142/206] chore: bump timberjack to v1.4.1 (#7618) From 0c7c91a447922022163bbcc107512bc0b7f8a48b Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Tue, 14 Apr 2026 09:33:02 +1000 Subject: [PATCH 143/206] logging: preserve ts for journald-wrapped JSON logs (#7644) --- modules/logging/journaldencoder.go | 15 ++++++--------- modules/logging/journaldencoder_test.go | 6 +++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/modules/logging/journaldencoder.go b/modules/logging/journaldencoder.go index 5826c6950..142e99335 100644 --- a/modules/logging/journaldencoder.go +++ b/modules/logging/journaldencoder.go @@ -81,7 +81,7 @@ func (je *JournaldEncoder) Provision(ctx caddy.Context) error { je.Encoder = val.(zapcore.Encoder) } - suppressEncoderTimestamp(je.Encoder) + suppressConsoleEncoderTimestamp(je.Encoder) return nil } @@ -106,7 +106,7 @@ func (je *JournaldEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error { } } - suppressEncoderTimestamp(je.Encoder) + suppressConsoleEncoderTimestamp(je.Encoder) return nil } @@ -194,22 +194,19 @@ func journaldPriorityPrefix(level zapcore.Level) string { } } -func suppressEncoderTimestamp(enc zapcore.Encoder) { +func suppressConsoleEncoderTimestamp(enc zapcore.Encoder) { empty := "" switch e := enc.(type) { case *ConsoleEncoder: e.TimeKey = &empty _ = e.Provision(caddy.Context{}) - case *JSONEncoder: - e.TimeKey = &empty - _ = e.Provision(caddy.Context{}) case *AppendEncoder: - suppressEncoderTimestamp(e.wrapped) + suppressConsoleEncoderTimestamp(e.wrapped) case *FilterEncoder: - suppressEncoderTimestamp(e.wrapped) + suppressConsoleEncoderTimestamp(e.wrapped) case *JournaldEncoder: - suppressEncoderTimestamp(e.Encoder) + suppressConsoleEncoderTimestamp(e.Encoder) } } diff --git a/modules/logging/journaldencoder_test.go b/modules/logging/journaldencoder_test.go index 7b9a17d77..8676140dd 100644 --- a/modules/logging/journaldencoder_test.go +++ b/modules/logging/journaldencoder_test.go @@ -89,7 +89,7 @@ journald { } } -func TestJournaldEncoderSuppressesJSONTimestamp(t *testing.T) { +func TestJournaldEncoderPreservesJSONTimestamp(t *testing.T) { enc := &JournaldEncoder{ Encoder: &JSONEncoder{}, } @@ -108,8 +108,8 @@ func TestJournaldEncoderSuppressesJSONTimestamp(t *testing.T) { defer buf.Free() got := buf.String() - if strings.Contains(got, `"ts"`) { - t.Fatalf("got JSON output with ts field: %q", got) + if !strings.Contains(got, `"ts"`) { + t.Fatalf("got JSON output without ts field: %q", got) } } From 7586e68e273fbe84b588e73c014d15d815413648 Mon Sep 17 00:00:00 2001 From: Max Truxa Date: Tue, 14 Apr 2026 20:49:30 +0200 Subject: [PATCH 144/206] fileserver: show symlink targets verbatim (#7579) `reveal_symlinks` was exposing symlink targets as fully resolved absolute paths, even if the target is a relative path. With this change the link target is shown as-is, without resolving anything. --- modules/caddyhttp/fileserver/browsetplcontext.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go index b9489c6a6..fee5edd4f 100644 --- a/modules/caddyhttp/fileserver/browsetplcontext.go +++ b/modules/caddyhttp/fileserver/browsetplcontext.go @@ -20,7 +20,6 @@ import ( "net/url" "os" "path" - "path/filepath" "slices" "sort" "strconv" @@ -100,7 +99,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, } if fsrv.Browse.RevealSymlinks { - symLinkTarget, err := filepath.EvalSymlinks(path) + symLinkTarget, err := os.Readlink(path) if err == nil { symlinkPath = symLinkTarget } From 7dedd1486c252133c9fb0d2d28992c928ffc451d Mon Sep 17 00:00:00 2001 From: prettysunflower Date: Wed, 15 Apr 2026 02:58:53 -0400 Subject: [PATCH 145/206] fix(caddyfile): {block} in snippet (#7558) * fix(caddyfile): {block} in snippet Resolve issue #7557 So, here is the situation: - Pull request #7206 included some changes to the doImport's function of Caddyfile's parser. What it does is that if there is no token within a block that follows the import, and the import contains `{block}`, then the `{block}` token is discarded. - After this pull request: - Issue #7518 noticed that in cases that `{block}` was not imported, a runtime error was raised due to the assumption that tokens were always added to `tokensCopy` on every iteration of `importedTokens`. This was fixed by pull request #7543. - Issue #7557 notices that {block} can be ignored when imported from a certain file. There, it's again an issue with how the import works. When `import snippets` is called, this import instruction doesn't contains any nested blocks. And when the argument replacer that is the `importedTokens` loop is called and finds `{block}`, it uses the block from the file's import (which in this case is nothing), `{block}` is erased, and unavailable when the import directive is called for the imported snippet. The changed in this commit addresses the second issue by checking before replacing `{block}` if we're currently in a snippet definition, and appending the `{block}` token to `tokensCopy` if we are. With this changes, when importing those snippets, the `{block}` token will be available to be replaced by the nested blocks in `tokensToAdd` if needed, or erased if there are no nested blocks and `tokensToAdd` is empty. Tests added in pull requests #7206 and #7543 passes with this new implementation, confirming that unused `{block}` are accepted if nothing is passed to `import`, as well as the other usual tests. A new test was also added based on issue #7557 reporting, and also passes. Signed-off-by: prettysunflower * caddyfile: add imported snippet block placeholder coverage --------- Signed-off-by: prettysunflower Co-authored-by: Zen Dodd --- caddyconfig/caddyfile/parse.go | 6 +- caddyconfig/caddyfile/parse_test.go | 71 +++++++++++++++++++ ...snippet_invalid_subdirective.caddyfiletest | 15 ++++ ...sue_7557_invalid_subdirective_snippet.conf | 7 ++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 caddytest/integration/caddyfile_adapt/import_block_snippet_invalid_subdirective.caddyfiletest create mode 100644 caddytest/integration/testdata/issue_7557_invalid_subdirective_snippet.conf diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index 6a4db5bbb..58d9b272c 100644 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -550,7 +550,11 @@ func (p *parser) doImport(nesting int) error { } if foundBlockDirective { - tokensCopy = append(tokensCopy, tokensToAdd...) + if maybeSnippet { + tokensCopy = append(tokensCopy, token) + } else { + tokensCopy = append(tokensCopy, tokensToAdd...) + } continue } diff --git a/caddyconfig/caddyfile/parse_test.go b/caddyconfig/caddyfile/parse_test.go index 516b7dfd9..9403f7ac3 100644 --- a/caddyconfig/caddyfile/parse_test.go +++ b/caddyconfig/caddyfile/parse_test.go @@ -960,6 +960,77 @@ import `+importFile2+` } } +func TestImportedSnippetDefinitionRetainsBlockPlaceholder(t *testing.T) { + tempDir := t.TempDir() + importFile := filepath.Join(tempDir, "snippets.caddy") + + err := os.WriteFile(importFile, []byte(` + (site) { + http://{args[0]} { + respond "before" + {block} + respond "after" + } + } + `), 0o644) + if err != nil { + t.Fatalf("writing imported snippet file: %v", err) + } + + for _, tc := range []struct { + name string + input string + expectedDirectives []string + }{ + { + name: "with nested block", + input: ` + import ` + importFile + ` + + import site example.com { + redir https://example.net + } + `, + expectedDirectives: []string{"respond", "redir", "respond"}, + }, + { + name: "without nested block", + input: ` + import ` + importFile + ` + + import site example.com + `, + expectedDirectives: []string{"respond", "respond"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + p := testParser(tc.input) + blocks, err := p.parseAll() + if err != nil { + t.Fatalf("parseAll: %v", err) + } + + if len(blocks) != 1 { + t.Fatalf("expected exactly one server block, got %d", len(blocks)) + } + + if actual := blocks[0].GetKeysText(); len(actual) != 1 || actual[0] != "http://example.com" { + t.Fatalf("expected server block key http://example.com, got %v", actual) + } + + if len(blocks[0].Segments) != len(tc.expectedDirectives) { + t.Fatalf("expected %d segments, got %d", len(tc.expectedDirectives), len(blocks[0].Segments)) + } + + for i, directive := range tc.expectedDirectives { + if actual := blocks[0].Segments[i].Directive(); actual != directive { + t.Fatalf("segment %d: expected directive %q, got %q", i, directive, actual) + } + } + }) + } +} + func testParser(input string) parser { return parser{Dispenser: NewTestDispenser(input)} } diff --git a/caddytest/integration/caddyfile_adapt/import_block_snippet_invalid_subdirective.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_block_snippet_invalid_subdirective.caddyfiletest new file mode 100644 index 000000000..6936bada1 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/import_block_snippet_invalid_subdirective.caddyfiletest @@ -0,0 +1,15 @@ +{ + admin off + auto_https off +} + +import testdata/issue_7557_invalid_subdirective_snippet.conf + +:8080 { + import test { + this_is_nonsense + } +} + +---------- +parsing caddyfile tokens for 'reverse_proxy': unrecognized subdirective this_is_nonsense \ No newline at end of file diff --git a/caddytest/integration/testdata/issue_7557_invalid_subdirective_snippet.conf b/caddytest/integration/testdata/issue_7557_invalid_subdirective_snippet.conf new file mode 100644 index 000000000..d7cb0c9ff --- /dev/null +++ b/caddytest/integration/testdata/issue_7557_invalid_subdirective_snippet.conf @@ -0,0 +1,7 @@ +# Used by import_block_snippet_invalid_subdirective.caddyfiletest + +(test) { + reverse_proxy { + {block} + } +} \ No newline at end of file From 24bebd0a07cef434ec9c807bdb3a5a2d5f5ae9c1 Mon Sep 17 00:00:00 2001 From: Steffen Busch <37350514+steffenbusch@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:13:15 +0200 Subject: [PATCH 146/206] caddyhttp: Document missing placeholders for escaped URI and prefixed query (#7659) --- modules/caddyhttp/app.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 74f1466be..673c36d77 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -69,6 +69,7 @@ func init() { // `{http.request.orig_uri.path.dir}` | The request's original directory // `{http.request.orig_uri.path.file}` | The request's original filename // `{http.request.orig_uri.query}` | The request's original query string (without `?`) +// `{http.request.orig_uri.prefixed_query}` | The request's original query string with a `?` prefix, if non-empty // `{http.request.port}` | The port part of the request's Host header // `{http.request.proto}` | The protocol of the request // `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on @@ -98,11 +99,15 @@ func init() { // `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional) // `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional) // `{http.request.uri}` | The full request URI +// `{http.request.uri_escaped}` | The full request URI with query-style URL encoding applied (using url.QueryEscape) // `{http.request.uri.path}` | The path component of the request URI +// `{http.request.uri.path_escaped}` | The path component of the request URI with query-style URL encoding applied (using url.QueryEscape) // `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left) // `{http.request.uri.path.dir}` | The directory, excluding leaf filename // `{http.request.uri.path.file}` | The filename of the path, excluding directory // `{http.request.uri.query}` | The query string (without `?`) +// `{http.request.uri.query_escaped}` | The query string with query-style URL encoding applied (using url.QueryEscape) +// `{http.request.uri.prefixed_query}` | The query string with a `?` prefix, if non-empty // `{http.request.uri.query.*}` | Individual query string value // `{http.response.header.*}` | Specific response header field // `{http.vars.*}` | Custom variables in the HTTP handler chain From bd9f1453219a2610c319192f23782abb01b96699 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Fri, 17 Apr 2026 23:49:58 +0300 Subject: [PATCH 147/206] chore: add `AGENTS.md` (#7652) * chore: add `AGENTS.md` Signed-off-by: Mohammed Al Sahaf * Apply suggestions from code review Co-authored-by: Francis Lavoie Co-authored-by: Matt Holt * review feedback Signed-off-by: Mohammed Al Sahaf --------- Signed-off-by: Mohammed Al Sahaf Co-authored-by: Francis Lavoie Co-authored-by: Matt Holt --- AGENTS.md | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..8b1b5eb8b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,217 @@ +# Caddy Project Guidelines + +## Mission + +**Every site on HTTPS.** Caddy is a security-first, modular, extensible server platform. + +## Code Style + +### Go Idioms + +Follow [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments): + +- **Error flow**: Early return, indent error handling—not else blocks + ```go + if err != nil { + return err + } + // normal code + ``` +- **Naming**: initialisms (`URL`, `HTTP`, `ID`—not `Url`, `Http`, `Id`) +- **Receiver names**: 1–2 letters reflecting type (`c` for `Client`, `h` for `Handler`) +- **Error strings**: Lowercase, no trailing punctuation (`"something failed"` not `"Something failed."`) +- **Doc comments**: Full sentences starting with the name being documented + ```go + // Handler serves HTTP requests for the file server. + type Handler struct { ... } + ``` +- **Empty slices**: `var t []string` (nil slice), not `t := []string{}` (non-nil zero-length) +- **Don't panic**: Use error returns for normal error handling + +### Caddy Patterns + +**Module registration**: +```go +func init() { + caddy.RegisterModule(MyModule{}) +} + +func (MyModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "namespace.category.name", + New: func() caddy.Module { return new(MyModule) }, + } +} +``` + +**Module lifecycle**: `New()` → JSON unmarshal → `Provision()` → `Validate()` → use → `Cleanup()` + +**Interface guards** — compile-time verification that modules implement required interfaces: +```go +var ( + _ caddy.Provisioner = (*MyModule)(nil) + _ caddy.Validator = (*MyModule)(nil) + _ caddyfile.Unmarshaler = (*MyModule)(nil) +) +``` + +**Structured logging** — use the module-scoped logger from context: +```go +func (m *MyModule) Provision(ctx caddy.Context) error { + m.logger = ctx.Logger() + m.logger.Debug("provisioning", zap.String("field", m.Field)) + return nil +} +``` + +**Caddyfile support** — implement `UnmarshalCaddyfile(*caddyfile.Dispenser)` using the `Dispenser` API: +```go +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// directive [arg1] [arg2] { +// subdir value +// } +func (m *MyModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume directive name + for d.NextArg() { + // handle inline arguments + } + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "subdir": + if !d.NextArg() { + return d.ArgErr() + } + m.Field = d.Val() + default: + return d.Errf("unrecognized subdirective: %s", d.Val()) + } + } + return nil +} +``` + +**Admin API**: Implement `caddy.AdminRouter` for custom endpoints. + +**Context**: Use `caddy.Context` for accessing other apps/modules and logging—don't store contexts in structs. + +## Architecture + +Caddy is built around a **module system** where everything is a module registered via `caddy.RegisterModule()`: + +- **Apps** (`caddy.App`): Top-level modules like `http`, `tls`, `pki` that Caddy loads and runs +- **Modules** (`caddy.Module`): Extensible components with namespaced IDs (e.g., `http.handlers.file_server`) +- **Configuration**: Native JSON with adapters (Caddyfile → JSON via `caddyconfig/httpcaddyfile`) + +| Directory | Purpose | +|-----------|---------| +| `modules/` | All standard modules (HTTP, TLS, PKI, etc.) | +| `modules/standard/imports.go` | Standard module registry | +| `caddyconfig/httpcaddyfile/` | Caddyfile → JSON adapter for HTTP | +| `caddytest/` | Test utilities and integration tests | +| `cmd/caddy/` | CLI entry point with module imports | + +### Critical Packages + +`caddyhttp` and `caddytls` require **extra scrutiny** in code review—these are security-critical. + +## Quality Gates + + +**All required before PR is merge-ready:** + +| Gate | Command | Notes | +|------|---------|-------| +| Tests pass | `go test -race -short ./...` | Race detection enabled | +| Lint clean | `golangci-lint run --timeout 10m` | No warnings in changed files | +| Builds | `go build ./...` | Must compile | +| Benchmarks | `go test -bench=. -benchmem` | Required for optimizations | + +CI runs tests on **Linux, macOS, and Windows**—ensure cross-platform compatibility. + +### Build & Test + +```bash +# Build +cd cmd/caddy && go build + +# Tests with race detection (matches CI) +go test -race -short ./... + +# Integration tests +go test ./caddytest/integration/... + +# Lint (matches CI) +golangci-lint run --timeout 10m +``` + +## Testing Conventions + +**Table-driven tests** (preferred pattern): +```go +func TestFeature(t *testing.T) { + for i, tc := range []struct { + input string + expected string + wantErr bool + }{ + {input: "valid", expected: "result", wantErr: false}, + {input: "invalid", expected: "", wantErr: true}, + } { + actual, err := Function(tc.input) + if tc.wantErr && err == nil { + t.Errorf("Test %d: expected error but got none", i) + } + if !tc.wantErr && err != nil { + t.Errorf("Test %d: unexpected error: %v", i, err) + } + if actual != tc.expected { + t.Errorf("Test %d: expected %q, got %q", i, tc.expected, actual) + } + } +} +``` + +**Integration tests** use `caddytest.Tester`: +```go +func TestHTTPFeature(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + } + localhost:9080 { + respond "hello" + }`, "caddyfile") + + tester.AssertGetResponse("http://localhost:9080/", 200, "hello") +} +``` + +Use non-standard ports (9080, 9443, 2999) to avoid conflicts with running servers. + +## AI Contribution Policy + +Per [CONTRIBUTING.md](.github/CONTRIBUTING.md), AI-assisted code **MUST** be: + +1. **Disclosed** — Tell reviewers when code was AI-generated or AI-assisted, mentioning which agent/model is used +2. **Fully comprehended** — You must be able to explain every line +3. **Tested** — Automated tests when feasible, thorough manual tests otherwise +4. **Licensed** — Verify AI output doesn't include plagiarized or incompatibly-licensed code +5. **Contributor License Agreement (CLA)** — The CLA must be signed by the human user + +**Do NOT submit code you cannot fully explain.** Contributors are responsible for their submissions. + +## Dependencies + +- **Avoid new dependencies** — Justify any additions; tiny deps can be inlined +- **No exported dependency types** — Caddy must not export types defined by external packages +- Use Go modules; check with `go mod tidy` + +## Further Reading + +- [CONTRIBUTING.md](.github/CONTRIBUTING.md) — Full PR process and expectations +- [Extending Caddy](https://caddyserver.com/docs/extending-caddy) — Module development guide +- [JSON Config](https://caddyserver.com/docs/json/) — Native configuration reference +- [Caddyfile](https://caddyserver.com/docs/caddyfile/concepts) — Caddyfile syntax guide From af89c5ab02c7260df28655697964f47a6181b481 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:50:53 -0600 Subject: [PATCH 148/206] build(deps): bump github.com/jackc/pgx/v5 from 5.8.0 to 5.9.0 (#7655) Bumps [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) from 5.8.0 to 5.9.0. - [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md) - [Commits](https://github.com/jackc/pgx/compare/v5.8.0...v5.9.0) --- updated-dependencies: - dependency-name: github.com/jackc/pgx/v5 dependency-version: 5.9.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index de2031c97..0df389b59 100644 --- a/go.mod +++ b/go.mod @@ -70,7 +70,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.19.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect - github.com/jackc/pgx/v5 v5.8.0 // indirect + github.com/jackc/pgx/v5 v5.9.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index 53018c2c1..4d6b748fa 100644 --- a/go.sum +++ b/go.sum @@ -205,8 +205,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.9.0 h1:T/dI+2TvmI2H8s/KH1/lXIbz1CUFk3gn5oTjr0/mBsE= +github.com/jackc/pgx/v5 v5.9.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= From 4430756d5c3047564c4d5d72793de6685ba3efda Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Sat, 18 Apr 2026 06:56:42 +1000 Subject: [PATCH 149/206] admin: Redact sensitive request headers in API logs (#7578) * admin: Redact sensitive request headers in API logs * Fix govulncheck and typed atomic lint failures * Sync Go module metadata after dependency downgrade --- admin.go | 4 +- admin_test.go | 47 ++++++++++++++++ go.mod | 32 +++++------ go.sum | 72 ++++++++++++------------- internal/logmarshalers.go | 54 +++++++++++++++++++ modules/caddyhttp/marshalers.go | 47 +++------------- modules/caddyhttp/reverseproxy/hosts.go | 3 +- modules/logging/filters.go | 26 ++++----- modules/logging/filters_test.go | 32 +++++------ 9 files changed, 192 insertions(+), 125 deletions(-) create mode 100644 internal/logmarshalers.go diff --git a/admin.go b/admin.go index 9c9102120..a93595416 100644 --- a/admin.go +++ b/admin.go @@ -45,6 +45,8 @@ import ( "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" "go.uber.org/zap/zapcore" + + "github.com/caddyserver/caddy/v2/internal" ) // testCertMagicStorageOverride is a package-level test hook. Tests may set @@ -800,7 +802,7 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { zap.String("uri", r.RequestURI), zap.String("remote_ip", ip), zap.String("remote_port", port), - zap.Reflect("headers", r.Header), + zap.Object("headers", internal.LoggableHTTPHeader{Header: r.Header}), ) if r.TLS != nil { log = log.With( diff --git a/admin_test.go b/admin_test.go index 97dc76f4d..3801c301a 100644 --- a/admin_test.go +++ b/admin_test.go @@ -31,6 +31,8 @@ import ( "github.com/caddyserver/certmagic" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" ) var testCfg = []byte(`{ @@ -242,6 +244,51 @@ func TestAdminHandlerErrorHandling(t *testing.T) { } } +func TestAdminHandlerServeHTTPRedactsSensitiveHeadersInLogs(t *testing.T) { + core, logs := observer.New(zap.InfoLevel) + + defaultLoggerMu.Lock() + origLogger := defaultLogger.logger + defaultLogger.logger = zap.New(core) + defaultLoggerMu.Unlock() + t.Cleanup(func() { + defaultLoggerMu.Lock() + defaultLogger.logger = origLogger + defaultLoggerMu.Unlock() + }) + + handler := adminHandler{ + mux: http.NewServeMux(), + } + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer secret") + req.Header.Set("Cookie", "session=secret") + req.Header.Set("X-Test", "ok") + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + + if logs.Len() == 0 { + t.Fatal("expected request log entry") + } + + ctx := logs.All()[0].ContextMap() + headers, ok := ctx["headers"].(map[string]any) + if !ok { + t.Fatalf("expected headers field in log context, got %T", ctx["headers"]) + } + + if got := headers["Authorization"]; !reflect.DeepEqual(got, []any{"REDACTED"}) { + t.Fatalf("expected redacted Authorization header, got %#v", got) + } + if got := headers["Cookie"]; !reflect.DeepEqual(got, []any{"REDACTED"}) { + t.Fatalf("expected redacted Cookie header, got %#v", got) + } + if got := headers["X-Test"]; !reflect.DeepEqual(got, []any{"ok"}) { + t.Fatalf("expected X-Test header to remain visible, got %#v", got) + } +} + func initAdminMetrics() { if adminMetrics.requestErrors != nil { prometheus.Unregister(adminMetrics.requestErrors) diff --git a/go.mod b/go.mod index 0df389b59..8796ad4d8 100644 --- a/go.mod +++ b/go.mod @@ -30,20 +30,20 @@ require ( github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 - go.step.sm/crypto v0.77.2 + go.step.sm/crypto v0.77.1 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.50.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 - golang.org/x/net v0.53.0 + golang.org/x/crypto v0.49.0 + golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 + golang.org/x/net v0.52.0 golang.org/x/sync v0.20.0 - golang.org/x/term v0.42.0 + golang.org/x/term v0.41.0 golang.org/x/time v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -68,7 +68,7 @@ require ( github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.19.0 // indirect + github.com/googleapis/gax-go/v2 v2.18.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/jackc/pgx/v5 v5.9.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -109,9 +109,9 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/oauth2 v0.36.0 // indirect - google.golang.org/api v0.272.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/api v0.271.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -168,10 +168,10 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.34.0 // indirect - golang.org/x/sys v0.43.0 - golang.org/x/text v0.36.0 - golang.org/x/tools v0.43.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/sys v0.42.0 + golang.org/x/text v0.35.0 + golang.org/x/tools v0.42.0 // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect howett.net/plist v1.0.0 // indirect diff --git a/go.sum b/go.sum index 4d6b748fa..48a7d22bd 100644 --- a/go.sum +++ b/go.sum @@ -179,8 +179,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/go-tpm-tools v0.4.8 h1:V4oIYyAD3BykOycwYQzO29WefDouQMTsYZqmG3HxOfM= -github.com/google/go-tpm-tools v0.4.8/go.mod h1:4DfiOtiS1KppJjwf1+tqtW4K3PrCJjAAqFKj/TYTJKg= +github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws= +github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -189,8 +189,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= -github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= +github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= +github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -375,14 +375,14 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= -go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 h1:0D3GFvELGIwQGfC6agLsbrEYSGWZTRTxIXxcQUqrOuk= -go.opentelemetry.io/contrib/exporters/autoexport v0.68.0/go.mod h1:DM2NV7Zb8CcGeVPt6glouY0FAiwZQ/iqgcWExhgWeN8= +go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g= +go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= -go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 h1:wLGFvNBPqQhzBn0QRBZjrriH8lZ9gqtTz8ufHEjLg7k= -go.opentelemetry.io/contrib/propagators/autoprop v0.68.0/go.mod h1:evWK9nCqCzH8nhclTlpkdUzmxrmJQ2mrWCdKIvyOYec= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s= +go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE= go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg= go.opentelemetry.io/contrib/propagators/aws v1.43.0/go.mod h1:CJjTym6F87tEdm61Qvnz5xrV8vKlH4C92djiqcn62k8= go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A= @@ -431,8 +431,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.step.sm/crypto v0.77.2 h1:qFjjei+RHc5kP5R7NW9OUWT7SqWIuAOvOkXqg4fNWj8= -go.step.sm/crypto v0.77.2/go.mod h1:W0YJb9onM5l78qgkXIJ2Up6grnwW8EtpCKIza/NCg0o= +go.step.sm/crypto v0.77.1 h1:4EEqfKdv0egQ1lqz2RhnU8Jv6QgXZfrgoxWMqJF9aDs= +go.step.sm/crypto v0.77.1/go.mod h1:U/SsmEm80mNnfD5WIkbhuW/B1eFp3fgFvdXyDLpU1AQ= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -456,10 +456,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 h1:sQVhWLXbNsa8CTzHOX3IHc7C4Q2JyxI5AweuMQZ/5H0= -golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -467,8 +467,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -477,8 +477,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -506,8 +506,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -517,8 +517,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -528,8 +528,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -538,19 +538,19 @@ 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.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= -google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= -google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE= -google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= -google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= -google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= +google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= +google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc= +google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= diff --git a/internal/logmarshalers.go b/internal/logmarshalers.go new file mode 100644 index 000000000..3a8a553e6 --- /dev/null +++ b/internal/logmarshalers.go @@ -0,0 +1,54 @@ +package internal + +import ( + "net/http" + "strings" + + "go.uber.org/zap/zapcore" +) + +// LoggableHTTPHeader makes an HTTP header loggable with zap.Object(). +// Headers with potentially sensitive information (Cookie, Set-Cookie, +// Authorization, and Proxy-Authorization) are logged with empty values. +type LoggableHTTPHeader struct { + http.Header + + ShouldLogCredentials bool +} + +// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface. +func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error { + if h.Header == nil { + return nil + } + for key, val := range h.Header { + if !h.ShouldLogCredentials { + switch strings.ToLower(key) { + case "cookie", "set-cookie", "authorization", "proxy-authorization": + val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool. + } + } + enc.AddArray(key, LoggableStringArray(val)) + } + return nil +} + +// LoggableStringArray makes a slice of strings marshalable for logging. +type LoggableStringArray []string + +// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface. +func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error { + if sa == nil { + return nil + } + for _, s := range sa { + enc.AppendString(s) + } + return nil +} + +// Interface guards +var ( + _ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil) + _ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil) +) diff --git a/modules/caddyhttp/marshalers.go b/modules/caddyhttp/marshalers.go index 2a40b6cd7..15fa3e8bc 100644 --- a/modules/caddyhttp/marshalers.go +++ b/modules/caddyhttp/marshalers.go @@ -18,9 +18,10 @@ import ( "crypto/tls" "net" "net/http" - "strings" "go.uber.org/zap/zapcore" + + "github.com/caddyserver/caddy/v2/internal" ) // LoggableHTTPRequest makes an HTTP request loggable with zap.Object(). @@ -47,12 +48,12 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error { enc.AddString("method", r.Method) enc.AddString("host", r.Host) enc.AddString("uri", r.RequestURI) - enc.AddObject("headers", LoggableHTTPHeader{ + enc.AddObject("headers", internal.LoggableHTTPHeader{ Header: r.Header, ShouldLogCredentials: r.ShouldLogCredentials, }) if r.TransferEncoding != nil { - enc.AddArray("transfer_encoding", LoggableStringArray(r.TransferEncoding)) + enc.AddArray("transfer_encoding", internal.LoggableStringArray(r.TransferEncoding)) } if r.TLS != nil { enc.AddObject("tls", LoggableTLSConnState(*r.TLS)) @@ -61,44 +62,10 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error { } // LoggableHTTPHeader makes an HTTP header loggable with zap.Object(). -// Headers with potentially sensitive information (Cookie, Set-Cookie, -// Authorization, and Proxy-Authorization) are logged with empty values. -type LoggableHTTPHeader struct { - http.Header - - ShouldLogCredentials bool -} - -// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface. -func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error { - if h.Header == nil { - return nil - } - for key, val := range h.Header { - if !h.ShouldLogCredentials { - switch strings.ToLower(key) { - case "cookie", "set-cookie", "authorization", "proxy-authorization": - val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool. - } - } - enc.AddArray(key, LoggableStringArray(val)) - } - return nil -} +type LoggableHTTPHeader = internal.LoggableHTTPHeader // LoggableStringArray makes a slice of strings marshalable for logging. -type LoggableStringArray []string - -// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface. -func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error { - if sa == nil { - return nil - } - for _, s := range sa { - enc.AppendString(s) - } - return nil -} +type LoggableStringArray = internal.LoggableStringArray // LoggableTLSConnState makes a TLS connection state loggable with zap.Object(). type LoggableTLSConnState tls.ConnectionState @@ -121,7 +88,5 @@ func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error // Interface guards var ( _ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil) - _ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil) - _ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil) _ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil) ) diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go index a5406e04e..e58d6825f 100644 --- a/modules/caddyhttp/reverseproxy/hosts.go +++ b/modules/caddyhttp/reverseproxy/hosts.go @@ -174,7 +174,7 @@ func (u *Upstream) fillDynamicHost() { // Host is the basic, in-memory representation of the state of a remote host. // Its fields are accessed atomically and Host values must not be copied. type Host struct { - numRequests atomic.Int64 // atomic.Int64 is automatically aligned for us (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG) + numRequests atomic.Int64 fails atomic.Int64 activePasses atomic.Int64 activeFails atomic.Int64 @@ -250,7 +250,6 @@ func (h *Host) resetHealth() { // (This returns the status only from the "active" health checks.) func (u *Upstream) healthy() bool { return u.unhealthy.Load() == 0 - // return atomic.LoadInt32(&u.unhealthy) == 0 } // SetHealthy sets the upstream has healthy or unhealthy diff --git a/modules/logging/filters.go b/modules/logging/filters.go index 4574b7ca0..087b872e7 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -29,7 +29,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/internal" ) func init() { @@ -100,8 +100,8 @@ func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Filter filters the input field with the replacement value. func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field { - if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { - newArray := make(caddyhttp.LoggableStringArray, len(array)) + if array, ok := in.Interface.(internal.LoggableStringArray); ok { + newArray := make(internal.LoggableStringArray, len(array)) for i, s := range array { newArray[i] = hash(s) } @@ -241,8 +241,8 @@ func (m *IPMaskFilter) Provision(ctx caddy.Context) error { // Filter filters the input field. func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { - if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { - newArray := make(caddyhttp.LoggableStringArray, len(array)) + if array, ok := in.Interface.(internal.LoggableStringArray); ok { + newArray := make(internal.LoggableStringArray, len(array)) for i, s := range array { newArray[i] = m.mask(s) } @@ -392,8 +392,8 @@ func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Filter filters the input field. func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field { - if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { - newArray := make(caddyhttp.LoggableStringArray, len(array)) + if array, ok := in.Interface.(internal.LoggableStringArray); ok { + newArray := make(internal.LoggableStringArray, len(array)) for i, s := range array { newArray[i] = m.processQueryString(s) } @@ -523,7 +523,7 @@ func (m *CookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Filter filters the input field. func (m CookieFilter) Filter(in zapcore.Field) zapcore.Field { - cookiesSlice, ok := in.Interface.(caddyhttp.LoggableStringArray) + cookiesSlice, ok := in.Interface.(internal.LoggableStringArray) if !ok { return in } @@ -559,7 +559,7 @@ OUTER: transformedRequest.AddCookie(c) } - in.Interface = caddyhttp.LoggableStringArray(transformedRequest.Header["Cookie"]) + in.Interface = internal.LoggableStringArray(transformedRequest.Header["Cookie"]) return in } @@ -613,8 +613,8 @@ func (m *RegexpFilter) Provision(ctx caddy.Context) error { // Filter filters the input field with the replacement value if it matches the regexp. func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field { - if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { - newArray := make(caddyhttp.LoggableStringArray, len(array)) + if array, ok := in.Interface.(internal.LoggableStringArray); ok { + newArray := make(internal.LoggableStringArray, len(array)) for i, s := range array { newArray[i] = f.regexp.ReplaceAllString(s, f.Value) } @@ -783,8 +783,8 @@ func (f *MultiRegexpFilter) Validate() error { // Filter applies all regexp operations sequentially to the input field. // Input is sanitized and validated for security. func (f *MultiRegexpFilter) Filter(in zapcore.Field) zapcore.Field { - if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { - newArray := make(caddyhttp.LoggableStringArray, len(array)) + if array, ok := in.Interface.(internal.LoggableStringArray); ok { + newArray := make(internal.LoggableStringArray, len(array)) for i, s := range array { newArray[i] = f.processString(s) } diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go index cf35e7178..8fbaed6f8 100644 --- a/modules/logging/filters_test.go +++ b/modules/logging/filters_test.go @@ -8,7 +8,7 @@ import ( "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/internal" ) func TestIPMaskSingleValue(t *testing.T) { @@ -55,11 +55,11 @@ func TestIPMaskMultiValue(t *testing.T) { f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32} f.Provision(caddy.Context{}) - out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{ + out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{ "255.255.255.255", "244.244.244.244", }}) - arr, ok := out.Interface.(caddyhttp.LoggableStringArray) + arr, ok := out.Interface.(internal.LoggableStringArray) if !ok { t.Fatalf("field is wrong type: %T", out.Integer) } @@ -70,11 +70,11 @@ func TestIPMaskMultiValue(t *testing.T) { t.Fatalf("field entry 1 has not been filtered: %s", arr[1]) } - out = f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{ + out = f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff", }}) - arr, ok = out.Interface.(caddyhttp.LoggableStringArray) + arr, ok = out.Interface.(internal.LoggableStringArray) if !ok { t.Fatalf("field is wrong type: %T", out.Integer) } @@ -120,11 +120,11 @@ func TestQueryFilterMultiValue(t *testing.T) { t.Fatalf("the filter must be valid") } - out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{ + out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{ "/path1?foo=a&foo=b&bar=c&bar=d&baz=e&hash=hashed", "/path2?foo=c&foo=d&bar=e&bar=f&baz=g&hash=hashed", }}) - arr, ok := out.Interface.(caddyhttp.LoggableStringArray) + arr, ok := out.Interface.(internal.LoggableStringArray) if !ok { t.Fatalf("field is wrong type: %T", out.Interface) } @@ -162,11 +162,11 @@ func TestCookieFilter(t *testing.T) { {hashAction, "hash", ""}, }} - out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{ + out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{ "foo=a; foo=b; bar=c; bar=d; baz=e; hash=hashed", }}) - outval := out.Interface.(caddyhttp.LoggableStringArray) - expected := caddyhttp.LoggableStringArray{ + outval := out.Interface.(internal.LoggableStringArray) + expected := internal.LoggableStringArray{ "foo=REDACTED; foo=REDACTED; baz=e; hash=1a06df82", } if outval[0] != expected[0] { @@ -204,8 +204,8 @@ func TestRegexpFilterMultiValue(t *testing.T) { f := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"} f.Provision(caddy.Context{}) - out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{"foo-secret-bar", "bar-secret-foo"}}) - arr, ok := out.Interface.(caddyhttp.LoggableStringArray) + out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{"foo-secret-bar", "bar-secret-foo"}}) + arr, ok := out.Interface.(internal.LoggableStringArray) if !ok { t.Fatalf("field is wrong type: %T", out.Integer) } @@ -229,8 +229,8 @@ func TestHashFilterSingleValue(t *testing.T) { func TestHashFilterMultiValue(t *testing.T) { f := HashFilter{} - out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{"foo", "bar"}}) - arr, ok := out.Interface.(caddyhttp.LoggableStringArray) + out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{"foo", "bar"}}) + arr, ok := out.Interface.(internal.LoggableStringArray) if !ok { t.Fatalf("field is wrong type: %T", out.Integer) } @@ -292,11 +292,11 @@ func TestMultiRegexpFilterMultiValue(t *testing.T) { t.Fatalf("unexpected error provisioning: %v", err) } - out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{ + out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{ "foo-secret-123", "bar-secret-456", }}) - arr, ok := out.Interface.(caddyhttp.LoggableStringArray) + arr, ok := out.Interface.(internal.LoggableStringArray) if !ok { t.Fatalf("field is wrong type: %T", out.Interface) } From aed1af59763d54520a9c72f1fd0222d43904ebfd Mon Sep 17 00:00:00 2001 From: Daniil Sivak Date: Tue, 21 Apr 2026 21:59:31 +0300 Subject: [PATCH 150/206] reverseproxy: add `lb_retry_match` condition on response status (#7569) --- ...se_proxy_retry_match_oneline.caddyfiletest | 58 +++ ...e_proxy_retry_match_response.caddyfiletest | 147 ++++++ caddytest/integration/reverseproxy_test.go | 231 +++++++++ modules/caddyhttp/matchers.go | 8 + modules/caddyhttp/reverseproxy/caddyfile.go | 2 +- .../caddyhttp/reverseproxy/retries_test.go | 475 ++++++++++++++++++ .../caddyhttp/reverseproxy/reverseproxy.go | 116 ++++- replacer.go | 12 + 8 files changed, 1029 insertions(+), 20 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/reverse_proxy_retry_match_oneline.caddyfiletest create mode 100644 caddytest/integration/caddyfile_adapt/reverse_proxy_retry_match_response.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_retry_match_oneline.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_retry_match_oneline.caddyfiletest new file mode 100644 index 000000000..8faed5220 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_retry_match_oneline.caddyfiletest @@ -0,0 +1,58 @@ +:8884 + +reverse_proxy 127.0.0.1:65535 { + lb_retries 3 + lb_retry_match expression `{rp.status_code} in [502, 503]` + lb_retry_match expression `{rp.is_transport_error} || {rp.status_code} == 502` + lb_retry_match expression `method('POST') && {rp.status_code} == 503` + lb_retry_match `{rp.status_code} == 504` + lb_retry_match `{rp.is_transport_error} && method('PUT')` +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "load_balancing": { + "retries": 3, + "retry_match": [ + { + "expression": "{http.reverse_proxy.status_code} in [502, 503]" + }, + { + "expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} == 502" + }, + { + "expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} == 503" + }, + { + "expression": "{http.reverse_proxy.status_code} == 504" + }, + { + "expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('PUT')" + } + ] + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_retry_match_response.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_retry_match_response.caddyfiletest new file mode 100644 index 000000000..d5a1a9a40 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_retry_match_response.caddyfiletest @@ -0,0 +1,147 @@ +:8884 + +reverse_proxy 127.0.0.1:65535 { + lb_retries 5 + + # request matchers (backward-compatible, non-expression) + lb_retry_match { + method POST PUT + } + lb_retry_match { + path /foo* + } + lb_retry_match { + header X-Idempotency-Key * + } + + # response status code via expression + lb_retry_match { + expression `{rp.status_code} in [502, 503, 504]` + } + + # response header via expression + lb_retry_match { + expression `{rp.header.X-Retry} == "true"` + } + + # CEL request functions combined with response placeholders + lb_retry_match { + expression `method('POST') && {rp.status_code} >= 500` + } + lb_retry_match { + expression `path('/api*') && {rp.status_code} in [502, 503]` + } + lb_retry_match { + expression `host('example.com') && {rp.status_code} == 503` + } + lb_retry_match { + expression `query({'retry': 'true'}) && {rp.status_code} >= 500` + } + lb_retry_match { + expression `header({'X-Idempotency-Key': '*'}) && {rp.status_code} in [502, 503]` + } + lb_retry_match { + expression `protocol('https') && {rp.status_code} == 502` + } + lb_retry_match { + expression `path_regexp('^/api/v[0-9]+/') && {rp.status_code} >= 500` + } + lb_retry_match { + expression `header_regexp('Content-Type', '^application/json') && {rp.status_code} == 502` + } + + # transport error handling via placeholder + lb_retry_match { + expression `{rp.is_transport_error} || {rp.status_code} in [502, 503]` + } + lb_retry_match { + expression `{rp.is_transport_error} && method('POST')` + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "load_balancing": { + "retries": 5, + "retry_match": [ + { + "method": [ + "POST", + "PUT" + ] + }, + { + "path": [ + "/foo*" + ] + }, + { + "header": { + "X-Idempotency-Key": [ + "*" + ] + } + }, + { + "expression": "{http.reverse_proxy.status_code} in [502, 503, 504]" + }, + { + "expression": "{http.reverse_proxy.header.X-Retry} == \"true\"" + }, + { + "expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500" + }, + { + "expression": "path('/api*') \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]" + }, + { + "expression": "host('example.com') \u0026\u0026 {http.reverse_proxy.status_code} == 503" + }, + { + "expression": "query({'retry': 'true'}) \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500" + }, + { + "expression": "header({'X-Idempotency-Key': '*'}) \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]" + }, + { + "expression": "protocol('https') \u0026\u0026 {http.reverse_proxy.status_code} == 502" + }, + { + "expression": "path_regexp('^/api/v[0-9]+/') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500" + }, + { + "expression": "header_regexp('Content-Type', '^application/json') \u0026\u0026 {http.reverse_proxy.status_code} == 502" + }, + { + "expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} in [502, 503]" + }, + { + "expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('POST')" + } + ] + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/reverseproxy_test.go b/caddytest/integration/reverseproxy_test.go index 6e0b3dcff..cbccfd74f 100644 --- a/caddytest/integration/reverseproxy_test.go +++ b/caddytest/integration/reverseproxy_test.go @@ -7,6 +7,7 @@ import ( "os" "runtime" "strings" + "sync/atomic" "testing" "github.com/caddyserver/caddy/v2/caddytest" @@ -562,3 +563,233 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) { tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") } + +// TestReverseProxyRetryMatchStatusCode verifies that lb_retry_match with a +// CEL expression matching on {rp.status_code} causes the request to be +// retried on the next upstream when the first upstream returns a matching +// status code +func TestReverseProxyRetryMatchStatusCode(t *testing.T) { + // Bad upstream: returns 502 + badSrv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadGateway) + }), + } + badLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + go badSrv.Serve(badLn) + t.Cleanup(func() { badSrv.Close(); badLn.Close() }) + + // Good upstream: returns 200 + goodSrv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + }), + } + goodLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + go goodSrv.Serve(goodLn) + t.Cleanup(func() { goodSrv.Close(); goodLn.Close() }) + + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + grace_period 1ns + } + http://localhost:9080 { + reverse_proxy %s %s { + lb_policy round_robin + lb_retries 1 + lb_retry_match { + expression `+"`{rp.status_code} in [502, 503]`"+` + } + } + } + `, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile") + + tester.AssertGetResponse("http://localhost:9080/", 200, "ok") +} + +// TestReverseProxyRetryMatchHeader verifies that lb_retry_match with a CEL +// expression matching on {rp.header.*} causes the request to be retried when +// the upstream sets a matching response header +func TestReverseProxyRetryMatchHeader(t *testing.T) { + var badHits atomic.Int32 + + // Bad upstream: returns 200 but signals retry via header + badSrv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + badHits.Add(1) + w.Header().Set("X-Upstream-Retry", "true") + w.Write([]byte("bad")) + }), + } + badLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + go badSrv.Serve(badLn) + t.Cleanup(func() { badSrv.Close(); badLn.Close() }) + + // Good upstream: returns 200 without retry header + goodSrv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("good")) + }), + } + goodLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + go goodSrv.Serve(goodLn) + t.Cleanup(func() { goodSrv.Close(); goodLn.Close() }) + + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + grace_period 1ns + } + http://localhost:9080 { + reverse_proxy %s %s { + lb_policy round_robin + lb_retries 1 + lb_retry_match { + expression `+"`{rp.header.X-Upstream-Retry} == \"true\"`"+` + } + } + } + `, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile") + + tester.AssertGetResponse("http://localhost:9080/", 200, "good") + + if badHits.Load() != 1 { + t.Errorf("bad upstream hits: got %d, want 1", badHits.Load()) + } +} + +// TestReverseProxyRetryMatchCombined verifies that a CEL expression combining +// request path matching with response status code matching works correctly - +// only retrying when both conditions are met +func TestReverseProxyRetryMatchCombined(t *testing.T) { + // Upstream: returns 502 for all requests + srv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadGateway) + }), + } + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + go srv.Serve(ln) + t.Cleanup(func() { srv.Close(); ln.Close() }) + + // Good upstream + goodSrv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + }), + } + goodLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + go goodSrv.Serve(goodLn) + t.Cleanup(func() { goodSrv.Close(); goodLn.Close() }) + + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + grace_period 1ns + } + http://localhost:9080 { + reverse_proxy %s %s { + lb_policy round_robin + lb_retries 1 + lb_retry_match { + expression `+"`path('/retry*') && {rp.status_code} in [502, 503]`"+` + } + } + } + `, goodLn.Addr().String(), ln.Addr().String()), "caddyfile") + + // /retry path matches the expression - should retry to good upstream + tester.AssertGetResponse("http://localhost:9080/retry", 200, "ok") + + // /other path does NOT match - should return the 502 + req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/other", nil) + tester.AssertResponse(req, 502, "") +} + +// TestReverseProxyRetryMatchIsTransportError verifies that the +// {rp.is_transport_error} == true CEL function correctly identifies transport errors +// and allows retrying them alongside response-based matching +func TestReverseProxyRetryMatchIsTransportError(t *testing.T) { + // Good upstream: returns 200 + goodSrv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + }), + } + goodLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + go goodSrv.Serve(goodLn) + t.Cleanup(func() { goodSrv.Close(); goodLn.Close() }) + + // Broken upstream: accepts connections but closes immediately + brokenLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + t.Cleanup(func() { brokenLn.Close() }) + go func() { + for { + conn, err := brokenLn.Accept() + if err != nil { + return + } + conn.Close() + } + }() + + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + grace_period 1ns + } + http://localhost:9080 { + reverse_proxy %s %s { + lb_policy round_robin + lb_retries 1 + lb_retry_match { + expression `+"`{rp.is_transport_error} || {rp.status_code} in [502, 503]`"+` + } + } + } + `, goodLn.Addr().String(), brokenLn.Addr().String()), "caddyfile") + + // Transport error on broken upstream should be retried to good upstream + tester.AssertGetResponse("http://localhost:9080/", 200, "ok") +} diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 27e5c5ae6..f179b9c11 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -1562,6 +1562,14 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er // instances of the matcher in this set tokensByMatcherName := make(map[string][]caddyfile.Token) for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { + // if the token is quoted (backtick), treat it as a shorthand + // for an expression matcher, same as @named matcher parsing + if d.Token().Quoted() { + expressionToken := d.Token().Clone() + expressionToken.Text = "expression" + tokensByMatcherName["expression"] = append(tokensByMatcherName["expression"], expressionToken, d.Token()) + continue + } matcherName := d.Val() tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...) } diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index a370a2873..8716babe3 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -67,7 +67,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // lb_retries // lb_try_duration // lb_try_interval -// lb_retry_match +// lb_retry_match // // # active health checking // health_uri diff --git a/modules/caddyhttp/reverseproxy/retries_test.go b/modules/caddyhttp/reverseproxy/retries_test.go index 056223d4c..b0f78bac0 100644 --- a/modules/caddyhttp/reverseproxy/retries_test.go +++ b/modules/caddyhttp/reverseproxy/retries_test.go @@ -1,6 +1,7 @@ package reverseproxy import ( + "context" "errors" "io" "net" @@ -8,11 +9,13 @@ import ( "net/http/httptest" "strings" "sync" + "sync/atomic" "testing" "go.uber.org/zap" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) @@ -255,3 +258,475 @@ func TestDialErrorBodyRetry(t *testing.T) { }) } } + +// newExpressionMatcher provisions a MatchExpression for use in tests +func newExpressionMatcher(t *testing.T, expr string) *caddyhttp.MatchExpression { + t.Helper() + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + t.Cleanup(cancel) + m := &caddyhttp.MatchExpression{Expr: expr} + if err := m.Provision(ctx); err != nil { + t.Fatalf("failed to provision expression %q: %v", expr, err) + } + return m +} + +// minimalHandlerWithRetryMatch is like minimalHandler but also configures +// RetryMatch so that response-based retry can be tested +func minimalHandlerWithRetryMatch(retries int, retryMatch caddyhttp.MatcherSets, upstreams ...*Upstream) *Handler { + h := minimalHandler(retries, upstreams...) + h.LoadBalancing.RetryMatch = retryMatch + return h +} + +// TestResponseRetryStatusCode verifies that when an upstream returns a status +// code matching a retry_match expression, the request is retried on the next +// upstream +func TestResponseRetryStatusCode(t *testing.T) { + // Bad upstream: returns 502 + badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadGateway) + })) + t.Cleanup(badServer.Close) + + // Good upstream: returns 200 + goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + })) + t.Cleanup(goodServer.Close) + + retryMatch := caddyhttp.MatcherSets{ + caddyhttp.MatcherSet{ + newExpressionMatcher(t, "{http.reverse_proxy.status_code} in [502, 503]"), + }, + } + + // RoundRobin picks index 1 first, then 0 + upstreams := []*Upstream{ + {Host: new(Host), Dial: goodServer.Listener.Addr().String()}, + {Host: new(Host), Dial: badServer.Listener.Addr().String()}, + } + + h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...) + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req = prepareTestRequest(req) + rec := httptest.NewRecorder() + + err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + + gotStatus := rec.Code + if err != nil { + if herr, ok := err.(caddyhttp.HandlerError); ok { + gotStatus = herr.StatusCode + } + } + + if gotStatus != http.StatusOK { + t.Errorf("status: got %d, want %d (err=%v)", gotStatus, http.StatusOK, err) + } +} + +// TestResponseRetryHeader verifies that response header matching triggers +// retries via a CEL expression checking {rp.header.*} +func TestResponseRetryHeader(t *testing.T) { + // Bad upstream: returns 200 but with X-Upstream-Retry header + badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Upstream-Retry", "true") + w.WriteHeader(http.StatusOK) + w.Write([]byte("bad")) + })) + t.Cleanup(badServer.Close) + + // Good upstream: returns 200 without retry header + goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("good")) + })) + t.Cleanup(goodServer.Close) + + retryMatch := caddyhttp.MatcherSets{ + caddyhttp.MatcherSet{ + newExpressionMatcher(t, `{http.reverse_proxy.header.X-Upstream-Retry} == "true"`), + }, + } + + // RoundRobin picks index 1 first, then 0 + upstreams := []*Upstream{ + {Host: new(Host), Dial: goodServer.Listener.Addr().String()}, + {Host: new(Host), Dial: badServer.Listener.Addr().String()}, + } + + h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...) + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req = prepareTestRequest(req) + rec := httptest.NewRecorder() + + err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if rec.Code != http.StatusOK { + t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK) + } + if rec.Body.String() != "good" { + t.Errorf("body: got %q, want %q (retried to wrong upstream)", rec.Body.String(), "good") + } +} + +// TestResponseRetryNoMatchNoRetry verifies that when no retry_match entries +// match the response, the original response is returned without retrying +func TestResponseRetryNoMatchNoRetry(t *testing.T) { + var hits atomic.Int32 + + // Server that returns 500 - but retry_match only matches 502/503 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hits.Add(1) + w.WriteHeader(http.StatusInternalServerError) + })) + t.Cleanup(server.Close) + + retryMatch := caddyhttp.MatcherSets{ + caddyhttp.MatcherSet{ + newExpressionMatcher(t, "{http.reverse_proxy.status_code} in [502, 503]"), + }, + } + + upstreams := []*Upstream{ + {Host: new(Host), Dial: server.Listener.Addr().String()}, + {Host: new(Host), Dial: server.Listener.Addr().String()}, + } + + h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...) + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req = prepareTestRequest(req) + rec := httptest.NewRecorder() + + _ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + + // Only one hit - no retry since 500 doesn't match [502, 503] + if hits.Load() != 1 { + t.Errorf("upstream hits: got %d, want 1 (should not have retried)", hits.Load()) + } +} + +// TestResponseRetryExhaustedPreservesStatusCode verifies that when retries +// are exhausted, the actual upstream status code (e.g. 503) is reported +// to the client, not a generic 502 +func TestResponseRetryExhaustedPreservesStatusCode(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) // 503 + })) + t.Cleanup(server.Close) + + retryMatch := caddyhttp.MatcherSets{ + caddyhttp.MatcherSet{ + newExpressionMatcher(t, "{http.reverse_proxy.status_code} == 503"), + }, + } + + upstreams := []*Upstream{ + {Host: new(Host), Dial: server.Listener.Addr().String()}, + {Host: new(Host), Dial: server.Listener.Addr().String()}, + } + + h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...) + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req = prepareTestRequest(req) + rec := httptest.NewRecorder() + + err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + + gotStatus := rec.Code + if err != nil { + if herr, ok := err.(caddyhttp.HandlerError); ok { + gotStatus = herr.StatusCode + } + } + + // Must return 503 (actual upstream status), not 502 (generic proxy error) + if gotStatus != http.StatusServiceUnavailable { + t.Errorf("status: got %d, want %d (status code not preserved)", gotStatus, http.StatusServiceUnavailable) + } +} + +// TestResponseRetryHeaderCleanup verifies that stale response header +// placeholders from a previous upstream attempt are cleaned up before the +// next retry evaluation. Without cleanup, a header like X-Retry: true from +// upstream A would leak into the retry match for upstream B even if B does +// not set that header +func TestResponseRetryHeaderCleanup(t *testing.T) { + // First upstream: returns 200 with X-Retry header (triggers retry) + firstServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Retry", "true") + w.WriteHeader(http.StatusOK) + w.Write([]byte("first")) + })) + t.Cleanup(firstServer.Close) + + // Second upstream: returns 200 WITHOUT X-Retry header (should NOT retry) + secondServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("second")) + })) + t.Cleanup(secondServer.Close) + + retryMatch := caddyhttp.MatcherSets{ + caddyhttp.MatcherSet{ + newExpressionMatcher(t, `{http.reverse_proxy.header.X-Retry} == "true"`), + }, + } + + // RoundRobin picks index 1 first, then 0 + upstreams := []*Upstream{ + {Host: new(Host), Dial: secondServer.Listener.Addr().String()}, + {Host: new(Host), Dial: firstServer.Listener.Addr().String()}, + } + + h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...) + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req = prepareTestRequest(req) + rec := httptest.NewRecorder() + + err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Should get "second" - the first upstream's X-Retry header must not + // leak into the second upstream's retry evaluation + if rec.Body.String() != "second" { + t.Errorf("body: got %q, want %q (stale header leaked between retries)", rec.Body.String(), "second") + } +} + +// TestRequestOnlyMatcherDoesNotRetryResponses verifies that a pure request +// matcher like method PUT in lb_retry_match does NOT trigger response-based +// retries. Only expression matchers (which can reference response data) +// should trigger response retries +func TestRequestOnlyMatcherDoesNotRetryResponses(t *testing.T) { + var hits atomic.Int32 + + // Server returns 200 OK for all requests + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hits.Add(1) + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + })) + t.Cleanup(server.Close) + + // method PUT matcher - should NOT trigger response retries + retryMatch := caddyhttp.MatcherSets{ + caddyhttp.MatcherSet{ + caddyhttp.MatchMethod{"PUT"}, + }, + } + + upstreams := []*Upstream{ + {Host: new(Host), Dial: server.Listener.Addr().String()}, + {Host: new(Host), Dial: server.Listener.Addr().String()}, + } + + h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...) + + req := httptest.NewRequest(http.MethodPut, "http://example.com/", nil) + req = prepareTestRequest(req) + rec := httptest.NewRecorder() + + err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Should hit only once - no retry for 200 OK even though method matches + if hits.Load() != 1 { + t.Errorf("upstream hits: got %d, want 1 (should not retry successful responses)", hits.Load()) + } + if rec.Code != http.StatusOK { + t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK) + } +} + +// brokenUpstreamAddr returns the address of a TCP listener that accepts +// connections but immediately closes them, causing a transport error (not +// a dial error). This simulates an upstream that is reachable but broken +func brokenUpstreamAddr(t *testing.T) string { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + t.Cleanup(func() { ln.Close() }) + go func() { + for { + conn, err := ln.Accept() + if err != nil { + return + } + conn.Close() + } + }() + return ln.Addr().String() +} + +// TestTransportErrorPlaceholder verifies that the is_transport_error +// placeholder is set to true during transport error evaluation in tryAgain() +// and that expression matchers using {rp.is_transport_error} can match it +func TestTransportErrorPlaceholder(t *testing.T) { + broken := brokenUpstreamAddr(t) + + goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + })) + t.Cleanup(goodServer.Close) + + retryMatch := caddyhttp.MatcherSets{ + caddyhttp.MatcherSet{ + newExpressionMatcher(t, "{http.reverse_proxy.is_transport_error} == true"), + }, + } + + // RoundRobin picks index 1 first (broken), then 0 (good) + upstreams := []*Upstream{ + {Host: new(Host), Dial: goodServer.Listener.Addr().String()}, + {Host: new(Host), Dial: broken}, + } + + h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...) + + req := httptest.NewRequest(http.MethodPost, "http://example.com/", nil) + req = prepareTestRequest(req) + rec := httptest.NewRecorder() + + err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + + gotStatus := rec.Code + if err != nil { + if herr, ok := err.(caddyhttp.HandlerError); ok { + gotStatus = herr.StatusCode + } + } + + // POST transport error should be retried because is_transport_error matched + if gotStatus != http.StatusOK { + t.Errorf("status: got %d, want %d (transport error should have been retried)", gotStatus, http.StatusOK) + } +} + +// TestTransportErrorPlaceholderNotSetForResponses verifies that the +// is_transport_error placeholder is NOT set when evaluating response +// matchers, so {rp.is_transport_error} is false for response retries +func TestTransportErrorPlaceholderNotSetForResponses(t *testing.T) { + var hits atomic.Int32 + + // Server returns 502 - but the matcher only checks is_transport_error + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hits.Add(1) + w.WriteHeader(http.StatusBadGateway) + })) + t.Cleanup(server.Close) + + // Only matches transport errors, not response errors + retryMatch := caddyhttp.MatcherSets{ + caddyhttp.MatcherSet{ + newExpressionMatcher(t, "{http.reverse_proxy.is_transport_error} == true"), + }, + } + + upstreams := []*Upstream{ + {Host: new(Host), Dial: server.Listener.Addr().String()}, + {Host: new(Host), Dial: server.Listener.Addr().String()}, + } + + h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...) + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req = prepareTestRequest(req) + rec := httptest.NewRecorder() + + _ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + + // Should hit only once - is_transport_error is false during response + // evaluation so the 502 is NOT retried + if hits.Load() != 1 { + t.Errorf("upstream hits: got %d, want 1 (is_transport_error should be false for responses)", hits.Load()) + } +} + +// TestRetryMatchAllowsExpressionMixedWithOtherMatchers verifies that +// lb_retry_match accepts a block mixing expression with other matchers +func TestRetryMatchAllowsExpressionMixedWithOtherMatchers(t *testing.T) { + tests := []struct { + name string + input string + }{ + { + name: "expression alone", + input: `reverse_proxy localhost:9080 { + lb_retry_match { + expression ` + "`{rp.status_code} in [502, 503]`" + ` + } + }`, + }, + { + name: "method alone", + input: `reverse_proxy localhost:9080 { + lb_retry_match { + method PUT + } + }`, + }, + { + name: "expression mixed with method", + input: `reverse_proxy localhost:9080 { + lb_retry_match { + method POST + expression ` + "`{rp.status_code} in [502, 503]`" + ` + } + }`, + }, + { + name: "expression mixed with path", + input: `reverse_proxy localhost:9080 { + lb_retry_match { + path /api* + expression ` + "`{rp.status_code} == 502`" + ` + } + }`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + h := &Handler{} + d := caddyfile.NewTestDispenser(tc.input) + err := h.UnmarshalCaddyfile(d) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 3b9b56a05..52d2b1ab3 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -670,8 +670,12 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h return true, succ.error } - // remember this failure (if enabled) - h.countFailure(upstream) + // remember this failure (if enabled); response-based retries + // are not counted as failures since the upstream did respond + // successfully - only the response content triggered a retry + if _, isRetryableResponse := proxyErr.(retryableResponseError); !isRetryableResponse { + h.countFailure(upstream) + } // if we've tried long enough, break if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) { @@ -1055,6 +1059,45 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe res.Body, _ = h.bufferedBody(res.Body, h.ResponseBuffers) } + // set response placeholders so they can be used in retry match + // expressions and handle_response routes; clear stale header + // placeholders from a previous attempt first so they don't + // leak into the next retry evaluation + repl.DeleteByPrefix("http.reverse_proxy.header.") + for field, value := range res.Header { + repl.Set("http.reverse_proxy.header."+field, strings.Join(value, ",")) + } + repl.Set("http.reverse_proxy.status_code", res.StatusCode) + repl.Set("http.reverse_proxy.status_text", res.Status) + + // check if the response matches a retry match entry; if so, + // close the body and return a retryable error so the request + // is retried with the next upstream. Only evaluate matcher sets + // that contain at least one expression matcher, since those are + // the ones that can reference response data ({rp.status_code}, + // {rp.header.*}). Pure request-only matchers (method, path, etc.) + // are skipped to avoid retrying every response that matches a + // request condition + if h.LoadBalancing != nil && len(h.LoadBalancing.RetryMatch) > 0 { + for _, matcherSet := range h.LoadBalancing.RetryMatch { + if !matcherSetHasExpressionMatcher(matcherSet) { + continue + } + match, err := matcherSet.MatchWithError(req) + if err != nil { + h.logger.Error("error matching request for retry", zap.Error(err)) + break + } + if match { + res.Body.Close() + return retryableResponseError{ + error: fmt.Errorf("upstream response matched retry_match (status %d)", res.StatusCode), + statusCode: res.StatusCode, + } + } + } + } + // see if any response handler is configured for this response from the backend for i, rh := range h.HandleResponse { if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) { @@ -1074,14 +1117,6 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe break } - // set up the replacer so that parts of the original response can be - // used for routing decisions - for field, value := range res.Header { - repl.Set("http.reverse_proxy.header."+field, strings.Join(value, ",")) - } - repl.Set("http.reverse_proxy.status_code", res.StatusCode) - repl.Set("http.reverse_proxy.status_text", res.Status) - if c := logger.Check(zapcore.DebugLevel, "handling response"); c != nil { c.Write(zap.Int("handler", i)) } @@ -1266,18 +1301,29 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int // specifically a dialer error, we need to be careful if proxyErr != nil { _, isDialError := proxyErr.(DialError) + _, isRetryableResponse := proxyErr.(retryableResponseError) herr, isHandlerError := proxyErr.(caddyhttp.HandlerError) // if the error occurred after a connection was established, // we have to assume the upstream received the request, and // retries need to be carefully decided, because some requests - // are not idempotent - if !isDialError && (!isHandlerError || !errors.Is(herr, errNoUpstream)) { + // are not idempotent; retryableResponseError is excluded here + // because its retry decision was already made in reverseProxy() + // when the response matchers were evaluated + if !isDialError && !isRetryableResponse && (!isHandlerError || !errors.Is(herr, errNoUpstream)) { if lb.RetryMatch == nil && req.Method != "GET" { // by default, don't retry requests if they aren't GET return false } + // set transport error flag so CEL expressions can use + // {rp.is_transport_error} to decide whether to retry + repl, _ := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if repl != nil { + repl.Set("http.reverse_proxy.is_transport_error", true) + defer repl.Delete("http.reverse_proxy.is_transport_error") + } + match, err := lb.RetryMatch.AnyMatchWithError(req) if err != nil { logger.Error("error matching request for retry", zap.Error(err)) @@ -1507,6 +1553,12 @@ func removeConnectionHeaders(h http.Header) { // statusError returns an error value that has a status code. func statusError(err error) error { + // if a response-based retry was exhausted, use the actual upstream + // status code instead of a generic 502 + if rre, ok := err.(retryableResponseError); ok { + return caddyhttp.Error(rre.statusCode, err) + } + // errors proxying usually mean there is a problem with the upstream(s) statusCode := http.StatusBadGateway @@ -1558,13 +1610,15 @@ type LoadBalancing struct { // to spin if all backends are down and latency is very low. TryInterval caddy.Duration `json:"try_interval,omitempty"` - // A list of matcher sets that restricts with which requests retries are - // allowed. A request must match any of the given matcher sets in order - // to be retried if the connection to the upstream succeeded but the - // subsequent round-trip failed. If the connection to the upstream failed, - // a retry is always allowed. If unspecified, only GET requests will be - // allowed to be retried. Note that a retry is done with the next available - // host according to the load balancing policy. + // A list of matcher sets that controls retry behavior. Matcher sets + // without expression matchers (e.g. method, path) restrict which + // requests are retried on transport errors - if unspecified, only + // GET requests will be retried. Matcher sets with CEL expression + // matchers are evaluated against upstream responses and can + // reference {rp.status_code}, {rp.header.*}, and + // {rp.is_transport_error}. Dial errors are always retried + // regardless of this setting. Retries use the next available + // upstream per the load balancing policy RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty" caddy:"namespace=http.matchers"` SelectionPolicy Selector `json:"-"` @@ -1662,10 +1716,34 @@ type RequestHeaderOpsTransport interface { RequestHeaderOps() *headers.HeaderOps } +// matcherSetHasExpressionMatcher reports whether a matcher set contains +// at least one expression matcher. Expression matchers can reference +// response data via placeholders like {rp.status_code}. Matcher sets +// without expression matchers only test request properties and should +// not be evaluated for response-based retry decisions +func matcherSetHasExpressionMatcher(matcherSet caddyhttp.MatcherSet) bool { + for _, m := range matcherSet { + if _, ok := m.(*caddyhttp.MatchExpression); ok { + return true + } + } + return false +} + // roundtripSucceededError is an error type that is returned if the // roundtrip succeeded, but an error occurred after-the-fact. type roundtripSucceededError struct{ error } +// retryableResponseError is returned when the upstream response matched +// a retry_match entry, indicating the request should be retried with the +// next upstream. It preserves the original status code so that if retries +// are exhausted, the actual upstream status is reported instead of a +// generic 502 +type retryableResponseError struct { + error + statusCode int +} + // bodyReadCloser is a reader that, upon closing, will return // its buffer to the pool and close the underlying body reader. type bodyReadCloser struct { diff --git a/replacer.go b/replacer.go index 1a2aa5771..2ab02b602 100644 --- a/replacer.go +++ b/replacer.go @@ -121,6 +121,18 @@ func (r *Replacer) Delete(variable string) { r.mapMutex.Unlock() } +// DeleteByPrefix removes all static variables with +// keys starting with the given prefix +func (r *Replacer) DeleteByPrefix(prefix string) { + r.mapMutex.Lock() + for key := range r.static { + if strings.HasPrefix(key, prefix) { + delete(r.static, key) + } + } + r.mapMutex.Unlock() +} + // fromStatic provides values from r.static. func (r *Replacer) fromStatic(key string) (any, bool) { r.mapMutex.RLock() From 441d5eb0628c4a9fbe74c13eca6ab255056e3e57 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Thu, 23 Apr 2026 01:29:03 -0600 Subject: [PATCH 151/206] caddyhttp: prefer port 443 in auto-HTTPS and add tests (#7666) --- caddytest/integration/autohttps_test.go | 22 +++++++++++++ modules/caddyhttp/autohttps.go | 38 +++++++++++++++++---- modules/caddyhttp/autohttps_test.go | 44 +++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 modules/caddyhttp/autohttps_test.go diff --git a/caddytest/integration/autohttps_test.go b/caddytest/integration/autohttps_test.go index fdfb5a93e..88a0aee03 100644 --- a/caddytest/integration/autohttps_test.go +++ b/caddytest/integration/autohttps_test.go @@ -55,6 +55,28 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect) } +func TestAutoHTTPtoHTTPSRedirectsPreferHTTPSPortOverAlternatePort(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + local_certs + } + localhost { + respond "Canonical" + } + + localhost:10443 { + respond "Alternate" + } + `, "caddyfile") + + tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect) +} + func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) { tester := caddytest.NewTester(t) tester.InitServer(` diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 32e9f106d..4d9759000 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -258,18 +258,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // an empty string to indicate a catch-all, which we have to // treat special later if len(serverDomainSet) == 0 { - redirDomains[""] = append(redirDomains[""], addr) + app.recordAutoHTTPSRedirectAddress(redirDomains, "", addr) continue } // ...and associate it with each domain in this server for d := range serverDomainSet { - // if this domain is used on more than one HTTPS-enabled - // port, we'll have to choose one, so prefer the HTTPS port - if _, ok := redirDomains[d]; !ok || - addr.StartPort == uint(app.httpsPort()) { - redirDomains[d] = append(redirDomains[d], addr) - } + app.recordAutoHTTPSRedirectAddress(redirDomains, d, addr) } } } @@ -517,6 +512,35 @@ redirServersLoop: return nil } +// recordAutoHTTPSRedirectAddress stores redirect destinations for one domain +// using a single winning port while keeping all bind addresses on that port. +// +// This is needed to avoid two opposite regressions in auto-HTTPS redirects: +// preserve all listener addresses when a site binds multiple addresses on the +// same HTTPS port, but do not mix in alternate HTTPS ports when the canonical +// app HTTPS port is also available. +func (app *App) recordAutoHTTPSRedirectAddress(redirDomains map[string][]caddy.NetworkAddress, domain string, addr caddy.NetworkAddress) { + existing := redirDomains[domain] + if len(existing) == 0 { + redirDomains[domain] = []caddy.NetworkAddress{addr} + return + } + + existingPort := existing[0].StartPort + if addr.StartPort != existingPort { + if addr.StartPort == uint(app.httpsPort()) && existingPort != uint(app.httpsPort()) { + redirDomains[domain] = []caddy.NetworkAddress{addr} + } + return + } + + if slices.Contains(existing, addr) { + return + } + + redirDomains[domain] = append(existing, addr) +} + func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route { redirTo := "https://{http.request.host}" diff --git a/modules/caddyhttp/autohttps_test.go b/modules/caddyhttp/autohttps_test.go new file mode 100644 index 000000000..b5cc64d94 --- /dev/null +++ b/modules/caddyhttp/autohttps_test.go @@ -0,0 +1,44 @@ +package caddyhttp + +import ( + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func TestRecordAutoHTTPSRedirectAddressPrefersHTTPSPort(t *testing.T) { + app := &App{HTTPSPort: 443} + redirDomains := make(map[string][]caddy.NetworkAddress) + + app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 2345, EndPort: 2345}) + app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 443, EndPort: 443}) + app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 8443, EndPort: 8443}) + + got := redirDomains["example.com"] + if len(got) != 1 { + t.Fatalf("expected 1 redirect address, got %d: %#v", len(got), got) + } + if got[0].StartPort != 443 { + t.Fatalf("expected redirect to prefer HTTPS port 443, got %#v", got[0]) + } +} + +func TestRecordAutoHTTPSRedirectAddressKeepsAllBindAddressesOnWinningPort(t *testing.T) { + app := &App{HTTPSPort: 443} + redirDomains := make(map[string][]caddy.NetworkAddress) + + app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "10.0.0.189", StartPort: 8443, EndPort: 8443}) + app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "10.0.0.189", StartPort: 443, EndPort: 443}) + app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "2603:c024:8002:9500:9eb:e5d3:3975:d056", StartPort: 443, EndPort: 443}) + + got := redirDomains["example.com"] + if len(got) != 2 { + t.Fatalf("expected 2 redirect addresses for both bind addresses on the winning port, got %d: %#v", len(got), got) + } + if got[0].StartPort != 443 || got[1].StartPort != 443 { + t.Fatalf("expected both redirect addresses to stay on HTTPS port 443, got %#v", got) + } + if got[0].Host != "10.0.0.189" || got[1].Host != "2603:c024:8002:9500:9eb:e5d3:3975:d056" { + t.Fatalf("expected both bind addresses to be preserved, got %#v", got) + } +} From 41aee97386ae8a52231ab8ff7790e49cff802d77 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Fri, 24 Apr 2026 05:33:41 +1000 Subject: [PATCH 152/206] core: propagate ECH keys to the QUIC listener (#7670) --- listeners.go | 15 +++++++++++- listeners_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/listeners.go b/listeners.go index 84ebaaaba..ace0215b0 100644 --- a/listeners.go +++ b/listeners.go @@ -462,7 +462,10 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config sqs := newSharedQUICState(tlsConf) // http3.ConfigureTLSConfig only uses this field and tls App sets this field as well //nolint:gosec - quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient} + quicTlsConfig := &tls.Config{ + GetConfigForClient: sqs.getConfigForClient, + GetEncryptedClientHelloKeys: sqs.getEncryptedClientHelloKeys, + } // Require clients to verify their source address when we're handling more than 1000 handshakes per second. // TODO: make tunable? limiter := rate.NewLimiter(1000, 1000) @@ -540,6 +543,16 @@ func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Co return sqs.activeTlsConf.GetConfigForClient(ch) } +// getEncryptedClientHelloKeys is used as tls.Config's GetEncryptedClientHelloKeys field. +func (sqs *sharedQUICState) getEncryptedClientHelloKeys(ch *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { + sqs.rmu.RLock() + defer sqs.rmu.RUnlock() + if sqs.activeTlsConf.GetEncryptedClientHelloKeys == nil { + return nil, nil + } + return sqs.activeTlsConf.GetEncryptedClientHelloKeys(ch) +} + // addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc // so that when cancelled, the active tls.Config will change func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) { diff --git a/listeners_test.go b/listeners_test.go index a4cadd3aa..7bbaca1f9 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -15,6 +15,7 @@ package caddy import ( + "crypto/tls" "reflect" "testing" @@ -175,6 +176,63 @@ func TestJoinNetworkAddress(t *testing.T) { } } +func TestSharedQUICStateGetEncryptedClientHelloKeys(t *testing.T) { + hello := &tls.ClientHelloInfo{ServerName: "example.com"} + initialKeys := []tls.EncryptedClientHelloKey{{Config: []byte("initial"), PrivateKey: []byte("initial-key")}} + updatedKeys := []tls.EncryptedClientHelloKey{{Config: []byte("updated"), PrivateKey: []byte("updated-key")}} + + initialConfig := &tls.Config{ + GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) { + return nil, nil + }, + GetEncryptedClientHelloKeys: func(*tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { + return initialKeys, nil + }, + } + + sqs := newSharedQUICState(initialConfig) + + keys, err := sqs.getEncryptedClientHelloKeys(hello) + if err != nil { + t.Fatalf("getting initial ECH keys: %v", err) + } + if !reflect.DeepEqual(keys, initialKeys) { + t.Fatalf("unexpected initial ECH keys: got %#v, want %#v", keys, initialKeys) + } + + updatedConfig := &tls.Config{ + GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) { + return nil, nil + }, + GetEncryptedClientHelloKeys: func(*tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { + return updatedKeys, nil + }, + } + + _, cancel := sqs.addState(updatedConfig) + sqs.rmu.Lock() + sqs.activeTlsConf = updatedConfig + sqs.rmu.Unlock() + + keys, err = sqs.getEncryptedClientHelloKeys(hello) + if err != nil { + t.Fatalf("getting updated ECH keys: %v", err) + } + if !reflect.DeepEqual(keys, updatedKeys) { + t.Fatalf("unexpected updated ECH keys: got %#v, want %#v", keys, updatedKeys) + } + + cancel(nil) + + keys, err = sqs.getEncryptedClientHelloKeys(hello) + if err != nil { + t.Fatalf("getting restored ECH keys: %v", err) + } + if !reflect.DeepEqual(keys, initialKeys) { + t.Fatalf("unexpected restored ECH keys: got %#v, want %#v", keys, initialKeys) + } +} + func TestParseNetworkAddress(t *testing.T) { for i, tc := range []struct { input string From cf42f615662a529311fb56209a38d6a93d83d6d9 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 24 Apr 2026 09:50:06 -0600 Subject: [PATCH 153/206] Typo fix in security policy --- .github/SECURITY.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 2b72b95b6..52f997149 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -8,7 +8,7 @@ The Caddy project would like to make sure that it stays on top of all relevant a | Version | Supported | | ----------- | ----------| | 2.latest | ✔️ | -| <= 2.latest | :x: | +| < 2.latest | :x: | ## Acceptable Scope @@ -25,6 +25,8 @@ Client-side exploits are out of scope. In other words, it is not a bug in Caddy Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code. +Many reports are not security bugs and can be addressed by updating the documentation. + We accept security reports and patches, but do not assign CVEs, for code that has not been released with a non-prerelease tag. From 48c08e3890fe507bb64d59ec8004586ced676171 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 24 Apr 2026 11:28:40 -0600 Subject: [PATCH 154/206] admin: Limit config size (by @omercnet) GitHub was giving me errors related to merge status so we are doing this instead --- admin.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/admin.go b/admin.go index a93595416..766d8506a 100644 --- a/admin.go +++ b/admin.go @@ -1063,6 +1063,9 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error { buf.Reset() defer bufPool.Put(buf) + const maxConfigSize = 100 * 1024 * 1024 // 100 MB + r.Body = http.MaxBytesReader(w, r.Body, maxConfigSize) + _, err := io.Copy(buf, r.Body) if err != nil { return APIError{ From f6ee80be1b1207a5dbb380fce5dad450ceedaf67 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 24 Apr 2026 11:40:54 -0600 Subject: [PATCH 155/206] go.mod: Upgrade dependencies including CertMagic --- go.mod | 18 +++++++++--------- go.sum | 36 ++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 8796ad4d8..6e7a81b29 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,12 @@ go 1.25.0 require ( github.com/BurntSushi/toml v1.6.0 - github.com/DeRuina/timberjack v1.4.1 + github.com/DeRuina/timberjack v1.4.2 github.com/KimMachineGun/automemlimit v0.7.5 github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.23.1 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.25.2 + github.com/caddyserver/certmagic v0.25.3 github.com/caddyserver/zerossl v0.1.5 github.com/cloudflare/circl v1.6.3 github.com/dustin/go-humanize v1.0.1 @@ -39,11 +39,11 @@ require ( go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.49.0 + golang.org/x/crypto v0.50.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 - golang.org/x/net v0.52.0 + golang.org/x/net v0.53.0 golang.org/x/sync v0.20.0 - golang.org/x/term v0.41.0 + golang.org/x/term v0.42.0 golang.org/x/time v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -168,10 +168,10 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/sys v0.42.0 - golang.org/x/text v0.35.0 - golang.org/x/tools v0.42.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/sys v0.43.0 + golang.org/x/text v0.36.0 + golang.org/x/tools v0.44.0 // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect howett.net/plist v1.0.0 // indirect diff --git a/go.sum b/go.sum index 48a7d22bd..50fb903b3 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/DeRuina/timberjack v1.4.1 h1:JftM5HN/ITKehAXjtdbGqN5XZIS1biHm7VSjU0Qbtqg= -github.com/DeRuina/timberjack v1.4.1/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= +github.com/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8= +github.com/DeRuina/timberjack v1.4.2/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk= github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -85,8 +85,8 @@ github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= -github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= +github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A= +github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA= github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0= @@ -456,8 +456,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8= golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= @@ -467,8 +467,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -477,8 +477,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -506,8 +506,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -517,8 +517,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -528,8 +528,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -538,8 +538,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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= From 355c1782137f678897495503dcfe0b9e77997737 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Sat, 25 Apr 2026 03:47:54 -0400 Subject: [PATCH 156/206] chore: Use atomics where appropriate (#7648) * chore: Use atomics where appropriate * Use atomic for shutdownAt --- caddy.go | 6 +++--- listen.go | 20 ++++++++++---------- listen_unix.go | 22 ++++++++++++---------- listeners.go | 10 +++++----- modules/caddyhttp/app.go | 6 +----- modules/caddyhttp/replacer.go | 11 ++++------- modules/caddyhttp/server.go | 5 ++--- usagepool.go | 18 ++++++++++-------- 8 files changed, 47 insertions(+), 51 deletions(-) diff --git a/caddy.go b/caddy.go index 2b4b9087b..b3144299d 100644 --- a/caddy.go +++ b/caddy.go @@ -766,7 +766,7 @@ func Validate(cfg *Config) error { // code is emitted. func exitProcess(ctx context.Context, logger *zap.Logger) { // let the rest of the program know we're quitting; only do it once - if !atomic.CompareAndSwapInt32(exiting, 0, 1) { + if !exiting.CompareAndSwap(false, true) { return } @@ -845,11 +845,11 @@ func exitProcess(ctx context.Context, logger *zap.Logger) { }() } -var exiting = new(int32) // accessed atomically +var exiting atomic.Bool // Exiting returns true if the process is exiting. // EXPERIMENTAL API: subject to change or removal. -func Exiting() bool { return atomic.LoadInt32(exiting) == 1 } +func Exiting() bool { return exiting.Load() } // OnExit registers a callback to invoke during process exit. // This registration is PROCESS-GLOBAL, meaning that each diff --git a/listen.go b/listen.go index fba9c3a6b..03b63c1e2 100644 --- a/listen.go +++ b/listen.go @@ -120,8 +120,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string, // re-wrapped in a new fakeCloseListener each time the listener // is reused. This type is atomic and values must not be copied. type fakeCloseListener struct { - closed int32 // accessed atomically; belongs to this struct only - *sharedListener // embedded, so we also become a net.Listener + closed atomic.Bool + *sharedListener // embedded, so we also become a net.Listener keepAliveConfig net.KeepAliveConfig } @@ -131,7 +131,7 @@ type canSetKeepAliveConfig interface { func (fcl *fakeCloseListener) Accept() (net.Conn, error) { // if the listener is already "closed", return error - if atomic.LoadInt32(&fcl.closed) == 1 { + if fcl.closed.Load() { return nil, fakeClosedErr(fcl) } @@ -155,7 +155,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) { // that we set when Close() was called, and return a non-temporary and // non-timeout error value to the caller, masking the "true" error, so // that server loops / goroutines won't retry, linger, and leak - if atomic.LoadInt32(&fcl.closed) == 1 { + if fcl.closed.Load() { // we dereference the sharedListener explicitly even though it's embedded // so that it's clear in the code that side-effects are shared with other // users of this listener, not just our own reference to it; we also don't @@ -175,7 +175,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) { // underlying listener. The underlying listener is only closed // if the caller is the last known user of the socket. func (fcl *fakeCloseListener) Close() error { - if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) { + if fcl.closed.CompareAndSwap(false, true) { // There are two ways I know of to get an Accept() // function to return to the server loop that called // it: close the listener, or set a deadline in the @@ -238,13 +238,13 @@ func (sl *sharedListener) Destruct() error { // fakeClosePacketConn is like fakeCloseListener, but for PacketConns, // or more specifically, *net.UDPConn type fakeClosePacketConn struct { - closed int32 // accessed atomically; belongs to this struct only - *sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close + closed atomic.Bool + *sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close } func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { // if the listener is already "closed", return error - if atomic.LoadInt32(&fcpc.closed) == 1 { + if fcpc.closed.Load() { return 0, nil, &net.OpError{ Op: "readfrom", Net: fcpc.LocalAddr().Network(), @@ -258,7 +258,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e if err != nil { // this server was stopped, so clear the deadline and let // any new server continue reading; but we will exit - if atomic.LoadInt32(&fcpc.closed) == 1 { + if fcpc.closed.Load() { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { if err = fcpc.SetReadDeadline(time.Time{}); err != nil { return n, addr, err @@ -273,7 +273,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e // Close won't close the underlying socket unless there is no more reference, then listenerPool will close it. func (fcpc *fakeClosePacketConn) Close() error { - if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) { + if fcpc.closed.CompareAndSwap(false, true) { _ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops _, _ = listenerPool.Delete(fcpc.sharedPacketConn.key) } diff --git a/listen_unix.go b/listen_unix.go index d6ae0cb8e..d60f69f3b 100644 --- a/listen_unix.go +++ b/listen_unix.go @@ -63,7 +63,7 @@ func reuseUnixSocket(network, addr string) (any, error) { if err != nil { return nil, err } - atomic.AddInt32(unixSocket.count, 1) + unixSocket.count.Add(1) unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count} case *unixConn: @@ -71,7 +71,7 @@ func reuseUnixSocket(network, addr string) (any, error) { if err != nil { return nil, err } - atomic.AddInt32(unixSocket.count, 1) + unixSocket.count.Add(1) unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count} } @@ -165,8 +165,9 @@ func listenReusable(ctx context.Context, lnKey string, network, address string, if !fd { // TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so... if unix, ok := ln.(*net.UnixConn); ok { - one := int32(1) - ln = &unixConn{unix, lnKey, &one} + cnt := new(atomic.Int32) + cnt.Store(1) + ln = &unixConn{unix, lnKey, cnt} unixSockets[lnKey] = ln.(*unixConn) } } @@ -181,8 +182,9 @@ func listenReusable(ctx context.Context, lnKey string, network, address string, // (we do our own "unlink on close" -- not required, but more tidy) if unix, ok := ln.(*net.UnixListener); ok { unix.SetUnlinkOnClose(false) - one := int32(1) - ln = &unixListener{unix, lnKey, &one} + cnt := new(atomic.Int32) + cnt.Store(1) + ln = &unixListener{unix, lnKey, cnt} unixSockets[lnKey] = ln.(*unixListener) } } @@ -216,11 +218,11 @@ func reusePort(network, address string, conn syscall.RawConn) error { type unixListener struct { *net.UnixListener mapKey string - count *int32 // accessed atomically + count *atomic.Int32 } func (uln *unixListener) Close() error { - newCount := atomic.AddInt32(uln.count, -1) + newCount := uln.count.Add(-1) if newCount == 0 { file, err := uln.File() var name string @@ -242,11 +244,11 @@ func (uln *unixListener) Close() error { type unixConn struct { *net.UnixConn mapKey string - count *int32 // accessed atomically + count *atomic.Int32 } func (uc *unixConn) Close() error { - newCount := atomic.AddInt32(uc.count, -1) + newCount := uc.count.Add(-1) if newCount == 0 { file, err := uc.File() var name string diff --git a/listeners.go b/listeners.go index ace0215b0..6031f98e4 100644 --- a/listeners.go +++ b/listeners.go @@ -624,8 +624,8 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error { var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉") type fakeCloseQuicListener struct { - closed int32 // accessed atomically; belongs to this struct only - *sharedQuicListener // embedded, so we also become a quic.EarlyListener + closed atomic.Int32 + *sharedQuicListener // embedded, so we also become a quic.EarlyListener context context.Context contextCancel context.CancelCauseFunc } @@ -642,16 +642,16 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error) } // if the listener is "closed", return a fake closed error instead - if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) { + if fcql.closed.Load() == 1 && errors.Is(err, context.Canceled) { return nil, fakeClosedErr(fcql) } return nil, err } func (fcql *fakeCloseQuicListener) Close() error { - if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) { + if fcql.closed.CompareAndSwap(0, 1) { fcql.contextCancel(errFakeClosed) - } else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) { + } else if fcql.closed.CompareAndSwap(1, 2) { _, _ = listenerPool.Delete(fcql.sharedQuicListener.key) } return nil diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 673c36d77..a3b71836d 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -219,8 +219,6 @@ func (app *App) Provision(ctx caddy.Context) error { srv.ctx = ctx srv.logger = app.logger.Named("log") srv.errorLogger = app.logger.Named("log.error") - srv.shutdownAtMu = new(sync.RWMutex) - if srv.Metrics != nil { srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead") app.Metrics = cmp.Or(app.Metrics, &Metrics{ @@ -694,9 +692,7 @@ func (app *App) Stop() error { for _, addr := range na.Expand() { if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 { app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String())) - server.shutdownAtMu.Lock() - server.shutdownAt = scheduledTime - server.shutdownAtMu.Unlock() + server.shutdownAt.Store(&scheduledTime) delay = true } else { app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String())) diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index e7974a561..623a6ef4b 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -387,17 +387,14 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo switch key { case "http.shutting_down": server := req.Context().Value(ServerCtxKey).(*Server) - server.shutdownAtMu.RLock() - defer server.shutdownAtMu.RUnlock() - return !server.shutdownAt.IsZero(), true + return server.shutdownAt.Load() != nil, true case "http.time_until_shutdown": server := req.Context().Value(ServerCtxKey).(*Server) - server.shutdownAtMu.RLock() - defer server.shutdownAtMu.RUnlock() - if server.shutdownAt.IsZero() { + t := server.shutdownAt.Load() + if t == nil { return nil, true } - return time.Until(server.shutdownAt), true + return time.Until(*t), true } return nil, false diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 41a8e55b0..3005bc273 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -28,7 +28,7 @@ import ( "runtime" "slices" "strings" - "sync" + "sync/atomic" "time" "github.com/caddyserver/certmagic" @@ -291,8 +291,7 @@ type Server struct { trustedProxies IPRangeSource - shutdownAt time.Time - shutdownAtMu *sync.RWMutex + shutdownAt atomic.Pointer[time.Time] // registered callback functions connStateFuncs []func(net.Conn, http.ConnState) diff --git a/usagepool.go b/usagepool.go index a6466b9b1..6b7a3c25e 100644 --- a/usagepool.go +++ b/usagepool.go @@ -79,14 +79,15 @@ func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loade up.Lock() upv, loaded = up.pool[key] if loaded { - atomic.AddInt32(&upv.refs, 1) + upv.refs.Add(1) up.Unlock() upv.RLock() value = upv.value err = upv.err upv.RUnlock() } else { - upv = &usagePoolVal{refs: 1} + upv = &usagePoolVal{} + upv.refs.Store(1) upv.Lock() up.pool[key] = upv up.Unlock() @@ -118,7 +119,7 @@ func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) { up.Lock() upv, loaded = up.pool[key] if loaded { - atomic.AddInt32(&upv.refs, 1) + upv.refs.Add(1) up.Unlock() upv.Lock() if upv.err == nil { @@ -129,7 +130,8 @@ func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) { } upv.Unlock() } else { - upv = &usagePoolVal{refs: 1, value: val} + upv = &usagePoolVal{value: val} + upv.refs.Store(1) up.pool[key] = upv up.Unlock() value = val @@ -173,7 +175,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) { up.Unlock() return false, nil } - refs := atomic.AddInt32(&upv.refs, -1) + refs := upv.refs.Add(-1) if refs == 0 { delete(up.pool, key) up.Unlock() @@ -188,7 +190,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) { up.Unlock() if refs < 0 { panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)", - upv.value, upv.refs)) + upv.value, upv.refs.Load())) } } return deleted, err @@ -203,7 +205,7 @@ func (up *UsagePool) References(key any) (int, bool) { if loaded { // I wonder if it'd be safer to read this value during // our lock on the UsagePool... guess we'll see... - refs := atomic.LoadInt32(&upv.refs) + refs := upv.refs.Load() return int(refs), true } return 0, false @@ -220,7 +222,7 @@ type Destructor interface { } type usagePoolVal struct { - refs int32 // accessed atomically; must be 64-bit aligned for 32-bit systems + refs atomic.Int32 value any err error sync.RWMutex From 2a3ed96f8cbf0c72ce4621de84939e8828726d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 25 Apr 2026 12:52:08 +0200 Subject: [PATCH 157/206] metrics: Implement pushing via OLTP (#7664) --- caddyconfig/httpcaddyfile/options.go | 2 + .../metrics_otlp.caddyfiletest | 35 ++++++++ go.mod | 4 +- modules/caddyhttp/app.go | 9 ++ modules/caddyhttp/metrics.go | 88 ++++++++++++++++++- modules/caddyhttp/metrics_test.go | 50 +++++++++++ modules/caddyhttp/tracing/tracer.go | 3 +- 7 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/metrics_otlp.caddyfiletest diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index ffe43ff7e..0b4ee5402 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -484,6 +484,8 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) { metrics.PerHost = true case "observe_catchall_hosts": metrics.ObserveCatchallHosts = true + case "otlp": + metrics.OTLP = true default: return nil, d.Errf("unrecognized servers option '%s'", d.Val()) } diff --git a/caddytest/integration/caddyfile_adapt/metrics_otlp.caddyfiletest b/caddytest/integration/caddyfile_adapt/metrics_otlp.caddyfiletest new file mode 100644 index 000000000..551c2f2ec --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/metrics_otlp.caddyfiletest @@ -0,0 +1,35 @@ +{ + metrics { + otlp + } +} +:80 { + respond "Hello" +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "handle": [ + { + "body": "Hello", + "handler": "static_response" + } + ] + } + ] + } + }, + "metrics": { + "otlp": true + } + } + } +} diff --git a/go.mod b/go.mod index 6e7a81b29..93b73f3f6 100644 --- a/go.mod +++ b/go.mod @@ -30,11 +30,13 @@ require ( github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc + go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/sdk/metric v1.43.0 go.step.sm/crypto v0.77.1 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 @@ -87,7 +89,6 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect go.opentelemetry.io/contrib/propagators/aws v1.43.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.43.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 // indirect @@ -104,7 +105,6 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index a3b71836d..571ac496e 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -208,6 +208,9 @@ func (app *App) Provision(ctx caddy.Context) error { app.Metrics.httpMetrics = &httpMetrics{} // Scan config for allowed hosts to prevent cardinality explosion app.Metrics.scanConfigForHosts(app) + if err := app.Metrics.provisionOTLP(ctx); err != nil { + return err + } } // prepare each server oldContext := ctx.Context @@ -817,6 +820,12 @@ func (app *App) Stop() error { } } + // flush and shut down the OTLP metrics exporter (if configured) so any + // last data point reaches the collector before the process exits + if err := app.Metrics.shutdown(ctx); err != nil { + app.logger.Error("shutting down OTLP metrics", zap.Error(err)) + } + app.stopped = true return nil } diff --git a/modules/caddyhttp/metrics.go b/modules/caddyhttp/metrics.go index b212bbfb8..8d20e01b6 100644 --- a/modules/caddyhttp/metrics.go +++ b/modules/caddyhttp/metrics.go @@ -3,6 +3,7 @@ package caddyhttp import ( "context" "errors" + "fmt" "net/http" "strings" "sync" @@ -10,9 +11,14 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + otelprom "go.opentelemetry.io/contrib/bridges/prometheus" + "go.opentelemetry.io/contrib/exporters/autoexport" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/internal/metrics" + caddymetrics "github.com/caddyserver/caddy/v2/internal/metrics" ) // Metrics configures metrics observations. @@ -67,10 +73,20 @@ type Metrics struct { // for production environments exposed to the internet). ObserveCatchallHosts bool `json:"observe_catchall_hosts,omitempty"` + // Enable pushing metrics via OTLP in addition to the existing Prometheus + // scrape endpoints. When set, a PeriodicReader is attached to the shared + // Prometheus registry (via a Prometheus -> OpenTelemetry bridge), and the + // exporter is autoconfigured from the standard OTEL_* environment + // variables (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL, + // OTEL_METRICS_EXPORTER, ...). Set OTEL_METRICS_EXPORTER=none or simply + // keep this field false to disable OTLP export. + OTLP bool `json:"otlp,omitempty"` + init sync.Once httpMetrics *httpMetrics allowedHosts map[string]struct{} hasHTTPSServer bool + meterProvider *sdkmetric.MeterProvider } type httpMetrics struct { @@ -147,6 +163,70 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) { }, httpLabels) } +// provisionOTLP wires a MeterProvider that periodically reads the process-wide +// Prometheus registry and pushes the result via OTLP. The exporter and reader +// are autoconfigured from the standard OTEL_* environment variables, matching +// the ergonomics of the existing `tracing` directive. It is a no-op when +// m.OTLP is false, and honors OTEL_METRICS_EXPORTER=none (autoexport +// short-circuits to a no-op reader in that case). +func (m *Metrics) provisionOTLP(ctx caddy.Context) error { + if !m.OTLP { + return nil + } + + // Register a Prometheus -> OpenTelemetry bridge against the process-wide + // Prometheus registry as the *default* source the NewMetricReader below + // will read from. + // + // NB: despite the "With*" naming, autoexport.WithFallbackMetricProducer is + // a package-level setter (it returns nothing) — it mutates autoexport's + // internal producer registry and takes effect on the very next call to + // NewMetricReader. It is NOT a MetricOption and must not be passed as one. + // Users can still override the source by setting OTEL_METRICS_PRODUCERS. + reg := ctx.GetMetricsRegistry() + autoexport.WithFallbackMetricProducer(func(context.Context) (sdkmetric.Producer, error) { + return otelprom.NewMetricProducer(otelprom.WithGatherer(reg)), nil + }) + + reader, err := autoexport.NewMetricReader(ctx) + if err != nil { + return fmt.Errorf("creating OTLP metric reader: %w", err) + } + + version, _ := caddy.Version() + res, err := resource.Merge(resource.Default(), resource.NewSchemaless( + semconv.WebEngineName(ServerHeader), + semconv.WebEngineVersion(version), + )) + if err != nil { + return fmt.Errorf("building OTLP metrics resource: %w", err) + } + + m.meterProvider = sdkmetric.NewMeterProvider( + sdkmetric.WithResource(res), + sdkmetric.WithReader(reader), + ) + + return nil +} + +// shutdown flushes and tears down the OTLP MeterProvider if one was provisioned. +// Both ForceFlush and Shutdown are always attempted so that a flush failure +// does not prevent the reader goroutines from being stopped; errors from both +// are returned joined. +func (m *Metrics) shutdown(ctx context.Context) error { + if m == nil || m.meterProvider == nil { + return nil + } + + // ForceFlush gives the final collection a chance to reach the collector + // before the reader goroutine is stopped by Shutdown. + return errors.Join( + m.meterProvider.ForceFlush(ctx), + m.meterProvider.Shutdown(ctx), + ) +} + // scanConfigForHosts scans the HTTP app configuration to build a set of allowed hosts // for metrics collection, similar to how auto-HTTPS scans for domain names. func (m *Metrics) scanConfigForHosts(app *App) { @@ -234,7 +314,7 @@ func newMetricsInstrumentedRoute(ctx caddy.Context, handler string, next Handler func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) error { server := serverNameFromContext(r.Context()) labels := prometheus.Labels{"server": server, "handler": h.handler} - method := metrics.SanitizeMethod(r.Method) + method := caddymetrics.SanitizeMethod(r.Method) // the "code" value is set later, but initialized here to eliminate the possibility // of a panic statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""} @@ -264,7 +344,7 @@ func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Requ // being called when the headers are written. // Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader. writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool { - statusLabels["code"] = metrics.SanitizeCode(status) + statusLabels["code"] = caddymetrics.SanitizeCode(status) ttfb := time.Since(start).Seconds() h.metrics.httpMetrics.responseDuration.With(statusLabels).Observe(ttfb) return false @@ -280,7 +360,7 @@ func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Requ if statusLabels["code"] == "" { // we still sanitize it, even though it's likely to be 0. A 200 is // returned on fallthrough so we want to reflect that. - statusLabels["code"] = metrics.SanitizeCode(status) + statusLabels["code"] = caddymetrics.SanitizeCode(status) } h.metrics.httpMetrics.requestDuration.With(statusLabels).Observe(dur) diff --git a/modules/caddyhttp/metrics_test.go b/modules/caddyhttp/metrics_test.go index 987b3f342..d75b3cae1 100644 --- a/modules/caddyhttp/metrics_test.go +++ b/modules/caddyhttp/metrics_test.go @@ -523,6 +523,56 @@ func TestMetricsInstrumentedRoute(t *testing.T) { } } +func TestMetricsProvisionOTLPDisabled(t *testing.T) { + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + + m := &Metrics{OTLP: false} + + if err := m.provisionOTLP(ctx); err != nil { + t.Fatalf("provisionOTLP returned unexpected error: %v", err) + } + if m.meterProvider != nil { + t.Fatalf("meterProvider should remain nil when OTLP is disabled") + } + + // shutdown must be safe on a never-provisioned Metrics. + if err := m.shutdown(context.Background()); err != nil { + t.Fatalf("shutdown returned unexpected error: %v", err) + } +} + +func TestMetricsProvisionOTLPNoopExporter(t *testing.T) { + // OTEL_METRICS_EXPORTER=none makes autoexport return its built-in + // no-op reader, which avoids any network I/O while still exercising + // the full provisionOTLP -> shutdown lifecycle. + t.Setenv("OTEL_METRICS_EXPORTER", "none") + + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + + m := &Metrics{OTLP: true} + + if err := m.provisionOTLP(ctx); err != nil { + t.Fatalf("provisionOTLP returned unexpected error: %v", err) + } + if m.meterProvider == nil { + t.Fatalf("provisionOTLP did not create a MeterProvider") + } + + if err := m.shutdown(context.Background()); err != nil { + t.Fatalf("shutdown returned unexpected error: %v", err) + } +} + +// shutdown on a nil receiver is a convenience so App.Stop can call it +// without guarding against app.Metrics being unset. +func TestMetricsShutdownNilReceiver(t *testing.T) { + var m *Metrics + + if err := m.shutdown(context.Background()); err != nil { + t.Fatalf("shutdown on nil Metrics returned unexpected error: %v", err) + } +} + func BenchmarkMetricsInstrumentedRoute(b *testing.B) { ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) m := &Metrics{ diff --git a/modules/caddyhttp/tracing/tracer.go b/modules/caddyhttp/tracing/tracer.go index bb0f81fc3..5d71059ed 100644 --- a/modules/caddyhttp/tracing/tracer.go +++ b/modules/caddyhttp/tracing/tracer.go @@ -21,7 +21,6 @@ import ( ) const ( - webEngineName = "Caddy" defaultSpanName = "handler" nextCallCtxKey caddy.CtxKey = "nextCall" ) @@ -58,7 +57,7 @@ func newOpenTelemetryWrapper( } version, _ := caddy.Version() - res, err := ot.newResource(webEngineName, version) + res, err := ot.newResource(caddyhttp.ServerHeader, version) if err != nil { return ot, fmt.Errorf("creating resource error: %w", err) } From fdbef2a6ef698efe414076836b631daf8f791111 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Sun, 26 Apr 2026 23:30:44 +1000 Subject: [PATCH 158/206] logging: add regression coverage for rotated file mode (#7620) --- modules/logging/filewriter_test.go | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/modules/logging/filewriter_test.go b/modules/logging/filewriter_test.go index 915784b53..de46891fa 100644 --- a/modules/logging/filewriter_test.go +++ b/modules/logging/filewriter_test.go @@ -174,6 +174,47 @@ func TestFileRotationPreserveMode(t *testing.T) { } } +func TestFileRotationPreserveModeWithUmask(t *testing.T) { + m := syscall.Umask(0o022) + defer syscall.Umask(m) + + dir, err := os.MkdirTemp("", "caddytest") + if err != nil { + t.Fatalf("failed to create tempdir: %v", err) + } + defer os.RemoveAll(dir) + + fpath := path.Join(dir, "test.log") + + roll := true + mode := fileMode(0o660) + fw := FileWriter{ + Filename: fpath, + Mode: mode, + Roll: &roll, + RollSizeMB: 1, + } + + logger, err := fw.OpenWriter() + if err != nil { + t.Fatalf("failed to create file: %v", err) + } + defer logger.Close() + + b := make([]byte, 1024*1024-1000) + logger.Write(b) + logger.Write(b[0:2000]) + + st, err := os.Stat(fpath) + if err != nil { + t.Fatalf("failed to check file permissions: %v", err) + } + + if got := st.Mode().Perm(); got != os.FileMode(mode) { + t.Errorf("file mode after rotation is %v, want %v", got, mode) + } +} + func TestFileModeConfig(t *testing.T) { tests := []struct { name string From c1918ff1ad6fd411d828c8ea82ad535c5c793219 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Sun, 26 Apr 2026 23:39:57 +1000 Subject: [PATCH 159/206] httpcaddyfile: inherit global ACME issuer settings in tls shortcuts (#7617) --- caddyconfig/httpcaddyfile/builtins.go | 23 +- caddyconfig/httpcaddyfile/options_test.go | 125 ++++++++++ caddyconfig/httpcaddyfile/tlsapp.go | 283 ++++++++++++++++++++++ 3 files changed, 412 insertions(+), 19 deletions(-) diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 6d6b71fa8..311a29e02 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -550,26 +550,11 @@ func parseTLS(h Helper) ([]ConfigValue, error) { } case acmeIssuer != nil: - // implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one - defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email) - - // if an ACME CA endpoint was set, the user expects to use that specific one, - // not any others that may be defaults, so replace all defaults with that ACME CA - if acmeIssuer.CA != "" { - defaultIssuers = []certmagic.Issuer{acmeIssuer} - } - + // implicit ACME issuers (from various subdirectives) should inherit from + // any globally-configured ACME issuer templates, then apply the local + // shortcut settings as overrides. + defaultIssuers := implicitACMEIssuers(h, acmeIssuer) for _, issuer := range defaultIssuers { - // apply settings from the implicitly-configured ACMEIssuer to any - // default ACMEIssuers, but preserve each default issuer's CA endpoint, - // because, for example, if you configure the DNS challenge, it should - // apply to any of the default ACMEIssuers, but you don't want to trample - // out their unique CA endpoints - if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil { - acmeCopy := *acmeIssuer - acmeCopy.CA = iss.CA - issuer = &acmeCopy - } configVals = append(configVals, ConfigValue{ Class: "tls.cert_issuer", Value: issuer, diff --git a/caddyconfig/httpcaddyfile/options_test.go b/caddyconfig/httpcaddyfile/options_test.go index 524187f30..50b431d3e 100644 --- a/caddyconfig/httpcaddyfile/options_test.go +++ b/caddyconfig/httpcaddyfile/options_test.go @@ -3,7 +3,9 @@ package httpcaddyfile import ( "encoding/json" "testing" + "time" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddytls" _ "github.com/caddyserver/caddy/v2/modules/logging" @@ -166,3 +168,126 @@ func TestGlobalResolversOption(t *testing.T) { }) } } + +func TestGlobalCertIssuerAppliesToImplicitACMEIssuer(t *testing.T) { + adapter := caddyfile.Adapter{ + ServerType: ServerType{}, + } + + input := `{ + cert_issuer acme { + disable_tlsalpn_challenge + } + } + report.company.intern { + tls { + ca https://deglacme01.company.intern/acme/acme/directory + ca_root /etc/certs/company_root2.crt + } + respond "ok" + }` + + out, _, err := adapter.Adapt([]byte(input), nil) + if err != nil { + t.Fatalf("adapting caddyfile: %v", err) + } + + var config struct { + Apps struct { + TLS *caddytls.TLS `json:"tls"` + } `json:"apps"` + } + if err := json.Unmarshal(out, &config); err != nil { + t.Fatalf("unmarshaling adapted config: %v", err) + } + if config.Apps.TLS == nil || config.Apps.TLS.Automation == nil { + t.Fatal("expected tls automation config") + } + + var subjectPolicy *caddytls.AutomationPolicy + for _, ap := range config.Apps.TLS.Automation.Policies { + if len(ap.SubjectsRaw) == 1 && ap.SubjectsRaw[0] == "report.company.intern" { + subjectPolicy = ap + break + } + } + if subjectPolicy == nil { + t.Fatal("expected subject-specific automation policy") + } + if len(subjectPolicy.IssuersRaw) != 1 { + t.Fatalf("expected one issuer for subject-specific policy, got %d", len(subjectPolicy.IssuersRaw)) + } + + var issuer caddytls.ACMEIssuer + if err := json.Unmarshal(subjectPolicy.IssuersRaw[0], &issuer); err != nil { + t.Fatalf("unmarshaling issuer: %v", err) + } + if issuer.CA != "https://deglacme01.company.intern/acme/acme/directory" { + t.Fatalf("expected custom ACME CA, got %q", issuer.CA) + } + if len(issuer.TrustedRootsPEMFiles) != 1 || issuer.TrustedRootsPEMFiles[0] != "/etc/certs/company_root2.crt" { + t.Fatalf("expected trusted roots to include site CA root, got %v", issuer.TrustedRootsPEMFiles) + } + if issuer.Challenges == nil || issuer.Challenges.TLSALPN == nil || !issuer.Challenges.TLSALPN.Disabled { + t.Fatalf("expected tls-alpn challenge to be disabled, got %#v", issuer.Challenges) + } +} + +func TestMergeACMEIssuers(t *testing.T) { + base := &caddytls.ACMEIssuer{ + Email: "ops@example.com", + Challenges: &caddytls.ChallengesConfig{ + HTTP: &caddytls.HTTPChallengeConfig{ + AlternatePort: 8080, + }, + TLSALPN: &caddytls.TLSALPNChallengeConfig{ + Disabled: true, + AlternatePort: 8443, + }, + DNS: &caddytls.DNSChallengeConfig{ + Resolvers: []string{"1.1.1.1"}, + OverrideDomain: "_acme-challenge.example.net", + }, + }, + TrustedRootsPEMFiles: []string{"global.pem"}, + } + overrides := &caddytls.ACMEIssuer{ + CA: "https://deglacme01.company.intern/acme/acme/directory", + Challenges: &caddytls.ChallengesConfig{ + HTTP: &caddytls.HTTPChallengeConfig{ + Disabled: true, + }, + DNS: &caddytls.DNSChallengeConfig{ + PropagationTimeout: caddy.Duration(time.Minute), + }, + }, + TrustedRootsPEMFiles: []string{"site.pem"}, + } + + merged := mergeACMEIssuers(base, overrides) + if merged.CA != overrides.CA { + t.Fatalf("expected merged CA %q, got %q", overrides.CA, merged.CA) + } + if merged.Email != base.Email { + t.Fatalf("expected merged email %q, got %q", base.Email, merged.Email) + } + if len(merged.TrustedRootsPEMFiles) != 2 || merged.TrustedRootsPEMFiles[0] != "global.pem" || merged.TrustedRootsPEMFiles[1] != "site.pem" { + t.Fatalf("expected merged roots [global.pem site.pem], got %v", merged.TrustedRootsPEMFiles) + } + if merged.Challenges == nil || merged.Challenges.HTTP == nil || !merged.Challenges.HTTP.Disabled || merged.Challenges.HTTP.AlternatePort != 8080 { + t.Fatalf("expected merged HTTP challenge config to preserve alternate port and apply disable flag, got %#v", merged.Challenges) + } + if merged.Challenges.TLSALPN == nil || !merged.Challenges.TLSALPN.Disabled || merged.Challenges.TLSALPN.AlternatePort != 8443 { + t.Fatalf("expected merged TLS-ALPN challenge config to preserve global settings, got %#v", merged.Challenges) + } + if merged.Challenges.DNS == nil || merged.Challenges.DNS.PropagationTimeout != caddy.Duration(time.Minute) || len(merged.Challenges.DNS.Resolvers) != 1 || merged.Challenges.DNS.Resolvers[0] != "1.1.1.1" || merged.Challenges.DNS.OverrideDomain != "_acme-challenge.example.net" { + t.Fatalf("expected merged DNS challenge config to preserve global values and apply overrides, got %#v", merged.Challenges) + } + + if base.CA != "" { + t.Fatalf("expected base issuer to remain unchanged, got CA %q", base.CA) + } + if len(base.TrustedRootsPEMFiles) != 1 || base.TrustedRootsPEMFiles[0] != "global.pem" { + t.Fatalf("expected base roots to remain unchanged, got %v", base.TrustedRootsPEMFiles) + } +} diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 22bc22816..7a72cd6fb 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -612,6 +612,289 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e return nil } +// implicitACMEIssuers returns the issuers to use for ACME-related tls +// shortcuts such as ca, ca_root, and dns. If any global cert_issuer options +// configure ACME issuers, those become the templates for the local shortcut +// configuration; otherwise, default ACME issuers are used. +func implicitACMEIssuers(h Helper, acmeIssuer *caddytls.ACMEIssuer) []certmagic.Issuer { + globalIssuers, _ := h.Option("cert_issuer").([]certmagic.Issuer) + + var implicitIssuers []certmagic.Issuer + for _, issuer := range globalIssuers { + acmeWrapper, ok := issuer.(acmeCapable) + if !ok { + continue + } + baseIssuer := acmeWrapper.GetACMEIssuer() + if baseIssuer == nil { + continue + } + implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer)) + } + if len(implicitIssuers) > 0 { + return implicitIssuers + } + + // If an ACME CA endpoint was set locally, the user expects to use only that + // CA rather than the usual default fallback issuers. + defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email) + if acmeIssuer.CA != "" { + defaultIssuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)} + } + + implicitIssuers = make([]certmagic.Issuer, 0, len(defaultIssuers)) + for _, issuer := range defaultIssuers { + acmeWrapper, ok := issuer.(acmeCapable) + if !ok { + implicitIssuers = append(implicitIssuers, issuer) + continue + } + baseIssuer := acmeWrapper.GetACMEIssuer() + if baseIssuer == nil { + implicitIssuers = append(implicitIssuers, issuer) + continue + } + implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer)) + } + return implicitIssuers +} + +func mergeACMEIssuers(base, overrides *caddytls.ACMEIssuer) *caddytls.ACMEIssuer { + if base == nil { + return cloneACMEIssuer(overrides) + } + + merged := cloneACMEIssuer(base) + if overrides == nil { + return merged + } + + if overrides.CA != "" { + merged.CA = overrides.CA + } + if overrides.TestCA != "" { + merged.TestCA = overrides.TestCA + } + if overrides.Email != "" { + merged.Email = overrides.Email + } + if overrides.Profile != "" { + merged.Profile = overrides.Profile + } + if overrides.AccountKey != "" { + merged.AccountKey = overrides.AccountKey + } + if overrides.ExternalAccount != nil { + merged.ExternalAccount = cloneACMEEAB(overrides.ExternalAccount) + } + if overrides.ACMETimeout != 0 { + merged.ACMETimeout = overrides.ACMETimeout + } + if len(overrides.TrustedRootsPEMFiles) > 0 { + merged.TrustedRootsPEMFiles = appendUniqueStrings(merged.TrustedRootsPEMFiles, overrides.TrustedRootsPEMFiles...) + } + if overrides.PreferredChains != nil { + merged.PreferredChains = cloneChainPreference(overrides.PreferredChains) + } + if overrides.CertificateLifetime != 0 { + merged.CertificateLifetime = overrides.CertificateLifetime + } + if len(overrides.NetworkProxyRaw) > 0 { + merged.NetworkProxyRaw = slices.Clone(overrides.NetworkProxyRaw) + } + merged.Challenges = mergeChallengesConfig(merged.Challenges, overrides.Challenges) + + return merged +} + +func mergeChallengesConfig(base, overrides *caddytls.ChallengesConfig) *caddytls.ChallengesConfig { + if base == nil { + return cloneChallengesConfig(overrides) + } + merged := cloneChallengesConfig(base) + if overrides == nil { + return merged + } + + merged.HTTP = mergeHTTPChallengeConfig(merged.HTTP, overrides.HTTP) + merged.TLSALPN = mergeTLSALPNChallengeConfig(merged.TLSALPN, overrides.TLSALPN) + merged.DNS = mergeDNSChallengeConfig(merged.DNS, overrides.DNS) + if overrides.BindHost != "" { + merged.BindHost = overrides.BindHost + } + if overrides.Distributed != nil { + value := *overrides.Distributed + merged.Distributed = &value + } + + return merged +} + +func mergeHTTPChallengeConfig(base, overrides *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig { + if base == nil { + return cloneHTTPChallengeConfig(overrides) + } + merged := cloneHTTPChallengeConfig(base) + if overrides == nil { + return merged + } + + if overrides.Disabled { + merged.Disabled = true + } + if overrides.AlternatePort != 0 { + merged.AlternatePort = overrides.AlternatePort + } + + return merged +} + +func mergeTLSALPNChallengeConfig(base, overrides *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig { + if base == nil { + return cloneTLSALPNChallengeConfig(overrides) + } + merged := cloneTLSALPNChallengeConfig(base) + if overrides == nil { + return merged + } + + if overrides.Disabled { + merged.Disabled = true + } + if overrides.AlternatePort != 0 { + merged.AlternatePort = overrides.AlternatePort + } + + return merged +} + +func mergeDNSChallengeConfig(base, overrides *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig { + if base == nil { + return cloneDNSChallengeConfig(overrides) + } + merged := cloneDNSChallengeConfig(base) + if overrides == nil { + return merged + } + + if len(overrides.ProviderRaw) > 0 { + merged.ProviderRaw = slices.Clone(overrides.ProviderRaw) + } + if overrides.PropagationDelay != 0 { + merged.PropagationDelay = overrides.PropagationDelay + } + if overrides.PropagationTimeout != 0 { + merged.PropagationTimeout = overrides.PropagationTimeout + } + if overrides.Resolvers != nil { + merged.Resolvers = slices.Clone(overrides.Resolvers) + } + if overrides.OverrideDomain != "" { + merged.OverrideDomain = overrides.OverrideDomain + } + if overrides.TTL != 0 { + merged.TTL = overrides.TTL + } + + return merged +} + +func cloneACMEIssuer(iss *caddytls.ACMEIssuer) *caddytls.ACMEIssuer { + if iss == nil { + return nil + } + + cloned := *iss + cloned.Challenges = cloneChallengesConfig(iss.Challenges) + cloned.ExternalAccount = cloneACMEEAB(iss.ExternalAccount) + cloned.TrustedRootsPEMFiles = slices.Clone(iss.TrustedRootsPEMFiles) + cloned.PreferredChains = cloneChainPreference(iss.PreferredChains) + cloned.NetworkProxyRaw = slices.Clone(iss.NetworkProxyRaw) + + return &cloned +} + +func cloneChallengesConfig(cfg *caddytls.ChallengesConfig) *caddytls.ChallengesConfig { + if cfg == nil { + return nil + } + + cloned := *cfg + cloned.HTTP = cloneHTTPChallengeConfig(cfg.HTTP) + cloned.TLSALPN = cloneTLSALPNChallengeConfig(cfg.TLSALPN) + cloned.DNS = cloneDNSChallengeConfig(cfg.DNS) + if cfg.Distributed != nil { + value := *cfg.Distributed + cloned.Distributed = &value + } + + return &cloned +} + +func cloneHTTPChallengeConfig(cfg *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig { + if cfg == nil { + return nil + } + + cloned := *cfg + return &cloned +} + +func cloneTLSALPNChallengeConfig(cfg *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig { + if cfg == nil { + return nil + } + + cloned := *cfg + return &cloned +} + +func cloneDNSChallengeConfig(cfg *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig { + if cfg == nil { + return nil + } + + cloned := *cfg + cloned.ProviderRaw = slices.Clone(cfg.ProviderRaw) + cloned.Resolvers = slices.Clone(cfg.Resolvers) + + return &cloned +} + +func cloneACMEEAB(eab *acme.EAB) *acme.EAB { + if eab == nil { + return nil + } + + cloned := *eab + return &cloned +} + +func cloneChainPreference(pref *caddytls.ChainPreference) *caddytls.ChainPreference { + if pref == nil { + return nil + } + + cloned := *pref + cloned.RootCommonName = slices.Clone(pref.RootCommonName) + cloned.AnyCommonName = slices.Clone(pref.AnyCommonName) + if pref.Smallest != nil { + value := *pref.Smallest + cloned.Smallest = &value + } + + return &cloned +} + +func appendUniqueStrings(existing []string, additions ...string) []string { + for _, value := range additions { + if !slices.Contains(existing, value) { + existing = append(existing, value) + } + } + return existing +} + // newBaseAutomationPolicy returns a new TLS automation policy that gets // its values from the global options map. It should be used as the base // for any other automation policies. A nil policy (and no error) will be From c653e7d61a24ce854d3ff6410dc8ea19ffd04131 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:51:26 +1000 Subject: [PATCH 160/206] build(deps): bump github.com/jackc/pgx/v5 from 5.9.0 to 5.9.2 (#7668) Bumps [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) from 5.9.0 to 5.9.2. - [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md) - [Commits](https://github.com/jackc/pgx/compare/v5.9.0...v5.9.2) --- updated-dependencies: - dependency-name: github.com/jackc/pgx/v5 dependency-version: 5.9.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 93b73f3f6..b00a03c2b 100644 --- a/go.mod +++ b/go.mod @@ -72,7 +72,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.18.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect - github.com/jackc/pgx/v5 v5.9.0 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index 50fb903b3..5cae77fe0 100644 --- a/go.sum +++ b/go.sum @@ -205,8 +205,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.9.0 h1:T/dI+2TvmI2H8s/KH1/lXIbz1CUFk3gn5oTjr0/mBsE= -github.com/jackc/pgx/v5 v5.9.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= From 2d332714829aa3b530424d47ae092074b09a2268 Mon Sep 17 00:00:00 2001 From: Amemoyoi Date: Mon, 27 Apr 2026 23:43:39 +0900 Subject: [PATCH 161/206] admin: require path segment boundary in remote access control (#7673) --- admin.go | 19 ++++++++-- admin_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/admin.go b/admin.go index 766d8506a..fcf8ab6cb 100644 --- a/admin.go +++ b/admin.go @@ -212,8 +212,8 @@ type AdminAccess struct { // AdminPermissions specifies what kinds of requests are allowed // to be made to the admin endpoint. type AdminPermissions struct { - // The API paths allowed. Paths are simple prefix matches. - // Any subpath of the specified paths will be allowed. + // The API paths allowed. A request path must either equal an + // allowed path or be a subpath with a path-segment boundary. Paths []string `json:"paths,omitempty"` // The HTTP methods allowed for the given paths. @@ -718,7 +718,7 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error { // verify path pathFound := accessPerm.Paths == nil for _, allowedPath := range accessPerm.Paths { - if strings.HasPrefix(r.URL.Path, allowedPath) { + if adminPathAllowed(r.URL.Path, allowedPath) { pathFound = true break } @@ -747,6 +747,19 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error { } } +func adminPathAllowed(reqPath, allowedPath string) bool { + if allowedPath == "" || allowedPath == "/" { + return strings.HasPrefix(reqPath, allowedPath) + } + if reqPath == allowedPath { + return true + } + if strings.HasSuffix(allowedPath, "/") { + return strings.HasPrefix(reqPath, allowedPath) + } + return strings.HasPrefix(reqPath, allowedPath+"/") +} + func stopAdminServer(srv *http.Server) error { if srv == nil { return fmt.Errorf("no admin server") diff --git a/admin_test.go b/admin_test.go index 3801c301a..db6d6c45a 100644 --- a/admin_test.go +++ b/admin_test.go @@ -16,8 +16,11 @@ package caddy import ( "context" + "crypto" + "crypto/tls" "crypto/x509" "encoding/json" + "errors" "fmt" "maps" "net/http" @@ -53,6 +56,13 @@ var testCfg = []byte(`{ } `) +type testAdminPublicKey string + +func (k testAdminPublicKey) Equal(x crypto.PublicKey) bool { + other, ok := x.(testAdminPublicKey) + return ok && k == other +} + func TestUnsyncedConfigAccess(t *testing.T) { // each test is performed in sequence, so // each change builds on the previous ones; @@ -651,6 +661,99 @@ func TestAllowedOriginsUnixSocket(t *testing.T) { } } +func TestRemoteAdminAccessControlPathSegmentMatching(t *testing.T) { + const authorizedKey testAdminPublicKey = "authorized" + peerCert := &x509.Certificate{PublicKey: authorizedKey} + + tests := []struct { + name string + allowedPath string + requestPath string + wantErr bool + }{ + { + name: "exact path", + allowedPath: "/pki/ca/prod", + requestPath: "/pki/ca/prod", + wantErr: false, + }, + { + name: "subpath", + allowedPath: "/pki/ca/prod", + requestPath: "/pki/ca/prod/certificates", + wantErr: false, + }, + { + name: "trailing slash subpath", + allowedPath: "/pki/ca/prod/", + requestPath: "/pki/ca/prod/certificates", + wantErr: false, + }, + { + name: "sibling with shared prefix", + allowedPath: "/pki/ca/prod", + requestPath: "/pki/ca/prod-backup", + wantErr: true, + }, + { + name: "same segment plus digit", + allowedPath: "/pki/ca/prod", + requestPath: "/pki/ca/prod1", + wantErr: true, + }, + { + name: "root path", + allowedPath: "/", + requestPath: "/pki/ca/prod", + wantErr: false, + }, + } + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + remote := RemoteAdmin{ + AccessControl: []*AdminAccess{ + { + Permissions: []AdminPermissions{ + { + Methods: []string{http.MethodGet}, + Paths: []string{test.allowedPath}, + }, + }, + publicKeys: []crypto.PublicKey{authorizedKey}, + }, + }, + } + + req := httptest.NewRequest(http.MethodGet, "https://localhost:2021"+test.requestPath, nil) + req.TLS = &tls.ConnectionState{ + VerifiedChains: [][]*x509.Certificate{{peerCert}}, + } + + err := remote.enforceAccessControls(req) + if test.wantErr { + if err == nil { + t.Errorf("test %d (%s): allowed path %q, request path %q: expected forbidden error, got nil", i, test.name, test.allowedPath, test.requestPath) + return + } + var apiErr APIError + if !errors.As(err, &apiErr) { + t.Errorf("test %d (%s): allowed path %q, request path %q: expected APIError with HTTP status %d, got %T: %v", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, err, err) + return + } + if apiErr.HTTPStatus != http.StatusForbidden { + t.Errorf("test %d (%s): allowed path %q, request path %q: expected HTTP status %d, got %d", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, apiErr.HTTPStatus) + } + return + } + + if err != nil { + t.Errorf("test %d (%s): allowed path %q, request path %q: expected no error, got %v", i, test.name, test.allowedPath, test.requestPath, err) + } + }) + } +} + func TestReplaceRemoteAdminServer(t *testing.T) { const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw From 4d6945769d205f2a60acf13eefb5af0a2eb428fc Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Tue, 28 Apr 2026 09:16:18 -0600 Subject: [PATCH 162/206] reverseproxy: Add ability to clear dynamic upstreams cache during retries (#7662) * reverseproxy: Add ability to clear dynamic upstreams cache during retries This is an optional interface for dynamic upstream modules to implement if they cache results. TODO: More documentation; this is an experiment. * Add some godoc * Export interface; update godoc --- .../caddyhttp/reverseproxy/reverseproxy.go | 29 +++++++++++++++++++ modules/caddyhttp/reverseproxy/upstreams.go | 21 +++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 52d2b1ab3..cefe645ee 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -574,6 +574,17 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h // get the updated list of upstreams upstreams := h.Upstreams if h.DynamicUpstreams != nil { + if retries > 0 { + // after a failure (and thus during a retry), give dynamic upstream modules an opportunity + // to purge their relevant cache entries so we don't keep retrying bad upstreams + if cachingDynamicUpstreams, ok := h.DynamicUpstreams.(CachingUpstreamSource); ok { + if err := cachingDynamicUpstreams.ResetCache(r); err != nil { + if c := h.logger.Check(zapcore.ErrorLevel, "failed clearing dynamic upstream source's cache"); c != nil { + c.Write(zap.Error(err)) + } + } + } + } dUpstreams, err := h.DynamicUpstreams.GetUpstreams(r) if err != nil { if c := h.logger.Check(zapcore.ErrorLevel, "failed getting dynamic upstreams; falling back to static upstreams"); c != nil { @@ -1640,10 +1651,28 @@ type Selector interface { // may be called during each retry, multiple times per request, and as // such, needs to be instantaneous. The returned slice will not be // modified. +// +// For upstream sources that cache results, implement the +// [CachingUpstreamSource] interface for optimal performance. type UpstreamSource interface { GetUpstreams(*http.Request) ([]*Upstream, error) } +// CachingUpstreamSource is an upstream source that caches its upstreams. +// The relevant cache entry can be cleared/reset for a given request during +// retries if a request fails. This can help ensure that failing backends +// are not retried. +// +// EXPERIMENTAL: Subject to change. +type CachingUpstreamSource interface { + UpstreamSource + + // ResetCache clears any cache entry related to the given request. + // The next time GetUpstreams is called, it should have new upstream + // information for the given request. + ResetCache(*http.Request) error +} + // Hop-by-hop headers. These are removed when sent to the backend. // As of RFC 7230, hop-by-hop headers are required to appear in the // Connection header field. These are the headers defined by the diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go index e9120725a..f7077ce78 100644 --- a/modules/caddyhttp/reverseproxy/upstreams.go +++ b/modules/caddyhttp/reverseproxy/upstreams.go @@ -119,6 +119,18 @@ func (su *SRVUpstreams) Provision(ctx caddy.Context) error { return nil } +func (su *SRVUpstreams) ResetCache(r *http.Request) error { + srvsMu.Lock() + if r == nil { + srvs = make(map[string]srvLookup) + } else { + suAddr, _, _, _ := su.expandedAddr(r) + delete(srvs, suAddr) + } + srvsMu.Unlock() + return nil +} + func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { suAddr, service, proto, name := su.expandedAddr(r) @@ -554,8 +566,9 @@ var ( // Interface guards var ( - _ caddy.Provisioner = (*SRVUpstreams)(nil) - _ UpstreamSource = (*SRVUpstreams)(nil) - _ caddy.Provisioner = (*AUpstreams)(nil) - _ UpstreamSource = (*AUpstreams)(nil) + _ caddy.Provisioner = (*SRVUpstreams)(nil) + _ UpstreamSource = (*SRVUpstreams)(nil) + _ CachingUpstreamSource = (*SRVUpstreams)(nil) + _ caddy.Provisioner = (*AUpstreams)(nil) + _ UpstreamSource = (*AUpstreams)(nil) ) From 6a64bb2ce55b9a0aaf89f0372a7381aefdebd530 Mon Sep 17 00:00:00 2001 From: mfrischknecht Date: Wed, 29 Apr 2026 13:52:04 +0200 Subject: [PATCH 163/206] listeners: clean up stale Unix socket files on Windows (#7676) * Delete old unix domain socket files on Windows While Windows doesn't have the need to reuse a socket file descriptor by dup()ing it on config reloads, there still is a valid need for an equivalent to the `syscall.Unlink()` call in listen_unix.go (also in `reuseUnixSocket`). If a previous Caddy instance didn't terminate properly, the chances it will leave behind a socket file are very high, breaking all subsequent starting attempts. Other than for regular files, Windows seemingly has no way for a process to flag a UNIX domain socket file with `FILE_DELETE_ON_CLOSE`, which means this scenario can never be avoided entirely (e.g. in the case of crashes). For the long comment on `isAbstractUnixSocket`: the logic itself is likely of dubious value, but I thought it better to explicitly reference the issue, as I have just spent half an hour searching the web to figure out whether abstract names will work or not on Windows. At least, the logic as-is should now do the sensible thing if these are ever implemented properly (and it matches what the Golang standard library does internally). * Add a dial attempt to check for active server processes As @steadytao pointed out (thanks!), the previous code didn't have solid proof that an existing unix socket file had really been orphaned, as it's also possible that there's another server process (still running). This would still give the Windows implementation parity with the unix one (as that one also unlinks the socket file without further checks), but I've performed a couple of small tests and found this way of handling socket files still problematic at least problematic if Caddy is used as a reverse proxy in real world scenarios. In tests with a simple Caddyfile that only declares an admin socket, starting two caddy instances with the same Caddyfile works and behaves like one would expect: the second instance removes the first instance's socket file and "wins" the race. When Caddy is used as a reverse proxy, though, what'll happen is more complicated: While the second instance wins the race for the admin socket, as long as the Caddyfile specifies a TCP downstream socket, the second process will not be able to take this one over from the first (also to be expected, that's how socket binding usually works). This results in a rather broken state: The first process still holds on to its TCP listening sockets, the second process fails to start because of the error in its listening attempt, leaving an orphaned admin socket file in the file system. Afterwards, the second process won't be running and the first _will_ be running but unable to be controlled because its admin socket has been replaced. This leaves the system in another state that is bad from an ops perspective. With this new change, we try first to connect to any unix socket that isn't already covered by our current process (with a very low timeout) and can easily decide if the socket is still in use by another process: - If the connection is accepted, there's obviously a server process. - If Windows returns WSACONNREFUSED [^1], there is either no active server process for the socket file anymore, or the socket file does not exist. - Any other errors are likely a sign that there still is a server process (e.g. a timeout would indicate that it's just slow in accepting new connection attempts). [^1]: https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2#wsaeconnrefused * chore: tidy Windows unix socket reuse helper --------- Co-authored-by: Zen Dodd --- listen.go | 4 -- listen_reuseUnixSocket.go | 21 ++++++++ listen_reuseUnixSocket_windows.go | 89 +++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 listen_reuseUnixSocket.go create mode 100644 listen_reuseUnixSocket_windows.go diff --git a/listen.go b/listen.go index 03b63c1e2..21df13ff4 100644 --- a/listen.go +++ b/listen.go @@ -30,10 +30,6 @@ import ( "go.uber.org/zap" ) -func reuseUnixSocket(_, _ string) (any, error) { - return nil, nil -} - func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) { var socketFile *os.File diff --git a/listen_reuseUnixSocket.go b/listen_reuseUnixSocket.go new file mode 100644 index 000000000..006610edc --- /dev/null +++ b/listen_reuseUnixSocket.go @@ -0,0 +1,21 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build (!unix || solaris) && !windows + +package caddy + +func reuseUnixSocket(_, _ string) (any, error) { + return nil, nil +} diff --git a/listen_reuseUnixSocket_windows.go b/listen_reuseUnixSocket_windows.go new file mode 100644 index 000000000..9c547933e --- /dev/null +++ b/listen_reuseUnixSocket_windows.go @@ -0,0 +1,89 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +package caddy + +import ( + "errors" + "fmt" + "io/fs" + "net" + "os" + "strings" + "syscall" + "time" +) + +var errUnixSocketAlreadyInUse = errors.New("unix socket is already in use by another process") + +func reuseUnixSocket(network, addr string) (any, error) { + if !IsUnixNetwork(network) { + return nil, nil + } + + // Note: This is here mainly for proper compatibility, because Unix sockets with abstract names are in an interesting limbo state on Windows: + // Go already translates `@` characters to `\0` for Windows: https://github.com/golang/go/blob/65d5c5f6dd8aa7b221cff6ec3f5101ea2e5f3efa/src/syscall/syscall_windows.go#L910 + // ...but there still is an open issue about the fact that this is not properly supported: https://github.com/microsoft/WSL/issues/4240#issuecomment-620805115 + // The main issue is that the original announcement proclaimed support for this feature, but it was (apparently) never implemented: https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ + isAbstractUnixSocket := strings.HasPrefix(addr, "@") + + if isAbstractUnixSocket { + // Abstract Unix sockets do not require us to remove stale socket files. + return nil, nil + } + + // On Windows, we're using the `fakeCloseListener` wrappers around a single, ever-living listener. + // So, if there's an active listener entry in the pool, we're the current owner of the Unix socket file. + _, socketBelongsToCurrentProcess := listenerPool.References(listenerKey(network, addr)) + + if socketBelongsToCurrentProcess { + // Reuse/cleanup is entirely handled by the refcounting mechanism in `listenerPool`. + return nil, nil + } + + // If the socket file does not exist or has no backing server process, this will fail instantly. + connection, err := net.DialTimeout("unix", addr, 10*time.Millisecond) + + if err == nil { + connection.Close() + return nil, fmt.Errorf("cannot reuse socket %v: %w", addr, errUnixSocketAlreadyInUse) + } + + // Windows returns this error code both if the socket file does not exist and if it isn't backed by a server process anymore. + // See: https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2#wsaeconnrefused + const WSAECONNREFUSED syscall.Errno = 10061 + + var errno syscall.Errno + hasNoListeningServerProcess := errors.As(err, &errno) && errno == WSAECONNREFUSED + + if !hasNoListeningServerProcess { + return nil, fmt.Errorf("cannot reuse socket %v: %w", addr, errUnixSocketAlreadyInUse) + } + + // If the socket file exists, it hasn't been created by our process, and it seemingly + // isn't backed by a server process anymore. Try to delete it so we can bind to it later. + err = os.Remove(addr) + + if err == nil { + return nil, nil + } else if errors.Is(err, fs.ErrNotExist) { + // Either the file didn't exist in the first place, or it was deleted before we were able to. + return nil, nil + } else { + // We failed to delete the file. Likely, it belongs to another (active) process. + return nil, err + } +} From 18ab0f955fc1075d7727c7658dbfb734c673a5c9 Mon Sep 17 00:00:00 2001 From: Amemoyoi Date: Fri, 1 May 2026 00:39:57 +0900 Subject: [PATCH 164/206] admin: reject non-canonical config array indices (#7592) * admin: reject non-canonical config array indices * admin: expand canonical array index test coverage * Update admin.go Co-authored-by: Matt Holt * Update admin.go Co-authored-by: Matt Holt * admin: improve canonical array index test diagnostics --------- Co-authored-by: Matt Holt --- admin.go | 19 +++++++++++++++++-- admin_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/admin.go b/admin.go index fcf8ab6cb..08465a923 100644 --- a/admin.go +++ b/admin.go @@ -1161,6 +1161,20 @@ func handleStop(w http.ResponseWriter, r *http.Request) error { return nil } +func parseCanonicalArrayIndex(idx string) (int, error) { + if idx == "" { + return 0, fmt.Errorf("empty index") + } + i, err := strconv.Atoi(idx) + if err != nil { + return 0, err + } + if strconv.Itoa(i) != idx { + return 0, fmt.Errorf("non-canonical array index") + } + return i, nil +} + // unsyncedConfigAccess traverses into the current config and performs // the operation at path according to method, using body and out as // needed. This is a low-level, unsynchronized function; most callers @@ -1222,11 +1236,12 @@ traverseLoop: var idx int if method != http.MethodPost { idxStr := parts[len(parts)-1] - idx, err = strconv.Atoi(idxStr) + idx, err = parseCanonicalArrayIndex(idxStr) if err != nil { return fmt.Errorf("[%s] invalid array index '%s': %v", path, idxStr, err) } + if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) { return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr) } @@ -1326,7 +1341,7 @@ traverseLoop: } case []any: - partInt, err := strconv.Atoi(part) + partInt, err := parseCanonicalArrayIndex(part) if err != nil { return fmt.Errorf("[/%s] invalid array index '%s': %v", strings.Join(parts[:i+1], "/"), part, err) diff --git a/admin_test.go b/admin_test.go index db6d6c45a..19cc6ae7c 100644 --- a/admin_test.go +++ b/admin_test.go @@ -15,6 +15,7 @@ package caddy import ( + "bytes" "context" "crypto" "crypto/tls" @@ -1106,3 +1107,47 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP }) } } + +func TestUnsyncedConfigAccessCanonicalArrayIndices(t *testing.T) { + rawCfg = map[string]any{ + rawConfigKey: map[string]any{ + "list": []any{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"}, + }, + } + + tests := []struct { + name string + path string + wantOutput string + wantErr bool + }{ + {name: "allow zero", path: "/" + rawConfigKey + "/list/0", wantOutput: "\"zero\"\n"}, + {name: "allow one", path: "/" + rawConfigKey + "/list/1", wantOutput: "\"one\"\n"}, + {name: "allow ten", path: "/" + rawConfigKey + "/list/10", wantOutput: "\"ten\"\n"}, + {name: "reject leading zero", path: "/" + rawConfigKey + "/list/01", wantErr: true}, + {name: "reject multiple leading zeros", path: "/" + rawConfigKey + "/list/002", wantErr: true}, + {name: "reject plus sign", path: "/" + rawConfigKey + "/list/+1", wantErr: true}, + {name: "reject negative zero", path: "/" + rawConfigKey + "/list/-0", wantErr: true}, + } + + for i, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var gotOutput bytes.Buffer + err := unsyncedConfigAccess(http.MethodGet, tc.path, nil, &gotOutput) + + if tc.wantErr { + if err == nil { + t.Errorf("test %d (%s): input path %q: expected error, got nil with output %q", i, tc.name, tc.path, gotOutput.String()) + } + return + } + + if err != nil { + t.Errorf("test %d (%s): input path %q: expected no error with output %q, got error %v with output %q", i, tc.name, tc.path, tc.wantOutput, err, gotOutput.String()) + } + if gotOutput.String() != tc.wantOutput { + t.Errorf("test %d (%s): input path %q: expected output %q, got %q", i, tc.name, tc.path, tc.wantOutput, gotOutput.String()) + } + }) + } +} From ef496e58ef9e7844cf8f1831030713ee5e9b354b Mon Sep 17 00:00:00 2001 From: Felix Eckhofer Date: Sat, 2 May 2026 23:13:57 +0200 Subject: [PATCH 165/206] caddytls: Expand ACME credentials (#7554) * caddytls: Expand ACME credentials This allows using global placeholders such as {file./run/secrets/key_id} when setting up the tls configuration. * chore(formatting): gofmt on acmeissuer_test --- modules/caddytls/acmeissuer.go | 36 ++++++++++++++++++++++++ modules/caddytls/acmeissuer_test.go | 43 +++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 modules/caddytls/acmeissuer_test.go diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index b511346b5..193c8bfc7 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -140,6 +140,42 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { iss.Email = email } + // expand CA endpoint, if non-empty + if iss.CA != "" { + ca, err := repl.ReplaceOrErr(iss.CA, true, true) + if err != nil { + return fmt.Errorf("expanding CA endpoint '%s': %v", iss.CA, err) + } + iss.CA = ca + } + + // expand TestCA endpoint, if non-empty + if iss.TestCA != "" { + testca, err := repl.ReplaceOrErr(iss.TestCA, true, true) + if err != nil { + return fmt.Errorf("expanding TestCA endpoint '%s': %v", iss.TestCA, err) + } + iss.TestCA = testca + } + + // expand EAB credentials, if non-empty + if iss.ExternalAccount != nil { + if iss.ExternalAccount.KeyID != "" { + keyID, err := repl.ReplaceOrErr(iss.ExternalAccount.KeyID, true, true) + if err != nil { + return fmt.Errorf("expanding EAB key ID '%s': %v", iss.ExternalAccount.KeyID, err) + } + iss.ExternalAccount.KeyID = keyID + } + if iss.ExternalAccount.MACKey != "" { + macKey, err := repl.ReplaceOrErr(iss.ExternalAccount.MACKey, true, true) + if err != nil { + return fmt.Errorf("expanding EAB MAC key (redacted): %v", err) + } + iss.ExternalAccount.MACKey = macKey + } + } + // expand account key, if non-empty if iss.AccountKey != "" { accountKey, err := repl.ReplaceOrErr(iss.AccountKey, true, true) diff --git a/modules/caddytls/acmeissuer_test.go b/modules/caddytls/acmeissuer_test.go new file mode 100644 index 000000000..661f7b9e5 --- /dev/null +++ b/modules/caddytls/acmeissuer_test.go @@ -0,0 +1,43 @@ +package caddytls + +import ( + "github.com/caddyserver/caddy/v2" + "github.com/mholt/acmez/v3/acme" + "testing" +) + +func TestACMEIssuerExpandPlaceholders(t *testing.T) { + t.Setenv("CADDY_TEST_CA_URL", "https://acme.example.com/directory") + t.Setenv("CADDY_TEST_TEST_CA_URL", "https://acme2.example.com/directory") + t.Setenv("CADDY_TEST_EAB_KEY_ID", "example-key-id") + t.Setenv("CADDY_TEST_EAB_MAC_KEY", "example-mac-key") + + caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()}) + defer cancel() + + iss := &ACMEIssuer{ + CA: "{env.CADDY_TEST_CA_URL}", + TestCA: "{env.CADDY_TEST_TEST_CA_URL}", + ExternalAccount: &acme.EAB{ + KeyID: "{env.CADDY_TEST_EAB_KEY_ID}", + MACKey: "{env.CADDY_TEST_EAB_MAC_KEY}", + }, + } + + if err := iss.Provision(caddyCtx); err != nil { + t.Fatalf("Provision() returned unexpected error: %v", err) + } + + if want := "https://acme.example.com/directory"; iss.CA != want { + t.Errorf("CA: got %q, want %q", iss.CA, want) + } + if want := "https://acme2.example.com/directory"; iss.TestCA != want { + t.Errorf("TestCA: got %q, want %q", iss.TestCA, want) + } + if want := "example-key-id"; iss.ExternalAccount.KeyID != want { + t.Errorf("ExternalAccount.KeyID: got %q, want %q", iss.ExternalAccount.KeyID, want) + } + if want := "example-mac-key"; iss.ExternalAccount.MACKey != want { + t.Errorf("ExternalAccount.MACKey: got %q, want %q", iss.ExternalAccount.MACKey, want) + } +} From 7e77eec0ae824d86f71f410acd78a0f938baeda7 Mon Sep 17 00:00:00 2001 From: Rayan Salhab Date: Sun, 3 May 2026 06:40:11 +0300 Subject: [PATCH 166/206] caddyauth: set user placeholders before auth rejection (#7685) * caddyauth: set user placeholders before auth rejection * docs: update auth placeholder comment --- modules/caddyhttp/caddyauth/caddyauth.go | 15 ++-- modules/caddyhttp/caddyauth/caddyauth_test.go | 82 +++++++++++++++++++ 2 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 modules/caddyhttp/caddyauth/caddyauth_test.go diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go index 792c198ee..de8e13f89 100644 --- a/modules/caddyhttp/caddyauth/caddyauth.go +++ b/modules/caddyhttp/caddyauth/caddyauth.go @@ -32,10 +32,10 @@ func init() { // Authentication is a middleware which provides user authentication. // Rejects requests with HTTP 401 if the request is not authenticated. // -// After a successful authentication, the placeholder +// When an authentication provider returns user information, the placeholder // `{http.auth.user.id}` will be set to the username, and also // `{http.auth.user.*}` placeholders may be set for any authentication -// modules that provide user metadata. +// modules that provide user metadata, even if authentication is rejected. // // In case of an error, the placeholder `{http.auth..error}` // will be set to the error message returned by the authentication @@ -91,6 +91,12 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c repl.Set("http.auth."+provName+".error", err.Error()) continue } + if authed || user.ID != "" || len(user.Metadata) > 0 { + repl.Set("http.auth.user.id", user.ID) + for k, v := range user.Metadata { + repl.Set("http.auth.user."+k, v) + } + } if authed { break } @@ -99,11 +105,6 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")) } - repl.Set("http.auth.user.id", user.ID) - for k, v := range user.Metadata { - repl.Set("http.auth.user."+k, v) - } - return next.ServeHTTP(w, r) } diff --git a/modules/caddyhttp/caddyauth/caddyauth_test.go b/modules/caddyhttp/caddyauth/caddyauth_test.go new file mode 100644 index 000000000..1f7211137 --- /dev/null +++ b/modules/caddyhttp/caddyauth/caddyauth_test.go @@ -0,0 +1,82 @@ +// 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 ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func TestAuthenticationSetsUserPlaceholdersOnUnauthorized(t *testing.T) { + repl := caddy.NewReplacer() + req := httptest.NewRequest(http.MethodGet, "/", nil) + req = req.WithContext(context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)) + + a := Authentication{ + Providers: map[string]Authenticator{ + "test": staticAuthenticator{ + user: User{ + ID: "alice", + Metadata: map[string]string{ + "role": "admin", + }, + }, + }, + }, + } + + nextCalled := false + err := a.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { + nextCalled = true + return nil + })) + if err == nil { + t.Fatal("expected unauthorized error") + } + + var handlerErr caddyhttp.HandlerError + if !errors.As(err, &handlerErr) { + t.Fatalf("expected caddyhttp.HandlerError, got %T", err) + } + if handlerErr.StatusCode != http.StatusUnauthorized { + t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, handlerErr.StatusCode) + } + if nextCalled { + t.Fatal("next handler was called") + } + + if got, ok := repl.GetString("http.auth.user.id"); !ok || got != "alice" { + t.Fatalf("expected http.auth.user.id to be alice, got %q (ok=%v)", got, ok) + } + if got, ok := repl.GetString("http.auth.user.role"); !ok || got != "admin" { + t.Fatalf("expected http.auth.user.role to be admin, got %q (ok=%v)", got, ok) + } +} + +type staticAuthenticator struct { + user User + authed bool + err error +} + +func (s staticAuthenticator) Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) { + return s.user, s.authed, s.err +} From c7c9f3108a4200a8099ae41175b8aa356b14109f Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Wed, 6 May 2026 01:12:46 +1000 Subject: [PATCH 167/206] caddyauth: Revert "set user placeholders before auth rejection (#7685)" (#7688) This reverts commit 7e77eec0ae824d86f71f410acd78a0f938baeda7. --- modules/caddyhttp/caddyauth/caddyauth.go | 15 ++-- modules/caddyhttp/caddyauth/caddyauth_test.go | 82 ------------------- 2 files changed, 7 insertions(+), 90 deletions(-) delete mode 100644 modules/caddyhttp/caddyauth/caddyauth_test.go diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go index de8e13f89..792c198ee 100644 --- a/modules/caddyhttp/caddyauth/caddyauth.go +++ b/modules/caddyhttp/caddyauth/caddyauth.go @@ -32,10 +32,10 @@ func init() { // Authentication is a middleware which provides user authentication. // Rejects requests with HTTP 401 if the request is not authenticated. // -// When an authentication provider returns user information, the placeholder +// After a successful authentication, the placeholder // `{http.auth.user.id}` will be set to the username, and also // `{http.auth.user.*}` placeholders may be set for any authentication -// modules that provide user metadata, even if authentication is rejected. +// modules that provide user metadata. // // In case of an error, the placeholder `{http.auth..error}` // will be set to the error message returned by the authentication @@ -91,12 +91,6 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c repl.Set("http.auth."+provName+".error", err.Error()) continue } - if authed || user.ID != "" || len(user.Metadata) > 0 { - repl.Set("http.auth.user.id", user.ID) - for k, v := range user.Metadata { - repl.Set("http.auth.user."+k, v) - } - } if authed { break } @@ -105,6 +99,11 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")) } + repl.Set("http.auth.user.id", user.ID) + for k, v := range user.Metadata { + repl.Set("http.auth.user."+k, v) + } + return next.ServeHTTP(w, r) } diff --git a/modules/caddyhttp/caddyauth/caddyauth_test.go b/modules/caddyhttp/caddyauth/caddyauth_test.go deleted file mode 100644 index 1f7211137..000000000 --- a/modules/caddyhttp/caddyauth/caddyauth_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyauth - -import ( - "context" - "errors" - "net/http" - "net/http/httptest" - "testing" - - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/modules/caddyhttp" -) - -func TestAuthenticationSetsUserPlaceholdersOnUnauthorized(t *testing.T) { - repl := caddy.NewReplacer() - req := httptest.NewRequest(http.MethodGet, "/", nil) - req = req.WithContext(context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)) - - a := Authentication{ - Providers: map[string]Authenticator{ - "test": staticAuthenticator{ - user: User{ - ID: "alice", - Metadata: map[string]string{ - "role": "admin", - }, - }, - }, - }, - } - - nextCalled := false - err := a.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { - nextCalled = true - return nil - })) - if err == nil { - t.Fatal("expected unauthorized error") - } - - var handlerErr caddyhttp.HandlerError - if !errors.As(err, &handlerErr) { - t.Fatalf("expected caddyhttp.HandlerError, got %T", err) - } - if handlerErr.StatusCode != http.StatusUnauthorized { - t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, handlerErr.StatusCode) - } - if nextCalled { - t.Fatal("next handler was called") - } - - if got, ok := repl.GetString("http.auth.user.id"); !ok || got != "alice" { - t.Fatalf("expected http.auth.user.id to be alice, got %q (ok=%v)", got, ok) - } - if got, ok := repl.GetString("http.auth.user.role"); !ok || got != "admin" { - t.Fatalf("expected http.auth.user.role to be admin, got %q (ok=%v)", got, ok) - } -} - -type staticAuthenticator struct { - user User - authed bool - err error -} - -func (s staticAuthenticator) Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) { - return s.user, s.authed, s.err -} From d2172bea61414635c55554b42714af94a3c9cefd Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 7 May 2026 17:40:26 +1000 Subject: [PATCH 168/206] chore: Fix golangci-lint 2.12.1 findings (#7690) --- cmd/packagesfuncs.go | 2 +- context.go | 2 +- modules/caddyhttp/fileserver/browse.go | 16 ++++++++++++++-- modules/caddyhttp/fileserver/staticfiles.go | 2 +- .../caddyhttp/reverseproxy/selectionpolicies.go | 10 ++++++---- modules/caddyhttp/routes.go | 9 +++++---- modules/caddyhttp/server.go | 6 +++--- 7 files changed, 31 insertions(+), 16 deletions(-) diff --git a/cmd/packagesfuncs.go b/cmd/packagesfuncs.go index 4d0ff0680..a26919922 100644 --- a/cmd/packagesfuncs.go +++ b/cmd/packagesfuncs.go @@ -234,7 +234,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) { // not sure why), and since New() should return a pointer // value, we need to dereference it first iface := any(modInfo.New()) - if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr { + if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Pointer { iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface() } modPkgPath := reflect.TypeOf(iface).PkgPath() diff --git a/context.go b/context.go index 980027275..f71d635e2 100644 --- a/context.go +++ b/context.go @@ -378,7 +378,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error // value must be a pointer for unmarshaling into concrete type, even if // the module's concrete type is a slice or map; New() *should* return // a pointer, otherwise unmarshaling errors or panics will occur - if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr { + if rv := reflect.ValueOf(val); rv.Kind() != reflect.Pointer { log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+ " so we are using reflection to make a pointer instead; please fix this by"+ " using new(Type) or &Type notation in your module's New() function.", id) diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go index 304417009..3b97f2ff3 100644 --- a/modules/caddyhttp/fileserver/browse.go +++ b/modules/caddyhttp/fileserver/browse.go @@ -281,7 +281,13 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re sortParam = sortCookie.Value } case sortByName, sortByNameDirFirst, sortBySize, sortByTime: - http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil}) + http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS + Name: "sort", + Value: sortParam, + Secure: r.TLS != nil, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) } // then figure out the order @@ -292,7 +298,13 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re orderParam = orderCookie.Value } case sortOrderAsc, sortOrderDesc: - http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil}) + http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS + Name: "order", + Value: orderParam, + Secure: r.TLS != nil, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) } // finally, apply the sorting and limiting diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index dce40302d..507321ad6 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -785,7 +785,7 @@ func redirect(w http.ResponseWriter, r *http.Request, toPath string) error { if r.URL.RawQuery != "" { toPath += "?" + r.URL.RawQuery } - http.Redirect(w, r, toPath, http.StatusPermanentRedirect) + http.Redirect(w, r, toPath, http.StatusPermanentRedirect) //nolint:gosec // toPath is a same-origin path and leading // is stripped above return nil } diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index 050a4f671..648edcf76 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -664,10 +664,12 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http return upstream } cookie := &http.Cookie{ - Name: s.Name, - Value: sha, - Path: "/", - Secure: false, + Name: s.Name, + Value: sha, + Path: "/", + Secure: false, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, } isProxyHttps := false if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted { diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index ce2287488..7cc6dd79d 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "net/http" + "slices" "strings" "github.com/caddyserver/caddy/v2" @@ -241,8 +242,8 @@ func (routes RouteList) Compile(next Handler) Handler { mid = append(mid, wrapRoute(route)) } stack := next - for i := len(mid) - 1; i >= 0; i-- { - stack = mid[i](stack) + for _, middleware := range slices.Backward(mid) { + stack = middleware(stack) } return stack } @@ -305,8 +306,8 @@ func wrapRoute(route Route) Middleware { } // compile this route's handler stack - for i := len(route.middleware) - 1; i >= 0; i-- { - nextCopy = route.middleware[i](nextCopy) + for _, middleware := range slices.Backward(route.middleware) { + nextCopy = middleware(nextCopy) } // Apply metrics instrumentation once for the entire route, diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 3005bc273..9aca53578 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -1085,11 +1085,11 @@ func strictUntrustedClientIp(r *http.Request, headers []string, trusted []netip. for _, headerName := range headers { parts := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",") - for i := len(parts) - 1; i >= 0; i-- { + for _, part := range slices.Backward(parts) { // Some proxies may retain the port number, so split if possible - host, _, err := net.SplitHostPort(parts[i]) + host, _, err := net.SplitHostPort(part) if err != nil { - host = parts[i] + host = part } // Remove any zone identifier from the IP address From 0780d4489cc7199b87598b15aad3270e851ac138 Mon Sep 17 00:00:00 2001 From: tomholford <16504501+tomholford@users.noreply.github.com> Date: Thu, 7 May 2026 11:32:20 -0700 Subject: [PATCH 169/206] httpcaddyfile: accept duration strings for log sampling interval (#7694) Co-authored-by: tomholford --- caddyconfig/httpcaddyfile/builtins.go | 2 +- caddyconfig/httpcaddyfile/builtins_test.go | 4 ++-- .../caddyfile_adapt/global_options_log_sampling.caddyfiletest | 4 ++-- .../integration/caddyfile_adapt/log_sampling.caddyfiletest | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 311a29e02..da231fbd9 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -1053,7 +1053,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue if !d.NextArg() { return nil, d.ArgErr() } - interval, err := time.ParseDuration(d.Val() + "ns") + interval, err := caddy.ParseDuration(d.Val()) if err != nil { return nil, d.Errf("failed to parse interval: %v", err) } diff --git a/caddyconfig/httpcaddyfile/builtins_test.go b/caddyconfig/httpcaddyfile/builtins_test.go index c23531f22..9cff29039 100644 --- a/caddyconfig/httpcaddyfile/builtins_test.go +++ b/caddyconfig/httpcaddyfile/builtins_test.go @@ -66,14 +66,14 @@ func TestLogDirectiveSyntax(t *testing.T) { input: `:8080 { log { sampling { - interval 2 + interval 2s first 3 thereafter 4 } } } `, - output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, + output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2000000000,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, expectError: false, }, } { diff --git a/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest index 12b73b2b7..caa755a02 100644 --- a/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest @@ -1,7 +1,7 @@ { log { sampling { - interval 300 + interval 5m first 50 thereafter 40 } @@ -13,7 +13,7 @@ "logs": { "default": { "sampling": { - "interval": 300, + "interval": 300000000000, "first": 50, "thereafter": 40 } diff --git a/caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest index b58622572..fcda093a6 100644 --- a/caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest @@ -1,7 +1,7 @@ :80 { log { sampling { - interval 300 + interval 5m first 50 thereafter 40 } @@ -18,7 +18,7 @@ }, "log0": { "sampling": { - "interval": 300, + "interval": 300000000000, "first": 50, "thereafter": 40 }, From fb324331f40782ac7a48d83f591c2bb7615d7eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 7 May 2026 21:59:42 +0200 Subject: [PATCH 170/206] Merge commit from fork Both fallbacks in splitPos relied on golang.org/x/text/search with search.IgnoreCase, which performs Unicode equivalence matching far beyond ASCII case folding. Combined with the validated-ASCII guarantee on every SplitPath entry, that fallback turned non-PHP filenames into PHP scripts: - when the inner loop hit a non-ASCII byte and the IndexString fallback returned -1, the loop broke without resetting match=false, so a stale match=true caused a non-existent .php to be reported (PoC: "/name..txt"). - search.IgnoreCase folded fullwidth, mathematical and circled letters onto ASCII, so "/shell.", "/shell.hp", "/shell." were all detected as ".php" files. Replace the fallback with strict byte-level ASCII case-insensitive matching: any byte >= utf8.RuneSelf in the path can never be part of a match, since SplitPath entries are validated ASCII-only and lower-cased in Provision(). This keeps the hot path branch-light and removes the x/text/search dependency from the main module. Reported against FrankenPHP as GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8. The vulnerable function in this module was adapted from the same FrankenPHP code. --- go.mod | 2 +- .../caddyhttp/reverseproxy/fastcgi/fastcgi.go | 36 +++----- .../reverseproxy/fastcgi/fastcgi_test.go | 87 +++++++++++++++++++ 3 files changed, 101 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index b00a03c2b..bd44d644d 100644 --- a/go.mod +++ b/go.mod @@ -170,7 +170,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/sys v0.43.0 - golang.org/x/text v0.36.0 + golang.org/x/text v0.36.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index c4279d9a0..3e0436062 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -28,8 +28,6 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" - "golang.org/x/text/language" - "golang.org/x/text/search" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" @@ -418,14 +416,19 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) { return env, nil } -var splitSearchNonASCII = search.New(language.Und, search.IgnoreCase) - // splitPos returns the index where path should // be split based on t.SplitPath. // // example: if splitPath is [".php"] // "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path") // +// Matching is strictly ASCII case-insensitive. Bytes >= utf8.RuneSelf in path +// never match any split entry: split strings are validated ASCII-only and +// lower-cased in Provision(), so any Unicode equivalence (e.g. fullwidth or +// mathematical letters folding to ASCII) would let an attacker upload a file +// whose name contains such code points and have it served as PHP. See +// FrankenPHP advisories GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8. +// // Adapted from FrankenPHP's code (copyright 2026 Kévin Dunglas, MIT license) func (t Transport) splitPos(path string) int { // TODO: from v1... @@ -438,31 +441,18 @@ func (t Transport) splitPos(path string) int { pathLen := len(path) - // We are sure that split strings are all ASCII-only and lower-case because of validation and normalization in Provision(). for _, split := range t.SplitPath { splitLen := len(split) + if splitLen == 0 || splitLen > pathLen { + continue + } - for i := range pathLen { - if path[i] >= utf8.RuneSelf { - if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 { - return end - } - - break - } - - if i+splitLen > pathLen { - continue - } - + for i := 0; i <= pathLen-splitLen; i++ { match := true - for j := range splitLen { + for j := 0; j < splitLen; j++ { c := path[i+j] - if c >= utf8.RuneSelf { - if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 { - return end - } + match = false break } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go index 7097ff790..4977ae998 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go @@ -191,6 +191,65 @@ func TestSplitPos(t *testing.T) { splitPath: []string{".php"}, wantPos: 9, }, + // Regression tests adapted from FrankenPHP advisories + // GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8: search.IgnoreCase + // matched Unicode equivalents of ASCII letters as ".php", and an + // inner non-ASCII byte path could leave the match flag stale. + { + name: "non-ascii byte after dot must not match", + path: "/PoC-match-unset.¡.txt", + splitPath: []string{".php"}, + wantPos: -1, + }, + { + name: "non-ascii byte mid-extension must not match", + path: "/script.p\xc2\xa1p", + splitPath: []string{".php"}, + wantPos: -1, + }, + { + name: "small full stop ﹒ in extension must not match", + path: "/shell﹒php", + splitPath: []string{".php"}, + wantPos: -1, + }, + { + name: "fullwidth full stop . in extension must not match", + path: "/shell.php", + splitPath: []string{".php"}, + wantPos: -1, + }, + { + name: "fullwidth p in extension must not match", + path: "/shell.php", + splitPath: []string{".php"}, + wantPos: -1, + }, + { + name: "circled php must not match", + path: "/shell.ⓟⓗⓟ", + splitPath: []string{".php"}, + wantPos: -1, + }, + { + name: "mathematical sans-serif bold php must not match", + path: "/shell.\U0001D5FD\U0001D5F5\U0001D5FD", + splitPath: []string{".php"}, + wantPos: -1, + }, + { + name: "mathematical script php must not match", + path: "/shell.\U0001D4C5\U0001D4BD\U0001D4C5", + splitPath: []string{".php"}, + wantPos: -1, + }, + { + name: "circled php with later real php still picks the real one", + path: "/shell.ⓟⓗⓟ.anything-after-payload.php", + splitPath: []string{".php"}, + // "/shell." (7) + "ⓟⓗⓟ" (3*3 bytes) + ".anything-after-payload.php" (27) = 43 + wantPos: 43, + }, } for _, tt := range tests { @@ -244,3 +303,31 @@ func TestSplitPosUnicodeSecurityRegression(t *testing.T) { assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php") } } + +// TestSplitPosSecurityRegressionUnicodeBypass guards against the FrankenPHP +// advisories GHSA-3g8v-8r37-cgjm (uninitialized match flag on inner non-ASCII +// byte) and GHSA-v4h7-cj44-8fc8 (Unicode equivalence via search.IgnoreCase +// folding fullwidth/mathematical/circled letters onto ASCII). Every payload +// below produced a false positive in the vulnerable implementation; none +// must match here. +func TestSplitPosSecurityRegressionUnicodeBypass(t *testing.T) { + t.Parallel() + + tr := Transport{SplitPath: []string{".php"}} + payloads := []string{ + "/PoC-match-unset.¡.txt", // GHSA-3g8v: stale match=true on IndexString fallback + "/shell﹒php", // U+FE52 small full stop + "/shell.php", // U+FF0E fullwidth full stop + "/shell.php", // U+FF50 fullwidth p + "/shell.php", // U+FF48 fullwidth h + "/shell.php", // U+FF50 fullwidth p (trailing) + "/shell.\U0001D5C1\U0001D5B5\U0001D5C1", // mathematical sans-serif p/h + "/shell.\U0001D5FD\U0001D5F5\U0001D5FD", // mathematical sans-serif bold p/h + "/shell.\U0001D4C5\U0001D4BD\U0001D4C5", // mathematical script p/h + "/shell.ⓟⓗⓟ", // circled latin small + } + + for _, p := range payloads { + assert.Equalf(t, -1, tr.splitPos(p), "payload %q must not be detected as .php", p) + } +} From 9c78b97f9e79773d45cf3ad0326bfb5480861ac0 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 8 May 2026 10:46:28 -0600 Subject: [PATCH 171/206] fastcgi: Fix lint --- modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index 3e0436062..f91394e58 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -449,7 +449,7 @@ func (t Transport) splitPos(path string) int { for i := 0; i <= pathLen-splitLen; i++ { match := true - for j := 0; j < splitLen; j++ { + for j := range splitLen { c := path[i+j] if c >= utf8.RuneSelf { match = false From 5e76b5ee43e8ec9e78d07665cdd75515b4c07acd Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Sun, 10 May 2026 13:10:29 +1000 Subject: [PATCH 172/206] tls: add alpn to managed HTTPS records (#7653) * tls: add alpn to managed HTTPS records * tls: centralise HTTPS RR ALPN defaults and registration Reuse shared protocol defaults instead of repeating the default HTTP protocol list, unify server name registration to carry ALPN in one experimental API and reuse the TLS default ALPN ordering for HTTPS RR publication * http: centralise effective protocol resolution for HTTPS RR ALPN --- modules/caddyhttp/app.go | 34 +-------- modules/caddyhttp/autohttps.go | 16 +++- modules/caddyhttp/autohttps_test.go | 63 ++++++++-------- modules/caddyhttp/server.go | 56 +++++++++++--- modules/caddytls/connpolicy.go | 4 +- modules/caddytls/ech.go | 33 +++++++-- modules/caddytls/ech_dns_test.go | 65 +++++++++++++++++ modules/caddytls/tls.go | 109 ++++++++++++++++++++++++---- 8 files changed, 286 insertions(+), 94 deletions(-) create mode 100644 modules/caddytls/ech_dns_test.go diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 571ac496e..bc2b896cd 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -20,7 +20,6 @@ import ( "crypto/tls" "errors" "fmt" - "maps" "net" "net/http" "strconv" @@ -241,12 +240,7 @@ func (app *App) Provision(ctx caddy.Context) error { // if no protocols configured explicitly, enable all except h2c if len(srv.Protocols) == 0 { - srv.Protocols = []string{"h1", "h2", "h3"} - } - - srvProtocolsUnique := map[string]struct{}{} - for _, srvProtocol := range srv.Protocols { - srvProtocolsUnique[srvProtocol] = struct{}{} + srv.Protocols = srv.protocolsWithDefaults() } if srv.ListenProtocols != nil { @@ -257,31 +251,7 @@ func (app *App) Provision(ctx caddy.Context) error { for i, lnProtocols := range srv.ListenProtocols { if lnProtocols != nil { - // populate empty listen protocols with server protocols - lnProtocolsDefault := false - var lnProtocolsInclude []string - srvProtocolsInclude := maps.Clone(srvProtocolsUnique) - - // keep existing listener protocols unless they are empty - for _, lnProtocol := range lnProtocols { - if lnProtocol == "" { - lnProtocolsDefault = true - } else { - lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol) - delete(srvProtocolsInclude, lnProtocol) - } - } - - // append server protocols to listener protocols if any listener protocols were empty - if lnProtocolsDefault { - for _, srvProtocol := range srv.Protocols { - if _, ok := srvProtocolsInclude[srvProtocol]; ok { - lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol) - } - } - } - - srv.ListenProtocols[i] = lnProtocolsInclude + srv.ListenProtocols[i] = srv.listenerProtocolsWithDefaults(lnProtocols) } } } diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 4d9759000..4e5b85f65 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -173,7 +173,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er for d := range serverDomainSet { echDomains = append(echDomains, d) } - app.tlsApp.RegisterServerNames(echDomains) + app.tlsApp.RegisterServerNames(echDomains, httpsRRALPNs(srv)) // nothing more to do here if there are no domains that qualify for // automatic HTTPS and there are no explicit TLS connection policies: @@ -574,6 +574,20 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route { } } +func httpsRRALPNs(srv *Server) []string { + alpn := make(map[string]struct{}, 3) + if srv.protocol("h3") { + alpn["h3"] = struct{}{} + } + if srv.protocol("h2") { + alpn["h2"] = struct{}{} + } + if srv.protocol("h1") { + alpn["http/1.1"] = struct{}{} + } + return caddytls.OrderedHTTPSRRALPN(alpn) +} + // createAutomationPolicies ensures that automated certificates for this // app are managed properly. This adds up to two automation policies: // one for the public names, and one for the internal names. If a catch-all diff --git a/modules/caddyhttp/autohttps_test.go b/modules/caddyhttp/autohttps_test.go index b5cc64d94..89843844d 100644 --- a/modules/caddyhttp/autohttps_test.go +++ b/modules/caddyhttp/autohttps_test.go @@ -1,44 +1,47 @@ package caddyhttp import ( + "reflect" "testing" - - "github.com/caddyserver/caddy/v2" ) -func TestRecordAutoHTTPSRedirectAddressPrefersHTTPSPort(t *testing.T) { - app := &App{HTTPSPort: 443} - redirDomains := make(map[string][]caddy.NetworkAddress) +func TestHTTPSRRALPNsDefaultProtocols(t *testing.T) { + srv := &Server{} - app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 2345, EndPort: 2345}) - app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 443, EndPort: 443}) - app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 8443, EndPort: 8443}) + got := httpsRRALPNs(srv) + want := []string{"h3", "h2", "http/1.1"} - got := redirDomains["example.com"] - if len(got) != 1 { - t.Fatalf("expected 1 redirect address, got %d: %#v", len(got), got) - } - if got[0].StartPort != 443 { - t.Fatalf("expected redirect to prefer HTTPS port 443, got %#v", got[0]) + if !reflect.DeepEqual(got, want) { + t.Fatalf("unexpected ALPN values: got %v want %v", got, want) } } -func TestRecordAutoHTTPSRedirectAddressKeepsAllBindAddressesOnWinningPort(t *testing.T) { - app := &App{HTTPSPort: 443} - redirDomains := make(map[string][]caddy.NetworkAddress) - - app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "10.0.0.189", StartPort: 8443, EndPort: 8443}) - app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "10.0.0.189", StartPort: 443, EndPort: 443}) - app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "2603:c024:8002:9500:9eb:e5d3:3975:d056", StartPort: 443, EndPort: 443}) - - got := redirDomains["example.com"] - if len(got) != 2 { - t.Fatalf("expected 2 redirect addresses for both bind addresses on the winning port, got %d: %#v", len(got), got) +func TestHTTPSRRALPNsListenProtocolOverrides(t *testing.T) { + srv := &Server{ + Protocols: []string{"h1", "h2"}, + ListenProtocols: [][]string{ + {"h1"}, + nil, + {}, + {"h3", ""}, + }, } - if got[0].StartPort != 443 || got[1].StartPort != 443 { - t.Fatalf("expected both redirect addresses to stay on HTTPS port 443, got %#v", got) - } - if got[0].Host != "10.0.0.189" || got[1].Host != "2603:c024:8002:9500:9eb:e5d3:3975:d056" { - t.Fatalf("expected both bind addresses to be preserved, got %#v", got) + + got := httpsRRALPNs(srv) + want := []string{"h3", "h2", "http/1.1"} + + if !reflect.DeepEqual(got, want) { + t.Fatalf("unexpected ALPN values: got %v want %v", got, want) + } +} + +func TestHTTPSRRALPNsIgnoresH2COnly(t *testing.T) { + srv := &Server{ + Protocols: []string{"h2c"}, + } + + got := httpsRRALPNs(srv) + if len(got) != 0 { + t.Fatalf("unexpected ALPN values: got %v want none", got) } } diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 9aca53578..66f93989b 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -300,6 +300,8 @@ type Server struct { onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023) } +var defaultProtocols = []string{"h1", "h2", "h3"} + var ( ServerHeader = "Caddy" serverHeader = []string{ServerHeader} @@ -899,22 +901,58 @@ func (s *Server) logRequest( // protocol returns true if the protocol proto is configured/enabled. func (s *Server) protocol(proto string) bool { if s.ListenProtocols == nil { - if slices.Contains(s.Protocols, proto) { + return slices.Contains(s.protocolsWithDefaults(), proto) + } + + for _, lnProtocols := range s.ListenProtocols { + if slices.Contains(s.listenerProtocolsWithDefaults(lnProtocols), proto) { return true } - } else { - for _, lnProtocols := range s.ListenProtocols { - for _, lnProtocol := range lnProtocols { - if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto { - return true - } - } - } } return false } +func (s *Server) protocolsWithDefaults() []string { + if len(s.Protocols) == 0 { + return defaultProtocols + } + return s.Protocols +} + +func (s *Server) listenerProtocolsWithDefaults(lnProtocols []string) []string { + serverProtocols := s.protocolsWithDefaults() + if len(lnProtocols) == 0 { + return serverProtocols + } + + lnProtocolsDefault := false + lnProtocolsInclude := make([]string, 0, len(lnProtocols)+len(serverProtocols)) + srvProtocolsInclude := make(map[string]struct{}, len(serverProtocols)) + for _, srvProtocol := range serverProtocols { + srvProtocolsInclude[srvProtocol] = struct{}{} + } + + for _, lnProtocol := range lnProtocols { + if lnProtocol == "" { + lnProtocolsDefault = true + continue + } + lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol) + delete(srvProtocolsInclude, lnProtocol) + } + + if lnProtocolsDefault { + for _, srvProtocol := range serverProtocols { + if _, ok := srvProtocolsInclude[srvProtocol]; ok { + lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol) + } + } + } + + return lnProtocolsInclude +} + // Listeners returns the server's listeners. These are active listeners, // so calling Accept() or Close() on them will probably break things. // They are made available here for read-only purposes (e.g. Addr()) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index c9258da48..9597af359 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -153,9 +153,9 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { // in its config (remember, TLS connection policies are used by *other* apps to // run TLS servers) -- we skip names with placeholders if tlsApp.EncryptedClientHello.Publication == nil { - var echNames []string repl := caddy.NewReplacer() for _, p := range cp { + var echNames []string for _, m := range p.matchers { if sni, ok := m.(MatchServerName); ok { for _, name := range sni { @@ -164,8 +164,8 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { } } } + tlsApp.RegisterServerNames(echNames, p.ALPN) } - tlsApp.RegisterServerNames(echNames) } tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index b915fcfbe..4a48769d8 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -440,6 +440,10 @@ func (t *TLS) publishECHConfigs(logger *zap.Logger) error { zap.Strings("domains", dnsNamesToPublish), zap.Uint8s("config_ids", configIDs)) + if dnsPublisher, ok := publisher.(*ECHDNSPublisher); ok { + dnsPublisher.alpnByDomain = t.alpnValuesForServerNames(dnsNamesToPublish) + } + // publish this ECH config list with this publisher pubTime := time.Now() err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin) @@ -776,7 +780,8 @@ type ECHDNSPublisher struct { ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"` provider ECHDNSProvider - logger *zap.Logger + alpnByDomain map[string][]string + logger *zap.Logger } // CaddyModule returns the Caddy module information. @@ -872,12 +877,7 @@ nextName: continue } params := httpsRec.Params - if params == nil { - params = make(libdns.SvcParams) - } - - // overwrite only the "ech" SvcParamKey - params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)} + params = dnsPub.publishedSvcParams(domain, params, configListBin) // publish record _, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{ @@ -903,6 +903,25 @@ nextName: return nil } +func (dnsPub *ECHDNSPublisher) publishedSvcParams(domain string, existing libdns.SvcParams, configListBin []byte) libdns.SvcParams { + params := make(libdns.SvcParams, len(existing)+2) + for key, values := range existing { + params[key] = append([]string(nil), values...) + } + + params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)} + + if len(dnsPub.alpnByDomain) == 0 { + return params + } + + if alpn := dnsPub.alpnByDomain[strings.ToLower(domain)]; len(alpn) > 0 { + params["alpn"] = append([]string(nil), alpn...) + } + + return params +} + // echConfig represents an ECHConfig from the specification, // [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html). type echConfig struct { diff --git a/modules/caddytls/ech_dns_test.go b/modules/caddytls/ech_dns_test.go new file mode 100644 index 000000000..7c337366e --- /dev/null +++ b/modules/caddytls/ech_dns_test.go @@ -0,0 +1,65 @@ +package caddytls + +import ( + "encoding/base64" + "reflect" + "sync" + "testing" + + "github.com/libdns/libdns" +) + +func TestRegisterServerNamesWithALPN(t *testing.T) { + tlsApp := &TLS{ + serverNames: make(map[string]serverNameRegistration), + serverNamesMu: new(sync.Mutex), + } + + tlsApp.RegisterServerNames([]string{ + "Example.com:443", + "example.com", + "127.0.0.1:443", + }, []string{"h2", "http/1.1"}) + tlsApp.RegisterServerNames([]string{"EXAMPLE.COM"}, []string{"h3"}) + + got := tlsApp.alpnValuesForServerNames([]string{"example.com:443", "127.0.0.1:443"}) + want := map[string][]string{ + "example.com": {"h3", "h2", "http/1.1"}, + } + + if !reflect.DeepEqual(got, want) { + t.Fatalf("unexpected ALPN values: got %#v want %#v", got, want) + } +} + +func TestECHDNSPublisherPublishedSvcParams(t *testing.T) { + dnsPub := &ECHDNSPublisher{ + alpnByDomain: map[string][]string{ + "example.com": {"h3", "h2", "http/1.1"}, + }, + } + + existing := libdns.SvcParams{ + "alpn": {"h2"}, + "ipv4hint": {"203.0.113.10"}, + } + + got := dnsPub.publishedSvcParams("Example.com", existing, []byte{0x01, 0x02, 0x03}) + + if !reflect.DeepEqual(existing["alpn"], []string{"h2"}) { + t.Fatalf("existing params mutated: got %v", existing["alpn"]) + } + + if !reflect.DeepEqual(got["alpn"], []string{"h3", "h2", "http/1.1"}) { + t.Fatalf("unexpected ALPN params: got %v", got["alpn"]) + } + + if !reflect.DeepEqual(got["ipv4hint"], []string{"203.0.113.10"}) { + t.Fatalf("unexpected preserved params: got %v", got["ipv4hint"]) + } + + wantECH := base64.StdEncoding.EncodeToString([]byte{0x01, 0x02, 0x03}) + if !reflect.DeepEqual(got["ech"], []string{wantECH}) { + t.Fatalf("unexpected ECH params: got %v want %v", got["ech"], wantECH) + } +} diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 34ffbf62d..e5f6e6fc0 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -23,6 +23,7 @@ import ( "net" "net/http" "runtime/debug" + "slices" "strings" "sync" "time" @@ -140,7 +141,7 @@ type TLS struct { logger *zap.Logger events *caddyevents.App - serverNames map[string]struct{} + serverNames map[string]serverNameRegistration serverNamesMu *sync.Mutex // set of subjects with managed certificates, @@ -168,7 +169,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.logger = ctx.Logger() repl := caddy.NewReplacer() t.managing, t.loaded = make(map[string]string), make(map[string]string) - t.serverNames = make(map[string]struct{}) + t.serverNames = make(map[string]serverNameRegistration) t.serverNamesMu = new(sync.Mutex) // set up default DNS module, if any, and make sure it implements all the @@ -648,27 +649,109 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str return false } -// RegisterServerNames registers the provided DNS names with the TLS app. -// This is currently used to auto-publish Encrypted ClientHello (ECH) -// configurations, if enabled. Use of this function by apps using the TLS -// app removes the need for the user to redundantly specify domain names -// in their configuration. This function separates hostname and port -// (keeping only the hotsname) and filters IP addresses, which can't be -// used with ECH. +// RegisterServerNames registers the provided DNS names with the TLS app and +// associates them with the given HTTPS RR ALPN values, if any. This is +// currently used to auto-publish Encrypted ClientHello (ECH) configurations, +// if enabled. Use of this function by apps using the TLS app removes the need +// for the user to redundantly specify domain names in their configuration. +// This function separates hostname and port, keeping only the hostname, and +// filters IP addresses which can't be used with ECH. // // EXPERIMENTAL: This function and its semantics/behavior are subject to change. -func (t *TLS) RegisterServerNames(dnsNames []string) { +func (t *TLS) RegisterServerNames(dnsNames, alpnValues []string) { t.serverNamesMu.Lock() + defer t.serverNamesMu.Unlock() + for _, name := range dnsNames { host, _, err := net.SplitHostPort(name) if err != nil { host = name } - if strings.TrimSpace(host) != "" && !certmagic.SubjectIsIP(host) { - t.serverNames[strings.ToLower(host)] = struct{}{} + host = strings.ToLower(strings.TrimSpace(host)) + if host == "" || certmagic.SubjectIsIP(host) { + continue + } + + registration := t.serverNames[host] + + if len(alpnValues) == 0 { + t.serverNames[host] = registration + continue + } + + if registration.alpnValues == nil { + registration.alpnValues = make(map[string]struct{}, len(alpnValues)) + } + for _, alpn := range alpnValues { + if alpn == "" { + continue + } + registration.alpnValues[alpn] = struct{}{} + } + t.serverNames[host] = registration + } +} + +func (t *TLS) alpnValuesForServerNames(dnsNames []string) map[string][]string { + t.serverNamesMu.Lock() + defer t.serverNamesMu.Unlock() + + result := make(map[string][]string, len(dnsNames)) + for _, name := range dnsNames { + host, _, err := net.SplitHostPort(name) + if err != nil { + host = name + } + host = strings.ToLower(strings.TrimSpace(host)) + if host == "" { + continue + } + + registration, ok := t.serverNames[host] + if !ok || len(registration.alpnValues) == 0 { + continue + } + result[host] = OrderedHTTPSRRALPN(registration.alpnValues) + } + + return result +} + +// OrderedHTTPSRRALPN returns the HTTPS RR ALPN values in preferred order. +func OrderedHTTPSRRALPN(alpnSet map[string]struct{}) []string { + if len(alpnSet) == 0 { + return nil + } + + knownOrder := append([]string{"h3"}, defaultALPN...) + ordered := make([]string, 0, len(alpnSet)) + seen := make(map[string]struct{}, len(alpnSet)) + + for _, alpn := range knownOrder { + if _, ok := alpnSet[alpn]; ok { + ordered = append(ordered, alpn) + seen[alpn] = struct{}{} } } - t.serverNamesMu.Unlock() + + if len(ordered) == len(alpnSet) { + return ordered + } + + var remaining []string + for alpn := range alpnSet { + if _, ok := seen[alpn]; ok { + continue + } + remaining = append(remaining, alpn) + } + slices.Sort(remaining) + + return append(ordered, remaining...) +} + +type serverNameRegistration struct { + alpnValues map[string]struct{} } // HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP From 0fab9f0f7db04fdb0c1c3113d51e7c420d3c8f06 Mon Sep 17 00:00:00 2001 From: Rijul <31570722+Rijul-A@users.noreply.github.com> Date: Sun, 10 May 2026 19:38:40 +0530 Subject: [PATCH 173/206] caddytls: avoid duplicate automation for wildcard-covered hosts (#7697) * caddytls: Fix wildcard race in auto-HTTPS launch When evaluating whether to skip managing an individual subdomain due to an existing wildcard configuration, we now explicitly consult the automate loader. Because Caddy apps can start in any order, relying strictly on the TLS app's internal management state was non-deterministic if the HTTP app started first. Checking the automate loader guarantees predictable behavior since it is fully populated during the Provision phase, well before any apps are started. * respond to review comments 1. update requested comment 2. remove personal domain from test 3. add regression test * remove unnecessary mutex lock * refactor: -integration test, +explicit cases * refactor: remove redundant test, add comment * rename file and add header * update copyright year --- modules/caddytls/tls.go | 11 ++- modules/caddytls/tls_wildcard_test.go | 96 +++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 modules/caddytls/tls_wildcard_test.go diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index e5f6e6fc0..928e109e6 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -614,8 +614,8 @@ func (t *TLS) Manage(subjects map[string]struct{}) error { // managingWildcardFor returns true if the app is managing a certificate that covers that // subject name (including consideration of wildcards), either from its internal list of -// names that it IS managing certs for, or from the otherSubjsToManage which includes names -// that WILL be managed. +// names that it IS managing certs for, from the otherSubjsToManage which includes names +// that WILL be managed, or from names configured in the 'automate' loader. func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]struct{}) bool { // TODO: we could also consider manually-loaded certs using t.HasCertificateForSubject(), // but that does not account for how manually-loaded certs may be restricted as to which @@ -630,7 +630,9 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str return managing } - // replace labels of the domain with wildcards until we get a match + // replace labels of the domain with wildcards until we get a match from names + // already being managed, those about to be managed in this batch, or those + // configured for automation labels := strings.Split(subj, ".") for i := range labels { if labels[i] == "*" { @@ -644,6 +646,9 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str if _, ok := otherSubjsToManage[candidate]; ok { return true } + if _, ok := t.automateNames[candidate]; ok { + return true + } } return false diff --git a/modules/caddytls/tls_wildcard_test.go b/modules/caddytls/tls_wildcard_test.go new file mode 100644 index 000000000..0151ca5dd --- /dev/null +++ b/modules/caddytls/tls_wildcard_test.go @@ -0,0 +1,96 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddytls + +import ( + "encoding/json" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func TestAvoidDuplicateAutomation(t *testing.T) { + tests := []struct { + name string + automateNames []string + expectedToManage bool + }{ + { + name: "do not manage if wildcard is automated", + automateNames: []string{"*.example.com"}, + expectedToManage: false, + }, + { + name: "manage if no automation configured", + automateNames: []string{}, + expectedToManage: true, + }, + { + name: "manage if explicitly requested even when wildcard automated", + automateNames: []string{"*.example.com", "sub.example.com"}, + expectedToManage: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + automateJSON, err := json.Marshal(tc.automateNames) + if err != nil { + t.Fatal(err) + } + + tlsApp := &TLS{ + Automation: &AutomationConfig{ + Policies: []*AutomationPolicy{ + { + IssuersRaw: []json.RawMessage{ + []byte(`{"module": "internal"}`), + }, + }, + }, + }, + CertificatesRaw: map[string]json.RawMessage{ + "automate": automateJSON, + }, + } + + var cfg caddy.Config + ctx, err := caddy.ProvisionContext(&cfg) + if err != nil { + t.Fatal(err) + } + + if err := tlsApp.Provision(ctx); err != nil { + t.Fatal(err) + } + + // simulate a case wherein the HTTP app starts first and + // tells the TLS app about the following auto-HTTPS domains + httpDomains := map[string]struct{}{"sub.example.com": {}} + if err := tlsApp.Manage(httpDomains); err != nil { + t.Fatal(err) + } + + _, actuallyManaged := tlsApp.managing["sub.example.com"] + if actuallyManaged != tc.expectedToManage { + t.Errorf( + "expected sub.example.com individually managed: %v, got: %v", + tc.expectedToManage, + actuallyManaged, + ) + } + }) + } +} From 4ba16fe82c9dbf8affb670dc56f7c43f1296e3e9 Mon Sep 17 00:00:00 2001 From: Steffen Busch <37350514+steffenbusch@users.noreply.github.com> Date: Mon, 11 May 2026 20:23:58 +0200 Subject: [PATCH 174/206] docs: add documentation for fileExists and fileStat template functions (#7700) --- modules/caddyhttp/templates/templates.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 994beefab..f1f910857 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -162,6 +162,25 @@ func init() { // {{listFiles "/mydir"}} // ``` // +// ##### `fileExists` +// +// Returns true if the given file name, relative to the template context's file root, +// can be opened successfully. +// +// ``` +// {{fileExists "path/to/file.html"}} +// ``` +// +// ##### `fileStat` +// +// Returns [FileInfo](https://pkg.go.dev/io/fs#FileInfo) using [Stat](https://pkg.go.dev/io/fs#Stat) +// on the given file name, relative to the template context's file root. +// +// ``` +// {{$css := fileStat "css/style.css" -}} +// +// ``` +// // ##### `markdown` // // Renders the given Markdown text as HTML and returns it. This uses the From 761347aa635e14bf7a937e88d45a40e671d325c5 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 11 May 2026 16:45:49 -0600 Subject: [PATCH 175/206] templates: Explicitly warn about misconfigurations --- modules/caddyhttp/templates/templates.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index f1f910857..caac85b8a 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -36,13 +36,22 @@ func init() { // Templates is a middleware which executes response bodies as Go templates. // The syntax is documented in the Go standard library's // [text/template package](https://golang.org/pkg/text/template/). +// Note that ANY response body that matches and qualifies may be evaluated, +// even if it comes from a proxied backend. // -// ⚠️ Template functions/actions are still experimental, so they are subject to change. +// ⚠️ Template functions/actions can access the environment, files on disk, +// and make HTTP requests. This is extremely useful, but you need to make +// sure templates are only evaluated on content that you trust, control, or +// at least sanitize properly. // -// Custom template functions can be registered by creating a plugin module under the `http.handlers.templates.functions.*` namespace that implements the `CustomFunctions` interface. +// ⚠️ Templates are still experimental, so they are subject to change. // // [All Sprig functions](https://masterminds.github.io/sprig/) are supported. // +// Custom template functions can be registered by creating a plugin module +// under the `http.handlers.templates.functions.*` namespace that implements +// the `CustomFunctions` interface. +// // In addition to the standard functions and the Sprig library, Caddy adds // extra functions and data that are available to a template: // From a4a38c3e88952a361c6e44c5dc3200bba1497e15 Mon Sep 17 00:00:00 2001 From: Rayan Salhab Date: Tue, 12 May 2026 02:16:33 +0300 Subject: [PATCH 176/206] rewrite: escape file matcher paths before rewriting (#7683) * fix: escape file matcher paths in rewrites Preserve matched file paths containing literal '?' or '%' when try_files rewrites to http.matchers.file.relative. * test: cover nested escaped try_files rewrite paths * test: cover encoded slash try_files rewrite paths * fix: assert file matcher placeholder as string --------- Co-authored-by: cyphercodes --- modules/caddyhttp/fileserver/matcher_test.go | 100 +++++++++++++++++++ modules/caddyhttp/rewrite/rewrite.go | 32 ++++-- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go index 4342d5d31..b94b8444f 100644 --- a/modules/caddyhttp/fileserver/matcher_test.go +++ b/modules/caddyhttp/fileserver/matcher_test.go @@ -28,6 +28,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/internal/filesystems" "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" ) type testCase struct { @@ -188,6 +189,105 @@ func fileMatcherTest(t *testing.T, i int, tc testCase) { } } +func TestTryFilesRewriteEscapesMatchedPath(t *testing.T) { + root := t.TempDir() + + tests := []struct { + name string + requestTarget string + filename string + extraFiles []string + wantPath string + wantRequestURI string + skipWindows bool + }{ + { + name: "question mark in path", + requestTarget: "/%3F.html", + filename: "?.html", + wantPath: "/?.html", + wantRequestURI: "/%3F.html", + skipWindows: true, + }, + { + name: "percent in path", + requestTarget: "/%25.html", + filename: "%.html", + wantPath: "/%.html", + wantRequestURI: "/%25.html", + }, + { + name: "encoded question mark remains percent-encoded", + requestTarget: "/%253F.html", + filename: "%3F.html", + wantPath: "/%3F.html", + wantRequestURI: "/%253F.html", + }, + { + name: "question mark in nested path", + requestTarget: "/nested/%3F.html", + filename: filepath.Join("nested", "?.html"), + wantPath: "/nested/?.html", + wantRequestURI: "/nested/%3F.html", + skipWindows: true, + }, + { + name: "encoded slash in filename does not conflict with nesting", + requestTarget: "/nested%252Ffile.html", + filename: "nested%2Ffile.html", + extraFiles: []string{filepath.Join("nested", "file.html")}, + wantPath: "/nested%2Ffile.html", + wantRequestURI: "/nested%252Ffile.html", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.skipWindows && runtime.GOOS == "windows" { + t.Skip("Windows file names cannot contain question marks") + } + + for _, name := range append([]string{tc.filename}, tc.extraFiles...) { + filename := filepath.Join(root, name) + if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil { + t.Fatalf("creating test file parent directory: %v", err) + } + if err := os.WriteFile(filename, []byte(name), 0o600); err != nil { + t.Fatalf("writing test file: %v", err) + } + } + + m := &MatchFile{ + fsmap: &filesystems.FileSystemMap{}, + Root: root, + TryFiles: []string{"{http.request.uri.path}"}, + } + req := httptest.NewRequest(http.MethodGet, "http://example.com"+tc.requestTarget, nil) + repl := caddyhttp.NewTestReplacer(req) + + matched, err := m.MatchWithError(req) + if err != nil { + t.Fatalf("matching file: %v", err) + } + if !matched { + t.Fatalf("expected request %s to match %s", tc.requestTarget, tc.filename) + } + + rewrite.Rewrite{URI: "{http.matchers.file.relative}"}.Rewrite(req, repl) + + if req.URL.Path != tc.wantPath { + t.Errorf("rewritten path = %q, want %q", req.URL.Path, tc.wantPath) + } + if req.RequestURI != tc.wantRequestURI { + t.Errorf("rewritten request URI = %q, want %q", req.RequestURI, tc.wantRequestURI) + } + if req.URL.RawQuery != "" { + t.Errorf("rewritten raw query = %q, want empty", req.URL.RawQuery) + } + }) + } +} + func TestPHPFileMatcher(t *testing.T) { for i, tc := range []struct { path string diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index ba2ea5407..3500028f9 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -211,12 +211,7 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool { var newPath, newQuery, newFrag string if path != "" { - // replace the `path` placeholder to escaped path - pathPlaceholder := "{http.request.uri.path}" - if strings.Contains(path, pathPlaceholder) { - path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath()) - } - + path = escapePathPlaceholders(path, r, repl) newPath = repl.ReplaceAll(path, "") } @@ -300,6 +295,31 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool { return r.Method != oldMethod || r.RequestURI != oldURI } +func escapePathPlaceholders(path string, r *http.Request, repl *caddy.Replacer) string { + // Replace path-valued placeholders in escaped form before the URI is parsed, + // otherwise literal '?' and '%' bytes from the path can be interpreted as URI + // delimiters or percent-escape sequences during the rewrite. + pathPlaceholder := "{http.request.uri.path}" + if strings.Contains(path, pathPlaceholder) { + path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath()) + } + + fileMatchRelativePlaceholder := "{http.matchers.file.relative}" + if strings.Contains(path, fileMatchRelativePlaceholder) { + if val, ok := repl.Get("http.matchers.file.relative"); ok { + if relativePath, ok := val.(string); ok { + path = strings.ReplaceAll(path, fileMatchRelativePlaceholder, escapePathPreservingSlashes(relativePath)) + } + } + } + + return path +} + +func escapePathPreservingSlashes(path string) string { + return strings.ReplaceAll(url.PathEscape(path), "%2F", "/") +} + // buildQueryString takes an input query string and // performs replacements on each component, returning // the resulting query string. This function appends From d80774cb3fe6abcd0fea9dd0c68812ee94c7cf2a Mon Sep 17 00:00:00 2001 From: Br1an <932039080@qq.com> Date: Tue, 12 May 2026 07:27:03 +0800 Subject: [PATCH 177/206] metrics: Add nil check for metricsHandler in AdminMetrics.serveHTTP (#7553) * metrics: Add nil check for metricsHandler in AdminMetrics.serveHTTP Prevents panic when the admin metrics endpoint is accessed before the module is fully provisioned. Returns a proper API error instead of crashing. * admin: provision router modules before registering routes Instead of adding a nil check for metricsHandler, address the root cause by provisioning admin router modules before calling Routes(). This ensures all handler state is initialized before routes are registered on the mux. Merge newAdminHandler and provisionAdminRouters into a single step, removing the two-phase setup where routes were registered first and modules provisioned later. The AdminConfig.routers field is no longer needed since provisioning happens inline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: go fmt admin.go --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- admin.go | 51 +++++++++++++-------------------------------------- admin_test.go | 24 +++++++++--------------- caddy.go | 7 ------- 3 files changed, 22 insertions(+), 60 deletions(-) diff --git a/admin.go b/admin.go index 08465a923..97af846ef 100644 --- a/admin.go +++ b/admin.go @@ -120,10 +120,6 @@ type AdminConfig struct { // // EXPERIMENTAL: This feature is subject to change. Remote *RemoteAdmin `json:"remote,omitempty"` - - // Holds onto the routers so that we can later provision them - // if they require provisioning. - routers []AdminRouter } // ConfigSettings configures the management of configuration. @@ -222,7 +218,7 @@ type AdminPermissions struct { // newAdminHandler reads admin's config and returns an http.Handler suitable // for use in an admin endpoint server, which will be listening on listenAddr. -func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler { +func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) (adminHandler, error) { muxWrap := adminHandler{mux: http.NewServeMux()} // secure the local or remote endpoint respectively @@ -279,34 +275,21 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co // register third-party module endpoints for _, m := range GetModules("admin.api") { router := m.New().(AdminRouter) + + // provision the router before registering its routes, so + // handlers have access to all provisioned state + if provisioner, ok := router.(Provisioner); ok { + if err := provisioner.Provision(ctx); err != nil { + return adminHandler{}, fmt.Errorf("provisioning admin router module %s: %v", m.ID, err) + } + } + for _, route := range router.Routes() { addRoute(route.Pattern, handlerLabel, route.Handler) } - admin.routers = append(admin.routers, router) } - return muxWrap -} - -// provisionAdminRouters provisions all the router modules -// in the admin.api namespace that need provisioning. -func (admin *AdminConfig) provisionAdminRouters(ctx Context) error { - for _, router := range admin.routers { - provisioner, ok := router.(Provisioner) - if !ok { - continue - } - - err := provisioner.Provision(ctx) - if err != nil { - return err - } - } - - // We no longer need the routers once provisioned, allow for GC - admin.routers = nil - - return nil + return muxWrap, nil } // allowedOrigins returns a list of origins that are allowed. @@ -430,11 +413,7 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error { return err } - handler := cfg.Admin.newAdminHandler(addr, false, ctx) - - // run the provisioners for loaded modules to make sure local - // state is properly re-initialized in the new admin server - err = cfg.Admin.provisionAdminRouters(ctx) + handler, err := cfg.Admin.newAdminHandler(addr, false, ctx) if err != nil { return err } @@ -558,11 +537,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error { // make the HTTP handler but disable Host/Origin enforcement // because we are using TLS authentication instead - handler := cfg.Admin.newAdminHandler(addr, true, ctx) - - // run the provisioners for loaded modules to make sure local - // state is properly re-initialized in the new admin server - err = cfg.Admin.provisionAdminRouters(ctx) + handler, err := cfg.Admin.newAdminHandler(addr, true, ctx) if err != nil { return err } diff --git a/admin_test.go b/admin_test.go index 19cc6ae7c..dda06a9e9 100644 --- a/admin_test.go +++ b/admin_test.go @@ -340,7 +340,10 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) { if err != nil { t.Fatalf("Failed to parse address: %v", err) } - handler := cfg.Admin.newAdminHandler(addr, false, Context{}) + handler, err := cfg.Admin.newAdminHandler(addr, false, Context{}) + if err != nil { + t.Fatalf("Failed to create admin handler: %v", err) + } tests := []struct { name string @@ -461,7 +464,10 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) { admin := &AdminConfig{ EnforceOrigin: false, } - handler := admin.newAdminHandler(addr, false, Context{}) + handler, err := admin.newAdminHandler(addr, false, Context{}) + if err != nil { + t.Fatalf("Failed to create admin handler: %v", err) + } req := httptest.NewRequest("GET", "/mock", nil) req.Host = "localhost:2019" @@ -473,10 +479,6 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) { t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code) t.Logf("Response body: %s", rr.Body.String()) } - - if len(admin.routers) != 1 { - t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers)) - } } type mockProvisionableRouter struct { @@ -514,19 +516,16 @@ func TestAdminRouterProvisioning(t *testing.T) { name string provisionErr error wantErr bool - routersAfter int // expected number of routers after provisioning }{ { name: "successful provisioning", provisionErr: nil, wantErr: false, - routersAfter: 0, }, { name: "provisioning error", provisionErr: fmt.Errorf("provision failed"), wantErr: true, - routersAfter: 1, }, } @@ -562,8 +561,7 @@ func TestAdminRouterProvisioning(t *testing.T) { t.Fatalf("Failed to parse address: %v", err) } - _ = admin.newAdminHandler(addr, false, Context{}) - err = admin.provisionAdminRouters(Context{}) + _, err = admin.newAdminHandler(addr, false, Context{}) if test.wantErr { if err == nil { @@ -574,10 +572,6 @@ func TestAdminRouterProvisioning(t *testing.T) { t.Errorf("Expected no error but got: %v", err) } } - - if len(admin.routers) != test.routersAfter { - t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers)) - } }) } } diff --git a/caddy.go b/caddy.go index b3144299d..8799594a9 100644 --- a/caddy.go +++ b/caddy.go @@ -440,13 +440,6 @@ func run(newCfg *Config, start bool) (Context, error) { } }() - // Provision any admin routers which may need to access - // some of the other apps at runtime - err = ctx.cfg.Admin.provisionAdminRouters(ctx) - if err != nil { - return ctx, err - } - // Start err = func() error { started := make([]string, 0, len(ctx.cfg.apps)) From cc58caa1099240ef1a4c280b892260b380a85c86 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 11 May 2026 17:33:39 -0600 Subject: [PATCH 178/206] go.mod: Upgrade quic-go to v0.59.1 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bd44d644d..be1789782 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 github.com/mholt/acmez/v3 v3.1.6 github.com/prometheus/client_golang v1.23.2 - github.com/quic-go/quic-go v0.59.0 + github.com/quic-go/quic-go v0.59.1 github.com/smallstep/certificates v0.30.2 github.com/smallstep/nosql v0.8.0 github.com/smallstep/truststore v0.13.0 diff --git a/go.sum b/go.sum index 5cae77fe0..0112afcc8 100644 --- a/go.sum +++ b/go.sum @@ -280,8 +280,8 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= -github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= From 77e9ce7404c4a76853e101a9f5687a929ee56654 Mon Sep 17 00:00:00 2001 From: James Hartig Date: Tue, 12 May 2026 13:05:50 -0500 Subject: [PATCH 179/206] reverseproxy: further prevent body closes from dial errors (#7715) --- .../caddyhttp/reverseproxy/retries_test.go | 55 +++++++++++++++++++ .../caddyhttp/reverseproxy/reverseproxy.go | 17 +++--- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/retries_test.go b/modules/caddyhttp/reverseproxy/retries_test.go index b0f78bac0..bfd70978c 100644 --- a/modules/caddyhttp/reverseproxy/retries_test.go +++ b/modules/caddyhttp/reverseproxy/retries_test.go @@ -730,3 +730,58 @@ func TestRetryMatchAllowsExpressionMixedWithOtherMatchers(t *testing.T) { }) } } + +// TestSubrouteErrorFallbackWithBody is similar to TestDialErrorBodyRetry but +// mimics Subroute's Error handler rather than testing retries specifically +func TestSubrouteErrorFallbackWithBody(t *testing.T) { + // Good upstream: echoes the request body with 200 OK. + goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "read body: "+err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + _, err = w.Write(body) + if err != nil { + t.Errorf("error writing in good server: %v", err) + } + })) + t.Cleanup(goodServer.Close) + + // Handler which will dial error + badProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: deadUpstreamAddr(t)}) + + bodyReader := newCloseOnCloseReader("hello world") + req := httptest.NewRequest("POST", "http://localhost/", bodyReader) + // httptest.NewRequest wraps the reader in NopCloser; replace + // it with our close-aware reader so Close() is propagated. + req.Body = bodyReader + + req = prepareTestRequest(req) + rec := httptest.NewRecorder() + err := badProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + if err == nil { + t.Fatalf("Expected error from badProxy.ServeHTTP") + } + + // Simulate the Subroute's Error handler by calling another handler with the + // same request and recorder + goodProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: goodServer.Listener.Addr().String()}) + err = goodProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + })) + + if err != nil { + t.Fatalf("Expected no error from goodProxy.ServeHTTP, got: %v", err) + } + if rec.Code != http.StatusOK { + t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK) + } + expectedBody := "hello world" + if rec.Body.String() != expectedBody { + t.Errorf("body: got %q, want %q", rec.Body.String(), expectedBody) + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index cefe645ee..a11afcd79 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -488,20 +488,19 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht reqHost := clonedReq.Host reqHeader := clonedReq.Header - // When retries are configured and there is a body, wrap it in - // io.NopCloser to prevent Go's transport from closing it on dial - // errors. cloneRequest does a shallow copy, so clonedReq.Body and + // If the request contained a body, wrap it in io.NopCloser + // to prevent Go's transport from closing it on dial errors. + // cloneRequest does a shallow copy, so clonedReq.Body and // r.Body share the same io.ReadCloser — a dial-failure Close() - // would kill the original body for all subsequent retry attempts. - // The real body is closed by the HTTP server when the handler - // returns. + // would kill the original body for all subsequent retry + // attempts or subsequent handlers. The real body is closed by + // the HTTP server when the handler returns. // // If the body was already fully buffered (via request_buffers), // we also extract the buffer so the retry loop can replay it - // from the beginning on each attempt. (see #6259, #7546) + // from the beginning on each attempt. (see #6259, #7546, #7713) var bufferedReqBody *bytes.Buffer - if clonedReq.Body != nil && h.LoadBalancing != nil && - (h.LoadBalancing.Retries > 0 || h.LoadBalancing.TryDuration > 0) { + if clonedReq.Body != nil { if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil { bufferedReqBody = reqBodyBuf.buf reqBodyBuf.buf = nil From 6c675e29f87cbe7326983ddb6d739175119d394c Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Thu, 14 May 2026 10:05:57 -0600 Subject: [PATCH 180/206] caddytls: Fix client auth (fix #7724) (#7727) The peer certificates should be loaded even if existingVerifyPeerCert is nil. Patched with the assistance of Copilot, as an experiment. --- modules/caddytls/connpolicy.go | 11 ++-- .../connpolicy_verifyconnection_test.go | 59 +++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 modules/caddytls/connpolicy_verifyconnection_test.go diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 9597af359..c38ad0d4b 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -896,18 +896,19 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro // Unlike VerifyPeerCertificate, VerifyConnection is called on every // connection including resumed sessions, preventing session-resumption bypass. func (clientauth *ClientAuthentication) verifyConnection(cs tls.ConnectionState) error { + rawCerts := make([][]byte, len(cs.PeerCertificates)) + for i, cert := range cs.PeerCertificates { + rawCerts[i] = cert.Raw + } + // first use any pre-existing custom verification function if clientauth.existingVerifyPeerCert != nil { - rawCerts := make([][]byte, len(cs.PeerCertificates)) - for i, cert := range cs.PeerCertificates { - rawCerts[i] = cert.Raw - } if err := clientauth.existingVerifyPeerCert(rawCerts, cs.VerifiedChains); err != nil { return err } } for _, verifier := range clientauth.verifiers { - if err := verifier.VerifyClientCertificate(nil, cs.VerifiedChains); err != nil { + if err := verifier.VerifyClientCertificate(rawCerts, cs.VerifiedChains); err != nil { return err } } diff --git a/modules/caddytls/connpolicy_verifyconnection_test.go b/modules/caddytls/connpolicy_verifyconnection_test.go new file mode 100644 index 000000000..8b5511a3b --- /dev/null +++ b/modules/caddytls/connpolicy_verifyconnection_test.go @@ -0,0 +1,59 @@ +package caddytls + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "reflect" + "testing" +) + +type testClientCertificateVerifier struct { + rawCerts [][]byte + verifiedChains [][]*x509.Certificate + err error +} + +func (v *testClientCertificateVerifier) VerifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + v.rawCerts = rawCerts + v.verifiedChains = verifiedChains + return v.err +} + +func TestClientAuthenticationVerifyConnectionPassesRawCertsToVerifiers(t *testing.T) { + verifier := &testClientCertificateVerifier{} + clientauth := &ClientAuthentication{ + verifiers: []ClientCertificateVerifier{verifier}, + } + + peerCert := &x509.Certificate{Raw: []byte("peer-cert-raw")} + verifiedChains := [][]*x509.Certificate{{peerCert}} + connState := tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{peerCert}, + VerifiedChains: verifiedChains, + } + + if err := clientauth.verifyConnection(connState); err != nil { + t.Fatalf("verifyConnection failed: %v", err) + } + + if !reflect.DeepEqual(verifier.rawCerts, [][]byte{[]byte("peer-cert-raw")}) { + t.Fatalf("unexpected raw certs: got %#v", verifier.rawCerts) + } + if !reflect.DeepEqual(verifier.verifiedChains, verifiedChains) { + t.Fatalf("unexpected verified chains: got %#v", verifier.verifiedChains) + } +} + +func TestClientAuthenticationVerifyConnectionReturnsVerifierError(t *testing.T) { + wantErr := errors.New("verify failed") + verifier := &testClientCertificateVerifier{err: wantErr} + clientauth := &ClientAuthentication{ + verifiers: []ClientCertificateVerifier{verifier}, + } + + err := clientauth.verifyConnection(tls.ConnectionState{}) + if !errors.Is(err, wantErr) { + t.Fatalf("expected error %v, got %v", wantErr, err) + } +} From 704394d9d18e9fdcae2202117a617b4e50ef32f2 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Wed, 20 May 2026 02:42:19 +0300 Subject: [PATCH 181/206] chore: deps upgrade (#7751) Signed-off-by: Mohammed Al Sahaf --- go.mod | 28 ++++++------ go.sum | 136 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 82 insertions(+), 82 deletions(-) diff --git a/go.mod b/go.mod index be1789782..6fac53526 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/caddyserver/caddy/v2 -go 1.25.0 +go 1.25.1 require ( github.com/BurntSushi/toml v1.6.0 @@ -32,27 +32,27 @@ require ( github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 - go.step.sm/crypto v0.77.1 + go.step.sm/crypto v0.81.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.50.0 + golang.org/x/crypto v0.51.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 golang.org/x/net v0.53.0 golang.org/x/sync v0.20.0 - golang.org/x/term v0.42.0 + golang.org/x/term v0.43.0 golang.org/x/time v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) require ( cel.dev/expr v0.25.1 // indirect - cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect @@ -63,14 +63,14 @@ require ( github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/go-jose/go-jose/v3 v3.0.5 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.18.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect + github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -109,9 +109,9 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/oauth2 v0.36.0 // indirect - google.golang.org/api v0.271.0 // indirect + google.golang.org/api v0.277.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -169,10 +169,10 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.35.0 // indirect - golang.org/x/sys v0.43.0 - golang.org/x/text v0.36.0 // indirect + golang.org/x/sys v0.44.0 + golang.org/x/text v0.37.0 // indirect golang.org/x/tools v0.44.0 // indirect - google.golang.org/grpc v1.80.0 // indirect + google.golang.org/grpc v1.81.0 // indirect google.golang.org/protobuf v1.36.11 // indirect howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 0112afcc8..ac2ec3deb 100644 --- a/go.sum +++ b/go.sum @@ -2,18 +2,18 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= -cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= -cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= -cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= -cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= -cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U= +cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY= +cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE= +cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= +cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= +cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= @@ -53,36 +53,36 @@ github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmO github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= -github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= -github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= -github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= -github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY= -github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= +github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 h1:zuSf4olLKZW8cF/W9Y5wvGT+/0raY/3kVp49KsGs0QY= +github.com/aws/aws-sdk-go-v2/service/kms v1.51.1/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A= @@ -149,8 +149,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= -github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= -github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= +github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -179,18 +179,18 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws= -github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ= +github.com/google/go-tpm-tools v0.4.8 h1:V4oIYyAD3BykOycwYQzO29WefDouQMTsYZqmG3HxOfM= +github.com/google/go-tpm-tools v0.4.8/go.mod h1:4DfiOtiS1KppJjwf1+tqtW4K3PrCJjAAqFKj/TYTJKg= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= -github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= -github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= +github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= +github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -377,10 +377,10 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58 go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g= go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s= go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE= go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg= @@ -431,8 +431,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.step.sm/crypto v0.77.1 h1:4EEqfKdv0egQ1lqz2RhnU8Jv6QgXZfrgoxWMqJF9aDs= -go.step.sm/crypto v0.77.1/go.mod h1:U/SsmEm80mNnfD5WIkbhuW/B1eFp3fgFvdXyDLpU1AQ= +go.step.sm/crypto v0.81.0 h1:e+ouzpNt3Xm4dp7HGXhgYB5y4iFik3vh3phHKWmvugU= +go.step.sm/crypto v0.81.0/go.mod h1:fsTizqQeASjTXnbv9O00XtRlIuXRkCdoRiJNyXGQujc= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -456,8 +456,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8= golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= @@ -506,8 +506,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -517,8 +517,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -528,8 +528,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -543,16 +543,16 @@ golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= -google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= -google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc= -google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI= +google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk= +google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From 0125ae39cccfdf9b6fdfb16d5a59f3ad37a2caf6 Mon Sep 17 00:00:00 2001 From: Brett Bethke <10068296+bb4242@users.noreply.github.com> Date: Wed, 20 May 2026 01:19:11 -0500 Subject: [PATCH 182/206] caddyhttp: omit Last-Modified for unusable mod times (#7740) See #5548 and #7730 --- modules/caddyhttp/fileserver/staticfiles.go | 25 ++++++++- .../caddyhttp/fileserver/staticfiles_test.go | 56 +++++++++++++++++++ .../caddyhttp/fileserver/testdata/modtime.txt | 0 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 modules/caddyhttp/fileserver/testdata/modtime.txt diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 507321ad6..70fbd6192 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -29,6 +29,7 @@ import ( "runtime" "strconv" "strings" + "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -579,7 +580,17 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c // that errors generated by ServeContent are written immediately // to the response, so we cannot handle them (but errors there // are rare) - http.ServeContent(w, r, info.Name(), info.ModTime(), file.(io.ReadSeeker)) + // + // There are a few file modification times that aren't useful + // to send in Last-Modified headers, but the golang http library only + // omits Last-Modified headers for the Unix epoch time. So, force + // the modification time to the epoch time if it's not useful. + zeroTime := time.Time{} + modTime := info.ModTime() + if !usefulModTime(modTime) { + modTime = zeroTime + } + http.ServeContent(w, r, info.Name(), modTime, file.(io.ReadSeeker)) return nil } @@ -726,6 +737,14 @@ func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next ca return caddyhttp.Error(http.StatusNotFound, nil) } +// Indicates whether a file's modification time is useful for validator +// generation purposes (i.e. inclusion in ETag and Last-Modified headers). +// See issues #5548 and #7730. +func usefulModTime(modTime time.Time) bool { + mtimeunix := modTime.Unix() + return mtimeunix != 0 && mtimeunix != 1 +} + // calculateEtag computes an entity tag using a strong validator // without consuming the contents of the file. It requires the // file info contain the correct size and modification time. @@ -743,8 +762,8 @@ func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next ca // which we consider precise enough to qualify as a strong validator. func calculateEtag(d os.FileInfo) string { mtime := d.ModTime() - if mtimeUnix := mtime.Unix(); mtimeUnix == 0 || mtimeUnix == 1 { - return "" // not useful anyway; see issue #5548 + if !usefulModTime(mtime) { + return "" } var sb strings.Builder sb.WriteRune('"') diff --git a/modules/caddyhttp/fileserver/staticfiles_test.go b/modules/caddyhttp/fileserver/staticfiles_test.go index 5d6133c73..5d3bcbd06 100644 --- a/modules/caddyhttp/fileserver/staticfiles_test.go +++ b/modules/caddyhttp/fileserver/staticfiles_test.go @@ -15,10 +15,17 @@ package fileserver import ( + "context" + "net/http" + "net/http/httptest" + "os" "path/filepath" "runtime" "strings" "testing" + "time" + + "github.com/caddyserver/caddy/v2" ) func TestFileHidden(t *testing.T) { @@ -128,3 +135,52 @@ func TestFileHidden(t *testing.T) { } } } + +// Check to make sure that we don't serve ETag and Last-Modified headers +// for files with invalid modification times +func TestModTimeHeaders(t *testing.T) { + check_validator_headers(time.Now(), true, t) + check_validator_headers(time.Unix(0, 0), false, t) + check_validator_headers(time.Unix(1, 0), false, t) + check_validator_headers(time.Unix(2, 0), true, t) +} + +func check_validator_headers(modTime time.Time, expect_headers bool, t *testing.T) { + f := false + fsrv := FileServer{ + Root: "./testdata", + CanonicalURIs: &f, + } + w := httptest.NewRecorder() + r, err := http.NewRequest("GET", "/modtime.txt", nil) + if err != nil { + t.Fatal(err) + } + repl := caddy.NewReplacer() + ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl) + r = r.WithContext(ctx) + + ctx2, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) // module will be nil by default + fsrv.Provision(ctx2) + + path := "testdata/modtime.txt" + os.Chtimes(path, modTime, modTime) + + fsrv.ServeHTTP(w, r, nil) + + if expect_headers { + if w.Header().Get("ETag") == "" { + t.Errorf("Didn't get ETag header for file with valid mod time %s", modTime) + } + if w.Header().Get("Last-Modified") == "" { + t.Errorf("Didn't get Last-Modified header for file with valid mod time %s", modTime) + } + } else { + if w.Header().Get("ETag") != "" { + t.Errorf("Got ETag header for file with invalid mod time %s", modTime) + } + if w.Header().Get("Last-Modified") != "" { + t.Errorf("Got Last-Modified header for file with invalid mod time %s", modTime) + } + } +} diff --git a/modules/caddyhttp/fileserver/testdata/modtime.txt b/modules/caddyhttp/fileserver/testdata/modtime.txt new file mode 100644 index 000000000..e69de29bb From 325c244ea71a645c224afa1d0b46296ed76ef9fd Mon Sep 17 00:00:00 2001 From: cbro Date: Wed, 20 May 2026 02:35:40 -0400 Subject: [PATCH 183/206] caddytls: fix TLS state races and ECH rotation retry (#7756) * caddytls: fix data race in session ticket key rotation stayUpdated copies the map header (configs := s.configs) under the lock, then iterates the original map after releasing it. Concurrent calls to register/unregister mutate the same map. Hold the lock for the entire iteration instead. * caddytls: fix data race in AllMatchingCertificates AllMatchingCertificates reads the package-level certCache without acquiring certCacheMu, while Cleanup sets certCache to nil under the write lock. The adjacent HasCertificateForSubject correctly acquires certCacheMu.RLock. Add the missing RLock/RUnlock to match. * caddytls: fix ECH key rotation stopping permanently on error When rotateECHKeys returns an error, the rotation goroutine returns immediately, stopping all future key rotation for the lifetime of the process. Change return to continue, matching the error handling for publishECHConfigs two lines below. --- modules/caddytls/sessiontickets.go | 5 ++--- modules/caddytls/tls.go | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/caddytls/sessiontickets.go b/modules/caddytls/sessiontickets.go index bfc5628ac..7ebca4604 100644 --- a/modules/caddytls/sessiontickets.go +++ b/modules/caddytls/sessiontickets.go @@ -137,11 +137,10 @@ func (s *SessionTicketService) stayUpdated() { case newKeys := <-keysChan: s.mu.Lock() s.currentKeys = newKeys - configs := s.configs - s.mu.Unlock() - for cfg := range configs { + for cfg := range s.configs { cfg.SetSessionTicketKeys(newKeys) } + s.mu.Unlock() case <-s.stopChan: return } diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 928e109e6..b993cba6e 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -440,7 +440,7 @@ func (t *TLS) Start() error { t.EncryptedClientHello.configsMu.Unlock() if err != nil { echLogger.Error("rotating ECH configs failed", zap.Error(err)) - return + continue } err := t.publishECHConfigs(echLogger) if err != nil { @@ -879,6 +879,8 @@ func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy { // AllMatchingCertificates returns the list of all certificates in // the cache which could be used to satisfy the given SAN. func AllMatchingCertificates(san string) []certmagic.Certificate { + certCacheMu.RLock() + defer certCacheMu.RUnlock() return certCache.AllMatchingCertificates(san) } From 88037f1666eb9ce1b26453d32dc861cb3a87a4c7 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Wed, 20 May 2026 16:36:30 +1000 Subject: [PATCH 184/206] chore: clean up wording and typo fixes (#7745) * chore: clean up wording and typo fixes * chore: ASCII -> alphanumeric in lexer for heredoc marker --- caddyconfig/caddyfile/lexer.go | 4 ++-- caddyconfig/caddyfile/lexer_test.go | 2 +- caddyconfig/caddyfile/parse.go | 2 +- caddytest/caddytest.go | 12 ++++++------ .../heredoc_invalid_marker.caddyfiletest | 2 +- caddytest/integration/stream_test.go | 2 +- cmd/commands.go | 2 +- modules/caddyhttp/celmatcher.go | 6 +++--- modules/caddyhttp/celmatcher_test.go | 2 +- modules/caddyhttp/encode/encode.go | 4 ++-- modules/caddyhttp/fileserver/staticfiles.go | 2 +- modules/caddyhttp/http2listener.go | 2 +- modules/caddyhttp/httpredirectlistener.go | 2 +- modules/caddyhttp/reverseproxy/fastcgi/client.go | 10 +++++----- modules/caddyhttp/reverseproxy/healthchecks.go | 2 +- .../caddyhttp/reverseproxy/selectionpolicies_test.go | 4 ++-- modules/caddytls/automation.go | 2 +- modules/logging/filters.go | 4 ++-- 18 files changed, 33 insertions(+), 33 deletions(-) diff --git a/caddyconfig/caddyfile/lexer.go b/caddyconfig/caddyfile/lexer.go index 60dabe43d..40ea2e5f7 100644 --- a/caddyconfig/caddyfile/lexer.go +++ b/caddyconfig/caddyfile/lexer.go @@ -155,7 +155,7 @@ func (l *lexer) next() (bool, error) { // want to keep. if ch == '\n' { if len(val) == 2 { - return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line) + return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alphanumeric characters, dashes and underscores; got empty string", l.line) } // check if there's too many < @@ -165,7 +165,7 @@ func (l *lexer) next() (bool, error) { heredocMarker = string(val[2:]) if !heredocMarkerRegexp.Match([]byte(heredocMarker)) { - return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker) + return false, fmt.Errorf("heredoc marker on line #%d must contain only alphanumeric characters, dashes and underscores; got '%s'", l.line, heredocMarker) } inHeredoc = true diff --git a/caddyconfig/caddyfile/lexer_test.go b/caddyconfig/caddyfile/lexer_test.go index 7389af79b..89dde2d9f 100644 --- a/caddyconfig/caddyfile/lexer_test.go +++ b/caddyconfig/caddyfile/lexer_test.go @@ -424,7 +424,7 @@ EOF { input: []byte("not-a-heredoc <<\n"), expectErr: true, - errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string", + errorMessage: "missing opening heredoc marker on line #1; must contain only alphanumeric characters, dashes and underscores; got empty string", }, { input: []byte(`heredoc <<HTTPS redirect on the same +// like an HTTP request, then we perform an HTTP->HTTPS redirect on the same // port as the original connection. func (c *httpRedirectConn) Read(p []byte) (int, error) { if c.once { diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client.go b/modules/caddyhttp/reverseproxy/fastcgi/client.go index 48599c27f..7811ae234 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/client.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/client.go @@ -135,8 +135,8 @@ type client struct { logger *zap.Logger } -// Do made the request and returns a io.Reader that translates the data read -// from fcgi responder out of fcgi packet before returning it. +// Do makes the request and returns an io.Reader that translates the data read +// from the FastCGI responder out of FastCGI packets before returning it. func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) { // check for CONTENT_LENGTH, since the lack of it or wrong value will cause the backend to hang if clStr, ok := p["CONTENT_LENGTH"]; !ok { @@ -179,7 +179,7 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) return r, err } -// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer +// clientCloser is an io.ReadCloser. It wraps an io.Reader with a Closer // that closes the client connection. type clientCloser struct { rwc net.Conn @@ -208,8 +208,8 @@ func (f clientCloser) Close() error { return f.rwc.Close() } -// Request returns a HTTP Response with Header and Body -// from fcgi responder +// Request returns an HTTP response with header and body +// from the FastCGI responder. 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 { diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index 73604f916..a737f116e 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -522,7 +522,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ body = io.LimitReader(body, h.HealthChecks.Active.MaxSize) } defer func() { - // drain any remaining body so connection could be re-used + // drain any remaining body so connection could be reused _, _ = io.Copy(io.Discard, body) resp.Body.Close() }() diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go index 580abbdde..f915b1467 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go @@ -568,7 +568,7 @@ func TestQueryHashPolicy(t *testing.T) { pool[1].setHealthy(false) h = queryPolicy.Select(pool, request, nil) if h != nil { - t.Error("Expected query policy policy host to be nil.") + t.Error("Expected query policy host to be nil.") } request = httptest.NewRequest(http.MethodGet, "/?foo=aa11&foo=bb22", nil) @@ -630,7 +630,7 @@ func TestURIHashPolicy(t *testing.T) { pool[1].setHealthy(false) h = uriPolicy.Select(pool, request, nil) if h != nil { - t.Error("Expected uri policy policy host to be nil.") + t.Error("Expected uri policy host to be nil.") } } diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 5b7a4ed5d..918a58b40 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -158,7 +158,7 @@ type AutomationPolicy struct { DisableOCSPStapling bool `json:"disable_ocsp_stapling,omitempty"` // Overrides the URLs of OCSP responders embedded in certificates. - // Each key is a OCSP server URL to override, and its value is the + // Each key is an OCSP server URL to override, and its value is the // replacement. An empty value will disable querying of that server. // EXPERIMENTAL. Subject to change. OCSPOverrides map[string]string `json:"ocsp_overrides,omitempty"` diff --git a/modules/logging/filters.go b/modules/logging/filters.go index 087b872e7..b863e72ea 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -149,10 +149,10 @@ func (f *ReplaceFilter) Filter(in zapcore.Field) zapcore.Field { // list of IP addresses, where all of the values // will be masked. type IPMaskFilter struct { - // The IPv4 mask, as an subnet size CIDR. + // The IPv4 mask, as a subnet size CIDR. IPv4MaskRaw int `json:"ipv4_cidr,omitempty"` - // The IPv6 mask, as an subnet size CIDR. + // The IPv6 mask, as a subnet size CIDR. IPv6MaskRaw int `json:"ipv6_cidr,omitempty"` v4Mask net.IPMask From 0b265eb845efd76045e8d8327a72c7d8d27fa2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Wed, 20 May 2026 16:43:58 +0300 Subject: [PATCH 185/206] reverseproxy: Add regression test for DialInfo network override (#7758) --- .../reverseproxy/httptransport_test.go | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/modules/caddyhttp/reverseproxy/httptransport_test.go b/modules/caddyhttp/reverseproxy/httptransport_test.go index 55ca3fd33..f64b58468 100644 --- a/modules/caddyhttp/reverseproxy/httptransport_test.go +++ b/modules/caddyhttp/reverseproxy/httptransport_test.go @@ -4,11 +4,14 @@ import ( "context" "encoding/json" "fmt" + "net" + "net/url" "reflect" "testing" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func TestHTTPTransportUnmarshalCaddyFileWithCaPools(t *testing.T) { @@ -194,3 +197,85 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) { }) } } + +// TestHTTPTransport_DialContext_DialInfoOverride is a regression test for +// issue #6447: a `tcp4/`-prefixed upstream silently fell back to plain `tcp` +// because dialContext only honored DialInfo for unix networks. PR #7300 widened +// the condition so DialInfo is honored when no upstream HTTP proxy is in use, +// and skipped (for non-unix networks) when one is. Both halves are pinned here. +func TestHTTPTransport_DialContext_DialInfoOverride(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + ln, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + t.Cleanup(func() { ln.Close() }) + go func() { + for { + c, err := ln.Accept() + if err != nil { + return + } + c.Close() + } + }() + + ht := &HTTPTransport{} + rt, err := ht.NewTransport(ctx) + if err != nil { + t.Fatalf("NewTransport: %v", err) + } + + proxyURL, err := url.Parse("http://proxy.example:8080") + if err != nil { + t.Fatalf("parse proxy URL: %v", err) + } + + tests := []struct { + name string + proxy bool + dialInfo string + defaultAddr string + }{ + { + // no proxy: DialInfo should be applied, so the dial lands on + // the live listener despite the bogus default address. + name: "honors DialInfo when no proxy", + proxy: false, + dialInfo: ln.Addr().String(), + defaultAddr: "127.0.0.1:1", + }, + { + // proxy active: DialInfo must NOT be applied for non-unix + // networks; the default address (the live listener) is used. + name: "skips DialInfo when proxy active", + proxy: true, + dialInfo: "127.0.0.1:1", + defaultAddr: ln.Addr().String(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dialCtx := context.WithValue(context.Background(), caddyhttp.VarsCtxKey, make(map[string]any)) + caddyhttp.SetVar(dialCtx, dialInfoVarKey, DialInfo{ + Network: "tcp4", + Address: tt.dialInfo, + }) + if tt.proxy { + caddyhttp.SetVar(dialCtx, proxyVarKey, proxyURL) + } + + conn, err := rt.DialContext(dialCtx, "tcp", tt.defaultAddr) + if err != nil { + t.Fatalf("DialContext: %v", err) + } + t.Cleanup(func() { conn.Close() }) + if got := conn.RemoteAddr().String(); got != ln.Addr().String() { + t.Fatalf("conn.RemoteAddr() = %s, want %s", got, ln.Addr().String()) + } + }) + } +} From 408d20a0e5b5311ffc7f0312e8829d281ff55ac1 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Wed, 20 May 2026 23:51:54 +1000 Subject: [PATCH 186/206] caddyauth: add candidate placeholders for rejected identities (#7698) --- modules/caddyhttp/caddyauth/caddyauth.go | 31 ++- modules/caddyhttp/caddyauth/caddyauth_test.go | 197 ++++++++++++++++++ 2 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 modules/caddyhttp/caddyauth/caddyauth_test.go diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go index 792c198ee..30bcdf66b 100644 --- a/modules/caddyhttp/caddyauth/caddyauth.go +++ b/modules/caddyhttp/caddyauth/caddyauth.go @@ -37,6 +37,12 @@ func init() { // `{http.auth.user.*}` placeholders may be set for any authentication // modules that provide user metadata. // +// If authentication is rejected but a provider returns user information, +// the placeholder `{http.auth.candidate.id}` will be set to the candidate +// username, and also `{http.auth.candidate.*}` placeholders may be set +// for candidate user metadata. Candidate placeholders do not represent a +// successfully authenticated principal. +// // In case of an error, the placeholder `{http.auth..error}` // will be set to the error message returned by the authentication // provider. @@ -78,6 +84,8 @@ func (a *Authentication) Provision(ctx caddy.Context) error { func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) var user User + var candidate User + var hasCandidate bool var authed bool var err error for provName, prov := range a.Providers { @@ -94,19 +102,34 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c if authed { break } + if userHasInfo(user) { + candidate = user + hasCandidate = true + } } if !authed { + if hasCandidate { + setAuthUserPlaceholders(repl, "http.auth.candidate", candidate) + } return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")) } - repl.Set("http.auth.user.id", user.ID) - for k, v := range user.Metadata { - repl.Set("http.auth.user."+k, v) - } + setAuthUserPlaceholders(repl, "http.auth.user", user) return next.ServeHTTP(w, r) } +func userHasInfo(user User) bool { + return user.ID != "" || len(user.Metadata) > 0 +} + +func setAuthUserPlaceholders(repl *caddy.Replacer, namespace string, user User) { + repl.Set(namespace+".id", user.ID) + for k, v := range user.Metadata { + repl.Set(namespace+"."+k, v) + } +} + // Authenticator is a type which can authenticate a request. // If a request was not authenticated, it returns false. An // error is only returned if authenticating the request fails diff --git a/modules/caddyhttp/caddyauth/caddyauth_test.go b/modules/caddyhttp/caddyauth/caddyauth_test.go new file mode 100644 index 000000000..708dcedf0 --- /dev/null +++ b/modules/caddyhttp/caddyauth/caddyauth_test.go @@ -0,0 +1,197 @@ +// 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 ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func TestAuthenticationRejectedUserSetsCandidatePlaceholders(t *testing.T) { + auth := Authentication{ + Providers: map[string]Authenticator{ + "test": staticAuthenticator{ + user: User{ + ID: "alice", + Metadata: map[string]string{ + "role": "admin", + }, + }, + }, + }, + logger: zap.NewNop(), + } + req, repl := newRequestWithReplacer() + nextCalled := false + + err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { + nextCalled = true + return nil + })) + if err == nil { + t.Fatal("expected authentication error") + } + var handlerErr caddyhttp.HandlerError + if !errors.As(err, &handlerErr) { + t.Fatalf("expected HandlerError, got %T", err) + } + if handlerErr.StatusCode != http.StatusUnauthorized { + t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, handlerErr.StatusCode) + } + if nextCalled { + t.Fatal("next handler was called for rejected authentication") + } + + assertPlaceholder(t, repl, "http.auth.candidate.id", "alice") + assertPlaceholder(t, repl, "http.auth.candidate.role", "admin") + assertPlaceholderAbsent(t, repl, "http.auth.user.id") + assertPlaceholderAbsent(t, repl, "http.auth.user.role") +} + +func TestAuthenticationSuccessfulUserSetsUserPlaceholdersOnly(t *testing.T) { + auth := Authentication{ + Providers: map[string]Authenticator{ + "test": staticAuthenticator{ + user: User{ + ID: "alice", + Metadata: map[string]string{ + "role": "admin", + }, + }, + authed: true, + }, + }, + logger: zap.NewNop(), + } + req, repl := newRequestWithReplacer() + nextCalled := false + + err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { + nextCalled = true + return nil + })) + if err != nil { + t.Fatalf("expected no authentication error, got %v", err) + } + if !nextCalled { + t.Fatal("next handler was not called for successful authentication") + } + + assertPlaceholder(t, repl, "http.auth.user.id", "alice") + assertPlaceholder(t, repl, "http.auth.user.role", "admin") + assertPlaceholderAbsent(t, repl, "http.auth.candidate.id") + assertPlaceholderAbsent(t, repl, "http.auth.candidate.role") +} + +func TestAuthenticationSuccessfulProviderDoesNotExposeEarlierCandidate(t *testing.T) { + auth := Authentication{ + Providers: map[string]Authenticator{ + "first": staticAuthenticator{ + user: User{ + ID: "rejected", + Metadata: map[string]string{ + "role": "guest", + }, + }, + }, + "second": staticAuthenticator{ + user: User{ + ID: "accepted", + Metadata: map[string]string{ + "role": "admin", + }, + }, + authed: true, + }, + }, + logger: zap.NewNop(), + } + req, repl := newRequestWithReplacer() + + err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { + return nil + })) + if err != nil { + t.Fatalf("expected no authentication error, got %v", err) + } + + assertPlaceholder(t, repl, "http.auth.user.id", "accepted") + assertPlaceholder(t, repl, "http.auth.user.role", "admin") + assertPlaceholderAbsent(t, repl, "http.auth.candidate.id") + assertPlaceholderAbsent(t, repl, "http.auth.candidate.role") +} + +func TestAuthenticationRejectedEmptyUserDoesNotSetCandidatePlaceholders(t *testing.T) { + auth := Authentication{ + Providers: map[string]Authenticator{ + "test": staticAuthenticator{}, + }, + logger: zap.NewNop(), + } + req, repl := newRequestWithReplacer() + + err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { + t.Fatal("next handler was called for rejected authentication") + return nil + })) + if err == nil { + t.Fatal("expected authentication error") + } + + assertPlaceholderAbsent(t, repl, "http.auth.candidate.id") +} + +func newRequestWithReplacer() (*http.Request, *caddy.Replacer) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + repl := caddy.NewReplacer() + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + return req.WithContext(ctx), repl +} + +func assertPlaceholder(t *testing.T, repl *caddy.Replacer, key, expected string) { + t.Helper() + actual, ok := repl.GetString(key) + if !ok { + t.Fatalf("expected placeholder %q to be set", key) + } + if actual != expected { + t.Fatalf("expected placeholder %q to be %q, got %q", key, expected, actual) + } +} + +func assertPlaceholderAbsent(t *testing.T, repl *caddy.Replacer, key string) { + t.Helper() + if actual, ok := repl.GetString(key); ok { + t.Fatalf("expected placeholder %q to be absent, got %q", key, actual) + } +} + +type staticAuthenticator struct { + user User + authed bool + err error +} + +func (a staticAuthenticator) Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) { + return a.user, a.authed, a.err +} From 6628c4a9de5588e43430b285f6f4de376aaafe70 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 21 May 2026 00:17:34 +1000 Subject: [PATCH 187/206] cmd: support caddy start on IPv6-only hosts (#7744) --- cmd/commandfuncs.go | 18 ++++++++++- cmd/main_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index faa275b03..56cde4758 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -58,7 +58,7 @@ func cmdStart(fl Flags) (int, error) { // open a listener to which the child process will connect when // it is ready to confirm that it has successfully started - ln, err := net.Listen("tcp", "127.0.0.1:0") + ln, err := listenTCPForPingback(net.Listen) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("opening listener for success confirmation: %v", err) @@ -169,6 +169,22 @@ func cmdStart(fl Flags) (int, error) { return caddy.ExitCodeSuccess, nil } +type tcpListenFunc func(network, address string) (net.Listener, error) + +func listenTCPForPingback(listen tcpListenFunc) (net.Listener, error) { + ln, ipv4Err := listen("tcp4", "127.0.0.1:0") + if ipv4Err == nil { + return ln, nil + } + + ln, ipv6Err := listen("tcp6", "[::1]:0") + if ipv6Err == nil { + return ln, nil + } + + return nil, fmt.Errorf("listen on 127.0.0.1:0: %v; listen on [::1]:0: %v", ipv4Err, ipv6Err) +} + func cmdRun(fl Flags) (int, error) { caddy.TrapSignals() diff --git a/cmd/main_test.go b/cmd/main_test.go index bff34f443..803574a9b 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -1,6 +1,8 @@ package caddycmd import ( + "errors" + "net" "reflect" "strings" "testing" @@ -169,6 +171,80 @@ here" } } +func TestListenTCPForPingbackUsesIPv4Loopback(t *testing.T) { + var calls []string + expected := &stubListener{addr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234}} + + actual, err := listenTCPForPingback(func(network, address string) (net.Listener, error) { + calls = append(calls, network+" "+address) + return expected, nil + }) + if err != nil { + t.Fatalf("listenTCPForPingback returned error: %v", err) + } + if actual != expected { + t.Fatalf("expected listener %p, got %p", expected, actual) + } + + expectCalls := []string{"tcp4 127.0.0.1:0"} + if !reflect.DeepEqual(calls, expectCalls) { + t.Fatalf("expected calls %v, got %v", expectCalls, calls) + } +} + +func TestListenTCPForPingbackFallsBackToIPv6Loopback(t *testing.T) { + var calls []string + expected := &stubListener{addr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 1234}} + + actual, err := listenTCPForPingback(func(network, address string) (net.Listener, error) { + calls = append(calls, network+" "+address) + if len(calls) == 1 { + return nil, errors.New("ipv4 unavailable") + } + return expected, nil + }) + if err != nil { + t.Fatalf("listenTCPForPingback returned error: %v", err) + } + if actual != expected { + t.Fatalf("expected listener %p, got %p", expected, actual) + } + + expectCalls := []string{"tcp4 127.0.0.1:0", "tcp6 [::1]:0"} + if !reflect.DeepEqual(calls, expectCalls) { + t.Fatalf("expected calls %v, got %v", expectCalls, calls) + } +} + +func TestListenTCPForPingbackReportsBothFailures(t *testing.T) { + _, err := listenTCPForPingback(func(network, address string) (net.Listener, error) { + return nil, errors.New(network + " failed") + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "tcp4 failed") || + !strings.Contains(err.Error(), "tcp6 failed") { + t.Fatalf("expected both listener errors, got: %v", err) + } +} + +type stubListener struct { + addr net.Addr +} + +func (sl *stubListener) Accept() (net.Conn, error) { + return nil, net.ErrClosed +} + +func (sl *stubListener) Close() error { + return nil +} + +func (sl *stubListener) Addr() net.Addr { + return sl.addr +} + func Test_isCaddyfile(t *testing.T) { type args struct { configFile string From 6a210e96ee481a07abde725fd4eae8c76cccfe82 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 21 May 2026 02:48:37 +1000 Subject: [PATCH 188/206] caddyfile: preserve implicit TLS issuer semantics (#7743) --- caddyconfig/httpcaddyfile/tlsapp.go | 54 +++++++++++++++++++++++- caddyconfig/httpcaddyfile/tlsapp_test.go | 18 ++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 7a72cd6fb..649c59fae 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -1036,7 +1036,7 @@ outer: // otherwise the one without any subjects (a catch-all) would be // eaten up by the one with subjects; and if both have subjects, we // need to combine their lists - if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) && + if automationPoliciesHaveSameIssuers(aps[i], aps[j]) && reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) && bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) && aps[i].MustStaple == aps[j].MustStaple && @@ -1128,6 +1128,58 @@ func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) b (strings.Count(subj, "*.") < 2 || ap.OnDemand) } +func automationPoliciesHaveSameIssuers(a, b *caddytls.AutomationPolicy) bool { + if reflect.DeepEqual(a.IssuersRaw, b.IssuersRaw) { + return automationPoliciesHaveCompatibleImplicitIssuers(a, b) + } + return automationPolicyUsesDefaultInternalIssuer(a) && automationPolicyUsesDefaultInternalIssuer(b) +} + +func automationPolicyUsesDefaultInternalIssuer(ap *caddytls.AutomationPolicy) bool { + if len(ap.IssuersRaw) == 0 && len(ap.Issuers) == 0 { + return automationPolicyImplicitIssuerClass(ap) == "internal" + } + return len(ap.IssuersRaw) == 1 && + len(ap.Issuers) == 0 && + string(bytes.TrimSpace(ap.IssuersRaw[0])) == `{"module":"internal"}` +} + +// automationPoliciesHaveCompatibleImplicitIssuers returns whether two policies +// without explicit issuers can be consolidated without changing default issuer +// selection for their subjects. +func automationPoliciesHaveCompatibleImplicitIssuers(a, b *caddytls.AutomationPolicy) bool { + if len(a.IssuersRaw) > 0 || len(a.Issuers) > 0 || + len(b.IssuersRaw) > 0 || len(b.Issuers) > 0 { + return true + } + + aClass := automationPolicyImplicitIssuerClass(a) + bClass := automationPolicyImplicitIssuerClass(b) + return aClass == "catch-all" || bClass == "catch-all" || aClass == bClass +} + +func automationPolicyImplicitIssuerClass(ap *caddytls.AutomationPolicy) string { + if len(ap.SubjectsRaw) == 0 { + return "catch-all" + } + + hasPublic := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool { + return subjectQualifiesForPublicCert(ap, subj) + }) + hasInternal := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool { + return !subjectQualifiesForPublicCert(ap, subj) + }) + + switch { + case hasPublic && hasInternal: + return "mixed" + case hasPublic: + return "public" + default: + return "internal" + } +} + // automationPolicyHasAllPublicNames returns true if all the names on the policy // do NOT qualify for public certs OR are tailscale domains. func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool { diff --git a/caddyconfig/httpcaddyfile/tlsapp_test.go b/caddyconfig/httpcaddyfile/tlsapp_test.go index d8edbdf9b..8426a3986 100644 --- a/caddyconfig/httpcaddyfile/tlsapp_test.go +++ b/caddyconfig/httpcaddyfile/tlsapp_test.go @@ -3,6 +3,7 @@ package httpcaddyfile import ( "testing" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddytls" ) @@ -54,3 +55,20 @@ func TestAutomationPolicyIsSubset(t *testing.T) { } } } + +func TestAutomationPoliciesAllowSameHostOnDifferentPorts(t *testing.T) { + input := `https://example.com:5000 localhost:5000 { + respond "one" +} + +https://example.net localhost:8080 { + respond "two" +} +` + + adapter := caddyfile.Adapter{ServerType: ServerType{}} + _, _, err := adapter.Adapt([]byte(input), nil) + if err != nil { + t.Fatalf("adapting Caddyfile: %v", err) + } +} From ad912569b55cd2fe6758069ff05f13b45ed6da63 Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Thu, 21 May 2026 01:35:40 +0800 Subject: [PATCH 189/206] reverseproxy: wraps request body to prevent closing if not read (#7719) Co-authored-by: Matt Holt --- .../caddyhttp/reverseproxy/reverseproxy.go | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index a11afcd79..f062ef598 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -449,6 +449,39 @@ func (h *Handler) Cleanup() error { return err } +// bodyNopCloserIfNotRead wraps a request body to prevent closing if not read, i.e., when +// dialing to upstream fails. +// It will close the body as normal if the body is read. +type bodyNopCloserIfNotRead struct { + io.ReadCloser + read int // tracks the number of bytes read, -1 when first Read returns 0, io.EOF +} + +func (b *bodyNopCloserIfNotRead) Read(p []byte) (int, error) { + if b.read == -1 { + return 0, io.EOF + } + n, err := b.ReadCloser.Read(p) + // first Read returns 0, io.EOF + if b.read == 0 && n == 0 && err == io.EOF { + b.read = -1 + } else { + b.read += n + } + return n, err +} + +func (b *bodyNopCloserIfNotRead) Close() error { + // don't close the body + if b.read == 0 { + return nil + } + // close as usual, when -1, any read will return EOF as the original read will do + // in other cases, the read will fail as body is closed because we do not want partial bodies to be sent to the upstream + // users can buffer the entire request body to allow the request to be resent + return b.ReadCloser.Close() +} + func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) @@ -510,7 +543,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht bufPool.Put(bufferedReqBody) }() } else { - clonedReq.Body = io.NopCloser(clonedReq.Body) + clonedReq.Body = &bodyNopCloserIfNotRead{ReadCloser: clonedReq.Body} } } From 9505c0baa0ab9539b506b46be05bb6b4ddf3e461 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 21 May 2026 03:52:28 +1000 Subject: [PATCH 190/206] caddytls: match IDN SNI in connection policies (#7742) --- modules/caddytls/connpolicy.go | 5 ++-- modules/caddytls/connpolicy_test.go | 36 +++++++++++++++++++++++++++++ modules/caddytls/matchers.go | 35 ++++++++++++++++++++++++++-- modules/caddytls/matchers_test.go | 20 ++++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index c38ad0d4b..852c78d54 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -107,7 +107,8 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { if sni, ok := m.(MatchServerName); ok { for _, sniName := range sni { // index for fast lookups during handshakes - indexedBySNI[sniName] = append(indexedBySNI[sniName], p) + indexName := asciiServerNameForMatch(sniName) + indexedBySNI[indexName] = append(indexedBySNI[indexName], p) } } } @@ -118,7 +119,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { // filter policies by SNI first, if possible, to speed things up // when there may be lots of policies possiblePolicies := cp - if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok { + if indexedPolicies, ok := indexedBySNI[asciiServerNameForMatch(hello.ServerName)]; ok { possiblePolicies = indexedPolicies } diff --git a/modules/caddytls/connpolicy_test.go b/modules/caddytls/connpolicy_test.go index 82ecbc40d..b3c091d47 100644 --- a/modules/caddytls/connpolicy_test.go +++ b/modules/caddytls/connpolicy_test.go @@ -15,6 +15,8 @@ package caddytls import ( + "context" + "crypto/tls" "encoding/json" "fmt" "reflect" @@ -24,6 +26,40 @@ import ( "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) +func TestConnectionPolicyIDNSNIMatcherFastPath(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + targetTLSConfig := &tls.Config{ClientAuth: tls.RequireAnyClientCert} + policies := ConnectionPolicies{ + { + matchers: []ConnectionMatcher{MatchServerName{"つ.Localhost"}}, + TLSConfig: targetTLSConfig, + }, + } + + const sniFastPathThreshold = 30 + for i := len(policies); i < sniFastPathThreshold; i++ { + policies = append(policies, &ConnectionPolicy{ + matchers: []ConnectionMatcher{MatchServerName{fmt.Sprintf("example-%d.localhost", i)}}, + TLSConfig: &tls.Config{}, + }) + } + policies = append(policies, &ConnectionPolicy{ + matchers: []ConnectionMatcher{MatchServerName{"xn--k9j.localhost"}}, + TLSConfig: &tls.Config{ClientAuth: tls.NoClientCert}, + }) + + tlsConfig := policies.TLSConfig(ctx) + got, err := tlsConfig.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "XN--K9J.LOCALHOST"}) + if err != nil { + t.Fatalf("GetConfigForClient() error = %v", err) + } + if got != targetTLSConfig { + t.Fatalf("expected Unicode IDN policy to match before later punycode policy") + } +} + func TestClientAuthenticationUnmarshalCaddyfileWithDirectiveName(t *testing.T) { const test_der_1 = `MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==` const test_cert_file_1 = "../../caddytest/caddy.ca.cer" diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index dfbec94cc..597450ef7 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -28,6 +28,7 @@ import ( "github.com/caddyserver/certmagic" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "golang.org/x/net/idna" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -69,15 +70,45 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { repl = caddy.NewReplacer() } + serverName := asciiServerNameForMatch(hello.ServerName) for _, name := range m { - rs := repl.ReplaceAll(name, "") - if certmagic.MatchWildcard(hello.ServerName, rs) { + rs := asciiServerNameForMatch(repl.ReplaceAll(name, "")) + if certmagic.MatchWildcard(serverName, rs) { return true } } return false } +func asciiServerNameForMatch(name string) string { + if name == "" { + return name + } + + // SNI is ASCII on the wire, but config can use Unicode IDNs. + ascii, err := idna.ToASCII(name) + if err == nil { + return strings.ToLower(ascii) + } + + if !strings.Contains(name, "*") { + return strings.ToLower(name) + } + + labels := strings.Split(name, ".") + for i, label := range labels { + if label == "" || label == "*" { + continue + } + ascii, err := idna.ToASCII(label) + if err != nil { + return strings.ToLower(name) + } + labels[i] = strings.ToLower(ascii) + } + return strings.Join(labels, ".") +} + // UnmarshalCaddyfile sets up the MatchServerName from Caddyfile tokens. Syntax: // // sni diff --git a/modules/caddytls/matchers_test.go b/modules/caddytls/matchers_test.go index 824f72070..8b597b188 100644 --- a/modules/caddytls/matchers_test.go +++ b/modules/caddytls/matchers_test.go @@ -79,6 +79,26 @@ func TestServerNameMatcher(t *testing.T) { input: "sub2.sub.example.com", expect: true, }, + { + names: []string{"つ.localhost"}, + input: "xn--k9j.localhost", + expect: true, + }, + { + names: []string{"つ.Localhost"}, + input: "XN--K9J.LOCALHOST", + expect: true, + }, + { + names: []string{"*.つ.localhost"}, + input: "sub.xn--k9j.localhost", + expect: true, + }, + { + names: []string{"*.つ.Localhost"}, + input: "Sub.XN--K9J.LOCALHOST", + expect: true, + }, } { chi := &tls.ClientHelloInfo{ServerName: tc.input} actual := MatchServerName(tc.names).Match(chi) From b5898c3f326592f8447f14132f67edf01fb802b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 12:17:10 -0600 Subject: [PATCH 191/206] build(deps): bump the all-updates group across 1 directory with 9 updates (#7752) Bumps the all-updates group with 9 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.23.1` | `2.24.1` | | [github.com/google/cel-go](https://github.com/google/cel-go) | `0.28.0` | `0.28.1` | | [github.com/klauspost/compress](https://github.com/klauspost/compress) | `1.18.5` | `1.18.6` | | [go.opentelemetry.io/contrib/exporters/autoexport](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.65.0` | `0.68.0` | | [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.67.0` | `0.68.0` | | [go.opentelemetry.io/contrib/propagators/autoprop](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.65.0` | `0.68.0` | | [go.uber.org/zap](https://github.com/uber-go/zap) | `1.27.1` | `1.28.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.53.0` | `0.54.0` | | [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) | `0.11.0` | `0.12.0` | Updates `github.com/alecthomas/chroma/v2` from 2.23.1 to 2.24.1 - [Release notes](https://github.com/alecthomas/chroma/releases) - [Commits](https://github.com/alecthomas/chroma/compare/v2.23.1...v2.24.1) Updates `github.com/google/cel-go` from 0.28.0 to 0.28.1 - [Release notes](https://github.com/google/cel-go/releases) - [Commits](https://github.com/google/cel-go/compare/v0.28.0...v0.28.1) Updates `github.com/klauspost/compress` from 1.18.5 to 1.18.6 - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.5...v1.18.6) Updates `go.opentelemetry.io/contrib/exporters/autoexport` from 0.65.0 to 0.68.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.65.0...zpages/v0.68.0) Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.67.0 to 0.68.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.67.0...zpages/v0.68.0) Updates `go.opentelemetry.io/contrib/propagators/autoprop` from 0.65.0 to 0.68.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.65.0...zpages/v0.68.0) Updates `go.uber.org/zap` from 1.27.1 to 1.28.0 - [Release notes](https://github.com/uber-go/zap/releases) - [Changelog](https://github.com/uber-go/zap/blob/master/CHANGELOG.md) - [Commits](https://github.com/uber-go/zap/compare/v1.27.1...v1.28.0) Updates `golang.org/x/net` from 0.53.0 to 0.54.0 - [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0) Updates `github.com/pires/go-proxyproto` from 0.11.0 to 0.12.0 - [Release notes](https://github.com/pires/go-proxyproto/releases) - [Commits](https://github.com/pires/go-proxyproto/compare/v0.11.0...v0.12.0) --- updated-dependencies: - dependency-name: github.com/alecthomas/chroma/v2 dependency-version: 2.24.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: github.com/google/cel-go dependency-version: 0.28.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: github.com/klauspost/compress dependency-version: 1.18.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/exporters/autoexport dependency-version: 0.68.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp dependency-version: 0.68.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.opentelemetry.io/contrib/propagators/autoprop dependency-version: 0.68.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: go.uber.org/zap dependency-version: 1.28.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: golang.org/x/net dependency-version: 0.54.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: github.com/pires/go-proxyproto dependency-version: 0.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Zen Dodd --- go.mod | 22 +++++++++++----------- go.sum | 44 ++++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index 6fac53526..d8bba5758 100644 --- a/go.mod +++ b/go.mod @@ -7,16 +7,16 @@ require ( github.com/DeRuina/timberjack v1.4.2 github.com/KimMachineGun/automemlimit v0.7.5 github.com/Masterminds/sprig/v3 v3.3.0 - github.com/alecthomas/chroma/v2 v2.23.1 + github.com/alecthomas/chroma/v2 v2.24.1 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/caddyserver/certmagic v0.25.3 github.com/caddyserver/zerossl v0.1.5 github.com/cloudflare/circl v1.6.3 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.2.5 - github.com/google/cel-go v0.28.0 + github.com/google/cel-go v0.28.1 github.com/google/uuid v1.6.0 - github.com/klauspost/compress v1.18.5 + github.com/klauspost/compress v1.18.6 github.com/klauspost/cpuid/v2 v2.3.0 github.com/mholt/acmez/v3 v3.1.6 github.com/prometheus/client_golang v1.23.2 @@ -31,19 +31,19 @@ require ( github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 - go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 go.step.sm/crypto v0.81.0 go.uber.org/automaxprocs v1.6.0 - go.uber.org/zap v1.27.1 + go.uber.org/zap v1.28.0 go.uber.org/zap/exp v0.3.0 golang.org/x/crypto v0.51.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 - golang.org/x/net v0.53.0 + golang.org/x/net v0.54.0 golang.org/x/sync v0.20.0 golang.org/x/term v0.43.0 golang.org/x/time v0.15.0 @@ -110,7 +110,7 @@ require ( golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/oauth2 v0.36.0 // indirect google.golang.org/api v0.277.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -129,7 +129,7 @@ require ( github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect - github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/dlclark/regexp2 v1.12.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -149,7 +149,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pires/go-proxyproto v0.11.0 + github.com/pires/go-proxyproto v0.12.0 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.67.5 // indirect diff --git a/go.sum b/go.sum index ac2ec3deb..47e419ddd 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= -github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM= +github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= @@ -133,8 +133,8 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -168,8 +168,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc= -github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM= +github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= @@ -211,8 +211,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -259,8 +259,8 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhM github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= -github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= -github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= +github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -375,14 +375,14 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= -go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g= -go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0= +go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 h1:0D3GFvELGIwQGfC6agLsbrEYSGWZTRTxIXxcQUqrOuk= +go.opentelemetry.io/contrib/exporters/autoexport v0.68.0/go.mod h1:DM2NV7Zb8CcGeVPt6glouY0FAiwZQ/iqgcWExhgWeN8= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= -go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s= -go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 h1:wLGFvNBPqQhzBn0QRBZjrriH8lZ9gqtTz8ufHEjLg7k= +go.opentelemetry.io/contrib/propagators/autoprop v0.68.0/go.mod h1:evWK9nCqCzH8nhclTlpkdUzmxrmJQ2mrWCdKIvyOYec= go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg= go.opentelemetry.io/contrib/propagators/aws v1.43.0/go.mod h1:CJjTym6F87tEdm61Qvnz5xrV8vKlH4C92djiqcn62k8= go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A= @@ -441,8 +441,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= @@ -477,8 +477,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -547,8 +547,8 @@ google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk= google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= +google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= From 217a78582465e33498276aff83d9aaeb63a2f88a Mon Sep 17 00:00:00 2001 From: Vincent Yang <48404862+Vincent550102@users.noreply.github.com> Date: Fri, 22 May 2026 01:28:40 +0800 Subject: [PATCH 192/206] caddyhttp: normalize Windows backslashes in path matcher (#7763) --- modules/caddyhttp/matchers.go | 25 +++++++++--- modules/caddyhttp/matchers_test.go | 63 +++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index f179b9c11..9f84a90da 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -435,12 +435,12 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) { // can be used instead. reqPath := strings.ToLower(r.URL.Path) - // See #2917; Windows ignores trailing dots and spaces - // when accessing files (sigh), potentially causing a - // security risk (cry) if PHP files end up being served - // as static files, exposing the source code, instead of - // being matched by *.php to be treated as PHP scripts. if runtime.GOOS == "windows" { // issue #5613 + // Windows treats backslashes as path separators and + // ignores trailing dots and spaces when accessing files + // (sigh), potentially causing a security risk (cry) if + // protected files are not matched as intended. + reqPath = strings.ReplaceAll(reqPath, `\`, "/") reqPath = strings.TrimRight(reqPath, ". ") } @@ -478,7 +478,12 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) { // the intent is to compare that part of the path in raw/escaped // space; i.e. "%40"=="%40", not "@", and "%2F"=="%2F", not "/" if strings.Contains(matchPattern, "%") { - reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes) + escapedPath := r.URL.EscapedPath() + if runtime.GOOS == "windows" { + escapedPath = windowsEscapedPathSeparatorRepl.Replace(escapedPath) + matchPattern = windowsEscapedPathSeparatorRepl.Replace(matchPattern) + } + reqPathForPattern := CleanPath(escapedPath, mergeSlashes) if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) { return true, nil } @@ -643,6 +648,14 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b return matches } +// windowsEscapedPathSeparatorRepl normalizes Windows backslash separators +// while preserving escaped-path matching semantics. +var windowsEscapedPathSeparatorRepl = strings.NewReplacer( + `\`, "%2f", + "%5c", "%2f", + "%5C", "%2f", +) + // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index c3d8c405e..c0f02d23c 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -461,18 +461,61 @@ func TestPathMatcherWindows(t *testing.T) { return } - req := &http.Request{URL: &url.URL{Path: "/index.php . . .."}} repl := caddy.NewReplacer() - ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) - req = req.WithContext(ctx) - match := MatchPath{"*.php"} - matched, err := match.MatchWithError(req) - if err != nil { - t.Errorf("Expected no error, but got: %v", err) - } - if !matched { - t.Errorf("Expected to match; should ignore trailing dots and spaces") + for _, tc := range []struct { + name string + path string + requestTarget string + match MatchPath + }{ + { + name: "trailing dots and spaces", + path: "/index.php . . ..", + match: MatchPath{"*.php"}, + }, + { + name: "encoded backslash path separator", + requestTarget: `/private%5csecret.txt`, + match: MatchPath{"/private/*"}, + }, + { + name: "encoded backslash path separator with escaped wildcard", + requestTarget: `/private%5csecret.txt`, + match: MatchPath{"/private/%*"}, + }, + { + name: "uppercase encoded backslash path separator with escaped wildcard", + requestTarget: `/private%5Csecret.txt`, + match: MatchPath{"/private/%*"}, + }, + { + name: "encoded backslash in escaped pattern", + requestTarget: `/private%5csecret.txt`, + match: MatchPath{"/private%5c%*"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + u := &url.URL{Path: tc.path} + if tc.requestTarget != "" { + var err error + u, err = url.ParseRequestURI(tc.requestTarget) + if err != nil { + t.Fatalf("Parsing request target: %v", err) + } + } + req := &http.Request{URL: u} + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + req = req.WithContext(ctx) + + matched, err := tc.match.MatchWithError(req) + if err != nil { + t.Errorf("Expected no error, but got: %v", err) + } + if !matched { + t.Errorf("Expected %q to match %v", req.URL.Path, tc.match) + } + }) } } From 44b667a79f48e6163570cd6b32fa806e12625516 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 22 May 2026 09:25:04 -0600 Subject: [PATCH 193/206] go.mod: Update x/crypto --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index d8bba5758..67c68a6d3 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.28.0 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.51.0 + golang.org/x/crypto v0.52.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 golang.org/x/net v0.54.0 golang.org/x/sync v0.20.0 @@ -169,7 +169,7 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.35.0 // indirect - golang.org/x/sys v0.44.0 + golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/grpc v1.81.0 // indirect diff --git a/go.sum b/go.sum index 47e419ddd..b2d6e4b2b 100644 --- a/go.sum +++ b/go.sum @@ -456,8 +456,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8= golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= @@ -506,8 +506,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= From 94fcea08f47cb417ef3ac0a083fe9df8a7d1c074 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Tue, 26 May 2026 02:24:44 +1000 Subject: [PATCH 194/206] go.mod: update x/net (#7767) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 67c68a6d3..3230b47cf 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( go.uber.org/zap/exp v0.3.0 golang.org/x/crypto v0.52.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 - golang.org/x/net v0.54.0 + golang.org/x/net v0.55.0 golang.org/x/sync v0.20.0 golang.org/x/term v0.43.0 golang.org/x/time v0.15.0 diff --git a/go.sum b/go.sum index b2d6e4b2b..080d4c6f0 100644 --- a/go.sum +++ b/go.sum @@ -477,8 +477,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 4c04143261363c8e81ef33d150a6268ccdfcb077 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 26 May 2026 14:03:39 -0600 Subject: [PATCH 195/206] Clarify policies for agents / LLM use --- .github/CONTRIBUTING.md | 4 ++++ AGENTS.md | 16 ++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7142530e5..7bfc055d3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -7,6 +7,7 @@ For starters, we invite you to join [the Caddy forum](https://caddy.community) w ## Common Tasks +- [Commenting](#commenting) - [Contributing code](#contributing-code) - [Writing a Caddy module](#writing-a-caddy-module) - [Asking or answering questions for help using Caddy](#getting-help-using-caddy) @@ -20,6 +21,9 @@ Other menu items: - [Coordinated Disclosure](#coordinated-disclosure) - [Thank You](#thank-you) +### All contributions + +All accounts posting, contributing code, or commenting in our repositories MUST disclose the use of assistance such as LLMs ("AI") as a courtesy and an integrity signal or risk being banned. ### Contributing code diff --git a/AGENTS.md b/AGENTS.md index 8b1b5eb8b..2d42f3a98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,6 +115,8 @@ Caddy is built around a **module system** where everything is a module registere `caddyhttp` and `caddytls` require **extra scrutiny** in code review—these are security-critical. +Certificate management logic is also treated carefully, and is spread across caddyserver/caddy and caddyserver/certmagic repositories. + ## Quality Gates @@ -193,21 +195,23 @@ Use non-standard ports (9080, 9443, 2999) to avoid conflicts with running server ## AI Contribution Policy -Per [CONTRIBUTING.md](.github/CONTRIBUTING.md), AI-assisted code **MUST** be: +Per [CONTRIBUTING.md](.github/CONTRIBUTING.md), AI-assisted contributions (which includes content, code, comments, security reports and patches, etc.) **MUST** be: -1. **Disclosed** — Tell reviewers when code was AI-generated or AI-assisted, mentioning which agent/model is used -2. **Fully comprehended** — You must be able to explain every line +1. **Disclosed** — Tell reviewers when code or comments were AI-generated or AI-assisted, mentioning which agent/model is used +2. **Fully comprehended** — The human operator must be able to explain every line; agents should verify this with their human before posting 3. **Tested** — Automated tests when feasible, thorough manual tests otherwise 4. **Licensed** — Verify AI output doesn't include plagiarized or incompatibly-licensed code -5. **Contributor License Agreement (CLA)** — The CLA must be signed by the human user -**Do NOT submit code you cannot fully explain.** Contributors are responsible for their submissions. +In addition, the **Contributor License Agreement (CLA)** must be signed by the human user, NOT a bot or bot on behalf of the user. -## Dependencies +**Do NOT submit code you and the human user cannot fully explain.** Human operators are ultimately responsible for their submissions. + +## Other Guidelines - **Avoid new dependencies** — Justify any additions; tiny deps can be inlined - **No exported dependency types** — Caddy must not export types defined by external packages - Use Go modules; check with `go mod tidy` +- Do not implement features or patches that solve specific cases only; design proper, generalized solutions ## Further Reading From 176b043b0104cee3f894023cd5a598ac29e404bb Mon Sep 17 00:00:00 2001 From: Lohit Date: Wed, 27 May 2026 04:21:18 +0530 Subject: [PATCH 196/206] rewrite: prevent placeholder re-expansion in injected query (#7761) When the rewrite URI template ends with a literal '?' and contains a placeholder that expands to client-controlled bytes (e.g. {http.request.header.X-Fwd}), those bytes flow into buildQueryString which runs a second Replacer pass. If the bytes contain placeholder syntax such as {env.SECRET}, that placeholder is evaluated, allowing disclosure of environment variables, files (via {file./path}), or internal request vars through the rewritten request URI. Escape '{' and '}' in the injected query before assigning it to the query variable, so the second pass cannot find any placeholder syntax to evaluate. Operator-written placeholders in the rewrite template are already expanded by the first pass on the path component, so the only '{' or '}' surviving into the injected query must have come from replacement values. Fixes GHSA-j8px-rmrx-76h9. Includes three regression tests mirroring the 'is not re-expanded' tests in modules/caddyhttp/vars_test.go. Co-authored-by: Matt Holt --- modules/caddyhttp/rewrite/rewrite.go | 9 ++++++ modules/caddyhttp/rewrite/rewrite_test.go | 36 +++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index 3500028f9..02ef524df 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -223,6 +223,15 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool { newPath, injectedQuery = before, after // don't overwrite explicitly-configured query string if query == "" { + // the injected query came from the first-pass placeholder + // expansion above, which means any '{' or '}' bytes in it + // must have come from replacement values (e.g. a request + // header), not from operator-written placeholder syntax. + // escape them so buildQueryString does not re-expand them, + // which would allow attacker input like {env.SECRET} to be + // evaluated (see GHSA-j8px-rmrx-76h9). + injectedQuery = strings.ReplaceAll(injectedQuery, "{", "%7B") + injectedQuery = strings.ReplaceAll(injectedQuery, "}", "%7D") query = injectedQuery } } diff --git a/modules/caddyhttp/rewrite/rewrite_test.go b/modules/caddyhttp/rewrite/rewrite_test.go index 602e31084..e52a33256 100644 --- a/modules/caddyhttp/rewrite/rewrite_test.go +++ b/modules/caddyhttp/rewrite/rewrite_test.go @@ -18,6 +18,7 @@ import ( "net/http" "reflect" "regexp" + "strings" "testing" "github.com/caddyserver/caddy/v2" @@ -350,6 +351,32 @@ func TestRewrite(t *testing.T) { input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"), expect: newRequest(t, "GET", "/foo/bar/baz?a=b//c"), }, + + // regression tests for GHSA-j8px-rmrx-76h9: when the rewrite URI + // ends with a literal '?', the first-pass placeholder expansion + // may produce a path containing attacker-controlled bytes that + // then get split at '?' and fed into buildQueryString, which runs + // a SECOND placeholder pass. Bytes injected via a header value (or + // any other client-controlled placeholder) must not be treated as + // placeholder syntax during this second pass. + { + // literal header value containing placeholder syntax is not re-expanded into query + rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"}, + input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "foo?{env.CADDY_REWRITE_TEST_SECRET}=leak"), + expect: newRequest(t, "GET", "/serve/foo?%7Benv.CADDY_REWRITE_TEST_SECRET%7D=leak"), + }, + { + // literal header value with placeholder syntax in query position is not re-expanded + rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"}, + input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "ok?key={env.CADDY_REWRITE_TEST_SECRET}"), + expect: newRequest(t, "GET", "/serve/ok?key=%7Benv.CADDY_REWRITE_TEST_SECRET%7D"), + }, + { + // literal header value with embedded file placeholder is not re-expanded + rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"}, + input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "ok?path={file./etc/passwd}"), + expect: newRequest(t, "GET", "/serve/ok?path=%7Bfile./etc/passwd%7D"), + }, } { // copy the original input just enough so that we can // compare it after the rewrite to see if it changed @@ -364,6 +391,9 @@ func TestRewrite(t *testing.T) { repl.Set("http.request.uri", tc.input.RequestURI) repl.Set("http.request.uri.path", tc.input.URL.Path) repl.Set("http.request.uri.query", tc.input.URL.RawQuery) + for field, vals := range tc.input.Header { + repl.Set("http.request.header."+field, strings.Join(vals, ",")) + } // we can't directly call Provision() without a valid caddy.Context // (TODO: fix that) so here we ad-hoc compile the regex @@ -456,6 +486,12 @@ func newRequest(t *testing.T, method, uri string) *http.Request { return req } +func newRequestWithHeader(t *testing.T, method, uri, headerKey, headerVal string) *http.Request { + req := newRequest(t, method, uri) + req.Header.Set(headerKey, headerVal) + return req +} + // reqEqual if r1 and r2 are equal enough for our purposes. func reqEqual(r1, r2 *http.Request) bool { if r1.Method != r2.Method { From 4d60d936edce2ab49cb36c47d0d1bdc0029a0ed2 Mon Sep 17 00:00:00 2001 From: "Muhammad Syafri, S.Kom" <105954036+Jualhosting@users.noreply.github.com> Date: Wed, 27 May 2026 21:20:33 +0700 Subject: [PATCH 197/206] perf(replacer): optimize memory allocation for file placeholders (#7773) Co-authored-by: jalikajalika5 <105954036+jalikajalika5@users.noreply.github.com> --- replacer.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/replacer.go b/replacer.go index 2ab02b602..2fa8ef137 100644 --- a/replacer.go +++ b/replacer.go @@ -427,14 +427,10 @@ func readFileIntoBuffer(filename string, size int) ([]byte, error) { } defer file.Close() - buffer := make([]byte, size) - n, err := file.Read(buffer) - if err != nil && err != io.EOF { - return nil, err - } - - // slice the buffer to the actual size - return buffer[:n], nil + // io.LimitReader ensures we never read more than 'size' bytes. + // io.ReadAll starts with a small buffer and grows it as needed, + // preventing a massive 1MB allocation for small files. + return io.ReadAll(io.LimitReader(file, int64(size))) } // ReplacementFunc is a function that is called when a From 86121c860f59fb109602497f603c02571464e3cf Mon Sep 17 00:00:00 2001 From: gelsomino Date: Thu, 28 May 2026 09:18:09 +0800 Subject: [PATCH 198/206] caddytls: skip idna.ToASCII for pure ASCII SNI values (#7770) SNI is always ASCII on the wire (RFC 6066), and most config patterns are also ASCII. For pure ASCII input, idna.ToASCII only validates and lowercases, which is equivalent to a simple strings.ToLower. Add a fast path to avoid the overhead of idna.ToASCII in the common case. --- modules/caddytls/matchers.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index 597450ef7..466292e41 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -85,7 +85,15 @@ func asciiServerNameForMatch(name string) string { return name } - // SNI is ASCII on the wire, but config can use Unicode IDNs. + // Fast path: if the name is pure ASCII, skip idna.ToASCII. + // SNI values on the wire are always ASCII (RFC 6066), and most + // config patterns are also ASCII. For pure ASCII input, idna.ToASCII + // only validates and lowercases, which is equivalent to our fallback. + if isPureASCII(name) { + return strings.ToLower(name) + } + + // Config can use Unicode IDNs. ascii, err := idna.ToASCII(name) if err == nil { return strings.ToLower(ascii) @@ -109,6 +117,15 @@ func asciiServerNameForMatch(name string) string { return strings.Join(labels, ".") } +func isPureASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] >= 0x80 { + return false + } + } + return true +} + // UnmarshalCaddyfile sets up the MatchServerName from Caddyfile tokens. Syntax: // // sni From 03e08ee6a9cfbebed591c765c210c5488b9aefc2 Mon Sep 17 00:00:00 2001 From: "Muhammad Syafri, S.Kom" <105954036+Jualhosting@users.noreply.github.com> Date: Fri, 29 May 2026 02:26:19 +0700 Subject: [PATCH 199/206] encode: prioritize zstd and br over gzip in content negotiation (#7772) * fix(encode): prioritize zstd and br over gzip in content negotiation * test(encode): update unit tests to reflect new default priority ties * fix(encode): move default preferences to dynamic encode handler and restore generic negotiation helper * test(encode): call real Provision function in served-response test * test(encode): rename served-response test to TestServeHTTPDefaultEncodingPreference * refactor(encode): use slices.SortStableFunc and httptest.NewRecorder as recommended * refactor(encode): simplify sorting with cmp.Compare and check request error in test * test(encode): fix variable redeclaration in TestServeHTTPDefaultEncodingPreference Fix 'no new variables on left side of :=' error by changing 'err :=' to 'err =' on line 347, since err was already declared on line 332. This fixes the build failure in the encode module tests. --- modules/caddyhttp/encode/encode.go | 18 ++++-- modules/caddyhttp/encode/encode_test.go | 75 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index 3b6522745..22c9e7abf 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -20,12 +20,12 @@ package encode import ( + "cmp" "fmt" "io" "math" "net/http" "slices" - "sort" "strconv" "strings" "sync" @@ -127,6 +127,14 @@ func (enc *Encode) Provision(ctx caddy.Context) error { } } + if len(enc.Prefer) == 0 { + for _, encName := range []string{"zstd", "br", "gzip"} { + if _, ok := enc.writerPools[encName]; ok { + enc.Prefer = append(enc.Prefer, encName) + } + } + } + return nil } @@ -538,11 +546,11 @@ func AcceptedEncodings(r *http.Request, preferredOrder []string) []string { } // sort preferences by descending q-factor first, then by preferOrder - sort.Slice(prefs, func(i, j int) bool { - if math.Abs(prefs[i].q-prefs[j].q) < 0.00001 { - return prefs[i].preferOrder > prefs[j].preferOrder + slices.SortStableFunc(prefs, func(a, b encodingPreference) int { + if math.Abs(a.q-b.q) < 0.00001 { + return cmp.Compare(b.preferOrder, a.preferOrder) } - return prefs[i].q > prefs[j].q + return cmp.Compare(b.q, a.q) }) prefEncNames := make([]string, len(prefs)) diff --git a/modules/caddyhttp/encode/encode_test.go b/modules/caddyhttp/encode/encode_test.go index 818f76745..0f306777f 100644 --- a/modules/caddyhttp/encode/encode_test.go +++ b/modules/caddyhttp/encode/encode_test.go @@ -1,10 +1,16 @@ package encode import ( + "context" + "io" "net/http" + "net/http/httptest" "slices" "sync" "testing" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func BenchmarkOpenResponseWriter(b *testing.B) { @@ -295,3 +301,72 @@ func TestIsEncodeAllowed(t *testing.T) { }) } } + +type mockEncoder struct{} + +func (mockEncoder) Write(p []byte) (n int, err error) { return len(p), nil } +func (mockEncoder) Close() error { return nil } +func (mockEncoder) Reset(w io.Writer) {} +func (mockEncoder) Flush() error { return nil } + +func TestServeHTTPDefaultEncodingPreference(t *testing.T) { + enc := new(Encode) + enc.MinLength = 1 // compress everything + enc.writerPools = map[string]*sync.Pool{ + "gzip": { + New: func() any { return mockEncoder{} }, + }, + "zstd": { + New: func() any { return mockEncoder{} }, + }, + } + + // Call Provision() with a valid caddy.Context to exercise the real path + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + if err := enc.Provision(ctx); err != nil { + t.Fatalf("Provision failed: %v", err) + } + + // Test default preference: zstd preferred over gzip + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatalf("error creating request: %v", err) + } + r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") + + w := httptest.NewRecorder() + w.Header().Set("Content-Type", "text/plain") + + next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("Hello, world! This is a long enough string to satisfy min length if it wasn't 1.")) + return err + }) + + err = enc.ServeHTTP(w, r, next) + if err != nil { + t.Fatalf("ServeHTTP returned error: %v", err) + } + + // ETag suffix or Content-Encoding header should reflect zstd + contentEncoding := w.Header().Get("Content-Encoding") + if contentEncoding != "zstd" { + t.Errorf("Expected Content-Encoding to be 'zstd' by default, got '%s'", contentEncoding) + } + + // Test explicit user preference: gzip over zstd + enc.Prefer = []string{"gzip", "zstd"} + + w2 := httptest.NewRecorder() + w2.Header().Set("Content-Type", "text/plain") + err = enc.ServeHTTP(w2, r, next) + if err != nil { + t.Fatalf("ServeHTTP returned error: %v", err) + } + + contentEncoding2 := w2.Header().Get("Content-Encoding") + if contentEncoding2 != "gzip" { + t.Errorf("Expected Content-Encoding to be 'gzip' when explicitly preferred, got '%s'", contentEncoding2) + } +} From 3eb8e48ff052e1ad16d88c683672c306d2077a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 29 May 2026 19:37:17 +0200 Subject: [PATCH 200/206] Merge commit from fork * feat: drop headers with underscore in their names * feat: Caddyfile binding and tests for underscore-in-header drop Add the `allow_underscore_in_headers` global server option, refine the doc comment, and cover the filter end-to-end: server-level unit tests (drop, opt-out, debug log, RFC-7230 space rejection), a fastcgi unit test for the trimmed header name replacer, and forward_auth integration tests for both the default-drop and opt-out paths. * remove allow_underscore_in_headers option for now --- caddytest/integration/forwardauth_test.go | 65 ++++++++++++++++ .../caddyhttp/reverseproxy/fastcgi/fastcgi.go | 2 +- .../reverseproxy/fastcgi/fastcgi_test.go | 24 ++++++ modules/caddyhttp/server.go | 13 ++++ modules/caddyhttp/server_test.go | 77 +++++++++++++++++++ 5 files changed, 180 insertions(+), 1 deletion(-) diff --git a/caddytest/integration/forwardauth_test.go b/caddytest/integration/forwardauth_test.go index 513c80906..5f703bdfa 100644 --- a/caddytest/integration/forwardauth_test.go +++ b/caddytest/integration/forwardauth_test.go @@ -22,6 +22,8 @@ import ( "sync" "testing" + "github.com/stretchr/testify/assert" + "github.com/caddyserver/caddy/v2/caddytest" ) @@ -204,3 +206,66 @@ func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) { t.Errorf("X-User-Role: want %q, got %q", wantUserRole, gotRole) } } + +// TestForwardAuthCopyHeadersUnderscoreAlias guards GHSA-f59h-q822-g45g: +// a client-supplied `Remote_user` alias of the copy_headers target +// `Remote-User` must be stripped before the auth route runs, otherwise +// a downstream CGI/FastCGI backend would fold both names into the same +// HTTP_REMOTE_USER variable and the attacker would override the trusted +// identity. +func TestForwardAuthCopyHeadersUnderscoreAlias(t *testing.T) { + const wantRemoteUser = "alice" + + authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Remote-User", wantRemoteUser) + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(authSrv.Close) + + type received struct { + remoteUserHyphen, remoteUserUnderscore string + } + var ( + mu sync.Mutex + last received + ) + backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + last = received{ + remoteUserHyphen: r.Header.Get("Remote-User"), + remoteUserUnderscore: strings.Join(r.Header["Remote_user"], ","), + } + mu.Unlock() + fmt.Fprint(w, "ok") + })) + t.Cleanup(backendSrv.Close) + + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + grace_period 1ns + } + http://localhost:9080 { + forward_auth %s { + uri / + copy_headers Remote-User + } + reverse_proxy %s + } + `, strings.TrimPrefix(authSrv.URL, "http://"), strings.TrimPrefix(backendSrv.URL, "http://")), "caddyfile") + + req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) + // Set the underscore alias via raw map access to bypass http.Header + // canonicalization, as an attacker would on the wire. + req.Header["Remote_user"] = []string{"attacker"} + tester.AssertResponse(req, http.StatusOK, "ok") + + mu.Lock() + defer mu.Unlock() + assert.Equal(t, wantRemoteUser, last.remoteUserHyphen, "trusted Remote-User must reach the backend") + assert.Empty(t, last.remoteUserUnderscore, "underscore alias must be dropped") +} diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index f91394e58..9b602ee5d 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -507,7 +507,7 @@ var tlsProtocolStrings = map[uint16]string{ tls.VersionTLS13: "TLSv1.3", } -var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") +var headerNameReplacer = strings.NewReplacer("-", "_") // Interface guards var ( diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go index 4977ae998..2b22c813e 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go @@ -304,6 +304,30 @@ func TestSplitPosUnicodeSecurityRegression(t *testing.T) { } } +// TestHeaderNameReplacer asserts the CGI header-to-env normalization rule: +// hyphens are mapped to underscores while every other character (including +// spaces) is passed through. Spaces are not RFC 7230 tokens, so they cannot +// reach this function from the wire; the only header names that survive +// untouched at the server layer are sanitized by the underscore filter in +// caddyhttp.Server.serveHTTP (see GHSA-f59h-q822-g45g). +func TestHeaderNameReplacer(t *testing.T) { + tests := []struct { + in, want string + }{ + {"X-Forwarded-For", "X_Forwarded_For"}, + {"Remote-User", "Remote_User"}, + // Underscores are preserved (the server has already dropped any + // underscore-named headers when the filter is on). + {"Remote_User", "Remote_User"}, + // Spaces are not rewritten because Go's HTTP parser rejects whitespace in + // header field names. + {"Foo Bar", "Foo Bar"}, + } + for _, tt := range tests { + assert.Equal(t, tt.want, headerNameReplacer.Replace(tt.in), "input %q", tt.in) + } +} + // TestSplitPosSecurityRegressionUnicodeBypass guards against the FrankenPHP // advisories GHSA-3g8v-8r37-cgjm (uninitialized match flag on inner non-ASCII // byte) and GHSA-v4h7-cj44-8fc8 (Unicode equivalence via search.IgnoreCase diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 66f93989b..0479af83d 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -494,6 +494,19 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error { } } + // Drop headers whose names contain `_`: once FastCGI/CGI/FrankenPHP etc. rewrites `-` to + // `_`, an underscore alias collides with the legitimate hyphenated header + // and can bypass `forward_auth copy_headers` (GHSA-f59h-q822-g45g). + for k := range r.Header { + if strings.ContainsRune(k, '_') { + delete(r.Header, k) + + if c := s.logger.Check(zapcore.DebugLevel, "dropping header containing underscore"); c != nil { + c.Write(zap.String("header", k)) + } + } + } + // execute the primary handler chain return s.primaryHandlerChain.ServeHTTP(w, r) } diff --git a/modules/caddyhttp/server_test.go b/modules/caddyhttp/server_test.go index eecb392e4..fb6b2d49c 100644 --- a/modules/caddyhttp/server_test.go +++ b/modules/caddyhttp/server_test.go @@ -1,16 +1,20 @@ package caddyhttp import ( + "bufio" "bytes" "context" "io" + "net" "net/http" "net/http/httptest" "net/netip" + "strings" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -478,6 +482,79 @@ func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedSkippingTrusted(t * assert.Equal(t, clientIP, "45.54.45.54") } +// TestServer_serveHTTP_DropsUnderscoreHeader covers GHSA-f59h-q822-g45g: an +// underscore-named alias (e.g. `Remote_user`) of a hyphenated header must be +// dropped before any handler runs. +func TestServer_serveHTTP_DropsUnderscoreHeader(t *testing.T) { + got := &http.Header{} + s := &Server{ + logger: zap.NewNop(), + primaryHandlerChain: HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + *got = r.Header.Clone() + return nil + }), + } + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req.Header["X-Real-Header"] = []string{"ok"} + req.Header["Remote_user"] = []string{"attacker"} + req.Header["Remote_groups"] = []string{"admin"} + + require.NoError(t, s.serveHTTP(httptest.NewRecorder(), req)) + assert.NotContains(t, *got, "Remote_user") + assert.NotContains(t, *got, "Remote_groups") + assert.Equal(t, "ok", got.Get("X-Real-Header")) +} + +// TestServer_serveHTTP_LogsDroppedUnderscoreHeader verifies each dropped +// header is emitted at debug level so operators can diagnose unexpectedly +// missing headers without spamming the log on adversarial traffic. +func TestServer_serveHTTP_LogsDroppedUnderscoreHeader(t *testing.T) { + var buf bytes.Buffer + s := &Server{ + logger: testLogger(buf.Write), + primaryHandlerChain: HandlerFunc(func(http.ResponseWriter, *http.Request) error { + return nil + }), + } + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req.Header["Remote_user"] = []string{"attacker"} + + require.NoError(t, s.serveHTTP(httptest.NewRecorder(), req)) + assert.Contains(t, buf.String(), `"level":"debug"`) + assert.Contains(t, buf.String(), `"msg":"dropping header containing underscore"`) + assert.Contains(t, buf.String(), `"header":"Remote_user"`) +} + +// TestServer_SpaceInHeaderNameReturnsBadRequest documents why the underscore +// filter does not also strip space-named headers: Go's HTTP parser rejects a +// space in a field name with 400 before any handler runs, so such a request +// can never reach Caddy's pipeline. +func TestServer_SpaceInHeaderNameReturnsBadRequest(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("handler must not be reached; got headers %v", r.Header) + })) + t.Cleanup(srv.Close) + + addr := strings.TrimPrefix(srv.URL, "http://") + conn, err := net.DialTimeout("tcp", addr, 5*time.Second) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second))) + + _, err = conn.Write([]byte("GET / HTTP/1.1\r\n" + + "Host: " + addr + "\r\n" + + "Remote User: attacker\r\n" + + "Connection: close\r\n\r\n")) + require.NoError(t, err) + + resp, err := http.ReadResponse(bufio.NewReader(conn), nil) + require.NoError(t, err) + t.Cleanup(func() { _ = resp.Body.Close() }) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) +} + func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedFirst(t *testing.T) { localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8") From 0e8eb41b87ab60803d48cb2a183face3f4e4248e Mon Sep 17 00:00:00 2001 From: Bruno Teixeira Lopes <143887730+Brunotlps@users.noreply.github.com> Date: Fri, 29 May 2026 18:05:41 -0300 Subject: [PATCH 201/206] httpcaddyfile: fix incorrect error message on duplicate matchers (#7780) Parse each matcher segment individually using NewDispenser(segment) instead of DispenseDirective(dir), which coalesced all same-name segments into one token stream. This caused the second definition name to be misinterpreted as a matcher module name, producing 'module not registered: http.matchers.@name' instead of the correct 'matcher is defined more than once' error. By parsing segments individually, the existing duplicate check in parseMatcherDefinitions naturally catches the duplicate on the second pass. Signed-off-by: Brunotlps --- caddyconfig/httpcaddyfile/httptype.go | 2 +- caddyconfig/httpcaddyfile/httptype_test.go | 41 ++++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index c6979e56d..1c907572f 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -108,7 +108,7 @@ func (st ServerType) Setup( matcherDefs := make(map[string]caddy.ModuleMap) for _, segment := range sb.block.Segments { if dir := segment.Directive(); strings.HasPrefix(dir, matcherPrefix) { - d := sb.block.DispenseDirective(dir) + d := caddyfile.NewDispenser(segment) err := parseMatcherDefinitions(d, matcherDefs) if err != nil { return nil, warnings, err diff --git a/caddyconfig/httpcaddyfile/httptype_test.go b/caddyconfig/httpcaddyfile/httptype_test.go index 2436efcd9..b9a94fca9 100644 --- a/caddyconfig/httpcaddyfile/httptype_test.go +++ b/caddyconfig/httpcaddyfile/httptype_test.go @@ -2,6 +2,7 @@ package httpcaddyfile import ( "encoding/json" + "strings" "testing" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -10,8 +11,9 @@ import ( func TestMatcherSyntax(t *testing.T) { for i, tc := range []struct { - input string - expectError bool + input string + expectError bool + expectContains string }{ { input: `http://localhost @@ -53,6 +55,34 @@ func TestMatcherSyntax(t *testing.T) { `, expectError: false, }, + { + input: `http://localhost { + @test { + path /test + } + @test { + path /other + } + respond @test "hello" + } + `, + expectError: true, + expectContains: "is defined more than once", + }, + { + input: `(snippet) { + @{args[0]} { + path /{args[0]} + } + respond @{args[0]} "hello" + } + http://localhost { + import snippet foo + import snippet bar + } + `, + expectError: false, + }, { input: `@matcher { path /matcher-not-allowed/outside-of-site-block/* @@ -73,6 +103,13 @@ func TestMatcherSyntax(t *testing.T) { t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err) continue } + + if err != nil && tc.expectContains != "" { + if !strings.Contains(err.Error(), tc.expectContains) { + t.Errorf("Test %d error message mismatch: expected to contain %q, got %q", + i, tc.expectContains, err.Error()) + } + } } } From e2eee6a7fce366321294c9c2a79f3146891dcbdf Mon Sep 17 00:00:00 2001 From: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> Date: Tue, 2 Jun 2026 03:35:02 +0800 Subject: [PATCH 202/206] templates: Patch for GHSA-vcc4-2c75-vc9v (#7785) * Patch GHSA-vcc4-2c75-vc9v in stripHTML templates: fix funcStripHTML bypass via depth counter The previous false-start approach allowed XSS bypass via inputs like <<>img src=x onerror=alert(1)> and failed on stacked angle brackets. Replace the tagStart/inTag state machine with a depth counter that mirrors PHP strip_tags behaviour: each '<' increments depth, each '>' decrements it, and text is only emitted at depth zero. Quoted attribute values (both single and double) are tracked so '>' inside href values does not prematurely close a tag. Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> * Update tplcontext_test.go Templates: expand TestStripHTML with attack path coverage Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> --------- Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> --- modules/caddyhttp/templates/tplcontext.go | 47 +++++++++---------- .../caddyhttp/templates/tplcontext_test.go | 40 ++++++++++++++-- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index ee553e7a5..4e8ec925a 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -312,35 +312,32 @@ func (c TemplateContext) Host() (string, error) { return host, nil } -// funcStripHTML returns s without HTML tags. It is fairly naive -// but works with most valid HTML inputs. +// funcStripHTML returns s without HTML tags. Similar to PHP's strip_tags() func (TemplateContext) funcStripHTML(s string) string { var buf bytes.Buffer - var inTag, inQuotes bool - var tagStart int - for i, ch := range s { - if inTag { - if ch == '>' && !inQuotes { - inTag = false - } else if ch == '<' && !inQuotes { - // false start - buf.WriteString(s[tagStart:i]) - tagStart = i - } else if ch == '"' { - inQuotes = !inQuotes + depth := 0 + var quoteChar rune + for _, ch := range s { + switch { + case depth > 0 && quoteChar == 0 && (ch == '"' || ch == '\''): + // entering a quoted attribute value + quoteChar = ch + case depth > 0 && ch == quoteChar: + // leaving a quoted attribute value + quoteChar = 0 + case ch == '<' && quoteChar == 0: + depth++ + case ch == '>' && quoteChar == 0: + if depth > 0 { + depth-- + } else { + buf.WriteRune(ch) // stray '>' with no opening '<', keep it + } + default: + if depth == 0 { + buf.WriteRune(ch) } - continue } - if ch == '<' { - inTag = true - tagStart = i - continue - } - buf.WriteRune(ch) - } - if inTag { - // false start - buf.WriteString(s[tagStart:]) } return buf.String() } diff --git a/modules/caddyhttp/templates/tplcontext_test.go b/modules/caddyhttp/templates/tplcontext_test.go index 67ebbac70..1ff6caef0 100644 --- a/modules/caddyhttp/templates/tplcontext_test.go +++ b/modules/caddyhttp/templates/tplcontext_test.go @@ -419,14 +419,44 @@ func TestStripHTML(t *testing.T) { expect: `h1`, }, { - // tags not closed + // unclosed tag — trailing text must be stripped, not emitted input: `hi`, - expect: `' only closes one level + input: `hi`, + expect: ``, + }, + { + // XSS bypass via double opening bracket + input: `<<>img src=x onerror=alert('XSS')>`, + expect: ``, + }, + { + // stacked angle brackets (PHP strip_tags parity) + input: `<<<<<>>>>>hello`, + expect: `hello`, + }, + { + // unclosed tag strips trailing text + input: `hello ' inside double-quoted attribute must not close tag early + input: `text`, + expect: `text`, + }, + { + // '>' inside single-quoted attribute must not close tag early + input: `text`, + expect: `text`, + }, + { + // stray '>' with no opening '<' is preserved + input: `stray > bracket`, + expect: `stray > bracket`, }, } { actual := tplContext.funcStripHTML(test.input) From fcc7860d038a5cb191cf8b1410bd3ea2feeea31a Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Wed, 3 Jun 2026 11:49:00 +0800 Subject: [PATCH 203/206] reverseproxy: replace placeholders specified for sni while using http3 (#7737) * reverseproxy: replace placeholders specified for sni while using http3 * add test for placeholder * reverseproxy: replace placeholders specified for sni while using http3 * add test for placeholder * reverseproxy: test HTTP/3 SNI host placeholder --------- Co-authored-by: Zen Dodd --- caddytest/integration/reverseproxy_test.go | 38 +++++++ .../caddyhttp/reverseproxy/httptransport.go | 98 ++++++++++++++++++- 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/caddytest/integration/reverseproxy_test.go b/caddytest/integration/reverseproxy_test.go index cbccfd74f..28af7c367 100644 --- a/caddytest/integration/reverseproxy_test.go +++ b/caddytest/integration/reverseproxy_test.go @@ -793,3 +793,41 @@ func TestReverseProxyRetryMatchIsTransportError(t *testing.T) { // Transport error on broken upstream should be retried to good upstream tester.AssertGetResponse("http://localhost:9080/", 200, "ok") } + +func TestReverseProxySNIPlaceHolder(t *testing.T) { + configTemplate := ` + { + skip_install_trust + local_certs + admin localhost:2999 + http_port 9080 + https_port 9443 + grace_period 1ns + } + localhost example.com { + @proxied header X-Transport caddy + respond @proxied {http.request.tls.server_name} + reverse_proxy 127.0.0.1:9443 { + header_up X-Transport caddy + header_up Host {host} + transport http { + versions %s + tls_server_name {header.X-SNI} + tls_insecure_skip_verify + } + } + } + ` + for _, versions := range []string{"1.1 2", "3"} { + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(configTemplate, versions), "caddyfile") + req, err := http.NewRequest("GET", "https://localhost:9443", nil) + if err != nil { + t.Errorf("failed to create request %s", err) + return + } + + req.Header.Set("X-SNI", "example.com") + tester.AssertResponse(req, 200, "example.com") + } +} diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index c65bd6185..d2645deed 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -32,6 +32,7 @@ import ( "time" "github.com/pires/go-proxyproto" + "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -161,7 +162,8 @@ type HTTPTransport struct { // `HTTPS_PROXY`, and `NO_PROXY` environment variables. NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"` - h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024) + h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024) + quicTransport *quic.Transport // used by h3Transport if sni placeholder is used, otherwise nil } // CaddyModule returns the Caddy module information. @@ -499,6 +501,25 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e if err != nil { return nil, fmt.Errorf("making TLS client config for HTTP/3 transport: %v", err) } + + if strings.Contains(h.TLS.ServerName, "{") { + // copied from quic-go + udpConn, err := net.ListenUDP("udp", nil) + if err != nil { + return nil, fmt.Errorf("making udp socket for HTTP/3 transport: %v", err) + } + h.quicTransport = &quic.Transport{Conn: udpConn} + h.h3Transport.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { + // tlsCfg is already cloned from h3Transport.TLSClientConfig + repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + tlsCfg.ServerName = repl.ReplaceAll(tlsCfg.ServerName, "") + udpAddr, err := resolveUDPAddr(ctx, "udp", addr) + if err != nil { + return nil, err + } + return h.quicTransport.DialEarly(ctx, udpAddr, tlsCfg, cfg) + } + } } } else if len(h.Versions) > 1 && slices.Contains(h.Versions, "3") { return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported") @@ -525,6 +546,71 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e return rt, nil } +// TODO: EXPERIMENTAL (May 2025) +// copied from quic-go +func resolveUDPAddr(ctx context.Context, network, addr string) (*net.UDPAddr, error) { + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + port, err := net.LookupPort(network, portStr) + if err != nil { + return nil, err + } + resolver := net.DefaultResolver + ipAddrs, err := resolver.LookupIPAddr(ctx, host) + if err != nil { + return nil, err + } + addrs := addrList(ipAddrs) + ip := addrs.forResolve(network, addr) + return &net.UDPAddr{IP: ip.IP, Port: port, Zone: ip.Zone}, nil +} + +// TODO: EXPERIMENTAL (May 2025) +// copied from quic-go +// An addrList represents a list of network endpoint addresses. +// Copy from [net.addrList] and change type from [net.Addr] to [net.IPAddr] +type addrList []net.IPAddr + +// isIPv4 reports whether addr contains an IPv4 address. +func isIPv4(addr net.IPAddr) bool { + return addr.IP.To4() != nil +} + +// isNotIPv4 reports whether addr does not contain an IPv4 address. +func isNotIPv4(addr net.IPAddr) bool { return !isIPv4(addr) } + +// forResolve returns the most appropriate address in address for +// a call to ResolveTCPAddr, ResolveUDPAddr, or ResolveIPAddr. +// IPv4 is preferred, unless addr contains an IPv6 literal. +func (addrs addrList) forResolve(network, addr string) net.IPAddr { + var want6 bool + switch network { + case "ip": + // IPv6 literal (addr does NOT contain a port) + want6 = strings.ContainsRune(addr, ':') + case "tcp", "udp": + // IPv6 literal. (addr contains a port, so look for '[') + want6 = strings.ContainsRune(addr, '[') + } + if want6 { + return addrs.first(isNotIPv4) + } + return addrs.first(isIPv4) +} + +// first returns the first address which satisfies strategy, or if +// none do, then the first address of any kind. +func (addrs addrList) first(strategy func(net.IPAddr) bool) net.IPAddr { + for _, addr := range addrs { + if strategy(addr) { + return addr + } + } + return addrs[0] +} + // RequestHeaderOps implements TransportHeaderOpsProvider. It returns header // operations for requests when the transport's configuration indicates they // should be applied. In particular, when TLS is enabled for this transport, @@ -623,6 +709,16 @@ func (h HTTPTransport) Cleanup() error { return nil } h.Transport.CloseIdleConnections() + // h3 related cleanup, errors are ignored as nothing can be done. + // TODO: log these errors if any + if h.h3Transport != nil { + h.h3Transport.CloseIdleConnections() + _ = h.h3Transport.Close() + if h.quicTransport != nil { + _ = h.quicTransport.Close() + _ = h.quicTransport.Conn.Close() + } + } return nil } From 915793f6e009669c4c750bc9a8bcf3c96784e646 Mon Sep 17 00:00:00 2001 From: "Muhammad Syafri, S.Kom" <105954036+Jualhosting@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:03:19 +0700 Subject: [PATCH 204/206] caddyhttp: add {http.request.proto_name} placeholder for spec-compliant protocol names (#7782) * caddyhttp: add {http.request.proto_name} placeholder for spec-compliant protocol names {http.request.proto} exposes Go's raw http.Request.Proto field which returns HTTP/2.0 and HTTP/3.0 for HTTP/2 and HTTP/3 respectively. These strings are non-standard since the specs define them as HTTP/2 and HTTP/3. To preserve backward compat (especially CGI/FastCGI expectations), {http.request.proto} is kept as-is. A new {http.request.proto_name} placeholder is introduced that normalises the version string to the spec-defined form: HTTP/2.0 -> HTTP/2 HTTP/3.0 -> HTTP/3 all others returned unchanged Closes #7734 * caddyhttp: Use ProtoMajor for proto_name normalization and update docs --------- Co-authored-by: jalikajalika5 <105954036+jalikajalika5@users.noreply.github.com> --- modules/caddyhttp/app.go | 3 ++- modules/caddyhttp/replacer.go | 8 ++++++++ modules/caddyhttp/replacer_test.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index bc2b896cd..79fd5f414 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -70,7 +70,8 @@ func init() { // `{http.request.orig_uri.query}` | The request's original query string (without `?`) // `{http.request.orig_uri.prefixed_query}` | The request's original query string with a `?` prefix, if non-empty // `{http.request.port}` | The port part of the request's Host header -// `{http.request.proto}` | The protocol of the request +// `{http.request.proto}` | The raw protocol of the request as returned by Go (e.g., HTTP/2.0 or HTTP/3.0) +// `{http.request.proto_name}` | The spec-defined protocol of the request (e.g., HTTP/2 or HTTP/3) // `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on // `{http.request.local.port}` | The port part of the local address the connection arrived on // `{http.request.local}` | The local address the connection arrived on diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 623a6ef4b..65f9dd475 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -105,6 +105,14 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo return "http", true case "http.request.proto": return req.Proto, true + case "http.request.proto_name": + if req.ProtoMajor == 2 { + return "HTTP/2", true + } + if req.ProtoMajor == 3 { + return "HTTP/3", true + } + return req.Proto, true case "http.request.host": host, _, err := net.SplitHostPort(req.Host) if err != nil { diff --git a/modules/caddyhttp/replacer_test.go b/modules/caddyhttp/replacer_test.go index c75fe82ed..4f8d8f0b2 100644 --- a/modules/caddyhttp/replacer_test.go +++ b/modules/caddyhttp/replacer_test.go @@ -266,3 +266,33 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV } } } + +func TestHTTPProtoNameNormalization(t *testing.T) { + for _, tc := range []struct { + proto string + major int + expectRaw string + expectName string + }{ + {proto: "HTTP/1.0", major: 1, expectRaw: "HTTP/1.0", expectName: "HTTP/1.0"}, + {proto: "HTTP/1.1", major: 1, expectRaw: "HTTP/1.1", expectName: "HTTP/1.1"}, + {proto: "HTTP/2.0", major: 2, expectRaw: "HTTP/2.0", expectName: "HTTP/2"}, + {proto: "HTTP/3.0", major: 3, expectRaw: "HTTP/3.0", expectName: "HTTP/3"}, + } { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Proto = tc.proto + req.ProtoMajor = tc.major + repl := caddy.NewReplacer() + addHTTPVarsToReplacer(repl, req, nil) + + gotRaw, okRaw := repl.GetString("http.request.proto") + if !okRaw || gotRaw != tc.expectRaw { + t.Errorf("proto=%s: expected http.request.proto to be %q, got %q (ok=%t)", tc.proto, tc.expectRaw, gotRaw, okRaw) + } + + gotName, okName := repl.GetString("http.request.proto_name") + if !okName || gotName != tc.expectName { + t.Errorf("proto=%s: expected http.request.proto_name to be %q, got %q (ok=%t)", tc.proto, tc.expectName, gotName, okName) + } + } +} From 3b7bde8f25122a3a83ae0777e5bdce47f449e47d Mon Sep 17 00:00:00 2001 From: Rhul <143727980+vijayvenkatj@users.noreply.github.com> Date: Fri, 5 Jun 2026 00:25:08 +0530 Subject: [PATCH 205/206] httpcaddyfile: error on duplicate named_routes (#7800) * fix: error on duplicate named_routes Fixes issue #7798 Validate named route names before inserting them into the named route map. This prevents later definitions from overwriting existing named routes and returns an error when a route name is defined more than once. * test: add test for duplicate named_routes --- caddyconfig/httpcaddyfile/httptype.go | 6 +++++- ...duplicate_named_route_challenge.caddyfiletest | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 caddytest/integration/caddyfile_adapt/duplicate_named_route_challenge.caddyfiletest diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 1c907572f..74cca4a4f 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -523,7 +523,11 @@ func (ServerType) extractNamedRoutes( route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)} } - namedRoutes[sb.block.GetKeysText()[0]] = &route + key := sb.block.GetKeysText()[0] + if _, exists := namedRoutes[key]; exists { + return nil, fmt.Errorf("cannot have duplicate named_routes: %s", key) + } + namedRoutes[key] = &route } options["named_routes"] = namedRoutes diff --git a/caddytest/integration/caddyfile_adapt/duplicate_named_route_challenge.caddyfiletest b/caddytest/integration/caddyfile_adapt/duplicate_named_route_challenge.caddyfiletest new file mode 100644 index 000000000..f0e648830 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/duplicate_named_route_challenge.caddyfiletest @@ -0,0 +1,16 @@ +&(api) { + header X-Version v1 + respond "API v1" +} + +&(api) { + header X-Version v2 + respond "API v2" +} + +localhost { + invoke api +} + +---------- +cannot have duplicate named_routes: api From d730df2a83e83ea3ec2990b213385cc34152c62e Mon Sep 17 00:00:00 2001 From: "Y.Horie" Date: Fri, 5 Jun 2026 10:41:35 +0900 Subject: [PATCH 206/206] cmd: colored error message in WrapCommandFuncForCobra (#7760) (#7768) Signed-off-by: Y.Horie Co-authored-by: Mohammed Al Sahaf --- cmd/cobra.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/cobra.go b/cmd/cobra.go index 14c8d2988..cc4792f17 100644 --- a/cmd/cobra.go +++ b/cmd/cobra.go @@ -149,8 +149,14 @@ func caddyCmdToCobra(caddyCmd Command) *cobra.Command { func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error { return func(cmd *cobra.Command, _ []string) error { status, err := f(Flags{cmd.Flags()}) - if status > 1 { + if err != nil { + // Route the error through Caddy's logger so it receives the same + // colored, structured formatting as INFO/WARN output, rather than + // cobra's plain "Error: ..." line which lacks any highlighting. + caddy.Log().Error(err.Error()) cmd.SilenceErrors = true + } + if status > 1 { return &exitError{ExitCode: status, Err: err} } return err