diff --git a/caddyconfig/caddyfile/formatter.go b/caddyconfig/caddyfile/formatter.go index 8a757ea58..833aff353 100644 --- a/caddyconfig/caddyfile/formatter.go +++ b/caddyconfig/caddyfile/formatter.go @@ -52,17 +52,16 @@ func Format(input []byte) []byte { newLines int // count of newlines consumed - comment bool // whether we're in a comment - quoted bool // whether we're in a quoted segment - escaped bool // whether current char is escaped + comment bool // whether we're in a comment + quotes string // encountered quotes ('', '`', '"', '"`', '`"') + escaped bool // whether current char is escaped heredoc heredocState // whether we're in a heredoc heredocEscaped bool // whether heredoc is escaped heredocMarker []rune heredocClosingMarker []rune - nesting int // indentation level - withinBackquote bool + nesting int // indentation level ) write := func(ch rune) { @@ -89,12 +88,8 @@ func Format(input []byte) []byte { } panic(err) } - if ch == '`' { - withinBackquote = !withinBackquote - } - // detect whether we have the start of a heredoc - if !quoted && (heredoc == heredocClosed && !heredocEscaped) && + if quotes == "" && (heredoc == heredocClosed && !heredocEscaped) && space && last == '<' && ch == '<' { write(ch) heredoc = heredocOpening @@ -180,16 +175,38 @@ func Format(input []byte) []byte { continue } - if quoted { + if ch == '`' { + switch quotes { + case "\"`": + quotes = "\"" + case "`": + quotes = "" + case "\"": + quotes = "\"`" + default: + quotes = "`" + } + } + + if quotes == "\"" { if ch == '"' { - quoted = false + quotes = "" } write(ch) continue } - if space && ch == '"' { - quoted = true + if ch == '"' { + switch quotes { + case "": + if space { + quotes = "\"" + } + case "`\"": + quotes = "`" + case "\"`": + quotes = "" + } } if unicode.IsSpace(ch) { @@ -245,7 +262,7 @@ func Format(input []byte) []byte { write(' ') } openBraceWritten = false - if withinBackquote { + if quotes == "`" { write('{') openBraceWritten = true continue @@ -253,7 +270,7 @@ func Format(input []byte) []byte { continue case ch == '}' && (spacePrior || !openBrace): - if withinBackquote { + if quotes == "`" { write('}') continue } diff --git a/caddyconfig/caddyfile/formatter_test.go b/caddyconfig/caddyfile/formatter_test.go index 29b910ff1..0092d1311 100644 --- a/caddyconfig/caddyfile/formatter_test.go +++ b/caddyconfig/caddyfile/formatter_test.go @@ -444,6 +444,11 @@ block2 { input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}", expect: "block {respond \"All braces should remain: {{now | date `2006`}}\"}", }, + { + description: "Preserve quoted backticks and backticked quotes", + input: "block { respond \"`\" } block { respond `\"`}", + expect: "block {\n\trespond \"`\"\n}\n\nblock {\n\trespond `\"`\n}", + }, { description: "No trailing space on line before env variable", input: `{ diff --git a/go.mod b/go.mod index 70f85aed9..9ed9b351d 100644 --- a/go.mod +++ b/go.mod @@ -25,15 +25,15 @@ require ( github.com/smallstep/nosql v0.7.0 github.com/smallstep/truststore v0.13.0 github.com/spf13/cobra v1.10.1 - github.com/spf13/pflag v1.0.9 + 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/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 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/exporters/otlp/otlptrace/otlptracegrpc 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 @@ -49,9 +49,9 @@ require ( require ( cel.dev/expr v0.24.0 // indirect - cloud.google.com/go/auth v0.16.4 // indirect + cloud.google.com/go/auth v0.16.5 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.8.0 // 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 @@ -62,13 +62,14 @@ require ( github.com/francoispqt/gojay v1.2.13 // indirect github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect - github.com/google/go-tpm v0.9.5 // indirect + github.com/google/go-tpm v0.9.6 // 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/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/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect @@ -76,6 +77,7 @@ require ( 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/quic-go/qpack v0.5.1 // indirect github.com/smallstep/cli-utils v0.12.1 // indirect github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect @@ -85,14 +87,28 @@ require ( 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/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.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect golang.org/x/oauth2 v0.31.0 // indirect - google.golang.org/api v0.247.0 // indirect + google.golang.org/api v0.251.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect @@ -142,7 +158,7 @@ require ( 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.5 // indirect + github.com/slackhq/nebula v1.9.7 // 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 @@ -151,13 +167,13 @@ 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.70.0 + go.step.sm/crypto v0.72.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/tools v0.38.0 // indirect - google.golang.org/grpc v1.75.1 // indirect + google.golang.org/grpc v1.76.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 5c4a2975b..20db1e628 100644 --- a/go.sum +++ b/go.sum @@ -6,16 +6,16 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= 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/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8= -cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= 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.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= -cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +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.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= -cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= +cloud.google.com/go/kms v1.23.1 h1:Mesyv84WoP3tPjUC0O5LRqPWICO0ufdpWf9jtBCEz64= +cloud.google.com/go/kms v1.23.1/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= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= @@ -60,34 +60,34 @@ 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.38.0 h1:UCRQ5mlqcFk9HJDIqENSLR3wiG1VTWlyUfLDEvY7RxU= -github.com/aws/aws-sdk-go-v2 v1.38.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= -github.com/aws/aws-sdk-go-v2/config v1.31.0 h1:9yH0xiY5fUnVNLRWO0AtayqwU1ndriZdN78LlhruJR4= -github.com/aws/aws-sdk-go-v2/config v1.31.0/go.mod h1:VeV3K72nXnhbe4EuxxhzsDc/ByrCSlZwUnWH52Nde/I= -github.com/aws/aws-sdk-go-v2/credentials v1.18.4 h1:IPd0Algf1b+Qy9BcDp0sCUcIWdCQPSzDoMK3a8pcbUM= -github.com/aws/aws-sdk-go-v2/credentials v1.18.4/go.mod h1:nwg78FjH2qvsRM1EVZlX9WuGUJOL5od+0qvm0adEzHk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 h1:o9RnO+YZ4X+kt5Z7Nvcishlz0nksIt2PIzDglLMP0vA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3/go.mod h1:+6aLJzOG1fvMOyzIySYjOFjcguGvVRL68R+uoRencN4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 h1:joyyUFhiTQQmVK6ImzNU9TQSNRNeD9kOklqTzyk5v6s= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3/go.mod h1:+vNIyZQP3b3B1tSLI0lxvrU9cfM7gpdRXMFfm67ZcPc= +github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= +github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2/config v1.31.12 h1:pYM1Qgy0dKZLHX2cXslNacbcEFMkDMl+Bcj5ROuS6p8= +github.com/aws/aws-sdk-go-v2/config v1.31.12/go.mod h1:/MM0dyD7KSDPR+39p9ZNVKaHDLb9qnfDurvVS2KAhN8= +github.com/aws/aws-sdk-go-v2/credentials v1.18.16 h1:4JHirI4zp958zC026Sm+V4pSDwW4pwLefKrc0bF2lwI= +github.com/aws/aws-sdk-go-v2/credentials v1.18.16/go.mod h1:qQMtGx9OSw7ty1yLclzLxXCRbrkjWAM7JnObZjmCB7I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 h1:Mv4Bc0mWmv6oDuSWTKnk+wgeqPL5DRFu5bQL9BGPQ8Y= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9/go.mod h1:IKlKfRppK2a1y0gy1yH6zD+yX5uplJ6UuPlgd48dJiQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM= -github.com/aws/aws-sdk-go-v2/service/kms v1.44.0 h1:Z95XCqqSnwXr0AY7PgsiOUBhUG2GoDM5getw6RfD1Lg= -github.com/aws/aws-sdk-go-v2/service/kms v1.44.0/go.mod h1:DqcSngL7jJeU1fOzh5Ll5rSvX/MlMV6OZlE4mVdFAQc= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 h1:Mc/MKBf2m4VynyJkABoVEN+QzkfLqGj0aiJuEe7cMeM= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.0/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 h1:6csaS/aJmqZQbKhi1EyEMM7yBW653Wy/B9hnBofW+sw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0/go.mod h1:59qHWaY5B+Rs7HGTuVGaC32m0rdpQ68N8QCN3khYiqs= -github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 h1:MG9VFW43M4A8BYeAfaJJZWrroinxeTi2r3+SnmLQfSA= -github.com/aws/aws-sdk-go-v2/service/sts v1.37.0/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8= -github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= -github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD4WZudeEKZ9/iKpiT6cM1JyEROpXjOcdWv8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I= +github.com/aws/aws-sdk-go-v2/service/kms v1.45.6 h1:Br3kil4j7RPW+7LoLVkYt8SuhIWlg6ylmbmzXJ7PgXY= +github.com/aws/aws-sdk-go-v2/service/kms v1.45.6/go.mod h1:FKXkHzw1fJZtg1P1qoAIiwen5thz/cDRTTDCIu8ljxc= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 h1:A1oRkiSQOWstGh61y4Wc/yQ04sqrQZr1Si/oAXj20/s= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.6/go.mod h1:5PfYspyCU5Vw1wNPsxi15LZovOnULudOQuVxphSflQA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 h1:5fm5RTONng73/QA73LhCNR7UT9RpFH3hR6HWL6bIgVY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1/go.mod h1:xBEjWD13h+6nq+z4AkqSfSvqRKFgDIQeaMguAJndOWo= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47hZb5HUQ0tn6Q9kA= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8= +github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= +github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -167,8 +167,8 @@ github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hH github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= -github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= +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-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= @@ -203,10 +203,10 @@ 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-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= -github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k= -github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8= +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-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= github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -222,6 +222,8 @@ github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE0 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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +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/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -321,6 +323,8 @@ github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvM github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 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.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 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= @@ -367,8 +371,8 @@ github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5k 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.5 h1:ZrxcvP/lxwFglaijmiwXLuCSkybZMJnqSYI1S8DtGnY= -github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= +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= @@ -402,8 +406,9 @@ 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/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 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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= @@ -450,6 +455,10 @@ go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 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/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/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= @@ -466,28 +475,52 @@ go.opentelemetry.io/contrib/propagators/ot v1.38.0 h1:k4gSyyohaDXI8F9BDXYC3uO2vr 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/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.70.0 h1:Q9Ft7N637mucyZcHZd1+0VVQJVwDCKqcb9CYcYi7cds= -go.step.sm/crypto v0.70.0/go.mod h1:pzfUhS5/ue7ev64PLlEgXvhx1opwbhFCjkvlhsxVds0= +go.step.sm/crypto v0.72.0 h1:cwkxbmnN8jj8YWmoXdoGhaac81d2SwXguwmHN9KJxHw= +go.step.sm/crypto v0.72.0/go.mod h1:EAy7MSOXxCvCaDAKJqz0bLdTSDdhpEM9xqye8XsfrM4= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +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= @@ -637,8 +670,8 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc= -google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM= +google.golang.org/api v0.251.0 h1:6lea5nHRT8RUmpy9kkC2PJYnhnDAB13LqrLSVQlMIE8= +google.golang.org/api v0.251.0/go.mod h1:Rwy0lPf/TD7+T2VhYcffCHhyyInyuxGjICxdfLqT7KI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -658,8 +691,8 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +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/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= diff --git a/listeners.go b/listeners.go index 8a862bacf..b673c86e1 100644 --- a/listeners.go +++ b/listeners.go @@ -38,6 +38,10 @@ 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(). @@ -305,6 +309,64 @@ 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 @@ -336,9 +398,27 @@ 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: host, + Host: fdAddr, }, nil } var start, end uint64 diff --git a/listeners_test.go b/listeners_test.go index a4cadd3aa..c2cc255f2 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -15,6 +15,7 @@ package caddy import ( + "os" "reflect" "testing" @@ -652,3 +653,286 @@ 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) + } + } +} diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 7611285f7..6ad18d051 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -198,6 +198,8 @@ func (app *App) Provision(ctx caddy.Context) error { if app.Metrics != nil { app.Metrics.init = sync.Once{} app.Metrics.httpMetrics = &httpMetrics{} + // Scan config for allowed hosts to prevent cardinality explosion + app.Metrics.scanConfigForHosts(app) } // prepare each server oldContext := ctx.Context diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go index 3d118ea79..66a60b817 100644 --- a/modules/caddyhttp/celmatcher.go +++ b/modules/caddyhttp/celmatcher.go @@ -665,7 +665,7 @@ 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) { - mapStrType := reflect.TypeOf(map[string]any{}) + mapStrType := reflect.TypeFor[map[string]any]() mapStrRaw, err := data.ConvertToNative(mapStrType) if err != nil { return nil, err diff --git a/modules/caddyhttp/intercept/intercept.go b/modules/caddyhttp/intercept/intercept.go index cb23adf0a..bacdc74b5 100644 --- a/modules/caddyhttp/intercept/intercept.go +++ b/modules/caddyhttp/intercept/intercept.go @@ -17,6 +17,7 @@ package intercept import ( "bytes" "fmt" + "io" "net/http" "strconv" "strings" @@ -175,10 +176,35 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy c.Write(zap.Int("handler", rec.handlerIndex)) } - // pass the request through the response handler routes - return rec.handler.Routes.Compile(next).ServeHTTP(w, r) + // response recorder doesn't create a new copy of the original headers, they're + // present in the original response writer + // create a new recorder to see if any response body from the new handler is present, + // if not, use the already buffered response body + recorder := caddyhttp.NewResponseRecorder(w, nil, nil) + if err := rec.handler.Routes.Compile(emptyHandler).ServeHTTP(recorder, r); err != nil { + return err + } + + // no new response status and the status is not 0 + if recorder.Status() == 0 && rec.Status() != 0 { + w.WriteHeader(rec.Status()) + } + + // no new response body and there is some in the original response + // TODO: what if the new response doesn't have a body by design? + // see: https://github.com/caddyserver/caddy/pull/6232#issue-2235224400 + if recorder.Size() == 0 && buf.Len() > 0 { + _, err := io.Copy(w, buf) + return err + } + return nil } +// this handler does nothing because everything we need is already buffered +var emptyHandler caddyhttp.Handler = caddyhttp.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) error { + return nil +}) + // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // intercept [] { diff --git a/modules/caddyhttp/metrics.go b/modules/caddyhttp/metrics.go index 9bb97e0b4..424170732 100644 --- a/modules/caddyhttp/metrics.go +++ b/modules/caddyhttp/metrics.go @@ -17,14 +17,60 @@ import ( // Metrics configures metrics observations. // EXPERIMENTAL and subject to change or removal. +// +// Example configuration: +// +// { +// "apps": { +// "http": { +// "metrics": { +// "per_host": true, +// "allow_catch_all_hosts": false +// }, +// "servers": { +// "srv0": { +// "routes": [{ +// "match": [{"host": ["example.com", "www.example.com"]}], +// "handle": [{"handler": "static_response", "body": "Hello"}] +// }] +// } +// } +// } +// } +// } +// +// In this configuration: +// - Requests to example.com and www.example.com get individual host labels +// - All other hosts (e.g., attacker.com) are aggregated under "_other" label +// - This prevents unlimited cardinality from arbitrary Host headers type Metrics struct { // Enable per-host metrics. Enabling this option may // incur high-memory consumption, depending on the number of hosts // managed by Caddy. + // + // CARDINALITY PROTECTION: To prevent unbounded cardinality attacks, + // only explicitly configured hosts (via host matchers) are allowed + // by default. Other hosts are aggregated under the "_other" label. + // See AllowCatchAllHosts to change this behavior. PerHost bool `json:"per_host,omitempty"` - init sync.Once - httpMetrics *httpMetrics `json:"-"` + // Allow metrics for catch-all hosts (hosts without explicit configuration). + // When false (default), only hosts explicitly configured via host matchers + // will get individual metrics labels. All other hosts will be aggregated + // under the "_other" label to prevent cardinality explosion. + // + // This is automatically enabled for HTTPS servers (since certificates provide + // some protection against unbounded cardinality), but disabled for HTTP servers + // by default to prevent cardinality attacks from arbitrary Host headers. + // + // 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"` + + init sync.Once + httpMetrics *httpMetrics + allowedHosts map[string]struct{} + hasHTTPSServer bool } type httpMetrics struct { @@ -101,6 +147,63 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) { }, httpLabels) } +// 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) { + if !m.PerHost { + return + } + + m.allowedHosts = make(map[string]struct{}) + m.hasHTTPSServer = false + + for _, srv := range app.Servers { + // Check if this server has TLS enabled + serverHasTLS := len(srv.TLSConnPolicies) > 0 + if serverHasTLS { + m.hasHTTPSServer = true + } + + // Collect hosts from route matchers + for _, route := range srv.Routes { + for _, matcherSet := range route.MatcherSets { + for _, matcher := range matcherSet { + if hm, ok := matcher.(*MatchHost); ok { + for _, host := range *hm { + // Only allow non-fuzzy hosts to prevent unbounded cardinality + if !hm.fuzzy(host) { + m.allowedHosts[strings.ToLower(host)] = struct{}{} + } + } + } + } + } + } + } +} + +// shouldAllowHostMetrics determines if metrics should be collected for the given host. +// This implements the cardinality protection by only allowing metrics for: +// 1. Explicitly configured hosts +// 2. Catch-all requests on HTTPS servers (if AllowCatchAllHosts is true or auto-enabled) +// 3. Catch-all requests on HTTP servers only if explicitly allowed +func (m *Metrics) shouldAllowHostMetrics(host string, isHTTPS bool) bool { + if !m.PerHost { + return true // host won't be used in labels anyway + } + + normalizedHost := strings.ToLower(host) + + // Always allow explicitly configured hosts + if _, exists := m.allowedHosts[normalizedHost]; exists { + return true + } + + // For catch-all requests (not in allowed hosts) + allowCatchAll := m.AllowCatchAllHosts || (isHTTPS && m.hasHTTPSServer) + return allowCatchAll +} + // serverNameFromContext extracts the current server name from the context. // Returns "UNKNOWN" if none is available (should probably never happen). func serverNameFromContext(ctx context.Context) string { @@ -133,9 +236,19 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re // of a panic statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""} + // Determine if this is an HTTPS request + isHTTPS := r.TLS != nil + if h.metrics.PerHost { - labels["host"] = strings.ToLower(r.Host) - statusLabels["host"] = strings.ToLower(r.Host) + // Apply cardinality protection for host metrics + if h.metrics.shouldAllowHostMetrics(r.Host, isHTTPS) { + labels["host"] = strings.ToLower(r.Host) + statusLabels["host"] = strings.ToLower(r.Host) + } else { + // Use a catch-all label for unallowed hosts to prevent cardinality explosion + labels["host"] = "_other" + statusLabels["host"] = "_other" + } } inFlight := h.metrics.httpMetrics.requestInFlight.With(labels) diff --git a/modules/caddyhttp/metrics_test.go b/modules/caddyhttp/metrics_test.go index 4e1aa8b30..9f6f59858 100644 --- a/modules/caddyhttp/metrics_test.go +++ b/modules/caddyhttp/metrics_test.go @@ -2,6 +2,7 @@ package caddyhttp import ( "context" + "crypto/tls" "errors" "net/http" "net/http/httptest" @@ -206,9 +207,11 @@ func TestMetricsInstrumentedHandler(t *testing.T) { func TestMetricsInstrumentedHandlerPerHost(t *testing.T) { ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) metrics := &Metrics{ - PerHost: true, - init: sync.Once{}, - httpMetrics: &httpMetrics{}, + PerHost: true, + AllowCatchAllHosts: 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!") @@ -379,6 +382,112 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) { } } +func TestMetricsCardinalityProtection(t *testing.T) { + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + + // 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{}), + } + + // Add one allowed host + metrics.allowedHosts["allowed.com"] = struct{}{} + + mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + w.Write([]byte("hello")) + return nil + }) + + ih := newMetricsInstrumentedHandler(ctx, "test", mh, 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 })) + + // 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 })) + + // 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 })) + + // Check that metrics contain: + // - One entry for "allowed.com" + // - One entry for "_other" (aggregating attacker.com and evil.com) + expected := ` + # HELP caddy_http_requests_total Counter of HTTP(S) requests made. + # TYPE caddy_http_requests_total counter + caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 2 + caddy_http_requests_total{handler="test",host="allowed.com",server="UNKNOWN"} 1 + ` + + if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected), + "caddy_http_requests_total", + ); err != nil { + t.Errorf("Cardinality protection test failed: %s", err) + } +} + +func TestMetricsHTTPSCatchAll(t *testing.T) { + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + + // 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 + } + + mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + w.Write([]byte("hello")) + return nil + }) + + ih := newMetricsInstrumentedHandler(ctx, "test", mh, 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 })) + + // 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 })) + + // Check that HTTPS request gets real host, HTTP gets "_other" + expected := ` + # HELP caddy_http_requests_total Counter of HTTP(S) requests made. + # TYPE caddy_http_requests_total counter + caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 1 + caddy_http_requests_total{handler="test",host="unknown.com",server="UNKNOWN"} 1 + ` + + if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected), + "caddy_http_requests_total", + ); err != nil { + t.Errorf("HTTPS catch-all test failed: %s", err) + } +} + type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error { diff --git a/modules/caddyhttp/reverseproxy/fastcgi/writer.go b/modules/caddyhttp/reverseproxy/fastcgi/writer.go index 3af00d9a1..225d8f5f8 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/writer.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/writer.go @@ -112,7 +112,7 @@ func encodeSize(b []byte, size uint32) int { binary.BigEndian.PutUint32(b, size) return 4 } - b[0] = byte(size) + b[0] = byte(size) //nolint:gosec // false positive; b is made 8 bytes long, then this function is always called with b being at least 4 or 1 byte long return 1 } diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go index 6d35ed821..fea85946d 100644 --- a/modules/caddyhttp/reverseproxy/hosts.go +++ b/modules/caddyhttp/reverseproxy/hosts.go @@ -285,3 +285,6 @@ type ProxyProtocolInfo struct { // tlsH1OnlyVarKey is the key used that indicates the connection will use h1 only for TLS. // https://github.com/caddyserver/caddy/issues/7292 const tlsH1OnlyVarKey = "reverse_proxy.tls_h1_only" + +// proxyVarKey is the key used that indicates the proxy server used for a request. +const proxyVarKey = "reverse_proxy.proxy" diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 3031bda46..1e4cfa743 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -24,6 +24,7 @@ import ( weakrand "math/rand" "net" "net/http" + "net/url" "os" "reflect" "slices" @@ -159,8 +160,7 @@ 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"` - h2cTransport *http2.Transport - h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024) + h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024) } // CaddyModule returns the Caddy module information. @@ -204,11 +204,16 @@ func (h *HTTPTransport) Provision(ctx caddy.Context) error { func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, error) { // Set keep-alive defaults if it wasn't otherwise configured if h.KeepAlive == nil { - h.KeepAlive = &KeepAlive{ - ProbeInterval: caddy.Duration(30 * time.Second), - IdleConnTimeout: caddy.Duration(2 * time.Minute), - MaxIdleConnsPerHost: 32, // seems about optimal, see #2805 - } + h.KeepAlive = new(KeepAlive) + } + if h.KeepAlive.ProbeInterval == 0 { + h.KeepAlive.ProbeInterval = caddy.Duration(30 * time.Second) + } + if h.KeepAlive.IdleConnTimeout == 0 { + h.KeepAlive.IdleConnTimeout = caddy.Duration(2 * time.Minute) + } + if h.KeepAlive.MaxIdleConnsPerHost == 0 { + h.KeepAlive.MaxIdleConnsPerHost = 32 // seems about optimal, see #2805 } // Set a relatively short default dial timeout. @@ -267,15 +272,15 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e } dialContext := func(ctx context.Context, network, address string) (net.Conn, error) { - // For unix socket upstreams, we need to recover the dial info from - // the request's context, because the Host on the request's URL - // will have been modified by directing the request, overwriting - // the unix socket filename. - // Also, we need to avoid overwriting the address at this point - // when not necessary, because http.ProxyFromEnvironment may have - // modified the address according to the user's env proxy config. + // The network is usually tcp, and the address is the host in http.Request.URL.Host + // and that's been overwritten in directRequest + // However, if proxy is used according to http.ProxyFromEnvironment or proxy providers, + // address will be the address of the proxy server. + + // This means we can safely use the address in dialInfo if proxy is not used (the address and network will be same any way) + // or if the upstream is unix (because there is no way socks or http proxy can be used for unix address). if dialInfo, ok := GetDialInfo(ctx); ok { - if strings.HasPrefix(dialInfo.Network, "unix") { + if caddyhttp.GetVar(ctx, proxyVarKey) == nil || strings.HasPrefix(dialInfo.Network, "unix") { network = dialInfo.Network address = dialInfo.Address } @@ -376,9 +381,19 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e return nil, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``") } } + // we need to keep track if a proxy is used for a request + proxyWrapper := func(req *http.Request) (*url.URL, error) { + u, err := proxy(req) + if u == nil || err != nil { + return u, err + } + // there must be a proxy for this request + caddyhttp.SetVar(req.Context(), proxyVarKey, u) + return u, nil + } rt := &http.Transport{ - Proxy: proxy, + Proxy: proxyWrapper, DialContext: dialContext, MaxConnsPerHost: h.MaxConnsPerHost, ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout), @@ -457,24 +472,10 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e rt.IdleConnTimeout = time.Duration(h.KeepAlive.IdleConnTimeout) } - // The proxy protocol header can only be sent once right after opening the connection. - // So single connection must not be used for multiple requests, which can potentially - // come from different clients. - if !rt.DisableKeepAlives && h.ProxyProtocol != "" { - caddyCtx.Logger().Warn("disabling keepalives, they are incompatible with using PROXY protocol") - rt.DisableKeepAlives = true - } - if h.Compression != nil { rt.DisableCompression = !*h.Compression } - if slices.Contains(h.Versions, "2") { - if err := http2.ConfigureTransport(rt); err != nil { - return nil, err - } - } - // configure HTTP/3 transport if enabled; however, this does not // automatically fall back to lower versions like most web browsers // do (that'd add latency and complexity, besides, we expect that @@ -492,25 +493,22 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported") } - // if h2c is enabled, configure its transport (std lib http.Transport - // does not "HTTP/2 over cleartext TCP") - if slices.Contains(h.Versions, "h2c") { - // crafting our own http2.Transport doesn't allow us to utilize - // most of the customizations/preferences on the http.Transport, - // because, for some reason, only http2.ConfigureTransport() - // is allowed to set the unexported field that refers to a base - // http.Transport config; oh well - h2t := &http2.Transport{ - // kind of a hack, but for plaintext/H2C requests, pretend to dial TLS - DialTLSContext: func(ctx context.Context, network, address string, _ *tls.Config) (net.Conn, error) { - return dialContext(ctx, network, address) - }, - AllowHTTP: true, + // if h2/c is enabled, configure it explicitly + if slices.Contains(h.Versions, "2") || slices.Contains(h.Versions, "h2c") { + if err := http2.ConfigureTransport(rt); err != nil { + return nil, err } - if h.Compression != nil { - h2t.DisableCompression = !*h.Compression + + // DisableCompression from h2 is configured by http2.ConfigureTransport + // Likewise, DisableKeepAlives from h1 is used too. + + // Protocols field is only used when the request is not using TLS, + // http1/2 over tls is still allowed + if slices.Contains(h.Versions, "h2c") { + rt.Protocols = new(http.Protocols) + rt.Protocols.SetUnencryptedHTTP2(true) + rt.Protocols.SetHTTP1(false) } - h.h2cTransport = h2t } return rt, nil @@ -525,15 +523,6 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { return h.h3Transport.RoundTrip(req) } - // if H2C ("HTTP/2 over cleartext") is enabled and the upstream request is - // HTTP without TLS, use the alternate H2C-capable transport instead - if req.URL.Scheme == "http" && h.h2cTransport != nil { - // There is no dedicated DisableKeepAlives field in *http2.Transport. - // This is an alternative way to disable keep-alive. - req.Close = h.Transport.DisableKeepAlives - return h.h2cTransport.RoundTrip(req) - } - return h.Transport.RoundTrip(req) } diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 0c4028ce7..794860d8e 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -1198,7 +1198,7 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int // directRequest modifies only req.URL so that it points to the upstream // in the given DialInfo. It must modify ONLY the request URL. -func (Handler) directRequest(req *http.Request, di DialInfo) { +func (h *Handler) directRequest(req *http.Request, di DialInfo) { // we need a host, so set the upstream's host address reqHost := di.Address @@ -1209,6 +1209,13 @@ func (Handler) directRequest(req *http.Request, di DialInfo) { reqHost = di.Host } + // add client address to the host to let transport differentiate requests from different clients + if ht, ok := h.Transport.(*HTTPTransport); ok && ht.ProxyProtocol != "" { + if proxyProtocolInfo, ok := caddyhttp.GetVar(req.Context(), proxyProtocolInfoVarKey).(ProxyProtocolInfo); ok { + reqHost = proxyProtocolInfo.AddrPort.String() + "->" + reqHost + } + } + req.URL.Host = reqHost } diff --git a/modules/caddyhttp/tracing/tracer.go b/modules/caddyhttp/tracing/tracer.go index 261952aa6..ab2ddf8a2 100644 --- a/modules/caddyhttp/tracing/tracer.go +++ b/modules/caddyhttp/tracing/tracer.go @@ -5,9 +5,9 @@ import ( "fmt" "net/http" + "go.opentelemetry.io/contrib/exporters/autoexport" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/contrib/propagators/autoprop" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -59,7 +59,7 @@ func newOpenTelemetryWrapper( return ot, fmt.Errorf("creating resource error: %w", err) } - traceExporter, err := otlptracegrpc.New(ctx) + traceExporter, err := autoexport.NewSpanExporter(ctx) if err != nil { return ot, fmt.Errorf("creating trace exporter error: %w", err) } diff --git a/modules/caddytls/leaffolderloader.go b/modules/caddytls/leaffolderloader.go index 20f5aa82c..fe5e9e244 100644 --- a/modules/caddytls/leaffolderloader.go +++ b/modules/caddytls/leaffolderloader.go @@ -29,9 +29,9 @@ func init() { caddy.RegisterModule(LeafFolderLoader{}) } -// LeafFolderLoader loads certificates and their associated keys from disk +// LeafFolderLoader loads certificates from disk // by recursively walking the specified directories, looking for PEM -// files which contain both a certificate and a key. +// files which contain a certificate. type LeafFolderLoader struct { Folders []string `json:"folders,omitempty"` } diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go index 42aa29757..cf35e7178 100644 --- a/modules/logging/filters_test.go +++ b/modules/logging/filters_test.go @@ -404,12 +404,12 @@ func TestMultiRegexpFilterInputSizeLimit(t *testing.T) { // Test with very large input (should be truncated) largeInput := strings.Repeat("test", 300000) // Creates ~1.2MB string out := f.Filter(zapcore.Field{String: largeInput}) - + // The input should be truncated to 1MB and still processed if len(out.String) > 1000000 { t.Fatalf("output string not truncated: length %d", len(out.String)) } - + // Should still contain replacements within the truncated portion if !strings.Contains(out.String, "REPLACED") { t.Fatalf("replacements not applied to truncated input")