diff --git a/.gitignore b/.gitignore index 6cde9db00..381bf7403 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ _gitignore/ Caddyfile Caddyfile.* !caddyfile/ +!caddyfile.go # artifacts from pprof tooling *.prof diff --git a/README.md b/README.md index e5f99efdf..57463180f 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@

-## [Features](https://caddyserver.com/v2) +## [Features](https://caddyserver.com/features) - **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile) - **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/) @@ -75,7 +75,7 @@ - **Runs anywhere** with **no external dependencies** (not even libc) - Written in Go, a language with higher **memory safety guarantees** than other servers - Actually **fun to use** -- So much more to [discover](https://caddyserver.com/v2) +- So much more to [discover](https://caddyserver.com/features) ## Install diff --git a/admin.go b/admin.go index c6151d114..3950a92fa 100644 --- a/admin.go +++ b/admin.go @@ -474,7 +474,6 @@ func manageIdentity(ctx Context, cfg *Config) error { // import the caddytls package -- but it works if cfg.Admin.Identity.IssuersRaw == nil { cfg.Admin.Identity.IssuersRaw = []json.RawMessage{ - json.RawMessage(`{"module": "zerossl"}`), json.RawMessage(`{"module": "acme"}`), } } @@ -954,17 +953,28 @@ func makeEtag(path string, hash hash.Hash) string { return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil)) } +// This buffer pool is used to keep buffers for +// reading the config file during eTag header generation +var bufferPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, +} + func handleConfig(w http.ResponseWriter, r *http.Request) error { switch r.Method { case http.MethodGet: w.Header().Set("Content-Type", "application/json") - // Set the ETag as a trailer header. - // The alternative is to write the config to a buffer, and - // then hash that. - w.Header().Set("Trailer", "ETag") - hash := etagHasher() - configWriter := io.MultiWriter(w, hash) + + // Read the config into a buffer instead of writing directly to + // the response writer, as we want to set the ETag as the header, + // not the trailer. + buf := bufferPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufferPool.Put(buf) + + configWriter := io.MultiWriter(buf, hash) err := readConfig(r.URL.Path, configWriter) if err != nil { return APIError{HTTPStatus: http.StatusBadRequest, Err: err} @@ -973,6 +983,10 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error { // we could consider setting up a sync.Pool for the summed // hashes to reduce GC pressure. w.Header().Set("Etag", makeEtag(r.URL.Path, hash)) + _, err = w.Write(buf.Bytes()) + if err != nil { + return APIError{HTTPStatus: http.StatusInternalServerError, Err: err} + } return nil diff --git a/caddyconfig/caddyfile/formatter.go b/caddyconfig/caddyfile/formatter.go index d506219c4..d35f0ac6b 100644 --- a/caddyconfig/caddyfile/formatter.go +++ b/caddyconfig/caddyfile/formatter.go @@ -17,9 +17,8 @@ package caddyfile import ( "bytes" "io" + "slices" "unicode" - - "golang.org/x/exp/slices" ) // Format formats the input Caddyfile to a standard, nice-looking diff --git a/caddyconfig/caddyfile/importgraph.go b/caddyconfig/caddyfile/importgraph.go index d27f4710b..d5037fe62 100644 --- a/caddyconfig/caddyfile/importgraph.go +++ b/caddyconfig/caddyfile/importgraph.go @@ -21,18 +21,18 @@ import ( type adjacency map[string][]string type importGraph struct { - nodes map[string]bool + nodes map[string]struct{} edges adjacency } func (i *importGraph) addNode(name string) { if i.nodes == nil { - i.nodes = make(map[string]bool) + i.nodes = make(map[string]struct{}) } if _, exists := i.nodes[name]; exists { return } - i.nodes[name] = true + i.nodes[name] = struct{}{} } func (i *importGraph) addNodes(names []string) { @@ -66,7 +66,7 @@ func (i *importGraph) addEdge(from, to string) error { } if i.nodes == nil { - i.nodes = make(map[string]bool) + i.nodes = make(map[string]struct{}) } if i.edges == nil { i.edges = make(adjacency) diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index 9f79d913a..cdb9a7619 100644 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -50,7 +50,7 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) { p := parser{ Dispenser: NewDispenser(tokens), importGraph: importGraph{ - nodes: make(map[string]bool), + nodes: make(map[string]struct{}), edges: make(adjacency), }, } diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 505885d2d..a7f4247f5 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -24,7 +24,7 @@ import ( "time" "github.com/caddyserver/certmagic" - "github.com/mholt/acmez/acme" + "github.com/mholt/acmez/v2/acme" "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2" @@ -107,7 +107,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) { var onDemand bool var reusePrivateKeys bool - // file certificate loader firstLine := h.RemainingArgs() switch len(firstLine) { case 0: @@ -117,13 +116,13 @@ func parseTLS(h Helper) ([]ConfigValue, error) { } else if !strings.Contains(firstLine[0], "@") { return nil, h.Err("single argument must either be 'internal' or an email address") } else { - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) + acmeIssuer = &caddytls.ACMEIssuer{ + Email: firstLine[0], } - acmeIssuer.Email = firstLine[0] } case 2: + // file certificate loader certFilename := firstLine[0] keyFilename := firstLine[1] @@ -488,19 +487,24 @@ func parseTLS(h Helper) ([]ConfigValue, error) { case acmeIssuer != nil: // implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one - defaultIssuers := caddytls.DefaultIssuers() + defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email) - // if a CA endpoint was set, override multiple implicit issuers since it's a specific one + // if an ACME CA endpoint was set, the user expects to use that specific one, + // not any others that may be defaults, so replace all defaults with that ACME CA if acmeIssuer.CA != "" { defaultIssuers = []certmagic.Issuer{acmeIssuer} } for _, issuer := range defaultIssuers { - switch iss := issuer.(type) { - case *caddytls.ACMEIssuer: - issuer = acmeIssuer - case *caddytls.ZeroSSLIssuer: - iss.ACMEIssuer = acmeIssuer + // apply settings from the implicitly-configured ACMEIssuer to any + // default ACMEIssuers, but preserve each default issuer's CA endpoint, + // because, for example, if you configure the DNS challenge, it should + // apply to any of the default ACMEIssuers, but you don't want to trample + // out their unique CA endpoints + if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil { + acmeCopy := *acmeIssuer + acmeCopy.CA = iss.CA + issuer = &acmeCopy } configVals = append(configVals, ConfigValue{ Class: "tls.cert_issuer", diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index da5557aa8..99411d1cc 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -19,12 +19,12 @@ import ( "fmt" "net" "reflect" + "slices" "sort" "strconv" "strings" "go.uber.org/zap" - "golang.org/x/exp/slices" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index 70d475d6b..bbc63ced8 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -18,7 +18,7 @@ import ( "strconv" "github.com/caddyserver/certmagic" - "github.com/mholt/acmez/acme" + "github.com/mholt/acmez/v2/acme" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -212,9 +212,9 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) { if err != nil { return nil, err } - prov, ok := unm.(certmagic.ACMEDNSProvider) + prov, ok := unm.(certmagic.DNSProvider) if !ok { - return nil, d.Errf("module %s (%T) is not a certmagic.ACMEDNSProvider", modID, unm) + return nil, d.Errf("module %s (%T) is not a certmagic.DNSProvider", modID, unm) } return prov, nil } diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 1adb2b6e0..08da3a5c7 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -24,7 +24,7 @@ import ( "strings" "github.com/caddyserver/certmagic" - "github.com/mholt/acmez/acme" + "github.com/mholt/acmez/v2/acme" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -224,7 +224,7 @@ func (st ServerType) buildTLSApp( var internal, external []string for _, s := range ap.SubjectsRaw { // do not create Issuers for Tailscale domains; they will be given a Manager instead - if strings.HasSuffix(strings.ToLower(s), ".ts.net") { + if isTailscaleDomain(s) { continue } if !certmagic.SubjectQualifiesForCert(s) { @@ -378,15 +378,12 @@ func (st ServerType) buildTLSApp( if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) { // for public names, create default issuers which will later be filled in with configured global defaults // (internal names will implicitly use the internal issuer at auto-https time) - ap.Issuers = caddytls.DefaultIssuers() + emailStr, _ := globalEmail.(string) + ap.Issuers = caddytls.DefaultIssuers(emailStr) // if a specific endpoint is configured, can't use multiple default issuers if globalACMECA != nil { - if strings.Contains(globalACMECA.(string), "zerossl") { - ap.Issuers = []certmagic.Issuer{&caddytls.ZeroSSLIssuer{ACMEIssuer: new(caddytls.ACMEIssuer)}} - } else { - ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)} - } + ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)} } } } @@ -666,17 +663,33 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int { // subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except // that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify // if the automation policy has OnDemand enabled (i.e. this function is more lenient). +// +// IP subjects are considered as non-qualifying for public certs. Technically, there are +// now public ACME CAs as well as non-ACME CAs that issue IP certificates. But this function +// is used solely for implicit automation (defaults), where it gets really complicated to +// keep track of which issuers support IP certificates in which circumstances. Currently, +// issuers that support IP certificates are very few, and all require some sort of config +// from the user anyway (such as an account credential). Since we cannot implicitly and +// automatically get public IP certs without configuration from the user, we treat IPs as +// not qualifying for public certificates. Users should expressly configure an issuer +// that supports IP certs for that purpose. func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool { return !certmagic.SubjectIsIP(subj) && !certmagic.SubjectIsInternal(subj) && (strings.Count(subj, "*.") < 2 || ap.OnDemand) } +// automationPolicyHasAllPublicNames returns true if all the names on the policy +// do NOT qualify for public certs OR are tailscale domains. func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool { for _, subj := range ap.SubjectsRaw { - if !subjectQualifiesForPublicCert(ap, subj) { + if !subjectQualifiesForPublicCert(ap, subj) || isTailscaleDomain(subj) { return false } } return true } + +func isTailscaleDomain(name string) bool { + return strings.HasSuffix(strings.ToLower(name), ".ts.net") +} diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go index 840af023f..ceacd1db0 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -13,8 +13,8 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddytest" - "github.com/mholt/acmez" - "github.com/mholt/acmez/acme" + "github.com/mholt/acmez/v2" + "github.com/mholt/acmez/v2/acme" smallstepacme "github.com/smallstep/certificates/acme" "go.uber.org/zap" ) @@ -77,7 +77,7 @@ func TestACMEServerWithDefaults(t *testing.T) { return } - certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"}) + certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"}) if err != nil { t.Errorf("obtaining certificate: %v", err) return @@ -146,7 +146,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) { return } - certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"}) + certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"}) if len(certs) > 0 { t.Errorf("expected '0' certificates, but received '%d'", len(certs)) } diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index 435bfc7b4..22b716f84 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -9,8 +9,8 @@ import ( "testing" "github.com/caddyserver/caddy/v2/caddytest" - "github.com/mholt/acmez" - "github.com/mholt/acmez/acme" + "github.com/mholt/acmez/v2" + "github.com/mholt/acmez/v2/acme" "go.uber.org/zap" ) @@ -105,12 +105,7 @@ func TestACMEServerAllowPolicy(t *testing.T) { return } { - certs, err := client.ObtainCertificate( - ctx, - account, - certPrivateKey, - []string{"localhost"}, - ) + certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"}) if err != nil { t.Errorf("obtaining certificate for allowed domain: %v", err) return @@ -126,7 +121,7 @@ func TestACMEServerAllowPolicy(t *testing.T) { } } { - _, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"not-matching.localhost"}) + _, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"}) if err == nil { t.Errorf("obtaining certificate for 'not-matching.localhost' domain") } else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") { @@ -199,7 +194,7 @@ func TestACMEServerDenyPolicy(t *testing.T) { return } { - _, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"deny.localhost"}) + _, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"}) if err == nil { t.Errorf("obtaining certificate for 'deny.localhost' domain") } else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") { diff --git a/caddytest/integration/caddyfile_adapt/file_server_etag_file_extensions.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_etag_file_extensions.caddyfiletest new file mode 100644 index 000000000..d0dc79216 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/file_server_etag_file_extensions.caddyfiletest @@ -0,0 +1,40 @@ +:8080 { + root * ./ + file_server { + etag_file_extensions .b3sum .sha256 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8080" + ], + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "./" + }, + { + "etag_file_extensions": [ + ".b3sum", + ".sha256" + ], + "handler": "file_server", + "hide": [ + "./Caddyfile" + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest index 9173b26bf..1f5d0093e 100644 --- a/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest @@ -40,12 +40,6 @@ example.com "preferred_chains": { "smallest": true } - }, - { - "module": "zerossl", - "preferred_chains": { - "smallest": true - } } ] } diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_file_cert.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_file_cert.txt new file mode 100644 index 000000000..d43aa1175 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_file_cert.txt @@ -0,0 +1,47 @@ +:8884 +reverse_proxy 127.0.0.1:65535 { + transport http { + tls_trust_pool file { + pem_file ../caddy.ca.cer + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "protocol": "http", + "tls": { + "ca": { + "pem_files": [ + "../caddy.ca.cer" + ], + "provider": "file" + } + } + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_inline_cert.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_inline_cert.txt new file mode 100644 index 000000000..ef9e8243a --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_inline_cert.txt @@ -0,0 +1,47 @@ +:8884 +reverse_proxy 127.0.0.1:65535 { + transport http { + tls_trust_pool inline { + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ== + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "protocol": "http", + "tls": { + "ca": { + "provider": "inline", + "trusted_ca_certs": [ + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==" + ] + } + } + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest index da5824a36..9daaf436d 100644 --- a/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest @@ -70,8 +70,9 @@ c.example.com { "module": "acme" }, { + "ca": "https://acme.zerossl.com/v2/DV90", "email": "abc@example.com", - "module": "zerossl" + "module": "acme" } ] }, diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest index d8f2164de..a4385a8f3 100644 --- a/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest @@ -131,8 +131,9 @@ abc.de { "module": "acme" }, { + "ca": "https://acme.zerossl.com/v2/DV90", "email": "my.email@example.com", - "module": "zerossl" + "module": "acme" } ] } diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest index 1703178eb..bd1bbf221 100644 --- a/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest @@ -86,8 +86,9 @@ http://localhost:8081 { "module": "acme" }, { + "ca": "https://acme.zerossl.com/v2/DV90", "email": "abc@example.com", - "module": "zerossl" + "module": "acme" } ] } diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest index e8ef3a7e9..50fbf51aa 100644 --- a/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest @@ -54,8 +54,9 @@ example.com { "module": "acme" }, { + "ca": "https://acme.zerossl.com/v2/DV90", "email": "foo@bar", - "module": "zerossl" + "module": "acme" } ] } diff --git a/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest index 02e46763d..c452bf79f 100644 --- a/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest @@ -58,14 +58,6 @@ tls { } }, "module": "acme" - }, - { - "challenges": { - "dns": { - "ttl": 310000000000 - } - }, - "module": "zerossl" } ] } diff --git a/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest index 53629e3a1..d552599ff 100644 --- a/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest @@ -5,7 +5,7 @@ tls { issuer acme { dns_ttl 5m10s } - issuer zerossl { + issuer zerossl api_key { dns_ttl 10m20s } } @@ -65,10 +65,9 @@ tls { "module": "acme" }, { - "challenges": { - "dns": { - "ttl": 620000000000 - } + "api_key": "api_key", + "cname_validation": { + "ttl": 620000000000 }, "module": "zerossl" } diff --git a/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest index 032f9284f..206d59ca5 100644 --- a/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest @@ -6,7 +6,7 @@ tls { propagation_delay 5m10s propagation_timeout 10m20s } - issuer zerossl { + issuer zerossl api_key { propagation_delay 5m30s propagation_timeout -1 } @@ -68,11 +68,10 @@ tls { "module": "acme" }, { - "challenges": { - "dns": { - "propagation_delay": 330000000000, - "propagation_timeout": -1 - } + "api_key": "api_key", + "cname_validation": { + "propagation_delay": 330000000000, + "propagation_timeout": -1 }, "module": "zerossl" } diff --git a/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest index ee4666b66..43ec9774b 100644 --- a/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest @@ -60,15 +60,6 @@ tls { } }, "module": "acme" - }, - { - "challenges": { - "dns": { - "propagation_delay": 310000000000, - "propagation_timeout": 620000000000 - } - }, - "module": "zerossl" } ] } diff --git a/cmd/caddy/setcap.sh b/cmd/caddy/setcap.sh index ac5335c56..39aea2d60 100755 --- a/cmd/caddy/setcap.sh +++ b/cmd/caddy/setcap.sh @@ -1,13 +1,16 @@ #!/bin/sh -# USAGE: go run -exec ./setcap.sh main.go +# USAGE: +# go run -exec ./setcap.sh main.go # # (Example: `go run -exec ./setcap.sh main.go run --config caddy.json`) # # For some reason this does not work on my Arch system, so if you find that's -# the case, you can instead do: go build && ./setcap.sh ./caddy -# but this will leave the ./caddy binary laying around. +# the case, you can instead do: # +# go build && ./setcap.sh ./caddy +# +# but this will leave the ./caddy binary laying around. # sudo setcap cap_net_bind_service=+ep "$1" diff --git a/cmd/main.go b/cmd/main.go index d832cbc5e..ac9786faf 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -157,11 +157,16 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ } } - // as a special case, if a config file called "Caddyfile" was - // specified, and no adapter is specified, assume caddyfile adapter - // for convenience - if strings.HasPrefix(filepath.Base(configFile), "Caddyfile") && - filepath.Ext(configFile) != ".json" && + // as a special case, if a config file starts with "caddyfile" or + // has a ".caddyfile" extension, and no adapter is specified, and + // no adapter module name matches the extension, assume + // caddyfile adapter for convenience + baseConfig := strings.ToLower(filepath.Base(configFile)) + baseConfigExt := filepath.Ext(baseConfig) + if (strings.HasPrefix(baseConfig, "caddyfile") || + strings.HasSuffix(baseConfig, ".caddyfile")) && + (len(baseConfigExt) == 0 || caddyconfig.GetAdapter(baseConfigExt[1:]) == nil) && + baseConfigExt != ".json" && adapterName == "" { adapterName = "caddyfile" } diff --git a/go.mod b/go.mod index 5a1920d82..c3dc1568f 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,31 @@ module github.com/caddyserver/caddy/v2 -go 1.21 - -toolchain go1.21.4 +go 1.22.0 require ( github.com/BurntSushi/toml v1.3.2 github.com/Masterminds/sprig/v3 v3.2.3 github.com/alecthomas/chroma/v2 v2.13.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.20.0 + github.com/caddyserver/certmagic v0.20.1-0.20240412214119-167015dd6570 + github.com/caddyserver/zerossl v0.1.2 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.0.12 github.com/google/cel-go v0.20.0 github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.17.0 - github.com/klauspost/cpuid/v2 v2.2.5 - github.com/mholt/acmez v1.2.0 + github.com/klauspost/cpuid/v2 v2.2.7 + github.com/mholt/acmez/v2 v2.0.0-beta.2 github.com/prometheus/client_golang v1.19.0 github.com/quic-go/quic-go v0.42.0 github.com/smallstep/certificates v0.25.3-rc5 github.com/smallstep/nosql v0.6.0 - github.com/smallstep/truststore v0.12.1 + github.com/smallstep/truststore v0.13.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 - github.com/yuin/goldmark v1.5.6 + github.com/yuin/goldmark v1.7.1 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 @@ -34,13 +33,12 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 go.opentelemetry.io/otel/sdk v1.21.0 go.uber.org/automaxprocs v1.5.3 - go.uber.org/zap v1.26.0 + go.uber.org/zap v1.27.0 go.uber.org/zap/exp v0.2.0 - golang.org/x/crypto v0.21.0 - golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 - golang.org/x/net v0.22.0 - golang.org/x/sync v0.6.0 - golang.org/x/term v0.18.0 + golang.org/x/crypto v0.22.0 + golang.org/x/net v0.24.0 + golang.org/x/sync v0.7.0 + golang.org/x/term v0.19.0 golang.org/x/time v0.5.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -72,8 +70,9 @@ require ( go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect go.uber.org/mock v0.4.0 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect ) require ( @@ -84,7 +83,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 github.com/chzyer/readline v1.5.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/dgraph-io/badger v1.6.2 // indirect @@ -112,12 +111,12 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgx/v4 v4.18.3 // indirect - github.com/libdns/libdns v0.2.1 // indirect + github.com/libdns/libdns v0.2.2 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/miekg/dns v1.1.55 // indirect + github.com/miekg/dns v1.1.58 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -144,10 +143,10 @@ require ( go.step.sm/crypto v0.42.1 go.step.sm/linkedca v0.20.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.18.0 + golang.org/x/mod v0.16.0 // indirect + golang.org/x/sys v0.19.0 golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.16.1 // indirect + golang.org/x/tools v0.19.0 // indirect google.golang.org/grpc v1.62.1 // indirect google.golang.org/protobuf v1.33.0 // indirect howett.net/plist v1.0.0 // indirect diff --git a/go.sum b/go.sum index 27c48a0b9..bd298867e 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,10 @@ github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= -github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= +github.com/caddyserver/certmagic v0.20.1-0.20240412214119-167015dd6570 h1:SsAXjoQx2wOmLl6mEwJEwh7wwys2hb/l/mhtmxA3wts= +github.com/caddyserver/certmagic v0.20.1-0.20240412214119-167015dd6570/go.mod h1:e1NhB1rF5KZnAuAX6oSyhE7sg1Ru5bWgggw5RtauhEY= +github.com/caddyserver/zerossl v0.1.2 h1:tlEu1VzWGoqcCpivs9liKAKhfpJWYJkHEMmlxRbVAxE= +github.com/caddyserver/zerossl v0.1.2/go.mod h1:wtiJEHbdvunr40ZzhXlnIkOB8Xj4eKtBKizCcZitJiQ= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -256,8 +258,8 @@ github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8 github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= -github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -275,8 +277,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= -github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= +github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= +github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= @@ -292,10 +294,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= -github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= -github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= -github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mholt/acmez/v2 v2.0.0-beta.2 h1:GIgGILx8AWN0ePyTd+bjs2WDgNiIWm0nBwDLWp59aHc= +github.com/mholt/acmez/v2 v2.0.0-beta.2/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -372,8 +374,8 @@ github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y= github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw= github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU= -github.com/smallstep/truststore v0.12.1 h1:guLUKkc1UlsXeS3t6BuVMa4leOOpdiv02PCRTiy1WdY= -github.com/smallstep/truststore v0.12.1/go.mod h1:M4mebeNy28KusGX3lJxpLARIktLcyqBOrj3ZiZ46pqw= +github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4= +github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -418,8 +420,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA= -github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= @@ -486,8 +488,8 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -503,17 +505,17 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= -golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -523,15 +525,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -558,8 +560,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -567,8 +569,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -593,8 +595,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -608,8 +610,8 @@ google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/b google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index aec43c7c7..54a2d9ccd 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -260,7 +260,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // port, we'll have to choose one, so prefer the HTTPS port if _, ok := redirDomains[d]; !ok || addr.StartPort == uint(app.httpsPort()) { - redirDomains[d] = []caddy.NetworkAddress{addr} + redirDomains[d] = append(redirDomains[d], addr) } } } @@ -287,6 +287,16 @@ uniqueDomainsLoop: for _, ap := range app.tlsApp.Automation.Policies { for _, apHost := range ap.Subjects() { if apHost == d { + // if the automation policy has all internal subjects but no issuers, + // it will default to CertMagic's issuers which are public CAs; use + // our internal issuer instead + if len(ap.Issuers) == 0 && ap.AllInternalSubjects() { + iss := new(caddytls.InternalIssuer) + if err := iss.Provision(ctx); err != nil { + return err + } + ap.Issuers = append(ap.Issuers, iss) + } continue uniqueDomainsLoop } } diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html index 484d90020..fdc4b9c3c 100644 --- a/modules/caddyhttp/fileserver/browse.html +++ b/modules/caddyhttp/fileserver/browse.html @@ -21,7 +21,7 @@ {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}} {{- if eq .Tpl.Layout "grid"}} - + {{- else}} diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go index d90e4f9a0..f65695018 100644 --- a/modules/caddyhttp/fileserver/caddyfile.go +++ b/modules/caddyhttp/fileserver/caddyfile.go @@ -164,6 +164,13 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } fsrv.PassThru = true + case "etag_file_extensions": + etagFileExtensions := d.RemainingArgs() + if len(etagFileExtensions) == 0 { + return d.ArgErr() + } + fsrv.EtagFileExtensions = etagFileExtensions + default: return d.Errf("unknown subdirective '%s'", d.Val()) } diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 57d1bc851..15ae35e38 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -161,6 +161,12 @@ type FileServer struct { PrecompressedOrder []string `json:"precompressed_order,omitempty"` precompressors map[string]encode.Precompressed + // List of file extensions to try to read Etags from. + // If set, file Etags will be read from sidecar files + // with any of these suffixes, instead of generating + // our own Etag. + EtagFileExtensions []string `json:"etag_file_extensions,omitempty"` + fsmap caddy.FileSystems logger *zap.Logger @@ -396,6 +402,14 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c w.Header().Del("Accept-Ranges") w.Header().Add("Vary", "Accept-Encoding") + // try to get the etag from pre computed files if an etag suffix list was provided + if etag == "" && fsrv.EtagFileExtensions != nil { + etag, err = fsrv.getEtagFromFile(fileSystem, compressedFilename) + if err != nil { + return err + } + } + // don't assign info = compressedInfo because sidecars are kind // of transparent; however we do need to set the Etag: // https://caddy.community/t/gzipped-sidecar-file-wrong-same-etag/16793 @@ -420,7 +434,13 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c return err // error is already structured } defer file.Close() - + // try to get the etag from pre computed files if an etag suffix list was provided + if etag == "" && fsrv.EtagFileExtensions != nil { + etag, err = fsrv.getEtagFromFile(fileSystem, filename) + if err != nil { + return err + } + } if etag == "" { etag = calculateEtag(info) } @@ -639,6 +659,22 @@ func calculateEtag(d os.FileInfo) string { return `"` + t + s + `"` } +// Finds the first corresponding etag file for a given file in the file system and return its content +func (fsrv *FileServer) getEtagFromFile(fileSystem fs.FS, filename string) (string, error) { + for _, suffix := range fsrv.EtagFileExtensions { + etagFilename := filename + suffix + etag, err := fs.ReadFile(fileSystem, etagFilename) + if errors.Is(err, fs.ErrNotExist) { + continue + } + if err != nil { + return "", fmt.Errorf("cannot read etag from file %s: %v", etagFilename, err) + } + return string(etag), nil + } + return "", nil +} + // redirect performs a redirect to a given path. The 'toPath' parameter // MUST be solely a path, and MUST NOT include a query. func redirect(w http.ResponseWriter, r *http.Request, toPath string) error { diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index f01f187fa..6e770c5e6 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -26,6 +26,7 @@ import ( "reflect" "regexp" "runtime" + "slices" "sort" "strconv" "strings" @@ -33,7 +34,6 @@ import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" - "golang.org/x/exp/slices" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -456,8 +456,7 @@ func (m MatchPath) Match(r *http.Request) bool { // treat it as a fast substring match if strings.Count(matchPattern, "*") == 2 && strings.HasPrefix(matchPattern, "*") && - strings.HasSuffix(matchPattern, "*") && - strings.Count(matchPattern, "*") == 2 { + strings.HasSuffix(matchPattern, "*") { if strings.Contains(reqPathForPattern, matchPattern[1:len(matchPattern)-1]) { return true } diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 9e0726be6..8dfbd93da 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -30,6 +30,7 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" + "github.com/caddyserver/caddy/v2/modules/caddytls" ) func init() { @@ -1145,6 +1146,9 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if h.TLS == nil { h.TLS = new(TLSConfig) } + if len(h.TLS.CARaw) != 0 { + return d.Err("cannot specify both 'tls_trust_pool' and 'tls_trusted_ca_certs") + } h.TLS.RootCAPEMFiles = args case "tls_server_name": @@ -1260,6 +1264,31 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } h.MaxConnsPerHost = num + case "tls_trust_pool": + if !d.NextArg() { + return d.ArgErr() + } + modStem := d.Val() + modID := "tls.ca_pool.source." + modStem + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + ca, ok := unm.(caddytls.CA) + if !ok { + return d.Errf("module %s is not a caddytls.CA", modID) + } + if h.TLS == nil { + h.TLS = new(TLSConfig) + } + if len(h.TLS.RootCAPEMFiles) != 0 { + return d.Err("cannot specify both 'tls_trust_pool' and 'tls_trusted_ca_certs'") + } + if h.TLS.CARaw != nil { + return d.Err("cannot specify \"tls_trust_pool\" twice in caddyfile") + } + h.TLS.CARaw = caddyconfig.JSONModuleObject(ca, "provider", modStem, nil) + default: return d.Errf("unrecognized subdirective %s", d.Val()) } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go index a2227a653..14a1cf684 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go @@ -213,8 +213,6 @@ func DisabledTest(t *testing.T) { // TODO: test chunked reader globalt = t - rand.Seed(time.Now().UTC().UnixNano()) - // server go func() { listener, err := net.Listen("tcp", ipPort) diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index dd8ece251..895873b9d 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -19,6 +19,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" + "encoding/json" "fmt" weakrand "math/rand" "net" @@ -472,9 +473,14 @@ func (h HTTPTransport) Cleanup() error { // TLSConfig holds configuration related to the TLS configuration for the // transport/client. type TLSConfig struct { + // Certificate authority module which provides the certificate pool of trusted certificates + CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` + + // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.inline` module instead. // Optional list of base64-encoded DER-encoded CA certificates to trust. RootCAPool []string `json:"root_ca_pool,omitempty"` + // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.file` module instead. // List of PEM-encoded CA certificate files to add to the same trust // store as RootCAPool (or root_ca_pool in the JSON). RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"` @@ -576,6 +582,7 @@ func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) { // trusted root CAs if len(t.RootCAPool) > 0 || len(t.RootCAPEMFiles) > 0 { + ctx.Logger().Warn("root_ca_pool and root_ca_pem_files are deprecated. Use one of the tls.ca_pool.source modules instead") rootPool := x509.NewCertPool() for _, encodedCACert := range t.RootCAPool { caCert, err := decodeBase64DERCert(encodedCACert) @@ -594,6 +601,21 @@ func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) { cfg.RootCAs = rootPool } + if t.CARaw != nil { + if len(t.RootCAPool) > 0 || len(t.RootCAPEMFiles) > 0 { + return nil, fmt.Errorf("conflicting config for Root CA pool") + } + caRaw, err := ctx.LoadModule(t, "CARaw") + if err != nil { + return nil, fmt.Errorf("failed to load ca module: %v", err) + } + ca, ok := caRaw.(caddytls.CA) + if !ok { + return nil, fmt.Errorf("CA module '%s' is not a certificate pool provider", ca) + } + cfg.RootCAs = ca.CertPool() + } + // Renegotiation switch t.Renegotiation { case "never", "": diff --git a/modules/caddyhttp/reverseproxy/httptransport_test.go b/modules/caddyhttp/reverseproxy/httptransport_test.go new file mode 100644 index 000000000..46931c8b1 --- /dev/null +++ b/modules/caddyhttp/reverseproxy/httptransport_test.go @@ -0,0 +1,96 @@ +package reverseproxy + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +func TestHTTPTransportUnmarshalCaddyFileWithCaPools(t *testing.T) { + const test_der_1 = `MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==` + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + args args + expectedTLSConfig TLSConfig + wantErr bool + }{ + { + name: "tls_trust_pool without a module argument returns an error", + args: args{ + d: caddyfile.NewTestDispenser( + `http { + tls_trust_pool + }`), + }, + wantErr: true, + }, + { + name: "providing both 'tls_trust_pool' and 'tls_trusted_ca_certs' returns an error", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf( + `http { + tls_trust_pool inline %s + tls_trusted_ca_certs %s + }`, test_der_1, test_der_1)), + }, + wantErr: true, + }, + { + name: "setting 'tls_trust_pool' and 'tls_trusted_ca_certs' produces an error", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf( + `http { + tls_trust_pool inline { + trust_der %s + } + tls_trusted_ca_certs %s + }`, test_der_1, test_der_1)), + }, + wantErr: true, + }, + { + name: "using 'inline' tls_trust_pool loads the module successfully", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf( + `http { + tls_trust_pool inline { + trust_der %s + } + } + `, test_der_1)), + }, + expectedTLSConfig: TLSConfig{CARaw: json.RawMessage(fmt.Sprintf(`{"provider":"inline","trusted_ca_certs":["%s"]}`, test_der_1))}, + }, + { + name: "setting 'tls_trusted_ca_certs' and 'tls_trust_pool' produces an error", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf( + `http { + tls_trusted_ca_certs %s + tls_trust_pool inline { + trust_der %s + } + }`, test_der_1, test_der_1)), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ht := &HTTPTransport{} + if err := ht.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("HTTPTransport.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(&tt.expectedTLSConfig, ht.TLS) { + t.Errorf("HTTPTransport.UnmarshalCaddyfile() = %v, want %v", ht, tt.expectedTLSConfig) + } + }) + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 1648b7791..deba304a7 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -922,7 +922,9 @@ func (h *Handler) finalizeResponse( ) error { // deal with 101 Switching Protocols responses: (WebSocket, h2c, etc) if res.StatusCode == http.StatusSwitchingProtocols { - h.handleUpgradeResponse(logger, rw, req, res) + var wg sync.WaitGroup + h.handleUpgradeResponse(logger, &wg, rw, req, res) + wg.Wait() return nil } diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index 2a5b5286a..6d8990575 100644 --- a/modules/caddyhttp/reverseproxy/streaming.go +++ b/modules/caddyhttp/reverseproxy/streaming.go @@ -34,7 +34,7 @@ import ( "golang.org/x/net/http/httpguts" ) -func (h *Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWriter, req *http.Request, res *http.Response) { +func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, rw http.ResponseWriter, req *http.Request, res *http.Response) { reqUpType := upgradeType(req.Header) resUpType := upgradeType(res.Header) @@ -121,7 +121,7 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrit defer deleteFrontConn() defer deleteBackConn() - spc := switchProtocolCopier{user: conn, backend: backConn} + spc := switchProtocolCopier{user: conn, backend: backConn, wg: wg} // setup the timeout if requested var timeoutc <-chan time.Time @@ -132,6 +132,7 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrit } errc := make(chan error, 1) + wg.Add(2) go spc.copyToBackend(errc) go spc.copyFromBackend(errc) select { @@ -529,16 +530,19 @@ func (m *maxLatencyWriter) stop() { // forth have nice names in stacks. type switchProtocolCopier struct { user, backend io.ReadWriteCloser + wg *sync.WaitGroup } func (c switchProtocolCopier) copyFromBackend(errc chan<- error) { _, err := io.Copy(c.user, c.backend) errc <- err + c.wg.Done() } func (c switchProtocolCopier) copyToBackend(errc chan<- error) { _, err := io.Copy(c.backend, c.user) errc <- err + c.wg.Done() } var streamingBufPool = sync.Pool{ diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 8a7f8b499..a14dc61a8 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -17,14 +17,19 @@ package caddytls import ( "context" "crypto/x509" + "encoding/json" "fmt" + "net/http" + "net/url" "os" "strconv" + "strings" "time" "github.com/caddyserver/certmagic" - "github.com/mholt/acmez" - "github.com/mholt/acmez/acme" + "github.com/caddyserver/zerossl" + "github.com/mholt/acmez/v2" + "github.com/mholt/acmez/v2/acme" "go.uber.org/zap" "github.com/caddyserver/caddy/v2" @@ -142,12 +147,14 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { iss.Challenges.DNS.solver = deprecatedProvider } else { iss.Challenges.DNS.solver = &certmagic.DNS01Solver{ - DNSProvider: val.(certmagic.ACMEDNSProvider), - TTL: time.Duration(iss.Challenges.DNS.TTL), - PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay), - PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout), - Resolvers: iss.Challenges.DNS.Resolvers, - OverrideDomain: iss.Challenges.DNS.OverrideDomain, + DNSManager: certmagic.DNSManager{ + DNSProvider: val.(certmagic.DNSProvider), + TTL: time.Duration(iss.Challenges.DNS.TTL), + PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay), + PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout), + Resolvers: iss.Challenges.DNS.Resolvers, + OverrideDomain: iss.Challenges.DNS.OverrideDomain, + }, } } } @@ -210,6 +217,18 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) { } } + // ZeroSSL requires EAB, but we can generate that automatically (requires an email address be configured) + if strings.HasPrefix(iss.CA, "https://acme.zerossl.com/") { + template.NewAccountFunc = func(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) { + if acmeIss.ExternalAccount != nil { + return acct, nil + } + var err error + acmeIss.ExternalAccount, acct, err = iss.generateZeroSSLEABCredentials(ctx, acct) + return acct, err + } + } + return template, nil } @@ -248,6 +267,65 @@ func (iss *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateRes // to be accessed and manipulated. func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss } +// generateZeroSSLEABCredentials generates ZeroSSL EAB credentials for the primary contact email +// on the issuer. It should only be usedif the CA endpoint is ZeroSSL. An email address is required. +func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, error) { + if strings.TrimSpace(iss.Email) == "" { + return nil, acme.Account{}, fmt.Errorf("your email address is required to use ZeroSSL's ACME endpoint") + } + + if len(acct.Contact) == 0 { + // we borrow the email from config or the default email, so ensure it's saved with the account + acct.Contact = []string{"mailto:" + iss.Email} + } + + endpoint := zerossl.BaseURL + "/acme/eab-credentials-email" + form := url.Values{"email": []string{iss.Email}} + body := strings.NewReader(form.Encode()) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) + if err != nil { + return nil, acct, fmt.Errorf("forming request: %v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("User-Agent", certmagic.UserAgent) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err) + } + defer resp.Body.Close() + + var result struct { + Success bool `json:"success"` + Error struct { + Code int `json:"code"` + Type string `json:"type"` + } `json:"error"` + EABKID string `json:"eab_kid"` + EABHMACKey string `json:"eab_hmac_key"` + } + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return nil, acct, fmt.Errorf("decoding API response: %v", err) + } + if result.Error.Code != 0 { + // do this check first because ZeroSSL's API returns 200 on errors + return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d: %s (code %d)", + resp.StatusCode, result.Error.Type, result.Error.Code) + } + if resp.StatusCode != http.StatusOK { + return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode) + } + + iss.logger.Info("generated EAB credentials", zap.String("key_id", result.EABKID)) + + return &acme.EAB{ + KeyID: result.EABKID, + MACKey: result.EABHMACKey, + }, acct, nil +} + // UnmarshalCaddyfile deserializes Caddyfile tokens into iss. // // ... acme [] { diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index a90e5ded8..803cb174e 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -24,7 +24,7 @@ import ( "strings" "github.com/caddyserver/certmagic" - "github.com/mholt/acmez" + "github.com/mholt/acmez/v2" "go.uber.org/zap" "github.com/caddyserver/caddy/v2" @@ -201,6 +201,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { // store them on the policy before putting it on the config // load and provision any cert manager modules + hadExplicitManagers := len(ap.ManagersRaw) > 0 if ap.ManagersRaw != nil { vals, err := tlsApp.ctx.LoadModule(ap, "ManagersRaw") if err != nil { @@ -256,12 +257,25 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { if ap.OnDemand || len(ap.Managers) > 0 { // permission module is now required after a number of negligence cases that allowed abuse; // but it may still be optional for explicit subjects (bounded, non-wildcard), for the - // internal issuer since it doesn't cause public PKI pressure on ACME servers - if ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil) { - return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details") + // internal issuer since it doesn't cause public PKI pressure on ACME servers; subtly, it + // is useful to allow on-demand TLS to be enabled so Managers can be used, but to still + // prevent issuance from Issuers (when Managers don't provide a certificate) if there's no + // permission module configured + noProtections := ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil) + failClosed := noProtections && hadExplicitManagers // don't allow on-demand issuance (other than implicit managers) if no managers have been explicitly configured + if noProtections { + if !hadExplicitManagers { + // no managers, no explicitly-configured permission module, this is a config error + return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details") + } + // allow on-demand to be enabled but only for the purpose of the Managers; issuance won't be allowed from Issuers + tlsApp.logger.Warn("on-demand TLS can only get certificates from the configured external manager(s) because no ask endpoint / permission module is specified") } ond = &certmagic.OnDemandConfig{ DecisionFunc: func(ctx context.Context, name string) error { + if failClosed { + return fmt.Errorf("no permission module configured; certificates not allowed except from external Managers") + } if tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil { return nil } @@ -344,6 +358,16 @@ func (ap *AutomationPolicy) Subjects() []string { return ap.subjects } +// AllInternalSubjects returns true if all the subjects on this policy are internal. +func (ap *AutomationPolicy) AllInternalSubjects() bool { + for _, subj := range ap.subjects { + if !certmagic.SubjectIsInternal(subj) { + return false + } + } + return true +} + func (ap *AutomationPolicy) onlyInternalIssuer() bool { if len(ap.Issuers) != 1 { return false @@ -370,17 +394,21 @@ func (ap *AutomationPolicy) isWildcardOrDefault() bool { // DefaultIssuers returns empty Issuers (not provisioned) to be used as defaults. // This function is experimental and has no compatibility promises. -func DefaultIssuers() []certmagic.Issuer { - return []certmagic.Issuer{ - new(ACMEIssuer), - &ZeroSSLIssuer{ACMEIssuer: new(ACMEIssuer)}, +func DefaultIssuers(userEmail string) []certmagic.Issuer { + issuers := []certmagic.Issuer{new(ACMEIssuer)} + if strings.TrimSpace(userEmail) != "" { + issuers = append(issuers, &ACMEIssuer{ + CA: certmagic.ZeroSSLProductionCA, + Email: userEmail, + }) } + return issuers } // DefaultIssuersProvisioned returns empty but provisioned default Issuers from // DefaultIssuers(). This function is experimental and has no compatibility promises. func DefaultIssuersProvisioned(ctx caddy.Context) ([]certmagic.Issuer, error) { - issuers := DefaultIssuers() + issuers := DefaultIssuers("") for i, iss := range issuers { if prov, ok := iss.(caddy.Provisioner); ok { err := prov.Provision(ctx) @@ -453,6 +481,7 @@ type TLSALPNChallengeConfig struct { type DNSChallengeConfig struct { // The DNS provider module to use which will manage // the DNS records relevant to the ACME challenge. + // Required. ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"` // The TTL of the TXT record used for the DNS challenge. diff --git a/modules/caddytls/capools.go b/modules/caddytls/capools.go index 44a2fa2c2..7e378aac4 100644 --- a/modules/caddytls/capools.go +++ b/modules/caddytls/capools.go @@ -579,8 +579,7 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error { customTransport.TLSClientConfig = tlsConfig } - var httpClient *http.Client - *httpClient = *http.DefaultClient + httpClient := *http.DefaultClient httpClient.Transport = customTransport for _, uri := range hcp.Endpoints { diff --git a/modules/caddytls/certmanagers.go b/modules/caddytls/certmanagers.go index 9bb436a37..b2e2eb073 100644 --- a/modules/caddytls/certmanagers.go +++ b/modules/caddytls/certmanagers.go @@ -96,6 +96,11 @@ type HTTPCertGetter struct { // To be valid, the response must be HTTP 200 with a PEM body // consisting of blocks for the certificate chain and the private // key. + // + // To indicate that this manager is not managing a certificate for + // the described handshake, the endpoint should return HTTP 204 + // (No Content). Error statuses will indicate that the manager is + // capable of providing a certificate but was unable to. URL string `json:"url,omitempty"` ctx context.Context @@ -147,6 +152,10 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH return nil, err } defer resp.Body.Close() + if resp.StatusCode == http.StatusNoContent { + // endpoint is not managing certs for this handshake + return nil, nil + } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("got HTTP %d", resp.StatusCode) } diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 49c7add49..1e1455446 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -26,7 +26,7 @@ import ( "path/filepath" "strings" - "github.com/mholt/acmez" + "github.com/mholt/acmez/v2" "go.uber.org/zap" "github.com/caddyserver/caddy/v2" diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 2ec7bd8fb..2a05d5235 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -176,8 +176,9 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.Automation.OnDemand.permission = val.(OnDemandPermission) } - // on-demand rate limiting + // on-demand rate limiting (TODO: deprecated, and should be removed later; rate limiting is ineffective now that permission modules are required) if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil { + t.logger.Warn("DEPRECATED: on_demand.rate_limit will be removed in a future release; use permission modules or external certificate managers instead") onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst) onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval)) } else { @@ -192,6 +193,13 @@ func (t *TLS) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("preparing 'ask' endpoint: %v", err) } + perm := PermissionByHTTP{ + Endpoint: t.Automation.OnDemand.Ask, + } + if err := perm.Provision(ctx); err != nil { + return fmt.Errorf("provisioning 'ask' module: %v", err) + } + t.Automation.OnDemand.permission = perm } // automation/management policies @@ -406,35 +414,49 @@ func (t *TLS) Manage(names []string) error { return nil } -// HandleHTTPChallenge ensures that the HTTP challenge is handled for the -// certificate named by r.Host, if it is an HTTP challenge request. It -// requires that the automation policy for r.Host has an issuer of type -// *certmagic.ACMEManager, or one that is ACME-enabled (GetACMEIssuer()). +// HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP +// validation request is handled for the certificate named by r.Host, if it +// is an HTTP challenge request. It requires that the automation policy for +// r.Host has an issuer that implements GetACMEIssuer() or is a *ZeroSSLIssuer. func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { + acmeChallenge := certmagic.LooksLikeHTTPChallenge(r) + zerosslValidation := certmagic.LooksLikeZeroSSLHTTPValidation(r) + // no-op if it's not an ACME challenge request - if !certmagic.LooksLikeHTTPChallenge(r) { + if !acmeChallenge && !zerosslValidation { return false } // try all the issuers until we find the one that initiated the challenge ap := t.getAutomationPolicyForName(r.Host) - type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer } - for _, iss := range ap.magic.Issuers { - if am, ok := iss.(acmeCapable); ok { - iss := am.GetACMEIssuer() - if iss.issuer.HandleHTTPChallenge(w, r) { - return true + + if acmeChallenge { + type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer } + + for _, iss := range ap.magic.Issuers { + if acmeIssuer, ok := iss.(acmeCapable); ok { + if acmeIssuer.GetACMEIssuer().issuer.HandleHTTPChallenge(w, r) { + return true + } } } - } - // it's possible another server in this process initiated the challenge; - // users have requested that Caddy only handle HTTP challenges it initiated, - // so that users can proxy the others through to their backends; but we - // might not have an automation policy for all identifiers that are trying - // to get certificates (e.g. the admin endpoint), so we do this manual check - if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok { - return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge) + // it's possible another server in this process initiated the challenge; + // users have requested that Caddy only handle HTTP challenges it initiated, + // so that users can proxy the others through to their backends; but we + // might not have an automation policy for all identifiers that are trying + // to get certificates (e.g. the admin endpoint), so we do this manual check + if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok { + return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge) + } + } else if zerosslValidation { + for _, iss := range ap.magic.Issuers { + if ziss, ok := iss.(*ZeroSSLIssuer); ok { + if ziss.issuer.HandleZeroSSLHTTPValidation(w, r) { + return true + } + } + } } return false diff --git a/modules/caddytls/zerosslissuer.go b/modules/caddytls/zerosslissuer.go index 1c091a076..b8727ab66 100644 --- a/modules/caddytls/zerosslissuer.go +++ b/modules/caddytls/zerosslissuer.go @@ -17,19 +17,15 @@ package caddytls import ( "context" "crypto/x509" - "encoding/json" "fmt" - "io" - "net/http" - "net/url" - "strings" - "sync" + "strconv" + "time" "github.com/caddyserver/certmagic" - "github.com/mholt/acmez/acme" "go.uber.org/zap" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) @@ -37,24 +33,36 @@ func init() { caddy.RegisterModule(new(ZeroSSLIssuer)) } -// ZeroSSLIssuer makes an ACME issuer for getting certificates -// from ZeroSSL by automatically generating EAB credentials. -// Please be sure to set a valid email address in your config -// so you can access/manage your domains in your ZeroSSL account. -// -// This issuer is only needed for automatic generation of EAB -// credentials. If manually configuring/reusing EAB credentials, -// the standard ACMEIssuer may be used if desired. +// ZeroSSLIssuer uses the ZeroSSL API to get certificates. +// Note that this is distinct from ZeroSSL's ACME endpoint. +// To use ZeroSSL's ACME endpoint, use the ACMEIssuer +// configured with ZeroSSL's ACME directory endpoint. type ZeroSSLIssuer struct { - *ACMEIssuer - // The API key (or "access key") for using the ZeroSSL API. - // This is optional, but can be used if you have an API key - // already and don't want to supply your email address. + // REQUIRED. APIKey string `json:"api_key,omitempty"` - mu sync.Mutex - logger *zap.Logger + // How many days the certificate should be valid for. + // Only certain values are accepted; see ZeroSSL docs. + ValidityDays int `json:"validity_days,omitempty"` + + // The host to bind to when opening a listener for + // verifying domain names (or IPs). + ListenHost string `json:"listen_host,omitempty"` + + // If HTTP is forwarded from port 80, specify the + // forwarded port here. + AlternateHTTPPort int `json:"alternate_http_port,omitempty"` + + // Use CNAME validation instead of HTTP. ZeroSSL's + // API uses CNAME records for DNS validation, similar + // to how Let's Encrypt uses TXT records for the + // DNS challenge. + CNAMEValidation *DNSChallengeConfig `json:"cname_validation,omitempty"` + + logger *zap.Logger + storage certmagic.Storage + issuer *certmagic.ZeroSSLIssuer } // CaddyModule returns the Caddy module information. @@ -65,178 +73,184 @@ func (*ZeroSSLIssuer) CaddyModule() caddy.ModuleInfo { } } -// Provision sets up iss. +// Provision sets up the issuer. func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error { iss.logger = ctx.Logger() - if iss.ACMEIssuer == nil { - iss.ACMEIssuer = new(ACMEIssuer) - } - if iss.ACMEIssuer.CA == "" { - iss.ACMEIssuer.CA = certmagic.ZeroSSLProductionCA - } - return iss.ACMEIssuer.Provision(ctx) -} + iss.storage = ctx.Storage() + repl := caddy.NewReplacer() -// newAccountCallback generates EAB if not already provided. It also sets a valid default contact on the account if not set. -func (iss *ZeroSSLIssuer) newAccountCallback(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) { - if acmeIss.ExternalAccount != nil { - return acct, nil - } - var err error - acmeIss.ExternalAccount, acct, err = iss.generateEABCredentials(ctx, acct) - return acct, err -} - -// generateEABCredentials generates EAB credentials using the API key if provided, -// otherwise using the primary contact email on the issuer. If an email is not set -// on the issuer, a default generic email is used. -func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, error) { - var endpoint string - var body io.Reader - - // there are two ways to generate EAB credentials: authenticated with - // their API key, or unauthenticated with their email address - if iss.APIKey != "" { - apiKey := caddy.NewReplacer().ReplaceAll(iss.APIKey, "") - if apiKey == "" { - return nil, acct, fmt.Errorf("missing API key: '%v'", iss.APIKey) + var dnsManager *certmagic.DNSManager + if iss.CNAMEValidation != nil && len(iss.CNAMEValidation.ProviderRaw) > 0 { + val, err := ctx.LoadModule(iss.CNAMEValidation, "ProviderRaw") + if err != nil { + return fmt.Errorf("loading DNS provider module: %v", err) } - qs := url.Values{"access_key": []string{apiKey}} - endpoint = fmt.Sprintf("%s/eab-credentials?%s", zerosslAPIBase, qs.Encode()) - } else { - email := iss.Email - if email == "" { - iss.logger.Warn("missing email address for ZeroSSL; it is strongly recommended to set one for next time") - email = "caddy@zerossl.com" // special email address that preserves backwards-compat, but which black-holes dashboard features, oh well + dnsManager = &certmagic.DNSManager{ + DNSProvider: val.(certmagic.DNSProvider), + TTL: time.Duration(iss.CNAMEValidation.TTL), + PropagationDelay: time.Duration(iss.CNAMEValidation.PropagationDelay), + PropagationTimeout: time.Duration(iss.CNAMEValidation.PropagationTimeout), + Resolvers: iss.CNAMEValidation.Resolvers, + OverrideDomain: iss.CNAMEValidation.OverrideDomain, + Logger: iss.logger.Named("cname"), } - if len(acct.Contact) == 0 { - // we borrow the email from config or the default email, so ensure it's saved with the account - acct.Contact = []string{"mailto:" + email} - } - endpoint = zerosslAPIBase + "/eab-credentials-email" - form := url.Values{"email": []string{email}} - body = strings.NewReader(form.Encode()) } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) - if err != nil { - return nil, acct, fmt.Errorf("forming request: %v", err) - } - if body != nil { - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - } - req.Header.Set("User-Agent", certmagic.UserAgent) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err) - } - defer resp.Body.Close() - - var result struct { - Success bool `json:"success"` - Error struct { - Code int `json:"code"` - Type string `json:"type"` - } `json:"error"` - EABKID string `json:"eab_kid"` - EABHMACKey string `json:"eab_hmac_key"` - } - err = json.NewDecoder(resp.Body).Decode(&result) - if err != nil { - return nil, acct, fmt.Errorf("decoding API response: %v", err) - } - if result.Error.Code != 0 { - return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d: %s (code %d)", - resp.StatusCode, result.Error.Type, result.Error.Code) - } - if resp.StatusCode != http.StatusOK { - return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode) + iss.issuer = &certmagic.ZeroSSLIssuer{ + APIKey: repl.ReplaceAll(iss.APIKey, ""), + ValidityDays: iss.ValidityDays, + ListenHost: iss.ListenHost, + AltHTTPPort: iss.AlternateHTTPPort, + Storage: iss.storage, + CNAMEValidation: dnsManager, + Logger: iss.logger, } - iss.logger.Info("generated EAB credentials", zap.String("key_id", result.EABKID)) - - return &acme.EAB{ - KeyID: result.EABKID, - MACKey: result.EABHMACKey, - }, acct, nil -} - -// initialize modifies the template for the underlying ACMEIssuer -// values by setting the CA endpoint to the ZeroSSL directory and -// setting the NewAccountFunc callback to one which allows us to -// generate EAB credentials only if a new account is being made. -// Since it modifies the stored template, its effect should only -// be needed once, but it is fine to call it repeatedly. -func (iss *ZeroSSLIssuer) initialize() { - iss.mu.Lock() - defer iss.mu.Unlock() - if iss.ACMEIssuer.issuer.NewAccountFunc == nil { - iss.ACMEIssuer.issuer.NewAccountFunc = iss.newAccountCallback - } -} - -// PreCheck implements the certmagic.PreChecker interface. -func (iss *ZeroSSLIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error { - iss.initialize() - return iss.ACMEIssuer.PreCheck(ctx, names, interactive) + return nil } // Issue obtains a certificate for the given csr. func (iss *ZeroSSLIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { - iss.initialize() - return iss.ACMEIssuer.Issue(ctx, csr) + return iss.issuer.Issue(ctx, csr) } // IssuerKey returns the unique issuer key for the configured CA endpoint. func (iss *ZeroSSLIssuer) IssuerKey() string { - iss.initialize() - return iss.ACMEIssuer.IssuerKey() + return iss.issuer.IssuerKey() } // Revoke revokes the given certificate. func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error { - iss.initialize() - return iss.ACMEIssuer.Revoke(ctx, cert, reason) + return iss.issuer.Revoke(ctx, cert, reason) } // UnmarshalCaddyfile deserializes Caddyfile tokens into iss. // -// ... zerossl [] { -// ... +// ... zerossl { +// validity_days +// alt_http_port +// dns ... +// propagation_delay +// propagation_timeout +// resolvers +// dns_ttl // } -// -// Any of the subdirectives for the ACME issuer can be used in the block. func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { d.Next() // consume issuer name + + // API key is required + if !d.NextArg() { + return d.ArgErr() + } + iss.APIKey = d.Val() if d.NextArg() { - iss.APIKey = d.Val() - if d.NextArg() { - return d.ArgErr() + return d.ArgErr() + } + + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "validity_days": + if iss.ValidityDays != 0 { + return d.Errf("validity days is already specified: %d", iss.ValidityDays) + } + days, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("invalid number of days %s: %v", d.Val(), err) + } + iss.ValidityDays = days + + case "alt_http_port": + if !d.NextArg() { + return d.ArgErr() + } + port, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("invalid port %s: %v", d.Val(), err) + } + iss.AlternateHTTPPort = port + + case "dns": + if !d.NextArg() { + return d.ArgErr() + } + provName := d.Val() + if iss.CNAMEValidation == nil { + iss.CNAMEValidation = new(DNSChallengeConfig) + } + unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName) + if err != nil { + return err + } + iss.CNAMEValidation.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil) + + case "propagation_delay": + if !d.NextArg() { + return d.ArgErr() + } + delayStr := d.Val() + delay, err := caddy.ParseDuration(delayStr) + if err != nil { + return d.Errf("invalid propagation_delay duration %s: %v", delayStr, err) + } + if iss.CNAMEValidation == nil { + iss.CNAMEValidation = new(DNSChallengeConfig) + } + iss.CNAMEValidation.PropagationDelay = caddy.Duration(delay) + + case "propagation_timeout": + if !d.NextArg() { + return d.ArgErr() + } + timeoutStr := d.Val() + var timeout time.Duration + if timeoutStr == "-1" { + timeout = time.Duration(-1) + } else { + var err error + timeout, err = caddy.ParseDuration(timeoutStr) + if err != nil { + return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err) + } + } + if iss.CNAMEValidation == nil { + iss.CNAMEValidation = new(DNSChallengeConfig) + } + iss.CNAMEValidation.PropagationTimeout = caddy.Duration(timeout) + + case "resolvers": + if iss.CNAMEValidation == nil { + iss.CNAMEValidation = new(DNSChallengeConfig) + } + iss.CNAMEValidation.Resolvers = d.RemainingArgs() + if len(iss.CNAMEValidation.Resolvers) == 0 { + return d.ArgErr() + } + + case "dns_ttl": + if !d.NextArg() { + return d.ArgErr() + } + ttlStr := d.Val() + ttl, err := caddy.ParseDuration(ttlStr) + if err != nil { + return d.Errf("invalid dns_ttl duration %s: %v", ttlStr, err) + } + if iss.CNAMEValidation == nil { + iss.CNAMEValidation = new(DNSChallengeConfig) + } + iss.CNAMEValidation.TTL = caddy.Duration(ttl) + + default: + return d.Errf("unrecognized zerossl issuer property: %s", d.Val()) } } - if iss.ACMEIssuer == nil { - iss.ACMEIssuer = new(ACMEIssuer) - } - err := iss.ACMEIssuer.UnmarshalCaddyfile(d.NewFromNextSegment()) - if err != nil { - return err - } return nil } -const zerosslAPIBase = "https://api.zerossl.com/acme" - // Interface guards var ( - _ certmagic.PreChecker = (*ZeroSSLIssuer)(nil) - _ certmagic.Issuer = (*ZeroSSLIssuer)(nil) - _ certmagic.Revoker = (*ZeroSSLIssuer)(nil) - _ caddy.Provisioner = (*ZeroSSLIssuer)(nil) - _ ConfigSetter = (*ZeroSSLIssuer)(nil) - - // a type which properly embeds an ACMEIssuer should implement - // this interface so it can be treated as an ACMEIssuer - _ interface{ GetACMEIssuer() *ACMEIssuer } = (*ZeroSSLIssuer)(nil) + _ certmagic.Issuer = (*ZeroSSLIssuer)(nil) + _ certmagic.Revoker = (*ZeroSSLIssuer)(nil) + _ caddy.Provisioner = (*ZeroSSLIssuer)(nil) )