Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 01:00 UTC
| 1 | // Copyright 2015 Matthew Holt and The Caddy Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package caddytls |
| 16 | |
| 17 | import ( |
| 18 | "context" |
| 19 | "crypto/x509" |
| 20 | "fmt" |
| 21 | "strconv" |
| 22 | "time" |
| 23 | |
| 24 | "github.com/caddyserver/certmagic" |
| 25 | "go.uber.org/zap" |
| 26 | |
| 27 | "github.com/caddyserver/caddy/v2" |
| 28 | "github.com/caddyserver/caddy/v2/caddyconfig" |
| 29 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" |
| 30 | ) |
| 31 | |
| 32 | func init() { |
| 33 | caddy.RegisterModule(new(ZeroSSLIssuer)) |
| 34 | } |
| 35 | |
| 36 | // ZeroSSLIssuer uses the ZeroSSL API to get certificates. |
| 37 | // Note that this is distinct from ZeroSSL's ACME endpoint. |
| 38 | // To use ZeroSSL's ACME endpoint, use the ACMEIssuer |
| 39 | // configured with ZeroSSL's ACME directory endpoint. |
| 40 | type ZeroSSLIssuer struct { |
| 41 | // The API key (or "access key") for using the ZeroSSL API. |
| 42 | // REQUIRED. |
| 43 | APIKey string `json:"api_key,omitempty"` //nolint:gosec // false positive... yes this is exported, for JSON interop |
| 44 | |
| 45 | // How many days the certificate should be valid for. |
| 46 | // Only certain values are accepted; see ZeroSSL docs. |
| 47 | ValidityDays int `json:"validity_days,omitempty"` |
| 48 | |
| 49 | // The host to bind to when opening a listener for |
| 50 | // verifying domain names (or IPs). |
| 51 | ListenHost string `json:"listen_host,omitempty"` |
| 52 | |
| 53 | // If HTTP is forwarded from port 80, specify the |
| 54 | // forwarded port here. |
| 55 | AlternateHTTPPort int `json:"alternate_http_port,omitempty"` |
| 56 | |
| 57 | // Use CNAME validation instead of HTTP. ZeroSSL's |
| 58 | // API uses CNAME records for DNS validation, similar |
| 59 | // to how Let's Encrypt uses TXT records for the |
| 60 | // DNS challenge. |
| 61 | CNAMEValidation *DNSChallengeConfig `json:"cname_validation,omitempty"` |
| 62 | |
| 63 | logger *zap.Logger |
| 64 | storage certmagic.Storage |
| 65 | issuer *certmagic.ZeroSSLIssuer |
| 66 | } |
| 67 | |
| 68 | // CaddyModule returns the Caddy module information. |
| 69 | func (*ZeroSSLIssuer) CaddyModule() caddy.ModuleInfo { |
| 70 | return caddy.ModuleInfo{ |
| 71 | ID: "tls.issuance.zerossl", |
| 72 | New: func() caddy.Module { return new(ZeroSSLIssuer) }, |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // Provision sets up the issuer. |
| 77 | func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error { |
| 78 | iss.logger = ctx.Logger() |
| 79 | iss.storage = ctx.Storage() |
| 80 | repl := caddy.NewReplacer() |
| 81 | |
| 82 | var dnsManager *certmagic.DNSManager |
| 83 | if iss.CNAMEValidation != nil && len(iss.CNAMEValidation.ProviderRaw) > 0 { |
| 84 | val, err := ctx.LoadModule(iss.CNAMEValidation, "ProviderRaw") |
| 85 | if err != nil { |
| 86 | return fmt.Errorf("loading DNS provider module: %v", err) |
| 87 | } |
| 88 | dnsManager = &certmagic.DNSManager{ |
| 89 | DNSProvider: val.(certmagic.DNSProvider), |
| 90 | TTL: time.Duration(iss.CNAMEValidation.TTL), |
| 91 | PropagationDelay: time.Duration(iss.CNAMEValidation.PropagationDelay), |
| 92 | PropagationTimeout: time.Duration(iss.CNAMEValidation.PropagationTimeout), |
| 93 | Resolvers: iss.CNAMEValidation.Resolvers, |
| 94 | OverrideDomain: iss.CNAMEValidation.OverrideDomain, |
| 95 | Logger: iss.logger.Named("cname"), |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | iss.issuer = &certmagic.ZeroSSLIssuer{ |
| 100 | APIKey: repl.ReplaceAll(iss.APIKey, ""), |
| 101 | ValidityDays: iss.ValidityDays, |
| 102 | ListenHost: iss.ListenHost, |
| 103 | AltHTTPPort: iss.AlternateHTTPPort, |
| 104 | Storage: iss.storage, |
| 105 | CNAMEValidation: dnsManager, |
| 106 | Logger: iss.logger, |
| 107 | } |
| 108 | |
| 109 | return nil |
| 110 | } |
| 111 | |
| 112 | // Issue obtains a certificate for the given csr. |
| 113 | func (iss *ZeroSSLIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { |
| 114 | return iss.issuer.Issue(ctx, csr) |
| 115 | } |
| 116 | |
| 117 | // IssuerKey returns the unique issuer key for the configured CA endpoint. |
| 118 | func (iss *ZeroSSLIssuer) IssuerKey() string { |
| 119 | return iss.issuer.IssuerKey() |
| 120 | } |
| 121 | |
| 122 | // Revoke revokes the given certificate. |
| 123 | func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error { |
| 124 | return iss.issuer.Revoke(ctx, cert, reason) |
| 125 | } |
| 126 | |
| 127 | // UnmarshalCaddyfile deserializes Caddyfile tokens into iss. |
| 128 | // |
| 129 | // ... zerossl <api_key> { |
| 130 | // validity_days <days> |
| 131 | // alt_http_port <port> |
| 132 | // dns <provider_name> ... |
| 133 | // propagation_delay <duration> |
| 134 | // propagation_timeout <duration> |
| 135 | // resolvers <list...> |
| 136 | // dns_ttl <duration> |
| 137 | // } |
| 138 | func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 139 | d.Next() // consume issuer name |
| 140 | |
| 141 | // API key is required |
| 142 | if !d.NextArg() { |
| 143 | return d.ArgErr() |
| 144 | } |
| 145 | iss.APIKey = d.Val() |
| 146 | if d.NextArg() { |
| 147 | return d.ArgErr() |
| 148 | } |
| 149 | |
| 150 | for nesting := d.Nesting(); d.NextBlock(nesting); { |
| 151 | switch d.Val() { |
| 152 | case "validity_days": |
| 153 | if iss.ValidityDays != 0 { |
| 154 | return d.Errf("validity days is already specified: %d", iss.ValidityDays) |
| 155 | } |
| 156 | days, err := strconv.Atoi(d.Val()) |
| 157 | if err != nil { |
| 158 | return d.Errf("invalid number of days %s: %v", d.Val(), err) |
| 159 | } |
| 160 | iss.ValidityDays = days |
| 161 | |
| 162 | case "alt_http_port": |
| 163 | if !d.NextArg() { |
| 164 | return d.ArgErr() |
| 165 | } |
| 166 | port, err := strconv.Atoi(d.Val()) |
| 167 | if err != nil { |
| 168 | return d.Errf("invalid port %s: %v", d.Val(), err) |
| 169 | } |
| 170 | iss.AlternateHTTPPort = port |
| 171 | |
| 172 | case "dns": |
| 173 | if !d.NextArg() { |
| 174 | return d.ArgErr() |
| 175 | } |
| 176 | provName := d.Val() |
| 177 | if iss.CNAMEValidation == nil { |
| 178 | iss.CNAMEValidation = new(DNSChallengeConfig) |
| 179 | } |
| 180 | unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName) |
| 181 | if err != nil { |
| 182 | return err |
| 183 | } |
| 184 | iss.CNAMEValidation.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil) |
| 185 | |
| 186 | case "propagation_delay": |
| 187 | if !d.NextArg() { |
| 188 | return d.ArgErr() |
| 189 | } |
| 190 | delayStr := d.Val() |
| 191 | delay, err := caddy.ParseDuration(delayStr) |
| 192 | if err != nil { |
| 193 | return d.Errf("invalid propagation_delay duration %s: %v", delayStr, err) |
| 194 | } |
| 195 | if iss.CNAMEValidation == nil { |
| 196 | iss.CNAMEValidation = new(DNSChallengeConfig) |
| 197 | } |
| 198 | iss.CNAMEValidation.PropagationDelay = caddy.Duration(delay) |
| 199 | |
| 200 | case "propagation_timeout": |
| 201 | if !d.NextArg() { |
| 202 | return d.ArgErr() |
| 203 | } |
| 204 | timeoutStr := d.Val() |
| 205 | var timeout time.Duration |
| 206 | if timeoutStr == "-1" { |
| 207 | timeout = time.Duration(-1) |
| 208 | } else { |
| 209 | var err error |
| 210 | timeout, err = caddy.ParseDuration(timeoutStr) |
| 211 | if err != nil { |
| 212 | return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err) |
| 213 | } |
| 214 | } |
| 215 | if iss.CNAMEValidation == nil { |
| 216 | iss.CNAMEValidation = new(DNSChallengeConfig) |
| 217 | } |
| 218 | iss.CNAMEValidation.PropagationTimeout = caddy.Duration(timeout) |
| 219 | |
| 220 | case "resolvers": |
| 221 | if iss.CNAMEValidation == nil { |
| 222 | iss.CNAMEValidation = new(DNSChallengeConfig) |
| 223 | } |
| 224 | iss.CNAMEValidation.Resolvers = d.RemainingArgs() |
| 225 | if len(iss.CNAMEValidation.Resolvers) == 0 { |
| 226 | return d.ArgErr() |
| 227 | } |
| 228 | |
| 229 | case "dns_ttl": |
| 230 | if !d.NextArg() { |
| 231 | return d.ArgErr() |
| 232 | } |
| 233 | ttlStr := d.Val() |
| 234 | ttl, err := caddy.ParseDuration(ttlStr) |
| 235 | if err != nil { |
| 236 | return d.Errf("invalid dns_ttl duration %s: %v", ttlStr, err) |
| 237 | } |
| 238 | if iss.CNAMEValidation == nil { |
| 239 | iss.CNAMEValidation = new(DNSChallengeConfig) |
| 240 | } |
| 241 | iss.CNAMEValidation.TTL = caddy.Duration(ttl) |
| 242 | |
| 243 | default: |
| 244 | return d.Errf("unrecognized zerossl issuer property: %s", d.Val()) |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | return nil |
| 249 | } |
| 250 | |
| 251 | // Interface guards |
| 252 | var ( |
| 253 | _ certmagic.Issuer = (*ZeroSSLIssuer)(nil) |
| 254 | _ certmagic.Revoker = (*ZeroSSLIssuer)(nil) |
| 255 | _ caddy.Provisioner = (*ZeroSSLIssuer)(nil) |
| 256 | ) |