diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fe65a6539ff..309ef79357f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: GO_SEMVER: '~1.21.0' - go: '1.22' - GO_SEMVER: '~1.22.0' + GO_SEMVER: '~1.22.1' # 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) diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index b097f75ed819..676607d0e6ba 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -35,7 +35,7 @@ jobs: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - go: '1.22' - GO_SEMVER: '~1.22.0' + GO_SEMVER: '~1.22.1' runs-on: ubuntu-latest continue-on-error: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 918734751854..bfb91dc66fe3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '~1.22.0' + go-version: '~1.22.1' check-latest: true - name: golangci-lint @@ -66,5 +66,5 @@ jobs: - name: govulncheck uses: golang/govulncheck-action@v1 with: - go-version-input: '~1.22.0' + go-version-input: '~1.22.1' check-latest: true diff --git a/caddyconfig/httpcaddyfile/shorthands.go b/caddyconfig/httpcaddyfile/shorthands.go index 75795ae61eeb..5855d127c8a0 100644 --- a/caddyconfig/httpcaddyfile/shorthands.go +++ b/caddyconfig/httpcaddyfile/shorthands.go @@ -33,7 +33,7 @@ func NewShorthandReplacer() ShorthandReplacer { {regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"}, {regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"}, {regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"}, - {regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"}, + {regexp.MustCompile(`{re\.([\w-\.]*)}`), "{http.regexp.$1}"}, {regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"}, {regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"}, {regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"}, diff --git a/caddytest/integration/caddyfile_adapt/log_append_encoder.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_append_encoder.caddyfiletest new file mode 100644 index 000000000000..88a6cd6be7aa --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/log_append_encoder.caddyfiletest @@ -0,0 +1,63 @@ +{ + log { + format append { + wrap json + fields { + wrap "foo" + } + env {env.EXAMPLE} + int 1 + float 1.1 + bool true + string "string" + } + } +} + +:80 { + respond "Hello, World!" +} +---------- +{ + "logging": { + "logs": { + "default": { + "encoder": { + "fields": { + "bool": true, + "env": "{env.EXAMPLE}", + "float": 1.1, + "int": 1, + "string": "string", + "wrap": "foo" + }, + "format": "append", + "wrap": { + "format": "json" + } + } + } + } + }, + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "handle": [ + { + "body": "Hello, World!", + "handler": "static_response" + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/log_filters.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_filters.caddyfiletest index 28524a346603..1b2fc2e502ea 100644 --- a/caddytest/integration/caddyfile_adapt/log_filters.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/log_filters.caddyfiletest @@ -4,27 +4,31 @@ log { output stdout format filter { wrap console + + # long form, with "fields" wrapper fields { uri query { replace foo REDACTED delete bar hash baz } - request>headers>Authorization replace REDACTED - request>headers>Server delete - request>headers>Cookie cookie { - replace foo REDACTED - delete bar - hash baz - } - request>remote_ip ip_mask { - ipv4 24 - ipv6 32 - } - request>client_ip ip_mask 16 32 - request>headers>Regexp regexp secret REDACTED - request>headers>Hash hash } + + # short form, flatter structure + request>headers>Authorization replace REDACTED + request>headers>Server delete + request>headers>Cookie cookie { + replace foo REDACTED + delete bar + hash baz + } + request>remote_ip ip_mask { + ipv4 24 + ipv6 32 + } + request>client_ip ip_mask 16 32 + request>headers>Regexp regexp secret REDACTED + request>headers>Hash hash } } ---------- diff --git a/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest b/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest index d5c35b3c3fbb..30bc2c128660 100644 --- a/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest @@ -1,5 +1,9 @@ localhost:80 + respond * "{header.content-type} {labels.0} {query.p} {path.0} {re.name.0}" + +@match path_regexp ^/foo(.*)$ +respond @match "{re.1}" ---------- { "apps": { @@ -22,6 +26,21 @@ respond * "{header.content-type} {labels.0} {query.p} {path.0} {re.name.0}" { "handler": "subroute", "routes": [ + { + "handle": [ + { + "body": "{http.regexp.1}", + "handler": "static_response" + } + ], + "match": [ + { + "path_regexp": { + "pattern": "^/foo(.*)$" + } + } + ] + }, { "handle": [ { diff --git a/caddytest/integration/leafcertloaders_test.go b/caddytest/integration/leafcertloaders_test.go new file mode 100644 index 000000000000..4399902eaeef --- /dev/null +++ b/caddytest/integration/leafcertloaders_test.go @@ -0,0 +1,70 @@ +package integration + +import ( + "testing" + + "github.com/caddyserver/caddy/v2/caddytest" +) + +func TestLeafCertLoaders(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + "admin": { + "listen": "localhost:2999" + }, + "apps": { + "http": { + "http_port": 9080, + "https_port": 9443, + "grace_period": 1, + "servers": { + "srv0": { + "listen": [ + ":9443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "client_authentication": { + "verifiers": [ + { + "verifier": "leaf", + "leaf_certs_loaders": [ + { + "loader": "file", + "files": ["../leafcert.pem"] + }, + { + "loader": "folder", + "folders": ["../"] + }, + { + "loader": "storage" + }, + { + "loader": "pem" + } + ] + } + ] + } + } + ] + } + } + } + } + }`, "json") +} diff --git a/caddytest/leafcert.pem b/caddytest/leafcert.pem new file mode 100644 index 000000000000..03febfd3ae1c --- /dev/null +++ b/caddytest/leafcert.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL +MAkGA1UECBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMC +VU4xFDASBgNVBAMTC0hlcm9uZyBZYW5nMB4XDTA1MDcxNTIxMTk0N1oXDTA1MDgx +NDIxMTk0N1owVzELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAlBOMQswCQYDVQQHEwJD +TjELMAkGA1UEChMCT04xCzAJBgNVBAsTAlVOMRQwEgYDVQQDEwtIZXJvbmcgWWFu +ZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCp5hnG7ogBhtlynpOS21cBewKE/B7j +V14qeyslnr26xZUsSVko36ZnhiaO/zbMOoRcKK9vEcgMtcLFuQTWDl3RAgMBAAGj +gbEwga4wHQYDVR0OBBYEFFXI70krXeQDxZgbaCQoR4jUDncEMH8GA1UdIwR4MHaA +FFXI70krXeQDxZgbaCQoR4jUDncEoVukWTBXMQswCQYDVQQGEwJDTjELMAkGA1UE +CBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMCVU4xFDAS +BgNVBAMTC0hlcm9uZyBZYW5nggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEE +BQADQQA/ugzBrjjK9jcWnDVfGHlk3icNRq0oV7Ri32z/+HQX67aRfgZu7KWdI+Ju +Wm7DCfrPNGVwFWUQOmsPue9rZBgO +-----END CERTIFICATE----- diff --git a/cmd/main.go b/cmd/main.go index 6fd58c62fda5..d832cbc5e666 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,6 +17,7 @@ package caddycmd import ( "bufio" "bytes" + "encoding/json" "errors" "flag" "fmt" @@ -107,6 +108,12 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) { } func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) { + // if no logger is provided, use a nop logger + // just so we don't have to check for nil + if logger == nil { + logger = zap.NewNop() + } + // specifying an adapter without a config file is ambiguous if adapterName != "" && configFile == "" { return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)") @@ -119,16 +126,16 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ if configFile != "" { if configFile == "-" { config, err = io.ReadAll(os.Stdin) + if err != nil { + return nil, "", fmt.Errorf("reading config from stdin: %v", err) + } + logger.Info("using config from stdin") } else { config, err = os.ReadFile(configFile) - } - if err != nil { - return nil, "", fmt.Errorf("reading config file: %v", err) - } - if logger != nil { - logger.Info("using provided configuration", - zap.String("config_file", configFile), - zap.String("config_adapter", adapterName)) + if err != nil { + return nil, "", fmt.Errorf("reading config from file: %v", err) + } + logger.Info("using config from file", zap.String("file", configFile)) } } else if adapterName == "" { // if the Caddyfile adapter is plugged in, we can try using an @@ -145,9 +152,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ } else { // success reading default Caddyfile configFile = "Caddyfile" - if logger != nil { - logger.Info("using adjacent Caddyfile") - } + logger.Info("using adjacent Caddyfile") } } } @@ -177,16 +182,24 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ if err != nil { return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err) } + logger.Info("adapted config to JSON", zap.String("adapter", adapterName)) for _, warn := range warnings { msg := warn.Message if warn.Directive != "" { msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) } - if logger != nil { - logger.Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line)) - } + logger.Warn(msg, + zap.String("adapter", adapterName), + zap.String("file", warn.File), + zap.Int("line", warn.Line)) } config = adaptedConfig + } else { + // validate that the config is at least valid JSON + err = json.Unmarshal(config, new(any)) + if err != nil { + return nil, "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err) + } } return config, configFile, nil diff --git a/go.mod b/go.mod index 0d02c289040d..8760d8351129 100644 --- a/go.mod +++ b/go.mod @@ -148,7 +148,7 @@ require ( golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.16.1 // indirect google.golang.org/grpc v1.60.1 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index b4e081a3e744..1c7d91d07f56 100644 --- a/go.sum +++ b/go.sum @@ -855,6 +855,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go index 2d21a5ca4ed6..46e45c64652d 100644 --- a/modules/caddyhttp/reverseproxy/upstreams.go +++ b/modules/caddyhttp/reverseproxy/upstreams.go @@ -49,6 +49,13 @@ type SRVUpstreams struct { // Results are cached between lookups. Default: 1m Refresh caddy.Duration `json:"refresh,omitempty"` + // If > 0 and there is an error with the lookup, + // continue to use the cached results for up to + // this long before trying again, (even though they + // are stale) instead of returning an error to the + // client. Default: 0s. + GracePeriod caddy.Duration `json:"grace_period,omitempty"` + // Configures the DNS resolver used to resolve the // SRV address to SRV records. Resolver *UpstreamResolver `json:"resolver,omitempty"` @@ -140,6 +147,12 @@ func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { // out and an error will be returned alongside the remaining results, if any." Thus, we // only return an error if no records were also returned. if len(records) == 0 { + if su.GracePeriod > 0 { + su.logger.Error("SRV lookup failed; using previously cached", zap.Error(err)) + cached.freshness = time.Now().Add(time.Duration(su.GracePeriod) - time.Duration(su.Refresh)) + srvs[suAddr] = cached + return allNew(cached.upstreams), nil + } return nil, err } su.logger.Warn("SRV records filtered", zap.Error(err)) diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index 917a150513f3..f5afe264a28f 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -57,6 +57,12 @@ func (m VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next H v = repl.ReplaceAll(valStr, "") } vars[keyExpanded] = v + + // Special case: the user ID is in the replacer, pulled from there + // for access logs. Allow users to override it with the vars handler. + if keyExpanded == "http.auth.user.id" { + repl.Set(keyExpanded, v) + } } return next.ServeHTTP(w, r) } diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 20b781274d2f..49c7add49d33 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -651,7 +651,7 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro } trustedLeafCerts = append(trustedLeafCerts, clientCert) } - clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{TrustedLeafCerts: trustedLeafCerts}) + clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{trustedLeafCerts: trustedLeafCerts}) } // if a custom verification function already exists, wrap it @@ -715,7 +715,8 @@ func setDefaultTLSParams(cfg *tls.Config) { // LeafCertClientAuth verifies the client's leaf certificate. type LeafCertClientAuth struct { - TrustedLeafCerts []*x509.Certificate + LeafCertificateLoadersRaw []json.RawMessage `json:"leaf_certs_loaders,omitempty" caddy:"namespace=tls.leaf_cert_loader inline_key=loader"` + trustedLeafCerts []*x509.Certificate } // CaddyModule returns the Caddy module information. @@ -726,6 +727,30 @@ func (LeafCertClientAuth) CaddyModule() caddy.ModuleInfo { } } +func (l *LeafCertClientAuth) Provision(ctx caddy.Context) error { + if l.LeafCertificateLoadersRaw == nil { + return nil + } + val, err := ctx.LoadModule(l, "LeafCertificateLoadersRaw") + if err != nil { + return fmt.Errorf("could not parse leaf certificates loaders: %s", err.Error()) + } + trustedLeafCertloaders := []LeafCertificateLoader{} + for _, loader := range val.([]any) { + trustedLeafCertloaders = append(trustedLeafCertloaders, loader.(LeafCertificateLoader)) + } + trustedLeafCertificates := []*x509.Certificate{} + for _, loader := range trustedLeafCertloaders { + certs, err := loader.LoadLeafCertificates() + if err != nil { + return fmt.Errorf("could not load leaf certificates: %s", err.Error()) + } + trustedLeafCertificates = append(trustedLeafCertificates, certs...) + } + l.trustedLeafCerts = trustedLeafCertificates + return nil +} + func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error { if len(rawCerts) == 0 { return fmt.Errorf("no client certificate provided") @@ -736,7 +761,7 @@ func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x5 return fmt.Errorf("can't parse the given certificate: %s", err.Error()) } - for _, trustedLeafCert := range l.TrustedLeafCerts { + for _, trustedLeafCert := range l.trustedLeafCerts { if remoteLeafCert.Equal(trustedLeafCert) { return nil } @@ -765,6 +790,12 @@ type ConnectionMatcher interface { Match(*tls.ClientHelloInfo) bool } +// LeafCertificateLoader is a type that loads the trusted leaf certificates +// for the tls.leaf_cert_loader modules +type LeafCertificateLoader interface { + LoadLeafCertificates() ([]*x509.Certificate, error) +} + // ClientCertificateVerifier is a type which verifies client certificates. // It is called during verifyPeerCertificate in the TLS handshake. type ClientCertificateVerifier interface { diff --git a/modules/caddytls/leaffileloader.go b/modules/caddytls/leaffileloader.go new file mode 100644 index 000000000000..1d3f3a3e59f7 --- /dev/null +++ b/modules/caddytls/leaffileloader.go @@ -0,0 +1,99 @@ +// 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/x509" + "encoding/pem" + "fmt" + "os" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(LeafFileLoader{}) +} + +// LeafFileLoader loads leaf certificates from disk. +type LeafFileLoader struct { + Files []string `json:"files,omitempty"` +} + +// Provision implements caddy.Provisioner. +func (fl *LeafFileLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, path := range fl.Files { + fl.Files[k] = repl.ReplaceKnown(path, "") + } + return nil +} + +// CaddyModule returns the Caddy module information. +func (LeafFileLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.leaf_cert_loader.file", + New: func() caddy.Module { return new(LeafFileLoader) }, + } +} + +// LoadLeafCertificates returns the certificates to be loaded by fl. +func (fl LeafFileLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { + certificates := make([]*x509.Certificate, 0, len(fl.Files)) + for _, path := range fl.Files { + ders, err := convertPEMFilesToDERBytes(path) + if err != nil { + return nil, err + } + certs, err := x509.ParseCertificates(ders) + if err != nil { + return nil, err + } + certificates = append(certificates, certs...) + } + return certificates, nil +} + +func convertPEMFilesToDERBytes(filename string) ([]byte, error) { + certDataPEM, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var ders []byte + // while block is not nil, we have more certificates in the file + for block, rest := pem.Decode(certDataPEM); block != nil; block, rest = pem.Decode(rest) { + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename) + } + ders = append( + ders, + block.Bytes..., + ) + } + // if we decoded nothing, return an error + if len(ders) == 0 { + return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename) + } + return ders, nil +} + +// Interface guard +var ( + _ LeafCertificateLoader = (*LeafFileLoader)(nil) + _ caddy.Provisioner = (*LeafFileLoader)(nil) +) diff --git a/modules/caddytls/leaffileloader_test.go b/modules/caddytls/leaffileloader_test.go new file mode 100644 index 000000000000..940ed78bd70b --- /dev/null +++ b/modules/caddytls/leaffileloader_test.go @@ -0,0 +1,38 @@ +package caddytls + +import ( + "context" + "encoding/pem" + "os" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func TestLeafFileLoader(t *testing.T) { + fl := LeafFileLoader{Files: []string{"../../caddytest/leafcert.pem"}} + fl.Provision(caddy.Context{Context: context.Background()}) + + out, err := fl.LoadLeafCertificates() + if err != nil { + t.Errorf("Leaf certs file loading test failed: %v", err) + } + if len(out) != 1 { + t.Errorf("Error loading leaf cert in memory struct") + return + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: out[0].Raw}) + + pemFileBytes, err := os.ReadFile("../../caddytest/leafcert.pem") + if err != nil { + t.Errorf("Unable to read the example certificate from the file") + } + + // Remove /r because windows. + pemFileString := strings.ReplaceAll(string(pemFileBytes), "\r\n", "\n") + + if string(pemBytes) != pemFileString { + t.Errorf("Leaf Certificate File Loader: Failed to load the correct certificate") + } +} diff --git a/modules/caddytls/leaffolderloader.go b/modules/caddytls/leaffolderloader.go new file mode 100644 index 000000000000..5c7b06e7681a --- /dev/null +++ b/modules/caddytls/leaffolderloader.go @@ -0,0 +1,97 @@ +// 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/x509" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(LeafFolderLoader{}) +} + +// LeafFolderLoader loads certificates and their associated keys from disk +// by recursively walking the specified directories, looking for PEM +// files which contain both a certificate and a key. +type LeafFolderLoader struct { + Folders []string `json:"folders,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (LeafFolderLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.leaf_cert_loader.folder", + New: func() caddy.Module { return new(LeafFolderLoader) }, + } +} + +// Provision implements caddy.Provisioner. +func (fl *LeafFolderLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, path := range fl.Folders { + fl.Folders[k] = repl.ReplaceKnown(path, "") + } + return nil +} + +// LoadLeafCertificates loads all the leaf certificates in the directories +// listed in fl from all files ending with .pem. +func (fl LeafFolderLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { + var certs []*x509.Certificate + for _, dir := range fl.Folders { + err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("unable to traverse into path: %s", fpath) + } + if info.IsDir() { + return nil + } + if !strings.HasSuffix(strings.ToLower(info.Name()), ".pem") { + return nil + } + + certData, err := convertPEMFilesToDERBytes(fpath) + if err != nil { + return err + } + cert, err := x509.ParseCertificate(certData) + if err != nil { + return fmt.Errorf("%s: %w", fpath, err) + } + + certs = append(certs, cert) + + return nil + }) + if err != nil { + return nil, err + } + } + return certs, nil +} + +var ( + _ LeafCertificateLoader = (*LeafFolderLoader)(nil) + _ caddy.Provisioner = (*LeafFolderLoader)(nil) +) diff --git a/modules/caddytls/leaffolderloader_test.go b/modules/caddytls/leaffolderloader_test.go new file mode 100644 index 000000000000..35fecba89b33 --- /dev/null +++ b/modules/caddytls/leaffolderloader_test.go @@ -0,0 +1,37 @@ +package caddytls + +import ( + "context" + "encoding/pem" + "os" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func TestLeafFolderLoader(t *testing.T) { + fl := LeafFolderLoader{Folders: []string{"../../caddytest"}} + fl.Provision(caddy.Context{Context: context.Background()}) + + out, err := fl.LoadLeafCertificates() + if err != nil { + t.Errorf("Leaf certs folder loading test failed: %v", err) + } + if len(out) != 1 { + t.Errorf("Error loading leaf cert in memory struct") + return + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: out[0].Raw}) + pemFileBytes, err := os.ReadFile("../../caddytest/leafcert.pem") + if err != nil { + t.Errorf("Unable to read the example certificate from the file") + } + + // Remove /r because windows. + pemFileString := strings.ReplaceAll(string(pemFileBytes), "\r\n", "\n") + + if string(pemBytes) != pemFileString { + t.Errorf("Leaf Certificate Folder Loader: Failed to load the correct certificate") + } +} diff --git a/modules/caddytls/leafpemloader.go b/modules/caddytls/leafpemloader.go new file mode 100644 index 000000000000..28467ccf2c7a --- /dev/null +++ b/modules/caddytls/leafpemloader.go @@ -0,0 +1,76 @@ +// 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/x509" + "fmt" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(LeafPEMLoader{}) +} + +// LeafPEMLoader loads leaf certificates by +// decoding their PEM blocks directly. This has the advantage +// of not needing to store them on disk at all. +type LeafPEMLoader struct { + Certificates []string `json:"certificates,omitempty"` +} + +// Provision implements caddy.Provisioner. +func (pl *LeafPEMLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for i, cert := range pl.Certificates { + pl.Certificates[i] = repl.ReplaceKnown(cert, "") + } + return nil +} + +// CaddyModule returns the Caddy module information. +func (LeafPEMLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.leaf_cert_loader.pem", + New: func() caddy.Module { return new(LeafPEMLoader) }, + } +} + +// LoadLeafCertificates returns the certificates contained in pl. +func (pl LeafPEMLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { + certs := make([]*x509.Certificate, 0, len(pl.Certificates)) + for i, cert := range pl.Certificates { + derBytes, err := convertPEMToDER([]byte(cert)) + if err != nil { + return nil, fmt.Errorf("PEM leaf certificate loader, cert %d: %v", i, err) + } + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + return nil, fmt.Errorf("PEM cert %d: %v", i, err) + } + certs = append(certs, cert) + } + return certs, nil +} + +// Interface guard +var ( + _ LeafCertificateLoader = (*LeafPEMLoader)(nil) + _ caddy.Provisioner = (*LeafPEMLoader)(nil) +) diff --git a/modules/caddytls/leafpemloader_test.go b/modules/caddytls/leafpemloader_test.go new file mode 100644 index 000000000000..04a9efd25310 --- /dev/null +++ b/modules/caddytls/leafpemloader_test.go @@ -0,0 +1,54 @@ +package caddytls + +import ( + "context" + "encoding/pem" + "os" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func TestLeafPEMLoader(t *testing.T) { + pl := LeafPEMLoader{Certificates: []string{` +-----BEGIN CERTIFICATE----- +MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL +MAkGA1UECBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMC +VU4xFDASBgNVBAMTC0hlcm9uZyBZYW5nMB4XDTA1MDcxNTIxMTk0N1oXDTA1MDgx +NDIxMTk0N1owVzELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAlBOMQswCQYDVQQHEwJD +TjELMAkGA1UEChMCT04xCzAJBgNVBAsTAlVOMRQwEgYDVQQDEwtIZXJvbmcgWWFu +ZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCp5hnG7ogBhtlynpOS21cBewKE/B7j +V14qeyslnr26xZUsSVko36ZnhiaO/zbMOoRcKK9vEcgMtcLFuQTWDl3RAgMBAAGj +gbEwga4wHQYDVR0OBBYEFFXI70krXeQDxZgbaCQoR4jUDncEMH8GA1UdIwR4MHaA +FFXI70krXeQDxZgbaCQoR4jUDncEoVukWTBXMQswCQYDVQQGEwJDTjELMAkGA1UE +CBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMCVU4xFDAS +BgNVBAMTC0hlcm9uZyBZYW5nggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEE +BQADQQA/ugzBrjjK9jcWnDVfGHlk3icNRq0oV7Ri32z/+HQX67aRfgZu7KWdI+Ju +Wm7DCfrPNGVwFWUQOmsPue9rZBgO +-----END CERTIFICATE----- +`}} + pl.Provision(caddy.Context{Context: context.Background()}) + + out, err := pl.LoadLeafCertificates() + if err != nil { + t.Errorf("Leaf certs pem loading test failed: %v", err) + } + if len(out) != 1 { + t.Errorf("Error loading leaf cert in memory struct") + return + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: out[0].Raw}) + + pemFileBytes, err := os.ReadFile("../../caddytest/leafcert.pem") + if err != nil { + t.Errorf("Unable to read the example certificate from the file") + } + + // Remove /r because windows. + pemFileString := strings.ReplaceAll(string(pemFileBytes), "\r\n", "\n") + + if string(pemBytes) != pemFileString { + t.Errorf("Leaf Certificate Folder Loader: Failed to load the correct certificate") + } +} diff --git a/modules/caddytls/leafstorageloader.go b/modules/caddytls/leafstorageloader.go new file mode 100644 index 000000000000..0215c8af2a6f --- /dev/null +++ b/modules/caddytls/leafstorageloader.go @@ -0,0 +1,129 @@ +// 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/x509" + "encoding/json" + "encoding/pem" + "fmt" + + "github.com/caddyserver/certmagic" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(LeafStorageLoader{}) +} + +// LeafStorageLoader loads leaf certificates from the +// globally configured storage module. +type LeafStorageLoader struct { + // A list of certificate file names to be loaded from storage. + Certificates []string `json:"certificates,omitempty"` + + // The storage module where the trusted leaf certificates are stored. Absent + // explicit storage implies the use of Caddy default storage. + StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` + + // Reference to the globally configured storage module. + storage certmagic.Storage + + ctx caddy.Context +} + +// CaddyModule returns the Caddy module information. +func (LeafStorageLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.leaf_cert_loader.storage", + New: func() caddy.Module { return new(LeafStorageLoader) }, + } +} + +// Provision loads the storage module for sl. +func (sl *LeafStorageLoader) Provision(ctx caddy.Context) error { + if sl.StorageRaw != nil { + val, err := ctx.LoadModule(sl, "StorageRaw") + if err != nil { + return fmt.Errorf("loading storage module: %v", err) + } + cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() + if err != nil { + return fmt.Errorf("creating storage configuration: %v", err) + } + sl.storage = cmStorage + } + if sl.storage == nil { + sl.storage = ctx.Storage() + } + sl.ctx = ctx + + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, path := range sl.Certificates { + sl.Certificates[k] = repl.ReplaceKnown(path, "") + } + return nil +} + +// LoadLeafCertificates returns the certificates to be loaded by sl. +func (sl LeafStorageLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { + certificates := make([]*x509.Certificate, 0, len(sl.Certificates)) + for _, path := range sl.Certificates { + certData, err := sl.storage.Load(sl.ctx, path) + if err != nil { + return nil, err + } + + ders, err := convertPEMToDER(certData) + if err != nil { + return nil, err + } + certs, err := x509.ParseCertificates(ders) + if err != nil { + return nil, err + } + certificates = append(certificates, certs...) + } + return certificates, nil +} + +func convertPEMToDER(pemData []byte) ([]byte, error) { + var ders []byte + // while block is not nil, we have more certificates in the file + for block, rest := pem.Decode(pemData); block != nil; block, rest = pem.Decode(rest) { + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("no CERTIFICATE pem block found in the given pem data") + } + ders = append( + ders, + block.Bytes..., + ) + } + // if we decoded nothing, return an error + if len(ders) == 0 { + return nil, fmt.Errorf("no CERTIFICATE pem block found in the given pem data") + } + return ders, nil +} + +// Interface guard +var ( + _ LeafCertificateLoader = (*LeafStorageLoader)(nil) + _ caddy.Provisioner = (*LeafStorageLoader)(nil) +) diff --git a/modules/logging/appendencoder.go b/modules/logging/appendencoder.go new file mode 100644 index 000000000000..63bd532d02b7 --- /dev/null +++ b/modules/logging/appendencoder.go @@ -0,0 +1,357 @@ +// 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" + "strings" + "time" + + "go.uber.org/zap" + "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(AppendEncoder{}) +} + +// AppendEncoder can be used to add fields to all log entries +// that pass through it. It is a wrapper around another +// encoder, which it uses to actually encode the log entries. +// It is most useful for adding information about the Caddy +// instance that is producing the log entries, possibly via +// an environment variable. +type AppendEncoder struct { + // 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"` + + // A map of field names to their values. The values + // can be global placeholders (e.g. env vars), or constants. + // Note that the encoder does not run as part of an HTTP + // request context, so request placeholders are not available. + Fields map[string]any `json:"fields,omitempty"` + + wrapped zapcore.Encoder + repl *caddy.Replacer + + wrappedIsDefault bool + ctx caddy.Context +} + +// CaddyModule returns the Caddy module information. +func (AppendEncoder) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.logging.encoders.append", + New: func() caddy.Module { return new(AppendEncoder) }, + } +} + +// Provision sets up the encoder. +func (fe *AppendEncoder) Provision(ctx caddy.Context) error { + fe.ctx = ctx + fe.repl = caddy.NewReplacer() + + if fe.WrappedRaw == nil { + // if wrap is not specified, default to JSON + fe.wrapped = &JSONEncoder{} + if p, ok := fe.wrapped.(caddy.Provisioner); ok { + if err := p.Provision(ctx); err != nil { + return fmt.Errorf("provisioning fallback encoder module: %v", err) + } + } + fe.wrappedIsDefault = true + } else { + // set up wrapped encoder + val, err := ctx.LoadModule(fe, "WrappedRaw") + if err != nil { + return fmt.Errorf("loading fallback encoder module: %v", err) + } + fe.wrapped = val.(zapcore.Encoder) + } + + return nil +} + +// ConfigureDefaultFormat will set the default 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 (fe *AppendEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error { + if !fe.wrappedIsDefault { + if cfd, ok := fe.wrapped.(caddy.ConfiguresFormatterDefault); ok { + return cfd.ConfigureDefaultFormat(wo) + } + return nil + } + + if caddy.IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) { + fe.wrapped = &ConsoleEncoder{} + if p, ok := fe.wrapped.(caddy.Provisioner); ok { + if err := p.Provision(fe.ctx); err != nil { + return fmt.Errorf("provisioning fallback encoder module: %v", err) + } + } + } + return nil +} + +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// append { +// wrap +// fields { +// +// } +// +// } +func (fe *AppendEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume encoder name + + // parse a field + parseField := func() error { + if fe.Fields == nil { + fe.Fields = make(map[string]any) + } + field := d.Val() + if !d.NextArg() { + return d.ArgErr() + } + fe.Fields[field] = d.ScalarVal() + if d.NextArg() { + return d.ArgErr() + } + return nil + } + + for d.NextBlock(0) { + switch d.Val() { + case "wrap": + 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) + } + fe.WrappedRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, nil) + + case "fields": + for nesting := d.Nesting(); d.NextBlock(nesting); { + err := parseField() + if err != nil { + return err + } + } + + default: + // if unknown, assume it's a field so that + // the config can be flat + err := parseField() + if err != nil { + return err + } + } + } + return nil +} + +// AddArray is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { + return fe.wrapped.AddArray(key, marshaler) +} + +// AddObject is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { + return fe.wrapped.AddObject(key, marshaler) +} + +// AddBinary is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddBinary(key string, value []byte) { + fe.wrapped.AddBinary(key, value) +} + +// AddByteString is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddByteString(key string, value []byte) { + fe.wrapped.AddByteString(key, value) +} + +// AddBool is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddBool(key string, value bool) { + fe.wrapped.AddBool(key, value) +} + +// AddComplex128 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddComplex128(key string, value complex128) { + fe.wrapped.AddComplex128(key, value) +} + +// AddComplex64 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddComplex64(key string, value complex64) { + fe.wrapped.AddComplex64(key, value) +} + +// AddDuration is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddDuration(key string, value time.Duration) { + fe.wrapped.AddDuration(key, value) +} + +// AddFloat64 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddFloat64(key string, value float64) { + fe.wrapped.AddFloat64(key, value) +} + +// AddFloat32 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddFloat32(key string, value float32) { + fe.wrapped.AddFloat32(key, value) +} + +// AddInt is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddInt(key string, value int) { + fe.wrapped.AddInt(key, value) +} + +// AddInt64 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddInt64(key string, value int64) { + fe.wrapped.AddInt64(key, value) +} + +// AddInt32 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddInt32(key string, value int32) { + fe.wrapped.AddInt32(key, value) +} + +// AddInt16 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddInt16(key string, value int16) { + fe.wrapped.AddInt16(key, value) +} + +// AddInt8 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddInt8(key string, value int8) { + fe.wrapped.AddInt8(key, value) +} + +// AddString is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddString(key, value string) { + fe.wrapped.AddString(key, value) +} + +// AddTime is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddTime(key string, value time.Time) { + fe.wrapped.AddTime(key, value) +} + +// AddUint is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUint(key string, value uint) { + fe.wrapped.AddUint(key, value) +} + +// AddUint64 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUint64(key string, value uint64) { + fe.wrapped.AddUint64(key, value) +} + +// AddUint32 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUint32(key string, value uint32) { + fe.wrapped.AddUint32(key, value) +} + +// AddUint16 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUint16(key string, value uint16) { + fe.wrapped.AddUint16(key, value) +} + +// AddUint8 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUint8(key string, value uint8) { + fe.wrapped.AddUint8(key, value) +} + +// AddUintptr is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUintptr(key string, value uintptr) { + fe.wrapped.AddUintptr(key, value) +} + +// AddReflected is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddReflected(key string, value any) error { + return fe.wrapped.AddReflected(key, value) +} + +// OpenNamespace is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) OpenNamespace(key string) { + fe.wrapped.OpenNamespace(key) +} + +// Clone is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) Clone() zapcore.Encoder { + return AppendEncoder{ + Fields: fe.Fields, + wrapped: fe.wrapped.Clone(), + repl: fe.repl, + } +} + +// EncodeEntry partially implements the zapcore.Encoder interface. +func (fe AppendEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { + fe.wrapped = fe.wrapped.Clone() + for _, field := range fields { + field.AddTo(fe) + } + + // append fields from config + for key, value := range fe.Fields { + // if the value is a string + if str, ok := value.(string); ok { + isPlaceholder := strings.HasPrefix(str, "{") && + strings.HasSuffix(str, "}") && + strings.Count(str, "{") == 1 + if isPlaceholder { + // and it looks like a placeholder, evaluate it + replaced, _ := fe.repl.Get(strings.Trim(str, "{}")) + zap.Any(key, replaced).AddTo(fe) + } else { + // just use the string as-is + zap.String(key, str).AddTo(fe) + } + } else { + // not a string, so use the value as any + zap.Any(key, value).AddTo(fe) + } + } + + return fe.wrapped.EncodeEntry(ent, nil) +} + +// Interface guards +var ( + _ zapcore.Encoder = (*AppendEncoder)(nil) + _ caddyfile.Unmarshaler = (*AppendEncoder)(nil) + _ caddy.ConfiguresFormatterDefault = (*AppendEncoder)(nil) +) diff --git a/modules/logging/filterencoder.go b/modules/logging/filterencoder.go index 9b1895d79e08..c46df0788bf4 100644 --- a/modules/logging/filterencoder.go +++ b/modules/logging/filterencoder.go @@ -145,9 +145,36 @@ func (fe *FilterEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error { // // } // } +// { +// +// } // } func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { d.Next() // consume encoder name + + // parse a field + parseField := func() error { + if fe.FieldsRaw == nil { + fe.FieldsRaw = make(map[string]json.RawMessage) + } + field := d.Val() + if !d.NextArg() { + return d.ArgErr() + } + filterName := d.Val() + moduleID := "caddy.logging.encoders.filter." + filterName + unm, err := caddyfile.UnmarshalModule(d, moduleID) + if err != nil { + return err + } + filter, ok := unm.(LogFieldFilter) + if !ok { + return d.Errf("module %s (%T) is not a logging.LogFieldFilter", moduleID, unm) + } + fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filter, "filter", filterName, nil) + return nil + } + for d.NextBlock(0) { switch d.Val() { case "wrap": @@ -168,28 +195,19 @@ func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { case "fields": for nesting := d.Nesting(); d.NextBlock(nesting); { - field := d.Val() - if !d.NextArg() { - return d.ArgErr() - } - filterName := d.Val() - moduleID := "caddy.logging.encoders.filter." + filterName - unm, err := caddyfile.UnmarshalModule(d, moduleID) + err := parseField() if err != nil { return err } - filter, ok := unm.(LogFieldFilter) - if !ok { - return d.Errf("module %s (%T) is not a logging.LogFieldFilter", moduleID, unm) - } - if fe.FieldsRaw == nil { - fe.FieldsRaw = make(map[string]json.RawMessage) - } - fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filter, "filter", filterName, nil) } default: - return d.Errf("unrecognized subdirective %s", d.Val()) + // if unknown, assume it's a field so that + // the config can be flat + err := parseField() + if err != nil { + return err + } } } return nil