Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 10, 22:30 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 caddy |
| 16 | |
| 17 | import ( |
| 18 | "bytes" |
| 19 | "context" |
| 20 | "crypto" |
| 21 | "crypto/tls" |
| 22 | "crypto/x509" |
| 23 | "encoding/base64" |
| 24 | "encoding/json" |
| 25 | "errors" |
| 26 | "expvar" |
| 27 | "fmt" |
| 28 | "hash" |
| 29 | "io" |
| 30 | "net" |
| 31 | "net/http" |
| 32 | "net/http/pprof" |
| 33 | "net/url" |
| 34 | "os" |
| 35 | "path" |
| 36 | "regexp" |
| 37 | "slices" |
| 38 | "strconv" |
| 39 | "strings" |
| 40 | "sync" |
| 41 | "time" |
| 42 | |
| 43 | "github.com/caddyserver/certmagic" |
| 44 | "github.com/cespare/xxhash/v2" |
| 45 | "github.com/prometheus/client_golang/prometheus" |
| 46 | "go.uber.org/zap" |
| 47 | "go.uber.org/zap/zapcore" |
| 48 | "golang.org/x/net/http/httpguts" |
| 49 | |
| 50 | "github.com/caddyserver/caddy/v2/caddyconfig/warning" |
| 51 | "github.com/caddyserver/caddy/v2/internal" |
| 52 | ) |
| 53 | |
| 54 | // testCertMagicStorageOverride is a package-level test hook. Tests may set |
| 55 | // this variable to provide a temporary certmagic.Storage so that cert |
| 56 | // management in tests does not hit the real default storage on disk. |
| 57 | // This must NOT be set in production code. |
| 58 | var testCertMagicStorageOverride certmagic.Storage |
| 59 | |
| 60 | func init() { |
| 61 | // The hard-coded default `DefaultAdminListen` can be overridden |
| 62 | // by setting the `CADDY_ADMIN` environment variable. |
| 63 | // The environment variable may be used by packagers to change |
| 64 | // the default admin address to something more appropriate for |
| 65 | // that platform. See #5317 for discussion. |
| 66 | if env, exists := os.LookupEnv("CADDY_ADMIN"); exists { |
| 67 | DefaultAdminListen = env |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | // AdminConfig configures Caddy's API endpoint, which is used |
| 72 | // to manage Caddy while it is running. |
| 73 | type AdminConfig struct { |
| 74 | // If true, the admin endpoint will be completely disabled. |
| 75 | // Note that this makes any runtime changes to the config |
| 76 | // impossible, since the interface to do so is through the |
| 77 | // admin endpoint. |
| 78 | Disabled bool `json:"disabled,omitempty"` |
| 79 | |
| 80 | // The address to which the admin endpoint's listener should |
| 81 | // bind itself. Can be any single network address that can be |
| 82 | // parsed by Caddy. Accepts placeholders. |
| 83 | // Default: the value of the `CADDY_ADMIN` environment variable, |
| 84 | // or `localhost:2019` otherwise. |
| 85 | // |
| 86 | // Remember: When changing this value through a config reload, |
| 87 | // be sure to use the `--address` CLI flag to specify the current |
| 88 | // admin address if the currently-running admin endpoint is not |
| 89 | // the default address. |
| 90 | Listen string `json:"listen,omitempty"` |
| 91 | |
| 92 | // If true, CORS headers will be emitted, and requests to the |
| 93 | // API will be rejected if their `Host` and `Origin` headers |
| 94 | // do not match the expected value(s). Use `origins` to |
| 95 | // customize which origins/hosts are allowed. If `origins` is |
| 96 | // not set, the listen address is the only value allowed by |
| 97 | // default. Enforced only on local (plaintext) endpoint. |
| 98 | EnforceOrigin bool `json:"enforce_origin,omitempty"` |
| 99 | |
| 100 | // The list of allowed origins/hosts for API requests. Only needed |
| 101 | // if accessing the admin endpoint from a host different from the |
| 102 | // socket's network interface or if `enforce_origin` is true. If not |
| 103 | // set, the listener address will be the default value. If set but |
| 104 | // empty, no origins will be allowed. Enforced only on local |
| 105 | // (plaintext) endpoint. |
| 106 | Origins []string `json:"origins,omitempty"` |
| 107 | |
| 108 | // Options pertaining to configuration management. |
| 109 | Config *ConfigSettings `json:"config,omitempty"` |
| 110 | |
| 111 | // Options that establish this server's identity. Identity refers to |
| 112 | // credentials which can be used to uniquely identify and authenticate |
| 113 | // this server instance. This is required if remote administration is |
| 114 | // enabled (but does not require remote administration to be enabled). |
| 115 | // Default: no identity management. |
| 116 | Identity *IdentityConfig `json:"identity,omitempty"` |
| 117 | |
| 118 | // Options pertaining to remote administration. By default, remote |
| 119 | // administration is disabled. If enabled, identity management must |
| 120 | // also be configured, as that is how the endpoint is secured. |
| 121 | // See the neighboring "identity" object. |
| 122 | // |
| 123 | // EXPERIMENTAL: This feature is subject to change. |
| 124 | Remote *RemoteAdmin `json:"remote,omitempty"` |
| 125 | } |
| 126 | |
| 127 | // ConfigSettings configures the management of configuration. |
| 128 | type ConfigSettings struct { |
| 129 | // Whether to keep a copy of the active config on disk. Default is true. |
| 130 | // Note that "pulled" dynamic configs (using the neighboring "load" module) |
| 131 | // are not persisted; only configs that are pushed to Caddy get persisted. |
| 132 | Persist *bool `json:"persist,omitempty"` |
| 133 | |
| 134 | // Loads a new configuration. This is helpful if your configs are |
| 135 | // managed elsewhere and you want Caddy to pull its config dynamically |
| 136 | // when it starts. The pulled config completely replaces the current |
| 137 | // one, just like any other config load. It is an error if a pulled |
| 138 | // config is configured to pull another config without a load_delay, |
| 139 | // as this creates a tight loop. |
| 140 | // |
| 141 | // EXPERIMENTAL: Subject to change. |
| 142 | LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"` |
| 143 | |
| 144 | // The duration after which to load config. If set, config will be pulled |
| 145 | // from the config loader after this duration. A delay is required if a |
| 146 | // dynamically-loaded config is configured to load yet another config. To |
| 147 | // load configs on a regular interval, ensure this value is set the same |
| 148 | // on all loaded configs; it can also be variable if needed, and to stop |
| 149 | // the loop, simply remove dynamic config loading from the next-loaded |
| 150 | // config. |
| 151 | // |
| 152 | // EXPERIMENTAL: Subject to change. |
| 153 | LoadDelay Duration `json:"load_delay,omitempty"` |
| 154 | } |
| 155 | |
| 156 | // IdentityConfig configures management of this server's identity. An identity |
| 157 | // consists of credentials that uniquely verify this instance; for example, |
| 158 | // TLS certificates (public + private key pairs). |
| 159 | type IdentityConfig struct { |
| 160 | // List of names or IP addresses which refer to this server. |
| 161 | // Certificates will be obtained for these identifiers so |
| 162 | // secure TLS connections can be made using them. |
| 163 | Identifiers []string `json:"identifiers,omitempty"` |
| 164 | |
| 165 | // Issuers that can provide this admin endpoint its identity |
| 166 | // certificate(s). Default: ACME issuers configured for |
| 167 | // ZeroSSL and Let's Encrypt. Be sure to change this if you |
| 168 | // require credentials for private identifiers. |
| 169 | IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"` |
| 170 | |
| 171 | issuers []certmagic.Issuer |
| 172 | } |
| 173 | |
| 174 | // RemoteAdmin enables and configures remote administration. If enabled, |
| 175 | // a secure listener enforcing mutual TLS authentication will be started |
| 176 | // on a different port from the standard plaintext admin server. |
| 177 | // |
| 178 | // This endpoint is secured using identity management, which must be |
| 179 | // configured separately (because identity management does not depend |
| 180 | // on remote administration). See the admin/identity config struct. |
| 181 | // |
| 182 | // EXPERIMENTAL: Subject to change. |
| 183 | type RemoteAdmin struct { |
| 184 | // The address on which to start the secure listener. Accepts placeholders. |
| 185 | // Default: :2021 |
| 186 | Listen string `json:"listen,omitempty"` |
| 187 | |
| 188 | // List of access controls for this secure admin endpoint. |
| 189 | // This configures TLS mutual authentication (i.e. authorized |
| 190 | // client certificates), but also application-layer permissions |
| 191 | // like which paths and methods each identity is authorized for. |
| 192 | AccessControl []*AdminAccess `json:"access_control,omitempty"` |
| 193 | } |
| 194 | |
| 195 | // AdminAccess specifies what permissions an identity or group |
| 196 | // of identities are granted. |
| 197 | type AdminAccess struct { |
| 198 | // Base64-encoded DER certificates containing public keys to accept. |
| 199 | // (The contents of PEM certificate blocks are base64-encoded DER.) |
| 200 | // Any of these public keys can appear in any part of a verified chain. |
| 201 | PublicKeys []string `json:"public_keys,omitempty"` |
| 202 | |
| 203 | // Limits what the associated identities are allowed to do. |
| 204 | // If unspecified, all permissions are granted. |
| 205 | Permissions []AdminPermissions `json:"permissions,omitempty"` |
| 206 | |
| 207 | publicKeys []crypto.PublicKey |
| 208 | } |
| 209 | |
| 210 | // AdminPermissions specifies what kinds of requests are allowed |
| 211 | // to be made to the admin endpoint. |
| 212 | type AdminPermissions struct { |
| 213 | // The API paths allowed. A request path must either equal an |
| 214 | // allowed path or be a subpath with a path-segment boundary. |
| 215 | Paths []string `json:"paths,omitempty"` |
| 216 | |
| 217 | // The HTTP methods allowed for the given paths. |
| 218 | Methods []string `json:"methods,omitempty"` |
| 219 | } |
| 220 | |
| 221 | // newAdminHandler reads admin's config and returns an http.Handler suitable |
| 222 | // for use in an admin endpoint server, which will be listening on listenAddr. |
| 223 | func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) (adminHandler, error) { |
| 224 | muxWrap := adminHandler{mux: http.NewServeMux()} |
| 225 | |
| 226 | // secure the local or remote endpoint respectively |
| 227 | if remote { |
| 228 | muxWrap.remoteControl = admin.Remote |
| 229 | } else { |
| 230 | // see comment in allowedOrigins() as to why we disable the host check for unix/fd networks |
| 231 | muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFdNetwork() |
| 232 | muxWrap.allowedOrigins = admin.allowedOrigins(addr) |
| 233 | muxWrap.enforceOrigin = admin.EnforceOrigin |
| 234 | } |
| 235 | |
| 236 | addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) { |
| 237 | labels := prometheus.Labels{"path": pattern, "handler": handlerLabel} |
| 238 | h = instrumentHandlerCounter( |
| 239 | adminMetrics.requestCount.MustCurryWith(labels), |
| 240 | h, |
| 241 | ) |
| 242 | muxWrap.mux.Handle(pattern, h) |
| 243 | } |
| 244 | // addRoute just calls muxWrap.mux.Handle after |
| 245 | // wrapping the handler with error handling |
| 246 | addRoute := func(pattern string, handlerLabel string, h AdminHandler) { |
| 247 | wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 248 | err := h.ServeHTTP(w, r) |
| 249 | if err != nil { |
| 250 | labels := prometheus.Labels{ |
| 251 | "path": pattern, |
| 252 | "handler": handlerLabel, |
| 253 | "method": strings.ToUpper(r.Method), |
| 254 | } |
| 255 | adminMetrics.requestErrors.With(labels).Inc() |
| 256 | } |
| 257 | muxWrap.handleError(w, r, err) |
| 258 | }) |
| 259 | addRouteWithMetrics(pattern, handlerLabel, wrapper) |
| 260 | } |
| 261 | |
| 262 | const handlerLabel = "admin" |
| 263 | |
| 264 | // register standard config control endpoints |
| 265 | addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig)) |
| 266 | addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID)) |
| 267 | addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop)) |
| 268 | |
| 269 | // register debugging endpoints |
| 270 | addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index)) |
| 271 | addRouteWithMetrics("/debug/pprof/cmdline", handlerLabel, http.HandlerFunc(pprof.Cmdline)) |
| 272 | addRouteWithMetrics("/debug/pprof/profile", handlerLabel, http.HandlerFunc(pprof.Profile)) |
| 273 | addRouteWithMetrics("/debug/pprof/symbol", handlerLabel, http.HandlerFunc(pprof.Symbol)) |
| 274 | addRouteWithMetrics("/debug/pprof/trace", handlerLabel, http.HandlerFunc(pprof.Trace)) |
| 275 | addRouteWithMetrics("/debug/vars", handlerLabel, expvar.Handler()) |
| 276 | |
| 277 | // register third-party module endpoints |
| 278 | for _, m := range GetModules("admin.api") { |
| 279 | router := m.New().(AdminRouter) |
| 280 | |
| 281 | // provision the router before registering its routes, so |
| 282 | // handlers have access to all provisioned state |
| 283 | if provisioner, ok := router.(Provisioner); ok { |
| 284 | if err := provisioner.Provision(ctx); err != nil { |
| 285 | return adminHandler{}, fmt.Errorf("provisioning admin router module %s: %v", m.ID, err) |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | for _, route := range router.Routes() { |
| 290 | addRoute(route.Pattern, handlerLabel, route.Handler) |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | return muxWrap, nil |
| 295 | } |
| 296 | |
| 297 | // allowedOrigins returns a list of origins that are allowed. |
| 298 | // If admin.Origins is nil (null), the provided listen address |
| 299 | // will be used as the default origin. If admin.Origins is |
| 300 | // empty, no origins will be allowed, effectively bricking the |
| 301 | // endpoint for non-unix-socket endpoints, but whatever. |
| 302 | func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL { |
| 303 | uniqueOrigins := make(map[string]struct{}) |
| 304 | for _, o := range admin.Origins { |
| 305 | uniqueOrigins[o] = struct{}{} |
| 306 | } |
| 307 | // RFC 2616, Section 14.26: |
| 308 | // "A client MUST include a Host header field in all HTTP/1.1 request |
| 309 | // messages. If the requested URI does not include an Internet host |
| 310 | // name for the service being requested, then the Host header field MUST |
| 311 | // be given with an empty value." |
| 312 | // |
| 313 | // UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6. |
| 314 | // Understandable, but frustrating. See: |
| 315 | // https://github.com/golang/go/issues/60374 |
| 316 | // See also the discussion here: |
| 317 | // https://github.com/golang/go/issues/61431 |
| 318 | // |
| 319 | // We can no longer conform to RFC 2616 Section 14.26 from either Go or curl |
| 320 | // in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a |
| 321 | // bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin |
| 322 | // security checks, the infosec community assures me that it is secure to do |
| 323 | // so, because: |
| 324 | // |
| 325 | // 1) Browsers do not allow access to unix sockets |
| 326 | // 2) DNS is irrelevant to unix sockets |
| 327 | // |
| 328 | // If either of those two statements ever fail to hold true, it is not the |
| 329 | // fault of Caddy. |
| 330 | // |
| 331 | // Thus, we do not fill out allowed origins and do not enforce Host |
| 332 | // requirements for unix sockets. Enforcing it leads to confusion and |
| 333 | // frustration, when UDS have their own permissions from the OS. |
| 334 | // Enforcing host requirements here is effectively security theater, |
| 335 | // and a false sense of security. |
| 336 | // |
| 337 | // See also the discussion in #6832. |
| 338 | if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFdNetwork() { |
| 339 | if addr.isLoopback() { |
| 340 | uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{} |
| 341 | uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{} |
| 342 | uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{} |
| 343 | } else { |
| 344 | uniqueOrigins[addr.JoinHostPort(0)] = struct{}{} |
| 345 | } |
| 346 | } |
| 347 | allowed := make([]*url.URL, 0, len(uniqueOrigins)) |
| 348 | for originStr := range uniqueOrigins { |
| 349 | var origin *url.URL |
| 350 | if strings.Contains(originStr, "://") { |
| 351 | var err error |
| 352 | origin, err = url.Parse(originStr) |
| 353 | if err != nil { |
| 354 | continue |
| 355 | } |
| 356 | origin.Path = "" |
| 357 | origin.RawPath = "" |
| 358 | origin.Fragment = "" |
| 359 | origin.RawFragment = "" |
| 360 | origin.RawQuery = "" |
| 361 | } else { |
| 362 | origin = &url.URL{Host: originStr} |
| 363 | } |
| 364 | allowed = append(allowed, origin) |
| 365 | } |
| 366 | return allowed |
| 367 | } |
| 368 | |
| 369 | // replaceLocalAdminServer replaces the running local admin server |
| 370 | // according to the relevant configuration in cfg. If no configuration |
| 371 | // for the admin endpoint exists in cfg, a default one is used, so |
| 372 | // that there is always an admin server (unless it is explicitly |
| 373 | // configured to be disabled). |
| 374 | // Critically note that some elements and functionality of the context |
| 375 | // may not be ready, e.g. storage. Tread carefully. |
| 376 | func replaceLocalAdminServer(cfg *Config, ctx Context) error { |
| 377 | // always* be sure to close down the old admin endpoint |
| 378 | // as gracefully as possible, even if the new one is |
| 379 | // disabled -- careful to use reference to the current |
| 380 | // (old) admin endpoint since it will be different |
| 381 | // when the function returns |
| 382 | // (* except if the new one fails to start) |
| 383 | oldAdminServer := localAdminServer |
| 384 | var err error |
| 385 | defer func() { |
| 386 | // do the shutdown asynchronously so that any |
| 387 | // current API request gets a response; this |
| 388 | // goroutine may last a few seconds |
| 389 | if oldAdminServer != nil && err == nil { |
| 390 | go func(oldAdminServer *http.Server) { |
| 391 | err := stopAdminServer(oldAdminServer) |
| 392 | if err != nil { |
| 393 | Log().Named("admin").Error("stopping current admin endpoint", zap.Error(err)) |
| 394 | } |
| 395 | }(oldAdminServer) |
| 396 | } |
| 397 | }() |
| 398 | |
| 399 | // set a default if admin wasn't otherwise configured |
| 400 | if cfg.Admin == nil { |
| 401 | cfg.Admin = &AdminConfig{ |
| 402 | Listen: DefaultAdminListen, |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | // if new admin endpoint is to be disabled, we're done |
| 407 | if cfg.Admin.Disabled { |
| 408 | Log().Named("admin").Warn("admin endpoint disabled") |
| 409 | return nil |
| 410 | } |
| 411 | |
| 412 | // extract a singular listener address |
| 413 | addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen) |
| 414 | if err != nil { |
| 415 | return err |
| 416 | } |
| 417 | |
| 418 | handler, err := cfg.Admin.newAdminHandler(addr, false, ctx) |
| 419 | if err != nil { |
| 420 | return err |
| 421 | } |
| 422 | |
| 423 | ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{}) |
| 424 | if err != nil { |
| 425 | return err |
| 426 | } |
| 427 | |
| 428 | serverMu.Lock() |
| 429 | localAdminServer = &http.Server{ |
| 430 | Addr: addr.String(), // for logging purposes only |
| 431 | Handler: handler, |
| 432 | ReadTimeout: 10 * time.Second, |
| 433 | ReadHeaderTimeout: 5 * time.Second, |
| 434 | IdleTimeout: 60 * time.Second, |
| 435 | MaxHeaderBytes: 1024 * 64, |
| 436 | } |
| 437 | serverMu.Unlock() |
| 438 | |
| 439 | adminLogger := Log().Named("admin") |
| 440 | go func() { |
| 441 | serverMu.Lock() |
| 442 | server := localAdminServer |
| 443 | serverMu.Unlock() |
| 444 | if err := server.Serve(ln.(net.Listener)); !errors.Is(err, http.ErrServerClosed) { |
| 445 | adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err)) |
| 446 | } |
| 447 | }() |
| 448 | |
| 449 | adminLogger.Info("admin endpoint started", |
| 450 | zap.String("address", addr.String()), |
| 451 | zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin), |
| 452 | zap.Array("origins", loggableURLArray(handler.allowedOrigins))) |
| 453 | |
| 454 | if !handler.enforceHost { |
| 455 | adminLogger.Warn("admin endpoint on open interface; host checking disabled", |
| 456 | zap.String("address", addr.String())) |
| 457 | } |
| 458 | |
| 459 | return nil |
| 460 | } |
| 461 | |
| 462 | // manageIdentity sets up automated identity management for this server. |
| 463 | func manageIdentity(ctx Context, cfg *Config) error { |
| 464 | if cfg == nil || cfg.Admin == nil || cfg.Admin.Identity == nil { |
| 465 | return nil |
| 466 | } |
| 467 | |
| 468 | // set default issuers; this is pretty hacky because we can't |
| 469 | // import the caddytls package -- but it works |
| 470 | if cfg.Admin.Identity.IssuersRaw == nil { |
| 471 | cfg.Admin.Identity.IssuersRaw = []json.RawMessage{ |
| 472 | json.RawMessage(`{"module": "acme"}`), |
| 473 | } |
| 474 | } |
| 475 | |
| 476 | // load and provision issuer modules |
| 477 | if cfg.Admin.Identity.IssuersRaw != nil { |
| 478 | val, err := ctx.LoadModule(cfg.Admin.Identity, "IssuersRaw") |
| 479 | if err != nil { |
| 480 | return fmt.Errorf("loading identity issuer modules: %s", err) |
| 481 | } |
| 482 | for _, issVal := range val.([]any) { |
| 483 | cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer)) |
| 484 | } |
| 485 | } |
| 486 | |
| 487 | // we'll make a new cache when we make the CertMagic config, so stop any previous cache |
| 488 | if identityCertCache != nil { |
| 489 | identityCertCache.Stop() |
| 490 | } |
| 491 | |
| 492 | logger := Log().Named("admin.identity") |
| 493 | cmCfg := cfg.Admin.Identity.certmagicConfig(logger, true) |
| 494 | |
| 495 | // issuers have circular dependencies with the configs because, |
| 496 | // as explained in the caddytls package, they need access to the |
| 497 | // correct storage and cache to solve ACME challenges |
| 498 | for _, issuer := range cfg.Admin.Identity.issuers { |
| 499 | // avoid import cycle with caddytls package, so manually duplicate the interface here, yuck |
| 500 | if annoying, ok := issuer.(interface{ SetConfig(cfg *certmagic.Config) }); ok { |
| 501 | annoying.SetConfig(cmCfg) |
| 502 | } |
| 503 | } |
| 504 | |
| 505 | // obtain and renew server identity certificate(s) |
| 506 | return cmCfg.ManageAsync(ctx, cfg.Admin.Identity.Identifiers) |
| 507 | } |
| 508 | |
| 509 | // replaceRemoteAdminServer replaces the running remote admin server |
| 510 | // according to the relevant configuration in cfg. It stops any previous |
| 511 | // remote admin server and only starts a new one if configured. |
| 512 | func replaceRemoteAdminServer(ctx Context, cfg *Config) error { |
| 513 | if cfg == nil { |
| 514 | return nil |
| 515 | } |
| 516 | |
| 517 | remoteLogger := Log().Named("admin.remote") |
| 518 | |
| 519 | oldAdminServer := remoteAdminServer |
| 520 | defer func() { |
| 521 | if oldAdminServer != nil { |
| 522 | go func(oldAdminServer *http.Server) { |
| 523 | err := stopAdminServer(oldAdminServer) |
| 524 | if err != nil { |
| 525 | Log().Named("admin").Error("stopping current secure admin endpoint", zap.Error(err)) |
| 526 | } |
| 527 | }(oldAdminServer) |
| 528 | } |
| 529 | }() |
| 530 | |
| 531 | if cfg.Admin == nil || cfg.Admin.Remote == nil { |
| 532 | return nil |
| 533 | } |
| 534 | |
| 535 | addr, err := parseAdminListenAddr(cfg.Admin.Remote.Listen, DefaultRemoteAdminListen) |
| 536 | if err != nil { |
| 537 | return err |
| 538 | } |
| 539 | |
| 540 | // make the HTTP handler but disable Host/Origin enforcement |
| 541 | // because we are using TLS authentication instead |
| 542 | handler, err := cfg.Admin.newAdminHandler(addr, true, ctx) |
| 543 | if err != nil { |
| 544 | return err |
| 545 | } |
| 546 | |
| 547 | // create client certificate pool for TLS mutual auth, and extract public keys |
| 548 | // so that we can enforce access controls at the application layer |
| 549 | clientCertPool := x509.NewCertPool() |
| 550 | for i, accessControl := range cfg.Admin.Remote.AccessControl { |
| 551 | for j, certBase64 := range accessControl.PublicKeys { |
| 552 | cert, err := decodeBase64DERCert(certBase64) |
| 553 | if err != nil { |
| 554 | return fmt.Errorf("access control %d public key %d: parsing base64 certificate DER: %v", i, j, err) |
| 555 | } |
| 556 | accessControl.publicKeys = append(accessControl.publicKeys, cert.PublicKey) |
| 557 | clientCertPool.AddCert(cert) |
| 558 | } |
| 559 | for j, perm := range accessControl.Permissions { |
| 560 | for _, permPath := range perm.Paths { |
| 561 | if permPath == "" || permPath == "/" { |
| 562 | continue |
| 563 | } |
| 564 | cleanPath := path.Clean(permPath) |
| 565 | hasCanonicalTrailingSlash := cleanPath != "/" && strings.TrimSuffix(permPath, "/") == cleanPath |
| 566 | if cleanPath != permPath && !hasCanonicalTrailingSlash { |
| 567 | return fmt.Errorf("access control %d permission %d: path %q is not canonical (did you mean %q?)", i, j, permPath, cleanPath) |
| 568 | } |
| 569 | } |
| 570 | } |
| 571 | } |
| 572 | |
| 573 | // create TLS config that will enforce mutual authentication |
| 574 | if identityCertCache == nil { |
| 575 | return fmt.Errorf("cannot enable remote admin without a certificate cache; configure identity management to initialize a certificate cache") |
| 576 | } |
| 577 | cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false) |
| 578 | tlsConfig := cmCfg.TLSConfig() |
| 579 | tlsConfig.NextProtos = nil // this server does not solve ACME challenges |
| 580 | tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert |
| 581 | tlsConfig.ClientCAs = clientCertPool |
| 582 | |
| 583 | // convert logger to stdlib so it can be used by HTTP server |
| 584 | serverLogger, err := zap.NewStdLogAt(remoteLogger, zap.DebugLevel) |
| 585 | if err != nil { |
| 586 | return err |
| 587 | } |
| 588 | |
| 589 | serverMu.Lock() |
| 590 | // create secure HTTP server |
| 591 | remoteAdminServer = &http.Server{ |
| 592 | Addr: addr.String(), // for logging purposes only |
| 593 | Handler: handler, |
| 594 | TLSConfig: tlsConfig, |
| 595 | ReadTimeout: 10 * time.Second, |
| 596 | ReadHeaderTimeout: 5 * time.Second, |
| 597 | IdleTimeout: 60 * time.Second, |
| 598 | MaxHeaderBytes: 1024 * 64, |
| 599 | ErrorLog: serverLogger, |
| 600 | } |
| 601 | serverMu.Unlock() |
| 602 | |
| 603 | // start listener |
| 604 | lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{}) |
| 605 | if err != nil { |
| 606 | return err |
| 607 | } |
| 608 | ln := lnAny.(net.Listener) |
| 609 | ln = tls.NewListener(ln, tlsConfig) |
| 610 | |
| 611 | go func() { |
| 612 | serverMu.Lock() |
| 613 | server := remoteAdminServer |
| 614 | serverMu.Unlock() |
| 615 | if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) { |
| 616 | remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err)) |
| 617 | } |
| 618 | }() |
| 619 | |
| 620 | remoteLogger.Info("secure admin remote control endpoint started", |
| 621 | zap.String("address", addr.String())) |
| 622 | |
| 623 | return nil |
| 624 | } |
| 625 | |
| 626 | func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config { |
| 627 | var cmCfg *certmagic.Config |
| 628 | if ident == nil { |
| 629 | // user might not have configured identity; that's OK, we can still make a |
| 630 | // certmagic config, although it'll be mostly useless for remote management |
| 631 | ident = new(IdentityConfig) |
| 632 | } |
| 633 | // Choose storage: prefer the package-level test override when present, |
| 634 | // otherwise use the configured DefaultStorage. Tests may set an override |
| 635 | // to divert storage into a temporary location. Otherwise, in production |
| 636 | // we use the DefaultStorage since we don't want to act as part of a |
| 637 | // cluster; this storage is for the server's local identity only. |
| 638 | var storage certmagic.Storage |
| 639 | if testCertMagicStorageOverride != nil { |
| 640 | storage = testCertMagicStorageOverride |
| 641 | } else { |
| 642 | storage = DefaultStorage |
| 643 | } |
| 644 | template := certmagic.Config{ |
| 645 | Storage: storage, |
| 646 | Logger: logger, |
| 647 | Issuers: ident.issuers, |
| 648 | } |
| 649 | if makeCache { |
| 650 | identityCertCache = certmagic.NewCache(certmagic.CacheOptions{ |
| 651 | GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) { |
| 652 | return cmCfg, nil |
| 653 | }, |
| 654 | Logger: logger.Named("cache"), |
| 655 | }) |
| 656 | } |
| 657 | cmCfg = certmagic.New(identityCertCache, template) |
| 658 | return cmCfg |
| 659 | } |
| 660 | |
| 661 | // IdentityCredentials returns this instance's configured, managed identity credentials |
| 662 | // that can be used in TLS client authentication. |
| 663 | func (ctx Context) IdentityCredentials(logger *zap.Logger) ([]tls.Certificate, error) { |
| 664 | if ctx.cfg == nil || ctx.cfg.Admin == nil || ctx.cfg.Admin.Identity == nil { |
| 665 | return nil, fmt.Errorf("no server identity configured") |
| 666 | } |
| 667 | ident := ctx.cfg.Admin.Identity |
| 668 | if len(ident.Identifiers) == 0 { |
| 669 | return nil, fmt.Errorf("no identifiers configured") |
| 670 | } |
| 671 | if logger == nil { |
| 672 | logger = Log() |
| 673 | } |
| 674 | magic := ident.certmagicConfig(logger, false) |
| 675 | return magic.ClientCredentials(ctx, ident.Identifiers) |
| 676 | } |
| 677 | |
| 678 | // enforceAccessControls enforces application-layer access controls for r based on remote. |
| 679 | // It expects that the TLS server has already established at least one verified chain of |
| 680 | // trust, and then looks for a matching, authorized public key that is allowed to access |
| 681 | // the defined path(s) using the defined method(s). |
| 682 | func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error { |
| 683 | // the same public key may appear in more than one access control entry; |
| 684 | // the request is allowed if any entry containing the key permits it |
| 685 | var keyRecognized, pathAllowedSomewhere bool |
| 686 | |
| 687 | for _, chain := range r.TLS.VerifiedChains { |
| 688 | for _, peerCert := range chain { |
| 689 | for _, adminAccess := range remote.AccessControl { |
| 690 | for _, allowedKey := range adminAccess.publicKeys { |
| 691 | // see if we found a matching public key; the TLS server already verified the chain |
| 692 | // so we know the client possesses the associated private key; this handy interface |
| 693 | // doesn't appear to be defined anywhere in the std lib, but was implemented here: |
| 694 | // https://github.com/golang/go/commit/b5f2c0f50297fa5cd14af668ddd7fd923626cf8c |
| 695 | comparer, ok := peerCert.PublicKey.(interface{ Equal(crypto.PublicKey) bool }) |
| 696 | if !ok || !comparer.Equal(allowedKey) { |
| 697 | continue |
| 698 | } |
| 699 | keyRecognized = true |
| 700 | |
| 701 | // key recognized; make sure its HTTP request is permitted |
| 702 | // by at least one of the permission entries (if none are |
| 703 | // configured, all permissions are granted) |
| 704 | if len(adminAccess.Permissions) == 0 { |
| 705 | return nil |
| 706 | } |
| 707 | for _, accessPerm := range adminAccess.Permissions { |
| 708 | // verify path |
| 709 | pathFound := accessPerm.Paths == nil |
| 710 | for _, allowedPath := range accessPerm.Paths { |
| 711 | if adminPathAllowed(r.URL.Path, allowedPath) { |
| 712 | pathFound = true |
| 713 | break |
| 714 | } |
| 715 | } |
| 716 | if !pathFound { |
| 717 | continue |
| 718 | } |
| 719 | pathAllowedSomewhere = true |
| 720 | |
| 721 | // verify method |
| 722 | if accessPerm.Methods == nil || slices.Contains(accessPerm.Methods, r.Method) { |
| 723 | // public key authorized, method and path allowed |
| 724 | return nil |
| 725 | } |
| 726 | } |
| 727 | |
| 728 | // this entry does not permit the request; other entries |
| 729 | // containing the same key may |
| 730 | break |
| 731 | } |
| 732 | } |
| 733 | } |
| 734 | } |
| 735 | |
| 736 | if keyRecognized { |
| 737 | // no entry allows this request; if the path is allowed by |
| 738 | // some entry, the method is what's missing |
| 739 | if pathAllowedSomewhere { |
| 740 | return APIError{ |
| 741 | HTTPStatus: http.StatusForbidden, |
| 742 | Message: "not authorized to use this method", |
| 743 | } |
| 744 | } |
| 745 | return APIError{ |
| 746 | HTTPStatus: http.StatusForbidden, |
| 747 | Message: "not authorized to access this path", |
| 748 | } |
| 749 | } |
| 750 | |
| 751 | // in theory, this should never happen; with an unverified chain, the TLS server |
| 752 | // should not accept the connection in the first place, and the acceptable cert |
| 753 | // pool is configured using the same list of public keys we verify against |
| 754 | return APIError{ |
| 755 | HTTPStatus: http.StatusUnauthorized, |
| 756 | Message: "client identity not authorized", |
| 757 | } |
| 758 | } |
| 759 | |
| 760 | func adminPathAllowed(reqPath, allowedPath string) bool { |
| 761 | reqPathHadTrailingSlash := strings.HasSuffix(reqPath, "/") |
| 762 | reqPath = path.Clean(reqPath) |
| 763 | if allowedPath == "" { |
| 764 | return true |
| 765 | } |
| 766 | allowedPathHadTrailingSlash := allowedPath != "/" && strings.HasSuffix(allowedPath, "/") |
| 767 | allowedPath = path.Clean(allowedPath) |
| 768 | if allowedPath == "/" { |
| 769 | return true |
| 770 | } |
| 771 | if reqPath == allowedPath { |
| 772 | return !allowedPathHadTrailingSlash || reqPathHadTrailingSlash |
| 773 | } |
| 774 | return strings.HasPrefix(reqPath, allowedPath+"/") |
| 775 | } |
| 776 | |
| 777 | func stopAdminServer(srv *http.Server) error { |
| 778 | if srv == nil { |
| 779 | return fmt.Errorf("no admin server") |
| 780 | } |
| 781 | timeout := 10 * time.Second |
| 782 | ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("stopping admin server: %ds timeout", int(timeout.Seconds()))) |
| 783 | defer cancel() |
| 784 | err := srv.Shutdown(ctx) |
| 785 | if err != nil { |
| 786 | if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) { |
| 787 | err = cause |
| 788 | } |
| 789 | return fmt.Errorf("shutting down admin server: %v", err) |
| 790 | } |
| 791 | Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr)) |
| 792 | return nil |
| 793 | } |
| 794 | |
| 795 | // AdminRouter is a type which can return routes for the admin API. |
| 796 | type AdminRouter interface { |
| 797 | Routes() []AdminRoute |
| 798 | } |
| 799 | |
| 800 | // AdminRoute represents a route for the admin endpoint. |
| 801 | type AdminRoute struct { |
| 802 | Pattern string |
| 803 | Handler AdminHandler |
| 804 | } |
| 805 | |
| 806 | type adminHandler struct { |
| 807 | mux *http.ServeMux |
| 808 | |
| 809 | // security for local/plaintext endpoint |
| 810 | enforceOrigin bool |
| 811 | enforceHost bool |
| 812 | allowedOrigins []*url.URL |
| 813 | |
| 814 | // security for remote/encrypted endpoint |
| 815 | remoteControl *RemoteAdmin |
| 816 | } |
| 817 | |
| 818 | // ServeHTTP is the external entry point for API requests. |
| 819 | // It will only be called once per request. |
| 820 | func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 821 | ip, port, err := net.SplitHostPort(r.RemoteAddr) |
| 822 | if err != nil { |
| 823 | ip = r.RemoteAddr |
| 824 | port = "" |
| 825 | } |
| 826 | log := Log().Named("admin.api").With( |
| 827 | zap.String("method", r.Method), |
| 828 | zap.String("host", r.Host), |
| 829 | zap.String("uri", r.RequestURI), |
| 830 | zap.String("remote_ip", ip), |
| 831 | zap.String("remote_port", port), |
| 832 | zap.Object("headers", internal.LoggableHTTPHeader{Header: r.Header}), |
| 833 | ) |
| 834 | if r.TLS != nil { |
| 835 | log = log.With( |
| 836 | zap.Bool("secure", true), |
| 837 | zap.Int("verified_chains", len(r.TLS.VerifiedChains)), |
| 838 | ) |
| 839 | } |
| 840 | if r.RequestURI == "/metrics" { |
| 841 | log.Debug("received request") |
| 842 | } else { |
| 843 | log.Info("received request") |
| 844 | } |
| 845 | h.serveHTTP(w, r) |
| 846 | } |
| 847 | |
| 848 | // serveHTTP is the internal entry point for API requests. It may |
| 849 | // be called more than once per request, for example if a request |
| 850 | // is rewritten (i.e. internal redirect). |
| 851 | func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) { |
| 852 | if h.remoteControl != nil { |
| 853 | // enforce access controls on secure endpoint |
| 854 | if err := h.remoteControl.enforceAccessControls(r); err != nil { |
| 855 | h.handleError(w, r, err) |
| 856 | return |
| 857 | } |
| 858 | } |
| 859 | |
| 860 | // common mitigations in browser contexts |
| 861 | if strings.Contains(r.Header.Get("Upgrade"), "websocket") { |
| 862 | // I've never been able demonstrate a vulnerability myself, but apparently |
| 863 | // WebSocket connections originating from browsers aren't subject to CORS |
| 864 | // restrictions, so we'll just be on the safe side |
| 865 | h.handleError(w, r, APIError{ |
| 866 | HTTPStatus: http.StatusBadRequest, |
| 867 | Err: errors.New("websocket connections aren't allowed"), |
| 868 | Message: "WebSocket connections aren't allowed.", |
| 869 | }) |
| 870 | return |
| 871 | } |
| 872 | if strings.Contains(r.Header.Get("Sec-Fetch-Mode"), "no-cors") { |
| 873 | // turns out web pages can just disable the same-origin policy (!???!?) |
| 874 | // but at least browsers let us know that's the case, holy heck |
| 875 | h.handleError(w, r, APIError{ |
| 876 | HTTPStatus: http.StatusBadRequest, |
| 877 | Err: errors.New("client attempted to make request by disabling same-origin policy using no-cors mode"), |
| 878 | Message: "Disabling same-origin restrictions is not allowed.", |
| 879 | }) |
| 880 | return |
| 881 | } |
| 882 | if r.Header.Get("Origin") == "null" { |
| 883 | // bug in Firefox in certain cross-origin situations (yikes?) |
| 884 | // (not strictly a security vuln on its own, but it's red flaggy, |
| 885 | // since it seems to manifest in cross-origin contexts) |
| 886 | h.handleError(w, r, APIError{ |
| 887 | HTTPStatus: http.StatusBadRequest, |
| 888 | Err: errors.New("invalid origin 'null'"), |
| 889 | Message: "Buggy browser is sending null Origin header.", |
| 890 | }) |
| 891 | return |
| 892 | } |
| 893 | |
| 894 | if h.enforceHost { |
| 895 | // DNS rebinding mitigation |
| 896 | err := h.checkHost(r) |
| 897 | if err != nil { |
| 898 | h.handleError(w, r, err) |
| 899 | return |
| 900 | } |
| 901 | } |
| 902 | |
| 903 | _, hasOriginHeader := r.Header["Origin"] |
| 904 | _, hasSecHeader := r.Header["Sec-Fetch-Mode"] |
| 905 | if h.enforceOrigin || hasOriginHeader || hasSecHeader { |
| 906 | // cross-site mitigation |
| 907 | origin, err := h.checkOrigin(r) |
| 908 | if err != nil { |
| 909 | h.handleError(w, r, err) |
| 910 | return |
| 911 | } |
| 912 | |
| 913 | if r.Method == http.MethodOptions { |
| 914 | w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE") |
| 915 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Cache-Control") |
| 916 | w.Header().Set("Access-Control-Allow-Credentials", "true") |
| 917 | } |
| 918 | w.Header().Set("Access-Control-Allow-Origin", origin) |
| 919 | } |
| 920 | |
| 921 | h.mux.ServeHTTP(w, r) |
| 922 | } |
| 923 | |
| 924 | func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err error) { |
| 925 | if err == nil { |
| 926 | return |
| 927 | } |
| 928 | if err == errInternalRedir { |
| 929 | h.serveHTTP(w, r) |
| 930 | return |
| 931 | } |
| 932 | |
| 933 | apiErr, ok := err.(APIError) |
| 934 | if !ok { |
| 935 | apiErr = APIError{ |
| 936 | HTTPStatus: http.StatusInternalServerError, |
| 937 | Err: err, |
| 938 | } |
| 939 | } |
| 940 | if apiErr.HTTPStatus == 0 { |
| 941 | apiErr.HTTPStatus = http.StatusInternalServerError |
| 942 | } |
| 943 | if apiErr.Message == "" && apiErr.Err != nil { |
| 944 | apiErr.Message = apiErr.Err.Error() |
| 945 | } |
| 946 | |
| 947 | Log().Named("admin.api").Error("request error", |
| 948 | zap.Error(err), |
| 949 | zap.Int("status_code", apiErr.HTTPStatus), |
| 950 | ) |
| 951 | |
| 952 | w.Header().Set("Content-Type", "application/json") |
| 953 | w.WriteHeader(apiErr.HTTPStatus) |
| 954 | encErr := json.NewEncoder(w).Encode(apiErr) |
| 955 | if encErr != nil { |
| 956 | Log().Named("admin.api").Error("failed to encode error response", zap.Error(encErr)) |
| 957 | } |
| 958 | } |
| 959 | |
| 960 | // checkHost returns a handler that wraps next such that |
| 961 | // it will only be called if the request's Host header matches |
| 962 | // a trustworthy/expected value. This helps to mitigate DNS |
| 963 | // rebinding attacks. |
| 964 | func (h adminHandler) checkHost(r *http.Request) error { |
| 965 | allowed := slices.ContainsFunc(h.allowedOrigins, func(u *url.URL) bool { |
| 966 | // Host comparison is case-insensitive per RFC 3986 §3.2.2, same as |
| 967 | // the Origin check below; url.Parse does not normalize host case. |
| 968 | return strings.EqualFold(r.Host, u.Host) |
| 969 | }) |
| 970 | if !allowed { |
| 971 | return APIError{ |
| 972 | HTTPStatus: http.StatusForbidden, |
| 973 | Err: fmt.Errorf("host not allowed: %s", r.Host), |
| 974 | } |
| 975 | } |
| 976 | return nil |
| 977 | } |
| 978 | |
| 979 | // checkOrigin ensures that the Origin header, if |
| 980 | // set, matches the intended target; prevents arbitrary |
| 981 | // sites from issuing requests to our listener. It |
| 982 | // returns the origin that was obtained from r. |
| 983 | func (h adminHandler) checkOrigin(r *http.Request) (string, error) { |
| 984 | originStr, origin := h.getOrigin(r) |
| 985 | if origin == nil { |
| 986 | return "", APIError{ |
| 987 | HTTPStatus: http.StatusForbidden, |
| 988 | Err: fmt.Errorf("required Origin header is missing or invalid"), |
| 989 | } |
| 990 | } |
| 991 | if !h.originAllowed(origin) { |
| 992 | return "", APIError{ |
| 993 | HTTPStatus: http.StatusForbidden, |
| 994 | Err: fmt.Errorf("client is not allowed to access from origin '%s'", originStr), |
| 995 | } |
| 996 | } |
| 997 | return origin.String(), nil |
| 998 | } |
| 999 | |
| 1000 | func (h adminHandler) getOrigin(r *http.Request) (string, *url.URL) { |
| 1001 | origin := r.Header.Get("Origin") |
| 1002 | if origin == "" { |
| 1003 | origin = r.Header.Get("Referer") |
| 1004 | } |
| 1005 | originURL, err := url.Parse(origin) |
| 1006 | if err != nil { |
| 1007 | return origin, nil |
| 1008 | } |
| 1009 | originURL.Path = "" |
| 1010 | originURL.RawPath = "" |
| 1011 | originURL.Fragment = "" |
| 1012 | originURL.RawFragment = "" |
| 1013 | originURL.RawQuery = "" |
| 1014 | return origin, originURL |
| 1015 | } |
| 1016 | |
| 1017 | func (h adminHandler) originAllowed(origin *url.URL) bool { |
| 1018 | for _, allowedOrigin := range h.allowedOrigins { |
| 1019 | if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme { |
| 1020 | continue |
| 1021 | } |
| 1022 | // Host comparison is case-insensitive per RFC 3986 §3.2.2; url.Parse |
| 1023 | // does not normalize host case, so fold it here. |
| 1024 | if strings.EqualFold(origin.Host, allowedOrigin.Host) { |
| 1025 | return true |
| 1026 | } |
| 1027 | } |
| 1028 | return false |
| 1029 | } |
| 1030 | |
| 1031 | // etagHasher returns the hasher we used on the config to both |
| 1032 | // produce and verify ETags. |
| 1033 | func etagHasher() hash.Hash { return xxhash.New() } |
| 1034 | |
| 1035 | // makeEtag returns an Etag header value (including quotes) for |
| 1036 | // the given config path and hash of contents at that path. |
| 1037 | func makeEtag(path string, hash hash.Hash) string { |
| 1038 | return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil)) |
| 1039 | } |
| 1040 | |
| 1041 | // This buffer pool is used to keep buffers for |
| 1042 | // reading the config file during eTag header generation |
| 1043 | var bufferPool = sync.Pool{ |
| 1044 | New: func() any { |
| 1045 | return new(bytes.Buffer) |
| 1046 | }, |
| 1047 | } |
| 1048 | |
| 1049 | func handleConfig(w http.ResponseWriter, r *http.Request) error { |
| 1050 | switch r.Method { |
| 1051 | case http.MethodGet: |
| 1052 | w.Header().Set("Content-Type", "application/json") |
| 1053 | hash := etagHasher() |
| 1054 | |
| 1055 | // Read the config into a buffer instead of writing directly to |
| 1056 | // the response writer, as we want to set the ETag as the header, |
| 1057 | // not the trailer. |
| 1058 | buf := bufferPool.Get().(*bytes.Buffer) |
| 1059 | buf.Reset() |
| 1060 | defer bufferPool.Put(buf) |
| 1061 | |
| 1062 | configWriter := io.MultiWriter(buf, hash) |
| 1063 | err := readConfig(r.URL.Path, configWriter) |
| 1064 | if err != nil { |
| 1065 | return APIError{HTTPStatus: http.StatusBadRequest, Err: err} |
| 1066 | } |
| 1067 | |
| 1068 | // we could consider setting up a sync.Pool for the summed |
| 1069 | // hashes to reduce GC pressure. |
| 1070 | w.Header().Set("Etag", makeEtag(r.URL.Path, hash)) |
| 1071 | _, err = w.Write(buf.Bytes()) |
| 1072 | if err != nil { |
| 1073 | return APIError{HTTPStatus: http.StatusInternalServerError, Err: err} |
| 1074 | } |
| 1075 | |
| 1076 | return nil |
| 1077 | |
| 1078 | case http.MethodPost, |
| 1079 | http.MethodPut, |
| 1080 | http.MethodPatch, |
| 1081 | http.MethodDelete: |
| 1082 | |
| 1083 | // DELETE does not use a body, but the others do |
| 1084 | var body []byte |
| 1085 | if r.Method != http.MethodDelete { |
| 1086 | if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") { |
| 1087 | return APIError{ |
| 1088 | HTTPStatus: http.StatusBadRequest, |
| 1089 | Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct), |
| 1090 | } |
| 1091 | } |
| 1092 | |
| 1093 | buf := bufPool.Get().(*bytes.Buffer) |
| 1094 | buf.Reset() |
| 1095 | defer bufPool.Put(buf) |
| 1096 | |
| 1097 | const maxConfigSize = 100 * 1024 * 1024 // 100 MB |
| 1098 | r.Body = http.MaxBytesReader(w, r.Body, maxConfigSize) |
| 1099 | |
| 1100 | _, err := io.Copy(buf, r.Body) |
| 1101 | if err != nil { |
| 1102 | return APIError{ |
| 1103 | HTTPStatus: http.StatusBadRequest, |
| 1104 | Err: fmt.Errorf("reading request body: %v", err), |
| 1105 | } |
| 1106 | } |
| 1107 | body = buf.Bytes() |
| 1108 | } |
| 1109 | |
| 1110 | forceReload := httpguts.HeaderValuesContainsToken(r.Header["Cache-Control"], "must-revalidate") |
| 1111 | |
| 1112 | err := changeConfig(r.Method, r.URL.Path, body, r.Header.Get("If-Match"), forceReload) |
| 1113 | if err != nil && !errors.Is(err, errSameConfig) { |
| 1114 | return err |
| 1115 | } |
| 1116 | |
| 1117 | // If this request changed the config, clear the last |
| 1118 | // config info we have stored, if it is different from |
| 1119 | // the original source. |
| 1120 | ClearLastConfigIfDifferent( |
| 1121 | r.Header.Get("Caddy-Config-Source-File"), |
| 1122 | r.Header.Get("Caddy-Config-Source-Adapter")) |
| 1123 | |
| 1124 | default: |
| 1125 | return APIError{ |
| 1126 | HTTPStatus: http.StatusMethodNotAllowed, |
| 1127 | Err: fmt.Errorf("method %s not allowed", r.Method), |
| 1128 | } |
| 1129 | } |
| 1130 | |
| 1131 | return nil |
| 1132 | } |
| 1133 | |
| 1134 | func handleConfigID(w http.ResponseWriter, r *http.Request) error { |
| 1135 | idPath := r.URL.Path |
| 1136 | |
| 1137 | parts := strings.Split(idPath, "/") |
| 1138 | if len(parts) < 3 || parts[2] == "" { |
| 1139 | return APIError{ |
| 1140 | HTTPStatus: http.StatusBadRequest, |
| 1141 | Err: fmt.Errorf("request path is missing object ID"), |
| 1142 | } |
| 1143 | } |
| 1144 | if parts[0] != "" || parts[1] != "id" { |
| 1145 | return APIError{ |
| 1146 | HTTPStatus: http.StatusBadRequest, |
| 1147 | Err: fmt.Errorf("malformed object path"), |
| 1148 | } |
| 1149 | } |
| 1150 | id := parts[2] |
| 1151 | |
| 1152 | // map the ID to the expanded path |
| 1153 | rawCfgMu.RLock() |
| 1154 | expanded, ok := rawCfgIndex[id] |
| 1155 | rawCfgMu.RUnlock() |
| 1156 | if !ok { |
| 1157 | return APIError{ |
| 1158 | HTTPStatus: http.StatusNotFound, |
| 1159 | Err: fmt.Errorf("unknown object ID '%s'", id), |
| 1160 | } |
| 1161 | } |
| 1162 | |
| 1163 | // piece the full URL path back together |
| 1164 | parts = append([]string{expanded}, parts[3:]...) |
| 1165 | r.URL.Path = path.Join(parts...) |
| 1166 | |
| 1167 | return errInternalRedir |
| 1168 | } |
| 1169 | |
| 1170 | func handleStop(w http.ResponseWriter, r *http.Request) error { |
| 1171 | if r.Method != http.MethodPost { |
| 1172 | return APIError{ |
| 1173 | HTTPStatus: http.StatusMethodNotAllowed, |
| 1174 | Err: fmt.Errorf("method not allowed"), |
| 1175 | } |
| 1176 | } |
| 1177 | |
| 1178 | exitProcess(context.Background(), Log().Named("admin.api")) |
| 1179 | return nil |
| 1180 | } |
| 1181 | |
| 1182 | func parseCanonicalArrayIndex(idx string) (int, error) { |
| 1183 | if idx == "" { |
| 1184 | return 0, fmt.Errorf("empty index") |
| 1185 | } |
| 1186 | i, err := strconv.Atoi(idx) |
| 1187 | if err != nil { |
| 1188 | return 0, err |
| 1189 | } |
| 1190 | if strconv.Itoa(i) != idx { |
| 1191 | return 0, fmt.Errorf("non-canonical array index") |
| 1192 | } |
| 1193 | return i, nil |
| 1194 | } |
| 1195 | |
| 1196 | // unsyncedConfigAccess traverses into the current config and performs |
| 1197 | // the operation at path according to method, using body and out as |
| 1198 | // needed. This is a low-level, unsynchronized function; most callers |
| 1199 | // will want to use changeConfig or readConfig instead. This requires a |
| 1200 | // read or write lock on currentCtxMu, depending on method (GET needs |
| 1201 | // only a read lock; all others need a write lock). |
| 1202 | func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error { |
| 1203 | var err error |
| 1204 | var val any |
| 1205 | |
| 1206 | // if there is a request body, decode it into the |
| 1207 | // variable that will be set in the config according |
| 1208 | // to method and path |
| 1209 | if len(body) > 0 { |
| 1210 | err = json.Unmarshal(body, &val) |
| 1211 | if err != nil { |
| 1212 | if jsonErr, ok := err.(*json.SyntaxError); ok { |
| 1213 | return fmt.Errorf("decoding request body: %w, at offset %d", jsonErr, jsonErr.Offset) |
| 1214 | } |
| 1215 | return fmt.Errorf("decoding request body: %w", err) |
| 1216 | } |
| 1217 | } |
| 1218 | |
| 1219 | enc := json.NewEncoder(out) |
| 1220 | |
| 1221 | cleanPath := strings.Trim(path, "/") |
| 1222 | if cleanPath == "" { |
| 1223 | return fmt.Errorf("no traversable path") |
| 1224 | } |
| 1225 | |
| 1226 | parts := strings.Split(cleanPath, "/") |
| 1227 | if len(parts) == 0 { |
| 1228 | return fmt.Errorf("path missing") |
| 1229 | } |
| 1230 | |
| 1231 | // A path that ends with "..." implies: |
| 1232 | // 1) the part before it is an array |
| 1233 | // 2) the payload is an array |
| 1234 | // and means that the user wants to expand the elements |
| 1235 | // in the payload array and append each one into the |
| 1236 | // destination array, like so: |
| 1237 | // array = append(array, elems...) |
| 1238 | // This special case is handled below. |
| 1239 | ellipses := parts[len(parts)-1] == "..." |
| 1240 | if ellipses { |
| 1241 | parts = parts[:len(parts)-1] |
| 1242 | } |
| 1243 | |
| 1244 | var ptr any = rawCfg |
| 1245 | |
| 1246 | traverseLoop: |
| 1247 | for i, part := range parts { |
| 1248 | switch v := ptr.(type) { |
| 1249 | case map[string]any: |
| 1250 | // if the next part enters a slice, and the slice is our destination, |
| 1251 | // handle it specially (because appending to the slice copies the slice |
| 1252 | // header, which does not replace the original one like we want) |
| 1253 | if arr, ok := v[part].([]any); ok && i == len(parts)-2 { |
| 1254 | var idx int |
| 1255 | if method != http.MethodPost { |
| 1256 | idxStr := parts[len(parts)-1] |
| 1257 | idx, err = parseCanonicalArrayIndex(idxStr) |
| 1258 | if err != nil { |
| 1259 | return fmt.Errorf("[%s] invalid array index '%s': %v", |
| 1260 | path, idxStr, err) |
| 1261 | } |
| 1262 | |
| 1263 | if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) { |
| 1264 | return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr) |
| 1265 | } |
| 1266 | } |
| 1267 | |
| 1268 | switch method { |
| 1269 | case http.MethodGet: |
| 1270 | err = enc.Encode(arr[idx]) |
| 1271 | if err != nil { |
| 1272 | return fmt.Errorf("encoding config: %v", err) |
| 1273 | } |
| 1274 | case http.MethodPost: |
| 1275 | if ellipses { |
| 1276 | valArray, ok := val.([]any) |
| 1277 | if !ok { |
| 1278 | return fmt.Errorf("final element is not an array") |
| 1279 | } |
| 1280 | v[part] = append(arr, valArray...) |
| 1281 | } else { |
| 1282 | v[part] = append(arr, val) |
| 1283 | } |
| 1284 | case http.MethodPut: |
| 1285 | // avoid creation of new slice and a second copy (see |
| 1286 | // https://github.com/golang/go/wiki/SliceTricks#insert) |
| 1287 | arr = append(arr, nil) |
| 1288 | copy(arr[idx+1:], arr[idx:]) |
| 1289 | arr[idx] = val |
| 1290 | v[part] = arr |
| 1291 | case http.MethodPatch: |
| 1292 | arr[idx] = val |
| 1293 | case http.MethodDelete: |
| 1294 | v[part] = append(arr[:idx], arr[idx+1:]...) |
| 1295 | default: |
| 1296 | return fmt.Errorf("unrecognized method %s", method) |
| 1297 | } |
| 1298 | break traverseLoop |
| 1299 | } |
| 1300 | |
| 1301 | if i == len(parts)-1 { |
| 1302 | switch method { |
| 1303 | case http.MethodGet: |
| 1304 | err = enc.Encode(v[part]) |
| 1305 | if err != nil { |
| 1306 | return fmt.Errorf("encoding config: %v", err) |
| 1307 | } |
| 1308 | case http.MethodPost: |
| 1309 | // if the part is an existing list, POST appends to |
| 1310 | // it, otherwise it just sets or creates the value |
| 1311 | if arr, ok := v[part].([]any); ok { |
| 1312 | if ellipses { |
| 1313 | valArray, ok := val.([]any) |
| 1314 | if !ok { |
| 1315 | return fmt.Errorf("final element is not an array") |
| 1316 | } |
| 1317 | v[part] = append(arr, valArray...) |
| 1318 | } else { |
| 1319 | v[part] = append(arr, val) |
| 1320 | } |
| 1321 | } else { |
| 1322 | v[part] = val |
| 1323 | } |
| 1324 | case http.MethodPut: |
| 1325 | if _, ok := v[part]; ok { |
| 1326 | return APIError{ |
| 1327 | HTTPStatus: http.StatusConflict, |
| 1328 | Err: fmt.Errorf("[%s] key already exists: %s", path, part), |
| 1329 | } |
| 1330 | } |
| 1331 | v[part] = val |
| 1332 | case http.MethodPatch: |
| 1333 | if _, ok := v[part]; !ok { |
| 1334 | return APIError{ |
| 1335 | HTTPStatus: http.StatusNotFound, |
| 1336 | Err: fmt.Errorf("[%s] key does not exist: %s", path, part), |
| 1337 | } |
| 1338 | } |
| 1339 | v[part] = val |
| 1340 | case http.MethodDelete: |
| 1341 | if _, ok := v[part]; !ok { |
| 1342 | return APIError{ |
| 1343 | HTTPStatus: http.StatusNotFound, |
| 1344 | Err: fmt.Errorf("[%s] key does not exist: %s", path, part), |
| 1345 | } |
| 1346 | } |
| 1347 | delete(v, part) |
| 1348 | default: |
| 1349 | return fmt.Errorf("unrecognized method %s", method) |
| 1350 | } |
| 1351 | } else { |
| 1352 | // if we are "PUTting" a new resource, the key(s) in its path |
| 1353 | // might not exist yet; that's OK but we need to make them as |
| 1354 | // we go, while we still have a pointer from the level above |
| 1355 | if v[part] == nil && method == http.MethodPut { |
| 1356 | v[part] = make(map[string]any) |
| 1357 | } |
| 1358 | ptr = v[part] |
| 1359 | } |
| 1360 | |
| 1361 | case []any: |
| 1362 | partInt, err := parseCanonicalArrayIndex(part) |
| 1363 | if err != nil { |
| 1364 | return fmt.Errorf("[/%s] invalid array index '%s': %v", |
| 1365 | strings.Join(parts[:i+1], "/"), part, err) |
| 1366 | } |
| 1367 | if partInt < 0 || partInt >= len(v) { |
| 1368 | return fmt.Errorf("[/%s] array index out of bounds: %s", |
| 1369 | strings.Join(parts[:i+1], "/"), part) |
| 1370 | } |
| 1371 | ptr = v[partInt] |
| 1372 | |
| 1373 | default: |
| 1374 | return fmt.Errorf("invalid traversal path at: %s", strings.Join(parts[:i+1], "/")) |
| 1375 | } |
| 1376 | } |
| 1377 | |
| 1378 | return nil |
| 1379 | } |
| 1380 | |
| 1381 | // RemoveMetaFields removes meta fields like "@id" from a JSON message |
| 1382 | // by using a simple regular expression. (An alternate way to do this |
| 1383 | // would be to delete them from the raw, map[string]any |
| 1384 | // representation as they are indexed, then iterate the index we made |
| 1385 | // and add them back after encoding as JSON, but this is simpler.) |
| 1386 | func RemoveMetaFields(rawJSON []byte) []byte { |
| 1387 | return idRegexp.ReplaceAllFunc(rawJSON, func(in []byte) []byte { |
| 1388 | // matches with a comma on both sides (when "@id" property is |
| 1389 | // not the first or last in the object) need to keep exactly |
| 1390 | // one comma for correct JSON syntax |
| 1391 | comma := []byte{','} |
| 1392 | if bytes.HasPrefix(in, comma) && bytes.HasSuffix(in, comma) { |
| 1393 | return comma |
| 1394 | } |
| 1395 | return []byte{} |
| 1396 | }) |
| 1397 | } |
| 1398 | |
| 1399 | // AdminHandler is like http.Handler except ServeHTTP may return an error. |
| 1400 | // |
| 1401 | // If any handler encounters an error, it should be returned for proper |
| 1402 | // handling. |
| 1403 | type AdminHandler interface { |
| 1404 | ServeHTTP(http.ResponseWriter, *http.Request) error |
| 1405 | } |
| 1406 | |
| 1407 | // AdminHandlerFunc is a convenience type like http.HandlerFunc. |
| 1408 | type AdminHandlerFunc func(http.ResponseWriter, *http.Request) error |
| 1409 | |
| 1410 | // ServeHTTP implements the Handler interface. |
| 1411 | func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error { |
| 1412 | return f(w, r) |
| 1413 | } |
| 1414 | |
| 1415 | // APIError is a structured error that every API |
| 1416 | // handler should return for consistency in logging |
| 1417 | // and client responses. If Message is unset, then |
| 1418 | // Err.Error() will be serialized in its place. |
| 1419 | type APIError struct { |
| 1420 | HTTPStatus int `json:"-"` |
| 1421 | Err error `json:"-"` |
| 1422 | Message string `json:"error"` |
| 1423 | Warnings []warning.Warning `json:"warnings,omitempty"` |
| 1424 | } |
| 1425 | |
| 1426 | func (e APIError) Error() string { |
| 1427 | if e.Err != nil { |
| 1428 | return e.Err.Error() |
| 1429 | } |
| 1430 | return e.Message |
| 1431 | } |
| 1432 | |
| 1433 | // parseAdminListenAddr extracts a singular listen address from either addr |
| 1434 | // or defaultAddr, returning the network and the address of the listener. |
| 1435 | func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) { |
| 1436 | input, err := NewReplacer().ReplaceOrErr(addr, true, true) |
| 1437 | if err != nil { |
| 1438 | return NetworkAddress{}, fmt.Errorf("replacing listen address: %v", err) |
| 1439 | } |
| 1440 | if input == "" { |
| 1441 | input = defaultAddr |
| 1442 | } |
| 1443 | listenAddr, err := ParseNetworkAddress(input) |
| 1444 | if err != nil { |
| 1445 | return NetworkAddress{}, fmt.Errorf("parsing listener address: %v", err) |
| 1446 | } |
| 1447 | if listenAddr.PortRangeSize() != 1 { |
| 1448 | return NetworkAddress{}, fmt.Errorf("must be exactly one listener address; cannot listen on: %s", listenAddr) |
| 1449 | } |
| 1450 | return listenAddr, nil |
| 1451 | } |
| 1452 | |
| 1453 | // decodeBase64DERCert base64-decodes, then DER-decodes, certStr. |
| 1454 | func decodeBase64DERCert(certStr string) (*x509.Certificate, error) { |
| 1455 | derBytes, err := base64.StdEncoding.DecodeString(certStr) |
| 1456 | if err != nil { |
| 1457 | return nil, err |
| 1458 | } |
| 1459 | return x509.ParseCertificate(derBytes) |
| 1460 | } |
| 1461 | |
| 1462 | type loggableURLArray []*url.URL |
| 1463 | |
| 1464 | func (ua loggableURLArray) MarshalLogArray(enc zapcore.ArrayEncoder) error { |
| 1465 | if ua == nil { |
| 1466 | return nil |
| 1467 | } |
| 1468 | for _, u := range ua { |
| 1469 | enc.AppendString(u.String()) |
| 1470 | } |
| 1471 | return nil |
| 1472 | } |
| 1473 | |
| 1474 | var ( |
| 1475 | // DefaultAdminListen is the address for the local admin |
| 1476 | // listener, if none is specified at startup. |
| 1477 | DefaultAdminListen = "localhost:2019" |
| 1478 | |
| 1479 | // DefaultRemoteAdminListen is the address for the remote |
| 1480 | // (TLS-authenticated) admin listener, if enabled and not |
| 1481 | // specified otherwise. |
| 1482 | DefaultRemoteAdminListen = ":2021" |
| 1483 | ) |
| 1484 | |
| 1485 | // PIDFile writes a pidfile to the file at filename. It |
| 1486 | // will get deleted before the process gracefully exits. |
| 1487 | func PIDFile(filename string) error { |
| 1488 | pid := []byte(strconv.Itoa(os.Getpid()) + "\n") |
| 1489 | err := os.WriteFile(filename, pid, 0o600) |
| 1490 | if err != nil { |
| 1491 | return err |
| 1492 | } |
| 1493 | pidfile = filename |
| 1494 | return nil |
| 1495 | } |
| 1496 | |
| 1497 | // idRegexp is used to match ID fields and their associated values |
| 1498 | // in the config. It also matches adjacent commas so that syntax |
| 1499 | // can be preserved no matter where in the object the field appears. |
| 1500 | // It supports string and most numeric values. |
| 1501 | var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`) |
| 1502 | |
| 1503 | // pidfile is the name of the pidfile, if any. |
| 1504 | var pidfile string |
| 1505 | |
| 1506 | // errInternalRedir indicates an internal redirect |
| 1507 | // and is useful when admin API handlers rewrite |
| 1508 | // the request; in that case, authentication and |
| 1509 | // authorization needs to happen again for the |
| 1510 | // rewritten request. |
| 1511 | var errInternalRedir = fmt.Errorf("internal redirect; re-authorization required") |
| 1512 | |
| 1513 | const ( |
| 1514 | rawConfigKey = "config" |
| 1515 | idKey = "@id" |
| 1516 | ) |
| 1517 | |
| 1518 | var bufPool = sync.Pool{ |
| 1519 | New: func() any { |
| 1520 | return new(bytes.Buffer) |
| 1521 | }, |
| 1522 | } |
| 1523 | |
| 1524 | // keep a reference to admin endpoint singletons while they're active |
| 1525 | var ( |
| 1526 | serverMu sync.Mutex |
| 1527 | localAdminServer, remoteAdminServer *http.Server |
| 1528 | identityCertCache *certmagic.Cache |
| 1529 | ) |