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 acmeserver |
| 16 | |
| 17 | import ( |
| 18 | "context" |
| 19 | "errors" |
| 20 | "fmt" |
| 21 | weakrand "math/rand/v2" |
| 22 | "net" |
| 23 | "net/http" |
| 24 | "os" |
| 25 | "path/filepath" |
| 26 | "regexp" |
| 27 | "strings" |
| 28 | "time" |
| 29 | |
| 30 | "github.com/go-chi/chi/v5" |
| 31 | "github.com/smallstep/certificates/acme" |
| 32 | "github.com/smallstep/certificates/acme/api" |
| 33 | acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql" |
| 34 | "github.com/smallstep/certificates/authority" |
| 35 | "github.com/smallstep/certificates/authority/provisioner" |
| 36 | "github.com/smallstep/certificates/db" |
| 37 | "github.com/smallstep/nosql" |
| 38 | bolterrors "go.etcd.io/bbolt/errors" |
| 39 | "go.uber.org/zap" |
| 40 | "go.uber.org/zap/zapcore" |
| 41 | |
| 42 | "github.com/caddyserver/caddy/v2" |
| 43 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" |
| 44 | "github.com/caddyserver/caddy/v2/modules/caddypki" |
| 45 | "github.com/caddyserver/caddy/v2/modules/caddytls" |
| 46 | ) |
| 47 | |
| 48 | func init() { |
| 49 | caddy.RegisterModule(Handler{}) |
| 50 | } |
| 51 | |
| 52 | // Handler is an ACME server handler. |
| 53 | type Handler struct { |
| 54 | // The ID of the CA to use for signing. This refers to |
| 55 | // the ID given to the CA in the `pki` app. If omitted, |
| 56 | // the default ID is "local". |
| 57 | CA string `json:"ca,omitempty"` |
| 58 | |
| 59 | // The lifetime for issued certificates |
| 60 | Lifetime caddy.Duration `json:"lifetime,omitempty"` |
| 61 | |
| 62 | // The hostname or IP address by which ACME clients |
| 63 | // will access the server. This is used to populate |
| 64 | // the ACME directory endpoint. If not set, the Host |
| 65 | // header of the request will be used. |
| 66 | // COMPATIBILITY NOTE / TODO: This property may go away in the |
| 67 | // future. Do not rely on this property long-term; check release notes. |
| 68 | Host string `json:"host,omitempty"` |
| 69 | |
| 70 | // The path prefix under which to serve all ACME |
| 71 | // endpoints. All other requests will not be served |
| 72 | // by this handler and will be passed through to |
| 73 | // the next one. Default: "/acme/". |
| 74 | // COMPATIBILITY NOTE / TODO: This property may go away in the |
| 75 | // future, as it is currently only required due to |
| 76 | // limitations in the underlying library. Do not rely |
| 77 | // on this property long-term; check release notes. |
| 78 | PathPrefix string `json:"path_prefix,omitempty"` |
| 79 | |
| 80 | // If true, the CA's root will be the issuer instead of |
| 81 | // the intermediate. This is NOT recommended and should |
| 82 | // only be used when devices/clients do not properly |
| 83 | // validate certificate chains. EXPERIMENTAL: Might be |
| 84 | // changed or removed in the future. |
| 85 | SignWithRoot bool `json:"sign_with_root,omitempty"` |
| 86 | |
| 87 | // The addresses of DNS resolvers to use when looking up |
| 88 | // the TXT records for solving DNS challenges. |
| 89 | // It accepts [network addresses](/docs/conventions#network-addresses) |
| 90 | // with port range of only 1. If the host is an IP address, |
| 91 | // it will be dialed directly to resolve the upstream server. |
| 92 | // If the host is not an IP address, the addresses are resolved |
| 93 | // using the [name resolution convention](https://golang.org/pkg/net/#hdr-Name_Resolution) |
| 94 | // of the Go standard library. If the array contains more |
| 95 | // than 1 resolver address, one is chosen at random. |
| 96 | Resolvers []string `json:"resolvers,omitempty"` |
| 97 | |
| 98 | // Specify the set of enabled ACME challenges. An empty or absent value |
| 99 | // means all challenges are enabled. Accepted values are: |
| 100 | // "http-01", "dns-01", "tls-alpn-01" |
| 101 | Challenges ACMEChallenges `json:"challenges,omitempty" ` |
| 102 | |
| 103 | // The policy to use for issuing certificates |
| 104 | Policy *Policy `json:"policy,omitempty"` |
| 105 | |
| 106 | logger *zap.Logger |
| 107 | resolvers []caddy.NetworkAddress |
| 108 | ctx caddy.Context |
| 109 | |
| 110 | acmeDB acme.DB |
| 111 | acmeAuth *authority.Authority |
| 112 | acmeClient acme.Client |
| 113 | acmeLinker acme.Linker |
| 114 | acmeEndpoints http.Handler |
| 115 | } |
| 116 | |
| 117 | // CaddyModule returns the Caddy module information. |
| 118 | func (Handler) CaddyModule() caddy.ModuleInfo { |
| 119 | return caddy.ModuleInfo{ |
| 120 | ID: "http.handlers.acme_server", |
| 121 | New: func() caddy.Module { return new(Handler) }, |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | // Provision sets up the ACME server handler. |
| 126 | func (ash *Handler) Provision(ctx caddy.Context) error { |
| 127 | ash.ctx = ctx |
| 128 | ash.logger = ctx.Logger() |
| 129 | |
| 130 | // set some defaults |
| 131 | if ash.CA == "" { |
| 132 | ash.CA = caddypki.DefaultCAID |
| 133 | } |
| 134 | if ash.PathPrefix == "" { |
| 135 | ash.PathPrefix = defaultPathPrefix |
| 136 | } |
| 137 | if ash.Lifetime == 0 { |
| 138 | ash.Lifetime = caddy.Duration(12 * time.Hour) |
| 139 | } |
| 140 | if len(ash.Challenges) > 0 { |
| 141 | if err := ash.Challenges.validate(); err != nil { |
| 142 | return err |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | ash.warnIfPolicyAllowsAll() |
| 147 | |
| 148 | // get a reference to the configured CA |
| 149 | appModule, err := ctx.App("pki") |
| 150 | if err != nil { |
| 151 | return err |
| 152 | } |
| 153 | pkiApp := appModule.(*caddypki.PKI) |
| 154 | ca, err := pkiApp.GetCA(ctx, ash.CA) |
| 155 | if err != nil { |
| 156 | return err |
| 157 | } |
| 158 | |
| 159 | // make sure leaf cert lifetime is less than the intermediate cert lifetime. this check only |
| 160 | // applies for caddy-managed intermediate certificates |
| 161 | if ca.Intermediate == nil && ash.Lifetime >= ca.IntermediateLifetime { |
| 162 | return fmt.Errorf("certificate lifetime (%s) should be less than intermediate certificate lifetime (%s)", time.Duration(ash.Lifetime), time.Duration(ca.IntermediateLifetime)) |
| 163 | } |
| 164 | |
| 165 | database, err := ash.openDatabase() |
| 166 | if err != nil { |
| 167 | return err |
| 168 | } |
| 169 | |
| 170 | authorityConfig := caddypki.AuthorityConfig{ |
| 171 | SignWithRoot: ash.SignWithRoot, |
| 172 | AuthConfig: &authority.AuthConfig{ |
| 173 | Provisioners: provisioner.List{ |
| 174 | &provisioner.ACME{ |
| 175 | Name: ash.CA, |
| 176 | Challenges: ash.Challenges.toSmallstepType(), |
| 177 | Options: &provisioner.Options{ |
| 178 | X509: ash.Policy.normalizeRules(), |
| 179 | }, |
| 180 | Type: provisioner.TypeACME.String(), |
| 181 | Claims: &provisioner.Claims{ |
| 182 | MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, |
| 183 | MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour * 365}, |
| 184 | DefaultTLSDur: &provisioner.Duration{Duration: time.Duration(ash.Lifetime)}, |
| 185 | }, |
| 186 | }, |
| 187 | }, |
| 188 | }, |
| 189 | DB: database, |
| 190 | } |
| 191 | |
| 192 | ash.acmeAuth, err = ca.NewAuthority(authorityConfig) |
| 193 | if err != nil { |
| 194 | return err |
| 195 | } |
| 196 | |
| 197 | ash.acmeDB, err = acmeNoSQL.New(ash.acmeAuth.GetDatabase().(nosql.DB)) |
| 198 | if err != nil { |
| 199 | return fmt.Errorf("configuring ACME DB: %v", err) |
| 200 | } |
| 201 | |
| 202 | ash.acmeClient, err = ash.makeClient() |
| 203 | if err != nil { |
| 204 | return err |
| 205 | } |
| 206 | |
| 207 | ash.acmeLinker = acme.NewLinker( |
| 208 | ash.Host, |
| 209 | strings.Trim(ash.PathPrefix, "/"), |
| 210 | ) |
| 211 | |
| 212 | // extract its http.Handler so we can use it directly |
| 213 | r := chi.NewRouter() |
| 214 | r.Route(ash.PathPrefix, func(r chi.Router) { |
| 215 | api.Route(r) |
| 216 | }) |
| 217 | ash.acmeEndpoints = r |
| 218 | |
| 219 | return nil |
| 220 | } |
| 221 | |
| 222 | func (ash *Handler) warnIfPolicyAllowsAll() { |
| 223 | allow := ash.Policy.normalizeAllowRules() |
| 224 | deny := ash.Policy.normalizeDenyRules() |
| 225 | if allow != nil || deny != nil { |
| 226 | return |
| 227 | } |
| 228 | |
| 229 | allowWildcardNames := ash.Policy != nil && ash.Policy.AllowWildcardNames |
| 230 | ash.logger.Warn( |
| 231 | "acme_server policy has no allow/deny rules; order identifiers are unrestricted (allow-all)", |
| 232 | zap.String("ca", ash.CA), |
| 233 | zap.Bool("allow_wildcard_names", allowWildcardNames), |
| 234 | ) |
| 235 | } |
| 236 | |
| 237 | func (ash Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { |
| 238 | if strings.HasPrefix(r.URL.Path, ash.PathPrefix) { |
| 239 | acmeCtx := acme.NewContext( |
| 240 | r.Context(), |
| 241 | ash.acmeDB, |
| 242 | ash.acmeClient, |
| 243 | ash.acmeLinker, |
| 244 | nil, |
| 245 | ) |
| 246 | acmeCtx = authority.NewContext(acmeCtx, ash.acmeAuth) |
| 247 | r = r.WithContext(acmeCtx) |
| 248 | |
| 249 | ash.acmeEndpoints.ServeHTTP(w, r) |
| 250 | return nil |
| 251 | } |
| 252 | return next.ServeHTTP(w, r) |
| 253 | } |
| 254 | |
| 255 | func (ash Handler) getDatabaseKey() string { |
| 256 | key := ash.CA |
| 257 | key = strings.ToLower(key) |
| 258 | key = strings.TrimSpace(key) |
| 259 | return keyCleaner.ReplaceAllLiteralString(key, "") |
| 260 | } |
| 261 | |
| 262 | // Cleanup implements caddy.CleanerUpper and closes any idle databases. |
| 263 | func (ash Handler) Cleanup() error { |
| 264 | key := ash.getDatabaseKey() |
| 265 | deleted, err := databasePool.Delete(key) |
| 266 | if deleted { |
| 267 | if c := ash.logger.Check(zapcore.DebugLevel, "unloading unused CA database"); c != nil { |
| 268 | c.Write(zap.String("db_key", key)) |
| 269 | } |
| 270 | } |
| 271 | if err != nil { |
| 272 | if c := ash.logger.Check(zapcore.ErrorLevel, "closing CA database"); c != nil { |
| 273 | c.Write(zap.String("db_key", key), zap.Error(err)) |
| 274 | } |
| 275 | } |
| 276 | return err |
| 277 | } |
| 278 | |
| 279 | func (ash Handler) openDatabase() (*db.AuthDB, error) { |
| 280 | key := ash.getDatabaseKey() |
| 281 | database, loaded, err := databasePool.LoadOrNew(key, func() (caddy.Destructor, error) { |
| 282 | dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server", key) |
| 283 | dbPath := filepath.Join(dbFolder, "db") |
| 284 | |
| 285 | err := os.MkdirAll(dbFolder, 0o755) |
| 286 | if err != nil { |
| 287 | return nil, fmt.Errorf("making folder for CA database: %v", err) |
| 288 | } |
| 289 | |
| 290 | dbConfig := &db.Config{ |
| 291 | Type: "bbolt", |
| 292 | DataSource: dbPath, |
| 293 | } |
| 294 | database, err := db.New(dbConfig) |
| 295 | if errors.Is(err, bolterrors.ErrTimeout) { |
| 296 | // bbolt holds an exclusive file lock, so something else has this database open |
| 297 | err = fmt.Errorf("%w: CA database %s is already locked; this is usually another running Caddy instance, in which case use 'caddy reload' to apply config changes to it", err, dbPath) |
| 298 | } |
| 299 | return databaseCloser{&database}, err |
| 300 | }) |
| 301 | |
| 302 | if loaded { |
| 303 | if c := ash.logger.Check(zapcore.DebugLevel, "loaded preexisting CA database"); c != nil { |
| 304 | c.Write(zap.String("db_key", key)) |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | return database.(databaseCloser).DB, err |
| 309 | } |
| 310 | |
| 311 | // makeClient creates an ACME client which will use a custom |
| 312 | // resolver instead of net.DefaultResolver. |
| 313 | func (ash Handler) makeClient() (acme.Client, error) { |
| 314 | // If no local resolvers are configured, check for global resolvers from TLS app |
| 315 | resolversToUse := ash.Resolvers |
| 316 | if len(resolversToUse) == 0 { |
| 317 | tlsAppIface, err := ash.ctx.App("tls") |
| 318 | if err == nil { |
| 319 | tlsApp := tlsAppIface.(*caddytls.TLS) |
| 320 | if len(tlsApp.Resolvers) > 0 { |
| 321 | resolversToUse = tlsApp.Resolvers |
| 322 | } |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | for _, v := range resolversToUse { |
| 327 | addr, err := caddy.ParseNetworkAddressWithDefaults(v, "udp", 53) |
| 328 | if err != nil { |
| 329 | return nil, err |
| 330 | } |
| 331 | if addr.PortRangeSize() != 1 { |
| 332 | return nil, fmt.Errorf("resolver address must have exactly one address; cannot call %v", addr) |
| 333 | } |
| 334 | ash.resolvers = append(ash.resolvers, addr) |
| 335 | } |
| 336 | |
| 337 | var resolver *net.Resolver |
| 338 | if len(ash.resolvers) != 0 { |
| 339 | dialer := &net.Dialer{ |
| 340 | Timeout: 2 * time.Second, |
| 341 | } |
| 342 | resolver = &net.Resolver{ |
| 343 | PreferGo: true, |
| 344 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { |
| 345 | //nolint:gosec |
| 346 | addr := ash.resolvers[weakrand.IntN(len(ash.resolvers))] |
| 347 | return dialer.DialContext(ctx, addr.Network, addr.JoinHostPort(0)) |
| 348 | }, |
| 349 | } |
| 350 | } else { |
| 351 | resolver = net.DefaultResolver |
| 352 | } |
| 353 | |
| 354 | return resolverClient{ |
| 355 | Client: acme.NewClient(), |
| 356 | resolver: resolver, |
| 357 | ctx: ash.ctx, |
| 358 | }, nil |
| 359 | } |
| 360 | |
| 361 | type resolverClient struct { |
| 362 | acme.Client |
| 363 | |
| 364 | resolver *net.Resolver |
| 365 | ctx context.Context |
| 366 | } |
| 367 | |
| 368 | func (c resolverClient) LookupTxt(name string) ([]string, error) { |
| 369 | return c.resolver.LookupTXT(c.ctx, name) |
| 370 | } |
| 371 | |
| 372 | const defaultPathPrefix = "/acme/" |
| 373 | |
| 374 | var ( |
| 375 | keyCleaner = regexp.MustCompile(`[^\w.-_]`) |
| 376 | databasePool = caddy.NewUsagePool() |
| 377 | ) |
| 378 | |
| 379 | type databaseCloser struct { |
| 380 | DB *db.AuthDB |
| 381 | } |
| 382 | |
| 383 | func (closer databaseCloser) Destruct() error { |
| 384 | return (*closer.DB).Shutdown() |
| 385 | } |
| 386 | |
| 387 | // Interface guards |
| 388 | var ( |
| 389 | _ caddyhttp.MiddlewareHandler = (*Handler)(nil) |
| 390 | _ caddy.Provisioner = (*Handler)(nil) |
| 391 | ) |