caddy-2.6.2/ 0000775 0000000 0000000 00000000000 14350072374 0012612 5 ustar 00root root 0000000 0000000 caddy-2.6.2/.editorconfig 0000664 0000000 0000000 00000000126 14350072374 0015266 0 ustar 00root root 0000000 0000000 [*]
end_of_line = lf
[caddytest/integration/caddyfile_adapt/*.txt]
indent_style = tab caddy-2.6.2/.gitattributes 0000664 0000000 0000000 00000000020 14350072374 0015475 0 ustar 00root root 0000000 0000000 *.go text eol=lf caddy-2.6.2/AUTHORS 0000664 0000000 0000000 00000000617 14350072374 0013666 0 ustar 00root root 0000000 0000000 # This is the official list of Caddy Authors for copyright purposes.
# Authors may be either individual people or legal entities.
#
# Not all individual contributors are authors. For the full list of
# contributors, refer to the project's page on GitHub or the repo's
# commit history.
Matthew Holt a
project
Caddy is an extensible server platform that uses TLS by default.
Releases · Documentation · Get Help
### Menu - [Features](#features) - [Install](#install) - [Build from source](#build-from-source) - [For development](#for-development) - [With version information and/or plugins](#with-version-information-andor-plugins) - [Quick start](#quick-start) - [Overview](#overview) - [Full documentation](#full-documentation) - [Getting help](#getting-help) - [About](#about) ## [Features](https://caddyserver.com/v2) - **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile) - **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/) - **Dynamic configuration** with the [JSON API](https://caddyserver.com/docs/api) - [**Config adapters**](https://caddyserver.com/docs/config-adapters) if you don't like JSON - **Automatic HTTPS** by default - [ZeroSSL](https://zerossl.com) and [Let's Encrypt](https://letsencrypt.org) for public names - Fully-managed local CA for internal names & IPs - Can coordinate with other Caddy instances in a cluster - Multi-issuer fallback - **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues - **Production-ready** after serving trillions of requests and managing millions of TLS certificates - **Scales to hundreds of thousands of sites** as proven in production - **HTTP/1.1, HTTP/2, and HTTP/3** supported all by default - **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat - **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) ## Install The simplest, cross-platform way to get started is to download Caddy from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH. See [our online documentation](https://caddyserver.com/docs/install) for other install instructions. ## Build from source Requirements: - [Go 1.18 or newer](https://golang.org/dl/) ### For development _**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions in the next section._ ```bash $ git clone "https://github.com/caddyserver/caddy.git" $ cd caddy/cmd/caddy/ $ go build ``` When you run Caddy, it may try to bind to low ports unless otherwise specified in your config. If your OS requires elevated privileges for this, you will need to give your new binary permission to do so. On Linux, this can be done easily with: `sudo setcap cap_net_bind_service=+ep ./caddy` If you prefer to use `go run` which only creates temporary binaries, you can still do this with the included `setcap.sh` like so: ```bash $ go run -exec ./setcap.sh main.go ``` If you don't want to type your password for `setcap`, use `sudo visudo` to edit your sudoers file and allow your user account to run that command without a password, for example: ``` username ALL=(ALL:ALL) NOPASSWD: /usr/sbin/setcap ``` replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments. ### With version information and/or plugins Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)... ``` $ xcaddy build ``` ...the following steps are automated: 1. Create a new folder: `mkdir caddy` 2. Change into it: `cd caddy` 3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add. 4. Initialize a Go module: `go mod init caddy` 5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag, commit, or branch name. 6. (Optional) Add plugins by adding their import: `_ "import/path/here"` 7. Compile: `go build` ## Quick start The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more. **We recommend that all users -- regardless of experience level -- do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.** If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂 ## Overview Caddy is most often used as an HTTPS server, but it is suitable for any long-running Go program. First and foremost, it is a platform to run Go applications. Caddy "apps" are just Go programs that are implemented as Caddy modules. Two apps -- `tls` and `http` -- ship standard with Caddy. Caddy apps instantly benefit from [automated documentation](https://caddyserver.com/docs/json/), graceful on-line [config changes via API](https://caddyserver.com/docs/api), and unification with other Caddy apps. Although [JSON](https://caddyserver.com/docs/json/) is Caddy's native config language, Caddy can accept input from [config adapters](https://caddyserver.com/docs/config-adapters) which can essentially convert any config format of your choice into JSON: Caddyfile, JSON 5, YAML, TOML, NGINX config, and more. The primary way to configure Caddy is through [its API](https://caddyserver.com/docs/api), but if you prefer config files, the [command-line interface](https://caddyserver.com/docs/command-line) supports those too. Caddy exposes an unprecedented level of control compared to any web server in existence. In Caddy, you are usually setting the actual values of the initialized types in memory that power everything from your HTTP handlers and TLS handshakes to your storage medium. Caddy is also ridiculously extensible, with a powerful plugin system that makes vast improvements over other web servers. To wield the power of this design, you need to know how the config document is structured. Please see [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/). Nearly all of Caddy's configuration is contained in a single config document, rather than being scattered across CLI flags and env variables and a configuration file as with other web servers. This makes managing your server config more straightforward and reduces hidden variables/factors. ## Full documentation Our website has complete documentation: **https://caddyserver.com/docs/** The docs are also open source. You can contribute to them here: https://github.com/caddyserver/website ## Getting help - We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed. - A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers! - Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first! Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums). ## About Matthew Holt began developing Caddy in 2014 while studying computer science at Brigham Young University. (The name "Caddy" was chosen because this software helps with the tedious, mundane tasks of serving the Web, and is also a single place for multiple things to be organized together.) It soon became the first web server to use HTTPS automatically and by default, and now has hundreds of contributors and has served trillions of HTTPS requests. **The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH. - _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_ - _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_ Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company. Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence. caddy-2.6.2/admin.go 0000664 0000000 0000000 00000123261 14350072374 0014236 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package caddy import ( "bytes" "context" "crypto" "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" "errors" "expvar" "fmt" "hash" "hash/fnv" "io" "net" "net/http" "net/http/pprof" "net/url" "os" "path" "regexp" "strconv" "strings" "sync" "time" "github.com/caddyserver/certmagic" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // AdminConfig configures Caddy's API endpoint, which is used // to manage Caddy while it is running. type AdminConfig struct { // If true, the admin endpoint will be completely disabled. // Note that this makes any runtime changes to the config // impossible, since the interface to do so is through the // admin endpoint. Disabled bool `json:"disabled,omitempty"` // The address to which the admin endpoint's listener should // bind itself. Can be any single network address that can be // parsed by Caddy. Accepts placeholders. Default: localhost:2019 Listen string `json:"listen,omitempty"` // If true, CORS headers will be emitted, and requests to the // API will be rejected if their `Host` and `Origin` headers // do not match the expected value(s). Use `origins` to // customize which origins/hosts are allowed. If `origins` is // not set, the listen address is the only value allowed by // default. Enforced only on local (plaintext) endpoint. EnforceOrigin bool `json:"enforce_origin,omitempty"` // The list of allowed origins/hosts for API requests. Only needed // if accessing the admin endpoint from a host different from the // socket's network interface or if `enforce_origin` is true. If not // set, the listener address will be the default value. If set but // empty, no origins will be allowed. Enforced only on local // (plaintext) endpoint. Origins []string `json:"origins,omitempty"` // Options pertaining to configuration management. Config *ConfigSettings `json:"config,omitempty"` // Options that establish this server's identity. Identity refers to // credentials which can be used to uniquely identify and authenticate // this server instance. This is required if remote administration is // enabled (but does not require remote administration to be enabled). // Default: no identity management. Identity *IdentityConfig `json:"identity,omitempty"` // Options pertaining to remote administration. By default, remote // administration is disabled. If enabled, identity management must // also be configured, as that is how the endpoint is secured. // See the neighboring "identity" object. // // EXPERIMENTAL: This feature is subject to change. Remote *RemoteAdmin `json:"remote,omitempty"` // Holds onto the routers so that we can later provision them // if they require provisioning. routers []AdminRouter } // ConfigSettings configures the management of configuration. type ConfigSettings struct { // Whether to keep a copy of the active config on disk. Default is true. // Note that "pulled" dynamic configs (using the neighboring "load" module) // are not persisted; only configs that are pushed to Caddy get persisted. Persist *bool `json:"persist,omitempty"` // Loads a new configuration. This is helpful if your configs are // managed elsewhere and you want Caddy to pull its config dynamically // when it starts. The pulled config completely replaces the current // one, just like any other config load. It is an error if a pulled // config is configured to pull another config without a load_delay, // as this creates a tight loop. // // EXPERIMENTAL: Subject to change. LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"` // The duration after which to load config. If set, config will be pulled // from the config loader after this duration. A delay is required if a // dynamically-loaded config is configured to load yet another config. To // load configs on a regular interval, ensure this value is set the same // on all loaded configs; it can also be variable if needed, and to stop // the loop, simply remove dynamic config loading from the next-loaded // config. // // EXPERIMENTAL: Subject to change. LoadDelay Duration `json:"load_delay,omitempty"` } // IdentityConfig configures management of this server's identity. An identity // consists of credentials that uniquely verify this instance; for example, // TLS certificates (public + private key pairs). type IdentityConfig struct { // List of names or IP addresses which refer to this server. // Certificates will be obtained for these identifiers so // secure TLS connections can be made using them. Identifiers []string `json:"identifiers,omitempty"` // Issuers that can provide this admin endpoint its identity // certificate(s). Default: ACME issuers configured for // ZeroSSL and Let's Encrypt. Be sure to change this if you // require credentials for private identifiers. IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"` issuers []certmagic.Issuer } // RemoteAdmin enables and configures remote administration. If enabled, // a secure listener enforcing mutual TLS authentication will be started // on a different port from the standard plaintext admin server. // // This endpoint is secured using identity management, which must be // configured separately (because identity management does not depend // on remote administration). See the admin/identity config struct. // // EXPERIMENTAL: Subject to change. type RemoteAdmin struct { // The address on which to start the secure listener. Accepts placeholders. // Default: :2021 Listen string `json:"listen,omitempty"` // List of access controls for this secure admin endpoint. // This configures TLS mutual authentication (i.e. authorized // client certificates), but also application-layer permissions // like which paths and methods each identity is authorized for. AccessControl []*AdminAccess `json:"access_control,omitempty"` } // AdminAccess specifies what permissions an identity or group // of identities are granted. type AdminAccess struct { // Base64-encoded DER certificates containing public keys to accept. // (The contents of PEM certificate blocks are base64-encoded DER.) // Any of these public keys can appear in any part of a verified chain. PublicKeys []string `json:"public_keys,omitempty"` // Limits what the associated identities are allowed to do. // If unspecified, all permissions are granted. Permissions []AdminPermissions `json:"permissions,omitempty"` publicKeys []crypto.PublicKey } // AdminPermissions specifies what kinds of requests are allowed // to be made to the admin endpoint. type AdminPermissions struct { // The API paths allowed. Paths are simple prefix matches. // Any subpath of the specified paths will be allowed. Paths []string `json:"paths,omitempty"` // The HTTP methods allowed for the given paths. Methods []string `json:"methods,omitempty"` } // newAdminHandler reads admin's config and returns an http.Handler suitable // for use in an admin endpoint server, which will be listening on listenAddr. func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler { muxWrap := adminHandler{mux: http.NewServeMux()} // secure the local or remote endpoint respectively if remote { muxWrap.remoteControl = admin.Remote } else { muxWrap.enforceHost = !addr.isWildcardInterface() muxWrap.allowedOrigins = admin.allowedOrigins(addr) muxWrap.enforceOrigin = admin.EnforceOrigin } addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) { labels := prometheus.Labels{"path": pattern, "handler": handlerLabel} h = instrumentHandlerCounter( adminMetrics.requestCount.MustCurryWith(labels), h, ) muxWrap.mux.Handle(pattern, h) } // addRoute just calls muxWrap.mux.Handle after // wrapping the handler with error handling addRoute := func(pattern string, handlerLabel string, h AdminHandler) { wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := h.ServeHTTP(w, r) if err != nil { labels := prometheus.Labels{ "path": pattern, "handler": handlerLabel, "method": strings.ToUpper(r.Method), } adminMetrics.requestErrors.With(labels).Inc() } muxWrap.handleError(w, r, err) }) addRouteWithMetrics(pattern, handlerLabel, wrapper) } const handlerLabel = "admin" // register standard config control endpoints addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig)) addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID)) addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop)) // register debugging endpoints addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index)) addRouteWithMetrics("/debug/pprof/cmdline", handlerLabel, http.HandlerFunc(pprof.Cmdline)) addRouteWithMetrics("/debug/pprof/profile", handlerLabel, http.HandlerFunc(pprof.Profile)) addRouteWithMetrics("/debug/pprof/symbol", handlerLabel, http.HandlerFunc(pprof.Symbol)) addRouteWithMetrics("/debug/pprof/trace", handlerLabel, http.HandlerFunc(pprof.Trace)) addRouteWithMetrics("/debug/vars", handlerLabel, expvar.Handler()) // register third-party module endpoints for _, m := range GetModules("admin.api") { router := m.New().(AdminRouter) handlerLabel := m.ID.Name() for _, route := range router.Routes() { addRoute(route.Pattern, handlerLabel, route.Handler) } admin.routers = append(admin.routers, router) } return muxWrap } // provisionAdminRouters provisions all the router modules // in the admin.api namespace that need provisioning. func (admin *AdminConfig) provisionAdminRouters(ctx Context) error { for _, router := range admin.routers { provisioner, ok := router.(Provisioner) if !ok { continue } err := provisioner.Provision(ctx) if err != nil { return err } } // We no longer need the routers once provisioned, allow for GC admin.routers = nil return nil } // allowedOrigins returns a list of origins that are allowed. // If admin.Origins is nil (null), the provided listen address // will be used as the default origin. If admin.Origins is // empty, no origins will be allowed, effectively bricking the // endpoint for non-unix-socket endpoints, but whatever. func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL { uniqueOrigins := make(map[string]struct{}) for _, o := range admin.Origins { uniqueOrigins[o] = struct{}{} } if admin.Origins == nil { if addr.isLoopback() { if addr.IsUnixNetwork() { // RFC 2616, Section 14.26: // "A client MUST include a Host header field in all HTTP/1.1 request // messages. If the requested URI does not include an Internet host // name for the service being requested, then the Host header field MUST // be given with an empty value." uniqueOrigins[""] = struct{}{} } else { uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{} } } if !addr.IsUnixNetwork() { uniqueOrigins[addr.JoinHostPort(0)] = struct{}{} } } allowed := make([]*url.URL, 0, len(uniqueOrigins)) for originStr := range uniqueOrigins { var origin *url.URL if strings.Contains(originStr, "://") { var err error origin, err = url.Parse(originStr) if err != nil { continue } origin.Path = "" origin.RawPath = "" origin.Fragment = "" origin.RawFragment = "" origin.RawQuery = "" } else { origin = &url.URL{Host: originStr} } allowed = append(allowed, origin) } return allowed } // replaceLocalAdminServer replaces the running local admin server // according to the relevant configuration in cfg. If no configuration // for the admin endpoint exists in cfg, a default one is used, so // that there is always an admin server (unless it is explicitly // configured to be disabled). func replaceLocalAdminServer(cfg *Config) error { // always* be sure to close down the old admin endpoint // as gracefully as possible, even if the new one is // disabled -- careful to use reference to the current // (old) admin endpoint since it will be different // when the function returns // (* except if the new one fails to start) oldAdminServer := localAdminServer var err error defer func() { // do the shutdown asynchronously so that any // current API request gets a response; this // goroutine may last a few seconds if oldAdminServer != nil && err == nil { go func(oldAdminServer *http.Server) { err := stopAdminServer(oldAdminServer) if err != nil { Log().Named("admin").Error("stopping current admin endpoint", zap.Error(err)) } }(oldAdminServer) } }() // set a default if admin wasn't otherwise configured if cfg.Admin == nil { cfg.Admin = &AdminConfig{ Listen: DefaultAdminListen, } } // if new admin endpoint is to be disabled, we're done if cfg.Admin.Disabled { Log().Named("admin").Warn("admin endpoint disabled") return nil } // extract a singular listener address addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen) if err != nil { return err } handler := cfg.Admin.newAdminHandler(addr, false) ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{}) if err != nil { return err } serverMu.Lock() localAdminServer = &http.Server{ Addr: addr.String(), // for logging purposes only Handler: handler, ReadTimeout: 10 * time.Second, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1024 * 64, } serverMu.Unlock() adminLogger := Log().Named("admin") go func() { serverMu.Lock() server := localAdminServer serverMu.Unlock() if err := server.Serve(ln.(net.Listener)); !errors.Is(err, http.ErrServerClosed) { adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err)) } }() adminLogger.Info("admin endpoint started", zap.String("address", addr.String()), zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin), zap.Array("origins", loggableURLArray(handler.allowedOrigins))) if !handler.enforceHost { adminLogger.Warn("admin endpoint on open interface; host checking disabled", zap.String("address", addr.String())) } return nil } // manageIdentity sets up automated identity management for this server. func manageIdentity(ctx Context, cfg *Config) error { if cfg == nil || cfg.Admin == nil || cfg.Admin.Identity == nil { return nil } // set default issuers; this is pretty hacky because we can't // 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"}`), } } // load and provision issuer modules if cfg.Admin.Identity.IssuersRaw != nil { val, err := ctx.LoadModule(cfg.Admin.Identity, "IssuersRaw") if err != nil { return fmt.Errorf("loading identity issuer modules: %s", err) } for _, issVal := range val.([]any) { cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer)) } } // we'll make a new cache when we make the CertMagic config, so stop any previous cache if identityCertCache != nil { identityCertCache.Stop() } logger := Log().Named("admin.identity") cmCfg := cfg.Admin.Identity.certmagicConfig(logger, true) // issuers have circular dependencies with the configs because, // as explained in the caddytls package, they need access to the // correct storage and cache to solve ACME challenges for _, issuer := range cfg.Admin.Identity.issuers { // avoid import cycle with caddytls package, so manually duplicate the interface here, yuck if annoying, ok := issuer.(interface{ SetConfig(cfg *certmagic.Config) }); ok { annoying.SetConfig(cmCfg) } } // obtain and renew server identity certificate(s) return cmCfg.ManageAsync(ctx, cfg.Admin.Identity.Identifiers) } // replaceRemoteAdminServer replaces the running remote admin server // according to the relevant configuration in cfg. It stops any previous // remote admin server and only starts a new one if configured. func replaceRemoteAdminServer(ctx Context, cfg *Config) error { if cfg == nil { return nil } remoteLogger := Log().Named("admin.remote") oldAdminServer := remoteAdminServer defer func() { if oldAdminServer != nil { go func(oldAdminServer *http.Server) { err := stopAdminServer(oldAdminServer) if err != nil { Log().Named("admin").Error("stopping current secure admin endpoint", zap.Error(err)) } }(oldAdminServer) } }() if cfg.Admin == nil || cfg.Admin.Remote == nil { return nil } addr, err := parseAdminListenAddr(cfg.Admin.Remote.Listen, DefaultRemoteAdminListen) if err != nil { return err } // make the HTTP handler but disable Host/Origin enforcement // because we are using TLS authentication instead handler := cfg.Admin.newAdminHandler(addr, true) // create client certificate pool for TLS mutual auth, and extract public keys // so that we can enforce access controls at the application layer clientCertPool := x509.NewCertPool() for i, accessControl := range cfg.Admin.Remote.AccessControl { for j, certBase64 := range accessControl.PublicKeys { cert, err := decodeBase64DERCert(certBase64) if err != nil { return fmt.Errorf("access control %d public key %d: parsing base64 certificate DER: %v", i, j, err) } accessControl.publicKeys = append(accessControl.publicKeys, cert.PublicKey) clientCertPool.AddCert(cert) } } // create TLS config that will enforce mutual authentication if identityCertCache == nil { return fmt.Errorf("cannot enable remote admin without a certificate cache; configure identity management to initialize a certificate cache") } cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false) tlsConfig := cmCfg.TLSConfig() tlsConfig.NextProtos = nil // this server does not solve ACME challenges tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert tlsConfig.ClientCAs = clientCertPool // convert logger to stdlib so it can be used by HTTP server serverLogger, err := zap.NewStdLogAt(remoteLogger, zap.DebugLevel) if err != nil { return err } serverMu.Lock() // create secure HTTP server remoteAdminServer = &http.Server{ Addr: addr.String(), // for logging purposes only Handler: handler, TLSConfig: tlsConfig, ReadTimeout: 10 * time.Second, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1024 * 64, ErrorLog: serverLogger, } serverMu.Unlock() // start listener lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{}) if err != nil { return err } ln := lnAny.(net.Listener) ln = tls.NewListener(ln, tlsConfig) go func() { serverMu.Lock() server := remoteAdminServer serverMu.Unlock() if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) { remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err)) } }() remoteLogger.Info("secure admin remote control endpoint started", zap.String("address", addr.String())) return nil } func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config { if ident == nil { // user might not have configured identity; that's OK, we can still make a // certmagic config, although it'll be mostly useless for remote management ident = new(IdentityConfig) } cmCfg := &certmagic.Config{ Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity) Logger: logger, Issuers: ident.issuers, } if makeCache { identityCertCache = certmagic.NewCache(certmagic.CacheOptions{ GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) { return cmCfg, nil }, }) } return certmagic.New(identityCertCache, *cmCfg) } // IdentityCredentials returns this instance's configured, managed identity credentials // that can be used in TLS client authentication. func (ctx Context) IdentityCredentials(logger *zap.Logger) ([]tls.Certificate, error) { if ctx.cfg == nil || ctx.cfg.Admin == nil || ctx.cfg.Admin.Identity == nil { return nil, fmt.Errorf("no server identity configured") } ident := ctx.cfg.Admin.Identity if len(ident.Identifiers) == 0 { return nil, fmt.Errorf("no identifiers configured") } if logger == nil { logger = Log() } magic := ident.certmagicConfig(logger, false) return magic.ClientCredentials(ctx, ident.Identifiers) } // enforceAccessControls enforces application-layer access controls for r based on remote. // It expects that the TLS server has already established at least one verified chain of // trust, and then looks for a matching, authorized public key that is allowed to access // the defined path(s) using the defined method(s). func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error { for _, chain := range r.TLS.VerifiedChains { for _, peerCert := range chain { for _, adminAccess := range remote.AccessControl { for _, allowedKey := range adminAccess.publicKeys { // see if we found a matching public key; the TLS server already verified the chain // so we know the client possesses the associated private key; this handy interface // doesn't appear to be defined anywhere in the std lib, but was implemented here: // https://github.com/golang/go/commit/b5f2c0f50297fa5cd14af668ddd7fd923626cf8c comparer, ok := peerCert.PublicKey.(interface{ Equal(crypto.PublicKey) bool }) if !ok || !comparer.Equal(allowedKey) { continue } // key recognized; make sure its HTTP request is permitted for _, accessPerm := range adminAccess.Permissions { // verify method methodFound := accessPerm.Methods == nil for _, method := range accessPerm.Methods { if method == r.Method { methodFound = true break } } if !methodFound { return APIError{ HTTPStatus: http.StatusForbidden, Message: "not authorized to use this method", } } // verify path pathFound := accessPerm.Paths == nil for _, allowedPath := range accessPerm.Paths { if strings.HasPrefix(r.URL.Path, allowedPath) { pathFound = true break } } if !pathFound { return APIError{ HTTPStatus: http.StatusForbidden, Message: "not authorized to access this path", } } } // public key authorized, method and path allowed return nil } } } } // in theory, this should never happen; with an unverified chain, the TLS server // should not accept the connection in the first place, and the acceptable cert // pool is configured using the same list of public keys we verify against return APIError{ HTTPStatus: http.StatusUnauthorized, Message: "client identity not authorized", } } func stopAdminServer(srv *http.Server) error { if srv == nil { return fmt.Errorf("no admin server") } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() err := srv.Shutdown(ctx) if err != nil { return fmt.Errorf("shutting down admin server: %v", err) } Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr)) return nil } // AdminRouter is a type which can return routes for the admin API. type AdminRouter interface { Routes() []AdminRoute } // AdminRoute represents a route for the admin endpoint. type AdminRoute struct { Pattern string Handler AdminHandler } type adminHandler struct { mux *http.ServeMux // security for local/plaintext endpoint enforceOrigin bool enforceHost bool allowedOrigins []*url.URL // security for remote/encrypted endpoint remoteControl *RemoteAdmin } // ServeHTTP is the external entry point for API requests. // It will only be called once per request. func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ip, port, err := net.SplitHostPort(r.RemoteAddr) if err != nil { ip = r.RemoteAddr port = "" } log := Log().Named("admin.api").With( zap.String("method", r.Method), zap.String("host", r.Host), zap.String("uri", r.RequestURI), zap.String("remote_ip", ip), zap.String("remote_port", port), zap.Reflect("headers", r.Header), ) if r.TLS != nil { log = log.With( zap.Bool("secure", true), zap.Int("verified_chains", len(r.TLS.VerifiedChains)), ) } if r.RequestURI == "/metrics" { log.Debug("received request") } else { log.Info("received request") } h.serveHTTP(w, r) } // serveHTTP is the internal entry point for API requests. It may // be called more than once per request, for example if a request // is rewritten (i.e. internal redirect). func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) { if h.remoteControl != nil { // enforce access controls on secure endpoint if err := h.remoteControl.enforceAccessControls(r); err != nil { h.handleError(w, r, err) return } } if strings.Contains(r.Header.Get("Upgrade"), "websocket") { // I've never been able demonstrate a vulnerability myself, but apparently // WebSocket connections originating from browsers aren't subject to CORS // restrictions, so we'll just be on the safe side h.handleError(w, r, fmt.Errorf("websocket connections aren't allowed")) return } if h.enforceHost { // DNS rebinding mitigation err := h.checkHost(r) if err != nil { h.handleError(w, r, err) return } } if h.enforceOrigin { // cross-site mitigation origin, err := h.checkOrigin(r) if err != nil { h.handleError(w, r, err) return } if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Cache-Control") w.Header().Set("Access-Control-Allow-Credentials", "true") } w.Header().Set("Access-Control-Allow-Origin", origin) } h.mux.ServeHTTP(w, r) } func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err error) { if err == nil { return } if err == errInternalRedir { h.serveHTTP(w, r) return } apiErr, ok := err.(APIError) if !ok { apiErr = APIError{ HTTPStatus: http.StatusInternalServerError, Err: err, } } if apiErr.HTTPStatus == 0 { apiErr.HTTPStatus = http.StatusInternalServerError } if apiErr.Message == "" && apiErr.Err != nil { apiErr.Message = apiErr.Err.Error() } Log().Named("admin.api").Error("request error", zap.Error(err), zap.Int("status_code", apiErr.HTTPStatus), ) w.Header().Set("Content-Type", "application/json") w.WriteHeader(apiErr.HTTPStatus) encErr := json.NewEncoder(w).Encode(apiErr) if encErr != nil { Log().Named("admin.api").Error("failed to encode error response", zap.Error(encErr)) } } // checkHost returns a handler that wraps next such that // it will only be called if the request's Host header matches // a trustworthy/expected value. This helps to mitigate DNS // rebinding attacks. func (h adminHandler) checkHost(r *http.Request) error { var allowed bool for _, allowedOrigin := range h.allowedOrigins { if r.Host == allowedOrigin.Host { allowed = true break } } if !allowed { return APIError{ HTTPStatus: http.StatusForbidden, Err: fmt.Errorf("host not allowed: %s", r.Host), } } return nil } // checkOrigin ensures that the Origin header, if // set, matches the intended target; prevents arbitrary // sites from issuing requests to our listener. It // returns the origin that was obtained from r. func (h adminHandler) checkOrigin(r *http.Request) (string, error) { originStr, origin := h.getOrigin(r) if origin == nil { return "", APIError{ HTTPStatus: http.StatusForbidden, Err: fmt.Errorf("required Origin header is missing or invalid"), } } if !h.originAllowed(origin) { return "", APIError{ HTTPStatus: http.StatusForbidden, Err: fmt.Errorf("client is not allowed to access from origin '%s'", originStr), } } return origin.String(), nil } func (h adminHandler) getOrigin(r *http.Request) (string, *url.URL) { origin := r.Header.Get("Origin") if origin == "" { origin = r.Header.Get("Referer") } originURL, err := url.Parse(origin) if err != nil { return origin, nil } originURL.Path = "" originURL.RawPath = "" originURL.Fragment = "" originURL.RawFragment = "" originURL.RawQuery = "" return origin, originURL } func (h adminHandler) originAllowed(origin *url.URL) bool { for _, allowedOrigin := range h.allowedOrigins { if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme { continue } if origin.Host == allowedOrigin.Host { return true } } return false } // etagHasher returns a the hasher we used on the config to both // produce and verify ETags. func etagHasher() hash.Hash32 { return fnv.New32a() } // makeEtag returns an Etag header value (including quotes) for // the given config path and hash of contents at that path. func makeEtag(path string, hash hash.Hash) string { return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil)) } 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) err := readConfig(r.URL.Path, configWriter) if err != nil { return APIError{HTTPStatus: http.StatusBadRequest, Err: err} } // 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)) return nil case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete: // DELETE does not use a body, but the others do var body []byte if r.Method != http.MethodDelete { if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") { return APIError{ HTTPStatus: http.StatusBadRequest, Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct), } } buf := bufPool.Get().(*bytes.Buffer) buf.Reset() defer bufPool.Put(buf) _, err := io.Copy(buf, r.Body) if err != nil { return APIError{ HTTPStatus: http.StatusBadRequest, Err: fmt.Errorf("reading request body: %v", err), } } body = buf.Bytes() } forceReload := r.Header.Get("Cache-Control") == "must-revalidate" err := changeConfig(r.Method, r.URL.Path, body, r.Header.Get("If-Match"), forceReload) if err != nil && !errors.Is(err, errSameConfig) { return err } default: return APIError{ HTTPStatus: http.StatusMethodNotAllowed, Err: fmt.Errorf("method %s not allowed", r.Method), } } return nil } func handleConfigID(w http.ResponseWriter, r *http.Request) error { idPath := r.URL.Path parts := strings.Split(idPath, "/") if len(parts) < 3 || parts[2] == "" { return APIError{ HTTPStatus: http.StatusBadRequest, Err: fmt.Errorf("request path is missing object ID"), } } if parts[0] != "" || parts[1] != "id" { return APIError{ HTTPStatus: http.StatusBadRequest, Err: fmt.Errorf("malformed object path"), } } id := parts[2] // map the ID to the expanded path currentCtxMu.RLock() expanded, ok := rawCfgIndex[id] defer currentCtxMu.RUnlock() if !ok { return APIError{ HTTPStatus: http.StatusNotFound, Err: fmt.Errorf("unknown object ID '%s'", id), } } // piece the full URL path back together parts = append([]string{expanded}, parts[3:]...) r.URL.Path = path.Join(parts...) return errInternalRedir } func handleStop(w http.ResponseWriter, r *http.Request) error { if r.Method != http.MethodPost { return APIError{ HTTPStatus: http.StatusMethodNotAllowed, Err: fmt.Errorf("method not allowed"), } } exitProcess(context.Background(), Log().Named("admin.api")) return nil } // unsyncedConfigAccess traverses into the current config and performs // the operation at path according to method, using body and out as // needed. This is a low-level, unsynchronized function; most callers // will want to use changeConfig or readConfig instead. This requires a // read or write lock on currentCtxMu, depending on method (GET needs // only a read lock; all others need a write lock). func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error { var err error var val any // if there is a request body, decode it into the // variable that will be set in the config according // to method and path if len(body) > 0 { err = json.Unmarshal(body, &val) if err != nil { return fmt.Errorf("decoding request body: %v", err) } } enc := json.NewEncoder(out) cleanPath := strings.Trim(path, "/") if cleanPath == "" { return fmt.Errorf("no traversable path") } parts := strings.Split(cleanPath, "/") if len(parts) == 0 { return fmt.Errorf("path missing") } // A path that ends with "..." implies: // 1) the part before it is an array // 2) the payload is an array // and means that the user wants to expand the elements // in the payload array and append each one into the // destination array, like so: // array = append(array, elems...) // This special case is handled below. ellipses := parts[len(parts)-1] == "..." if ellipses { parts = parts[:len(parts)-1] } var ptr any = rawCfg traverseLoop: for i, part := range parts { switch v := ptr.(type) { case map[string]any: // if the next part enters a slice, and the slice is our destination, // handle it specially (because appending to the slice copies the slice // header, which does not replace the original one like we want) if arr, ok := v[part].([]any); ok && i == len(parts)-2 { var idx int if method != http.MethodPost { idxStr := parts[len(parts)-1] idx, err = strconv.Atoi(idxStr) if err != nil { return fmt.Errorf("[%s] invalid array index '%s': %v", path, idxStr, err) } if idx < 0 || idx >= len(arr) { return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr) } } switch method { case http.MethodGet: err = enc.Encode(arr[idx]) if err != nil { return fmt.Errorf("encoding config: %v", err) } case http.MethodPost: if ellipses { valArray, ok := val.([]any) if !ok { return fmt.Errorf("final element is not an array") } v[part] = append(arr, valArray...) } else { v[part] = append(arr, val) } case http.MethodPut: // avoid creation of new slice and a second copy (see // https://github.com/golang/go/wiki/SliceTricks#insert) arr = append(arr, nil) copy(arr[idx+1:], arr[idx:]) arr[idx] = val v[part] = arr case http.MethodPatch: arr[idx] = val case http.MethodDelete: v[part] = append(arr[:idx], arr[idx+1:]...) default: return fmt.Errorf("unrecognized method %s", method) } break traverseLoop } if i == len(parts)-1 { switch method { case http.MethodGet: err = enc.Encode(v[part]) if err != nil { return fmt.Errorf("encoding config: %v", err) } case http.MethodPost: // if the part is an existing list, POST appends to // it, otherwise it just sets or creates the value if arr, ok := v[part].([]any); ok { if ellipses { valArray, ok := val.([]any) if !ok { return fmt.Errorf("final element is not an array") } v[part] = append(arr, valArray...) } else { v[part] = append(arr, val) } } else { v[part] = val } case http.MethodPut: if _, ok := v[part]; ok { return fmt.Errorf("[%s] key already exists: %s", path, part) } v[part] = val case http.MethodPatch: if _, ok := v[part]; !ok { return fmt.Errorf("[%s] key does not exist: %s", path, part) } v[part] = val case http.MethodDelete: delete(v, part) default: return fmt.Errorf("unrecognized method %s", method) } } else { // if we are "PUTting" a new resource, the key(s) in its path // might not exist yet; that's OK but we need to make them as // we go, while we still have a pointer from the level above if v[part] == nil && method == http.MethodPut { v[part] = make(map[string]any) } ptr = v[part] } case []any: partInt, err := strconv.Atoi(part) if err != nil { return fmt.Errorf("[/%s] invalid array index '%s': %v", strings.Join(parts[:i+1], "/"), part, err) } if partInt < 0 || partInt >= len(v) { return fmt.Errorf("[/%s] array index out of bounds: %s", strings.Join(parts[:i+1], "/"), part) } ptr = v[partInt] default: return fmt.Errorf("invalid traversal path at: %s", strings.Join(parts[:i+1], "/")) } } return nil } // RemoveMetaFields removes meta fields like "@id" from a JSON message // by using a simple regular expression. (An alternate way to do this // would be to delete them from the raw, map[string]any // representation as they are indexed, then iterate the index we made // and add them back after encoding as JSON, but this is simpler.) func RemoveMetaFields(rawJSON []byte) []byte { return idRegexp.ReplaceAllFunc(rawJSON, func(in []byte) []byte { // matches with a comma on both sides (when "@id" property is // not the first or last in the object) need to keep exactly // one comma for correct JSON syntax comma := []byte{','} if bytes.HasPrefix(in, comma) && bytes.HasSuffix(in, comma) { return comma } return []byte{} }) } // AdminHandler is like http.Handler except ServeHTTP may return an error. // // If any handler encounters an error, it should be returned for proper // handling. type AdminHandler interface { ServeHTTP(http.ResponseWriter, *http.Request) error } // AdminHandlerFunc is a convenience type like http.HandlerFunc. type AdminHandlerFunc func(http.ResponseWriter, *http.Request) error // ServeHTTP implements the Handler interface. func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error { return f(w, r) } // APIError is a structured error that every API // handler should return for consistency in logging // and client responses. If Message is unset, then // Err.Error() will be serialized in its place. type APIError struct { HTTPStatus int `json:"-"` Err error `json:"-"` Message string `json:"error"` } func (e APIError) Error() string { if e.Err != nil { return e.Err.Error() } return e.Message } // parseAdminListenAddr extracts a singular listen address from either addr // or defaultAddr, returning the network and the address of the listener. func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) { input, err := NewReplacer().ReplaceOrErr(addr, true, true) if err != nil { return NetworkAddress{}, fmt.Errorf("replacing listen address: %v", err) } if input == "" { input = defaultAddr } listenAddr, err := ParseNetworkAddress(input) if err != nil { return NetworkAddress{}, fmt.Errorf("parsing listener address: %v", err) } if listenAddr.PortRangeSize() != 1 { return NetworkAddress{}, fmt.Errorf("must be exactly one listener address; cannot listen on: %s", listenAddr) } return listenAddr, nil } // decodeBase64DERCert base64-decodes, then DER-decodes, certStr. func decodeBase64DERCert(certStr string) (*x509.Certificate, error) { derBytes, err := base64.StdEncoding.DecodeString(certStr) if err != nil { return nil, err } return x509.ParseCertificate(derBytes) } type loggableURLArray []*url.URL func (ua loggableURLArray) MarshalLogArray(enc zapcore.ArrayEncoder) error { if ua == nil { return nil } for _, u := range ua { enc.AppendString(u.String()) } return nil } var ( // DefaultAdminListen is the address for the local admin // listener, if none is specified at startup. DefaultAdminListen = "localhost:2019" // DefaultRemoteAdminListen is the address for the remote // (TLS-authenticated) admin listener, if enabled and not // specified otherwise. DefaultRemoteAdminListen = ":2021" ) // PIDFile writes a pidfile to the file at filename. It // will get deleted before the process gracefully exits. func PIDFile(filename string) error { pid := []byte(strconv.Itoa(os.Getpid()) + "\n") err := os.WriteFile(filename, pid, 0600) if err != nil { return err } pidfile = filename return nil } // idRegexp is used to match ID fields and their associated values // in the config. It also matches adjacent commas so that syntax // can be preserved no matter where in the object the field appears. // It supports string and most numeric values. var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`) // pidfile is the name of the pidfile, if any. var pidfile string // errInternalRedir indicates an internal redirect // and is useful when admin API handlers rewrite // the request; in that case, authentication and // authorization needs to happen again for the // rewritten request. var errInternalRedir = fmt.Errorf("internal redirect; re-authorization required") const ( rawConfigKey = "config" idKey = "@id" ) var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } // keep a reference to admin endpoint singletons while they're active var ( serverMu sync.Mutex localAdminServer, remoteAdminServer *http.Server identityCertCache *certmagic.Cache ) caddy-2.6.2/admin_test.go 0000664 0000000 0000000 00000013002 14350072374 0015264 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package caddy import ( "encoding/json" "fmt" "net/http" "reflect" "sync" "testing" ) var testCfg = []byte(`{ "apps": { "http": { "servers": { "myserver": { "listen": ["tcp/localhost:8080-8084"], "read_timeout": "30s" }, "yourserver": { "listen": ["127.0.0.1:5000"], "read_header_timeout": "15s" } } } } } `) func TestUnsyncedConfigAccess(t *testing.T) { // each test is performed in sequence, so // each change builds on the previous ones; // the config is not reset between tests for i, tc := range []struct { method string path string // rawConfigKey will be prepended payload string expect string // JSON representation of what the whole config is expected to be after the request shouldErr bool }{ { method: "POST", path: "", payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value expect: `{"foo": "bar", "list": ["a", "b", "c"]}`, }, { method: "POST", path: "/foo", payload: `"jet"`, expect: `{"foo": "jet", "list": ["a", "b", "c"]}`, }, { method: "POST", path: "/bar", payload: `{"aa": "bb", "qq": "zz"}`, expect: `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`, }, { method: "DELETE", path: "/bar/qq", expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`, }, { method: "POST", path: "/list", payload: `"e"`, expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`, }, { method: "PUT", path: "/list/3", payload: `"d"`, expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`, }, { method: "DELETE", path: "/list/3", expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`, }, { method: "PATCH", path: "/list/3", payload: `"d"`, expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`, }, { method: "POST", path: "/list/...", payload: `["e", "f", "g"]`, expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`, }, } { err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil) if tc.shouldErr && err == nil { t.Fatalf("Test %d: Expected error return value, but got: %v", i, err) } if !tc.shouldErr && err != nil { t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err) } // decode the expected config so we can do a convenient DeepEqual var expectedDecoded any err = json.Unmarshal([]byte(tc.expect), &expectedDecoded) if err != nil { t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err) } // make sure the resulting config is as we expect it if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) { t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v", i, expectedDecoded, rawCfg[rawConfigKey]) } } } // TestLoadConcurrent exercises Load under concurrent conditions // and is most useful under test with `-race` enabled. func TestLoadConcurrent(t *testing.T) { var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { _ = Load(testCfg, true) wg.Done() }() } wg.Wait() } type fooModule struct { IntField int StrField string } func (fooModule) CaddyModule() ModuleInfo { return ModuleInfo{ ID: "foo", New: func() Module { return new(fooModule) }, } } func (fooModule) Start() error { return nil } func (fooModule) Stop() error { return nil } func TestETags(t *testing.T) { RegisterModule(fooModule{}) if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil { t.Fatalf("loading: %s", err) } const key = "/" + rawConfigKey + "/apps/foo" // try update the config with the wrong etag err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false) if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed { t.Fatalf("expected precondition failed; got %v", err) } // get the etag hash := etagHasher() if err := readConfig(key, hash); err != nil { t.Fatalf("reading: %s", err) } // do the same update with the correct key err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false) if err != nil { t.Fatalf("expected update to work; got %v", err) } // now try another update. The hash should no longer match and we should get precondition failed err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false) if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed { t.Fatalf("expected precondition failed; got %v", err) } } func BenchmarkLoad(b *testing.B) { for i := 0; i < b.N; i++ { Load(testCfg, true) } } caddy-2.6.2/caddy.go 0000664 0000000 0000000 00000073513 14350072374 0014236 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package caddy import ( "bytes" "context" "encoding/hex" "encoding/json" "errors" "fmt" "io" "log" "net/http" "os" "path" "path/filepath" "runtime/debug" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/caddyserver/caddy/v2/notify" "github.com/caddyserver/certmagic" "github.com/google/uuid" "go.uber.org/zap" ) // Config is the top (or beginning) of the Caddy configuration structure. // Caddy config is expressed natively as a JSON document. If you prefer // not to work with JSON directly, there are [many config adapters](/docs/config-adapters) // available that can convert various inputs into Caddy JSON. // // Many parts of this config are extensible through the use of Caddy modules. // Fields which have a json.RawMessage type and which appear as dots (•••) in // the online docs can be fulfilled by modules in a certain module // namespace. The docs show which modules can be used in a given place. // // Whenever a module is used, its name must be given either inline as part of // the module, or as the key to the module's value. The docs will make it clear // which to use. // // Generally, all config settings are optional, as it is Caddy convention to // have good, documented default values. If a parameter is required, the docs // should say so. // // Go programs which are directly building a Config struct value should take // care to populate the JSON-encodable fields of the struct (i.e. the fields // with `json` struct tags) if employing the module lifecycle (e.g. Provision // method calls). type Config struct { Admin *AdminConfig `json:"admin,omitempty"` Logging *Logging `json:"logging,omitempty"` // StorageRaw is a storage module that defines how/where Caddy // stores assets (such as TLS certificates). The default storage // module is `caddy.storage.file_system` (the local file system), // and the default path // [depends on the OS and environment](/docs/conventions#data-directory). StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` // AppsRaw are the apps that Caddy will load and run. The // app module name is the key, and the app's config is the // associated value. AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="` apps map[string]App storage certmagic.Storage cancelFunc context.CancelFunc } // App is a thing that Caddy runs. type App interface { Start() error Stop() error } // Run runs the given config, replacing any existing config. func Run(cfg *Config) error { cfgJSON, err := json.Marshal(cfg) if err != nil { return err } return Load(cfgJSON, true) } // Load loads the given config JSON and runs it only // if it is different from the current config or // forceReload is true. func Load(cfgJSON []byte, forceReload bool) error { if err := notify.Reloading(); err != nil { Log().Error("unable to notify service manager of reloading state", zap.Error(err)) } // after reload, notify system of success or, if // failure, update with status (error message) var err error defer func() { if err != nil { if notifyErr := notify.Error(err, 0); notifyErr != nil { Log().Error("unable to notify to service manager of reload error", zap.Error(notifyErr), zap.String("reload_err", err.Error())) } return } if err := notify.Ready(); err != nil { Log().Error("unable to notify to service manager of ready state", zap.Error(err)) } }() err = changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload) if errors.Is(err, errSameConfig) { err = nil // not really an error } return err } // changeConfig changes the current config (rawCfg) according to the // method, traversed via the given path, and uses the given input as // the new value (if applicable; i.e. "DELETE" doesn't have an input). // If the resulting config is the same as the previous, no reload will // occur unless forceReload is true. If the config is unchanged and not // forcefully reloaded, then errConfigUnchanged This function is safe for // concurrent use. // The ifMatchHeader can optionally be given a string of the format: // // "]
//
// can be "permanent" for 301, "temporary" for 302 (default),
// a placeholder, or any number in the 3xx range or 401. The special
// code "html" can be used to redirect only browser clients (will
// respond with HTTP 200 and no Location header; redirect is performed
// with JS and a meta tag).
func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
if !h.Next() {
return nil, h.ArgErr()
}
if !h.NextArg() {
return nil, h.ArgErr()
}
to := h.Val()
var code string
if h.NextArg() {
code = h.Val()
}
var body string
var hdr http.Header
switch code {
case "permanent":
code = "301"
case "temporary", "":
code = "302"
case "html":
// Script tag comes first since that will better imitate a redirect in the browser's
// history, but the meta tag is a fallback for most non-JS clients.
const metaRedir = `
Redirecting...
Redirecting to %s...
`
safeTo := html.EscapeString(to)
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
code = "200" // don't redirect non-browser clients
default:
// Allow placeholders for the code
if strings.HasPrefix(code, "{") {
break
}
// Try to validate as an integer otherwise
codeInt, err := strconv.Atoi(code)
if err != nil {
return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code)
}
// Sometimes, a 401 with Location header is desirable because
// requests made with XHR will "eat" the 3xx redirect; so if
// the intent was to redirect to an auth page, a 3xx won't
// work. Responding with 401 allows JS code to read the
// Location header and do a window.location redirect manually.
// see https://stackoverflow.com/a/2573589/846934
// see https://github.com/oauth2-proxy/oauth2-proxy/issues/1522
if codeInt < 300 || (codeInt > 399 && codeInt != 401) {
return nil, h.Errf("Redir code not in the 3xx range or 401: '%v'", codeInt)
}
}
// don't redirect non-browser clients
if code != "200" {
hdr = http.Header{"Location": []string{to}}
}
return caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(code),
Headers: hdr,
Body: body,
}, nil
}
// parseRespond parses the respond directive.
func parseRespond(h Helper) (caddyhttp.MiddlewareHandler, error) {
sr := new(caddyhttp.StaticResponse)
err := sr.UnmarshalCaddyfile(h.Dispenser)
if err != nil {
return nil, err
}
return sr, nil
}
// parseAbort parses the abort directive.
func parseAbort(h Helper) (caddyhttp.MiddlewareHandler, error) {
h.Next() // consume directive
for h.Next() || h.NextBlock(0) {
return nil, h.ArgErr()
}
return &caddyhttp.StaticResponse{Abort: true}, nil
}
// parseError parses the error directive.
func parseError(h Helper) (caddyhttp.MiddlewareHandler, error) {
se := new(caddyhttp.StaticError)
err := se.UnmarshalCaddyfile(h.Dispenser)
if err != nil {
return nil, err
}
return se, nil
}
// parseRoute parses the route directive.
func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
sr := new(caddyhttp.Subroute)
allResults, err := parseSegmentAsConfig(h)
if err != nil {
return nil, err
}
for _, result := range allResults {
switch handler := result.Value.(type) {
case caddyhttp.Route:
sr.Routes = append(sr.Routes, handler)
case caddyhttp.Subroute:
// directives which return a literal subroute instead of a route
// means they intend to keep those handlers together without
// them being reordered; we're doing that anyway since we're in
// the route directive, so just append its handlers
sr.Routes = append(sr.Routes, handler.Routes...)
default:
return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", result.directive, result.Value)
}
}
return sr, nil
}
func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) {
return ParseSegmentAsSubroute(h)
}
func parseHandleErrors(h Helper) ([]ConfigValue, error) {
subroute, err := ParseSegmentAsSubroute(h)
if err != nil {
return nil, err
}
return []ConfigValue{
{
Class: "error_route",
Value: subroute,
},
}, nil
}
// parseLog parses the log directive. Syntax:
//
// log {
// output ...
// format ...
// level
// }
func parseLog(h Helper) ([]ConfigValue, error) {
return parseLogHelper(h, nil)
}
// parseLogHelper is used both for the parseLog directive within Server Blocks,
// as well as the global "log" option for configuring loggers at the global
// level. The parseAsGlobalOption parameter is used to distinguish any differing logic
// between the two.
func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue, error) {
// When the globalLogNames parameter is passed in, we make
// modifications to the parsing behavior.
parseAsGlobalOption := globalLogNames != nil
var configValues []ConfigValue
for h.Next() {
// Logic below expects that a name is always present when a
// global option is being parsed.
var globalLogName string
if parseAsGlobalOption {
if h.NextArg() {
globalLogName = h.Val()
// Only a single argument is supported.
if h.NextArg() {
return nil, h.ArgErr()
}
} else {
// If there is no log name specified, we
// reference the default logger. See the
// setupNewDefault function in the logging
// package for where this is configured.
globalLogName = caddy.DefaultLoggerName
}
// Verify this name is unused.
_, used := globalLogNames[globalLogName]
if used {
return nil, h.Err("duplicate global log option for: " + globalLogName)
}
globalLogNames[globalLogName] = struct{}{}
} else {
// No arguments are supported for the server block log directive
if h.NextArg() {
return nil, h.ArgErr()
}
}
cl := new(caddy.CustomLog)
for h.NextBlock(0) {
switch h.Val() {
case "output":
if !h.NextArg() {
return nil, h.ArgErr()
}
moduleName := h.Val()
// can't use the usual caddyfile.Unmarshaler flow with the
// standard writers because they are in the caddy package
// (because they are the default) and implementing that
// interface there would unfortunately create circular import
var wo caddy.WriterOpener
switch moduleName {
case "stdout":
wo = caddy.StdoutWriter{}
case "stderr":
wo = caddy.StderrWriter{}
case "discard":
wo = caddy.DiscardWriter{}
default:
modID := "caddy.logging.writers." + moduleName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
var ok bool
wo, ok = unm.(caddy.WriterOpener)
if !ok {
return nil, h.Errf("module %s (%T) is not a WriterOpener", modID, unm)
}
}
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
case "format":
if !h.NextArg() {
return nil, h.ArgErr()
}
moduleName := h.Val()
moduleID := "caddy.logging.encoders." + moduleName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, moduleID)
if err != nil {
return nil, err
}
enc, ok := unm.(zapcore.Encoder)
if !ok {
return nil, h.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm)
}
cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings)
case "level":
if !h.NextArg() {
return nil, h.ArgErr()
}
cl.Level = h.Val()
if h.NextArg() {
return nil, h.ArgErr()
}
case "include":
// This configuration is only allowed in the global options
if !parseAsGlobalOption {
return nil, h.ArgErr()
}
for h.NextArg() {
cl.Include = append(cl.Include, h.Val())
}
case "exclude":
// This configuration is only allowed in the global options
if !parseAsGlobalOption {
return nil, h.ArgErr()
}
for h.NextArg() {
cl.Exclude = append(cl.Exclude, h.Val())
}
default:
return nil, h.Errf("unrecognized subdirective: %s", h.Val())
}
}
var val namedCustomLog
// Skip handling of empty logging configs
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
if parseAsGlobalOption {
// Use indicated name for global log options
val.name = globalLogName
val.log = cl
} else {
// Construct a log name for server log streams
logCounter, ok := h.State["logCounter"].(int)
if !ok {
logCounter = 0
}
val.name = fmt.Sprintf("log%d", logCounter)
cl.Include = []string{"http.log.access." + val.name}
val.log = cl
logCounter++
h.State["logCounter"] = logCounter
}
}
configValues = append(configValues, ConfigValue{
Class: "custom_log",
Value: val,
})
}
return configValues, nil
}
// parseSkipLog parses the skip_log directive. Syntax:
//
// skip_log []
func parseSkipLog(h Helper) (caddyhttp.MiddlewareHandler, error) {
for h.Next() {
if h.NextArg() {
return nil, h.ArgErr()
}
}
return caddyhttp.VarsMiddleware{"skip_log": true}, nil
}
caddy-2.6.2/caddyconfig/httpcaddyfile/builtins_test.go 0000664 0000000 0000000 00000007777 14350072374 0023151 0 ustar 00root root 0000000 0000000 package httpcaddyfile
import (
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
_ "github.com/caddyserver/caddy/v2/modules/logging"
)
func TestLogDirectiveSyntax(t *testing.T) {
for i, tc := range []struct {
input string
output string
expectError bool
}{
{
input: `:8080 {
log
}
`,
output: `{"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{}}}}}}`,
expectError: false,
},
{
input: `:8080 {
log {
output file foo.log
}
}
`,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false,
},
{
input: `:8080 {
log {
format filter {
wrap console
fields {
request>remote_ip ip_mask {
ipv4 24
ipv6 32
}
}
}
}
}
`,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"request\u003eremote_ip":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false,
},
{
input: `:8080 {
log invalid {
output file foo.log
}
}
`,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
out, _, err := adapter.Adapt([]byte(tc.input), nil)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue
}
if string(out) != tc.output {
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
}
}
}
func TestRedirDirectiveSyntax(t *testing.T) {
for i, tc := range []struct {
input string
expectError bool
}{
{
input: `:8080 {
redir :8081
}`,
expectError: false,
},
{
input: `:8080 {
redir * :8081
}`,
expectError: false,
},
{
input: `:8080 {
redir /api/* :8081 300
}`,
expectError: false,
},
{
input: `:8080 {
redir :8081 300
}`,
expectError: false,
},
{
input: `:8080 {
redir /api/* :8081 399
}`,
expectError: false,
},
{
input: `:8080 {
redir :8081 399
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html temporary
}`,
expectError: false,
},
{
input: `:8080 {
redir https://example.com{uri} permanent
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html permanent
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html html
}`,
expectError: false,
},
{
// this is now allowed so a Location header
// can be written and consumed by JS
// in the case of XHR requests
input: `:8080 {
redir * :8081 401
}`,
expectError: false,
},
{
input: `:8080 {
redir * :8081 402
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 {http.reverse_proxy.status_code}
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html htlm
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 200
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 temp
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 perm
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 php
}`,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue
}
}
}
caddy-2.6.2/caddyconfig/httpcaddyfile/directives.go 0000664 0000000 0000000 00000043027 14350072374 0022406 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"encoding/json"
"net"
"sort"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
// directiveOrder specifies the order
// to apply directives in HTTP routes.
//
// The root directive goes first in case rewrites or
// redirects depend on existence of files, i.e. the
// file matcher, which must know the root first.
//
// The header directive goes second so that headers
// can be manipulated before doing redirects.
var directiveOrder = []string{
"tracing",
"map",
"vars",
"root",
"skip_log",
"header",
"copy_response_headers", // only in reverse_proxy's handle_response
"request_body",
"redir",
// incoming request manipulation
"method",
"rewrite",
"uri",
"try_files",
// middleware handlers; some wrap responses
"basicauth",
"forward_auth",
"request_header",
"encode",
"push",
"templates",
// special routing & dispatching directives
"handle",
"handle_path",
"route",
// handlers that typically respond to requests
"abort",
"error",
"copy_response", // only in reverse_proxy's handle_response
"respond",
"metrics",
"reverse_proxy",
"php_fastcgi",
"file_server",
"acme_server",
}
// directiveIsOrdered returns true if dir is
// a known, ordered (sorted) directive.
func directiveIsOrdered(dir string) bool {
for _, d := range directiveOrder {
if d == dir {
return true
}
}
return false
}
// RegisterDirective registers a unique directive dir with an
// associated unmarshaling (setup) function. When directive dir
// is encountered in a Caddyfile, setupFunc will be called to
// unmarshal its tokens.
func RegisterDirective(dir string, setupFunc UnmarshalFunc) {
if _, ok := registeredDirectives[dir]; ok {
panic("directive " + dir + " already registered")
}
registeredDirectives[dir] = setupFunc
}
// RegisterHandlerDirective is like RegisterDirective, but for
// directives which specifically output only an HTTP handler.
// Directives registered with this function will always have
// an optional matcher token as the first argument.
func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
RegisterDirective(dir, func(h Helper) ([]ConfigValue, error) {
if !h.Next() {
return nil, h.ArgErr()
}
matcherSet, err := h.ExtractMatcherSet()
if err != nil {
return nil, err
}
val, err := setupFunc(h)
if err != nil {
return nil, err
}
return h.NewRoute(matcherSet, val), nil
})
}
// RegisterGlobalOption registers a unique global option opt with
// an associated unmarshaling (setup) function. When the global
// option opt is encountered in a Caddyfile, setupFunc will be
// called to unmarshal its tokens.
func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
if _, ok := registeredGlobalOptions[opt]; ok {
panic("global option " + opt + " already registered")
}
registeredGlobalOptions[opt] = setupFunc
}
// Helper is a type which helps setup a value from
// Caddyfile tokens.
type Helper struct {
*caddyfile.Dispenser
// State stores intermediate variables during caddyfile adaptation.
State map[string]any
options map[string]any
warnings *[]caddyconfig.Warning
matcherDefs map[string]caddy.ModuleMap
parentBlock caddyfile.ServerBlock
groupCounter counter
}
// Option gets the option keyed by name.
func (h Helper) Option(name string) any {
return h.options[name]
}
// Caddyfiles returns the list of config files from
// which tokens in the current server block were loaded.
func (h Helper) Caddyfiles() []string {
// first obtain set of names of files involved
// in this server block, without duplicates
files := make(map[string]struct{})
for _, segment := range h.parentBlock.Segments {
for _, token := range segment {
files[token.File] = struct{}{}
}
}
// then convert the set into a slice
filesSlice := make([]string, 0, len(files))
for file := range files {
filesSlice = append(filesSlice, file)
}
return filesSlice
}
// JSON converts val into JSON. Any errors are added to warnings.
func (h Helper) JSON(val any) json.RawMessage {
return caddyconfig.JSON(val, h.warnings)
}
// MatcherToken assumes the next argument token is (possibly) a matcher,
// and if so, returns the matcher set along with a true value. If the next
// token is not a matcher, nil and false is returned. Note that a true
// value may be returned with a nil matcher set if it is a catch-all.
func (h Helper) MatcherToken() (caddy.ModuleMap, bool, error) {
if !h.NextArg() {
return nil, false, nil
}
return matcherSetFromMatcherToken(h.Dispenser.Token(), h.matcherDefs, h.warnings)
}
// ExtractMatcherSet is like MatcherToken, except this is a higher-level
// method that returns the matcher set described by the matcher token,
// or nil if there is none, and deletes the matcher token from the
// dispenser and resets it as if this look-ahead never happened. Useful
// when wrapping a route (one or more handlers) in a user-defined matcher.
func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
matcherSet, hasMatcher, err := h.MatcherToken()
if err != nil {
return nil, err
}
if hasMatcher {
// strip matcher token; we don't need to
// use the return value here because a
// new dispenser should have been made
// solely for this directive's tokens,
// with no other uses of same slice
h.Dispenser.Delete()
}
h.Dispenser.Reset() // pretend this lookahead never happened
return matcherSet, nil
}
// NewRoute returns config values relevant to creating a new HTTP route.
func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
handler caddyhttp.MiddlewareHandler) []ConfigValue {
mod, err := caddy.GetModule(caddy.GetModuleID(handler))
if err != nil {
*h.warnings = append(*h.warnings, caddyconfig.Warning{
File: h.File(),
Line: h.Line(),
Message: err.Error(),
})
return nil
}
var matcherSetsRaw []caddy.ModuleMap
if matcherSet != nil {
matcherSetsRaw = append(matcherSetsRaw, matcherSet)
}
return []ConfigValue{
{
Class: "route",
Value: caddyhttp.Route{
MatcherSetsRaw: matcherSetsRaw,
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", mod.ID.Name(), h.warnings)},
},
},
}
}
// GroupRoutes adds the routes (caddyhttp.Route type) in vals to the
// same group, if there is more than one route in vals.
func (h Helper) GroupRoutes(vals []ConfigValue) {
// ensure there's at least two routes; group of one is pointless
var count int
for _, v := range vals {
if _, ok := v.Value.(caddyhttp.Route); ok {
count++
if count > 1 {
break
}
}
}
if count < 2 {
return
}
// now that we know the group will have some effect, do it
groupName := h.groupCounter.nextGroup()
for i := range vals {
if route, ok := vals[i].Value.(caddyhttp.Route); ok {
route.Group = groupName
vals[i].Value = route
}
}
}
// NewBindAddresses returns config values relevant to adding
// listener bind addresses to the config.
func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
return []ConfigValue{{Class: "bind", Value: addrs}}
}
// WithDispenser returns a new instance based on d. All others Helper
// fields are copied, so typically maps are shared with this new instance.
func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
h.Dispenser = d
return h
}
// ParseSegmentAsSubroute parses the segment such that its subdirectives
// are themselves treated as directives, from which a subroute is built
// and returned.
func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
allResults, err := parseSegmentAsConfig(h)
if err != nil {
return nil, err
}
return buildSubroute(allResults, h.groupCounter)
}
// parseSegmentAsConfig parses the segment such that its subdirectives
// are themselves treated as directives, including named matcher definitions,
// and the raw Config structs are returned.
func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
var allResults []ConfigValue
for h.Next() {
// don't allow non-matcher args on the first line
if h.NextArg() {
return nil, h.ArgErr()
}
// slice the linear list of tokens into top-level segments
var segments []caddyfile.Segment
for nesting := h.Nesting(); h.NextBlock(nesting); {
segments = append(segments, h.NextSegment())
}
// copy existing matcher definitions so we can augment
// new ones that are defined only in this scope
matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
for key, val := range h.matcherDefs {
matcherDefs[key] = val
}
// find and extract any embedded matcher definitions in this scope
for i := 0; i < len(segments); i++ {
seg := segments[i]
if strings.HasPrefix(seg.Directive(), matcherPrefix) {
// parse, then add the matcher to matcherDefs
err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
if err != nil {
return nil, err
}
// remove the matcher segment (consumed), then step back the loop
segments = append(segments[:i], segments[i+1:]...)
i--
}
}
// with matchers ready to go, evaluate each directive's segment
for _, seg := range segments {
dir := seg.Directive()
dirFunc, ok := registeredDirectives[dir]
if !ok {
return nil, h.Errf("unrecognized directive: %s - are you sure your Caddyfile structure (nesting and braces) is correct?", dir)
}
subHelper := h
subHelper.Dispenser = caddyfile.NewDispenser(seg)
subHelper.matcherDefs = matcherDefs
results, err := dirFunc(subHelper)
if err != nil {
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
}
dir = normalizeDirectiveName(dir)
for _, result := range results {
result.directive = dir
allResults = append(allResults, result)
}
}
}
return allResults, nil
}
// ConfigValue represents a value to be added to the final
// configuration, or a value to be consulted when building
// the final configuration.
type ConfigValue struct {
// The kind of value this is. As the config is
// being built, the adapter will look in the
// "pile" for values belonging to a certain
// class when it is setting up a certain part
// of the config. The associated value will be
// type-asserted and placed accordingly.
Class string
// The value to be used when building the config.
// Generally its type is associated with the
// name of the Class.
Value any
directive string
}
func sortRoutes(routes []ConfigValue) {
dirPositions := make(map[string]int)
for i, dir := range directiveOrder {
dirPositions[dir] = i
}
sort.SliceStable(routes, func(i, j int) bool {
// if the directives are different, just use the established directive order
iDir, jDir := routes[i].directive, routes[j].directive
if iDir != jDir {
return dirPositions[iDir] < dirPositions[jDir]
}
// directives are the same; sub-sort by path matcher length if there's
// only one matcher set and one path (this is a very common case and
// usually -- but not always -- helpful/expected, oh well; user can
// always take manual control of order using handler or route blocks)
iRoute, ok := routes[i].Value.(caddyhttp.Route)
if !ok {
return false
}
jRoute, ok := routes[j].Value.(caddyhttp.Route)
if !ok {
return false
}
// decode the path matchers if there is just one matcher set
var iPM, jPM caddyhttp.MatchPath
if len(iRoute.MatcherSetsRaw) == 1 {
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM)
}
if len(jRoute.MatcherSetsRaw) == 1 {
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM)
}
// if there is only one path in the path matcher, sort by longer path
// (more specific) first; missing path matchers or multi-matchers are
// treated as zero-length paths
var iPathLen, jPathLen int
if len(iPM) == 1 {
iPathLen = len(iPM[0])
}
if len(jPM) == 1 {
jPathLen = len(jPM[0])
}
// some directives involve setting values which can overwrite
// each other, so it makes most sense to reverse the order so
// that the lease specific matcher is first; everything else
// has most-specific matcher first
if iDir == "vars" {
// we can only confidently compare path lengths if both
// directives have a single path to match (issue #5037)
if iPathLen > 0 && jPathLen > 0 {
// sort least-specific (shortest) path first
return iPathLen < jPathLen
}
// if both directives don't have a single path to compare,
// sort whichever one has no matcher first; if both have
// no matcher, sort equally (stable sort preserves order)
return len(iRoute.MatcherSetsRaw) == 0 && len(jRoute.MatcherSetsRaw) > 0
} else {
// we can only confidently compare path lengths if both
// directives have a single path to match (issue #5037)
if iPathLen > 0 && jPathLen > 0 {
// sort most-specific (longest) path first
return iPathLen > jPathLen
}
// if both directives don't have a single path to compare,
// sort whichever one has a matcher first; if both have
// a matcher, sort equally (stable sort preserves order)
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
}
})
}
// serverBlock pairs a Caddyfile server block with
// a "pile" of config values, keyed by class name,
// as well as its parsed keys for convenience.
type serverBlock struct {
block caddyfile.ServerBlock
pile map[string][]ConfigValue // config values obtained from directives
keys []Address
}
// hostsFromKeys returns a list of all the non-empty hostnames found in
// the keys of the server block sb. If logger mode is false, a key with
// an empty hostname portion will return an empty slice, since that
// server block is interpreted to effectively match all hosts. An empty
// string is never added to the slice.
//
// If loggerMode is true, then the non-standard ports of keys will be
// joined to the hostnames. This is to effectively match the Host
// header of requests that come in for that key.
//
// The resulting slice is not sorted but will never have duplicates.
func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
// ensure each entry in our list is unique
hostMap := make(map[string]struct{})
for _, addr := range sb.keys {
if addr.Host == "" {
if !loggerMode {
// server block contains a key like ":443", i.e. the host portion
// is empty / catch-all, which means to match all hosts
return []string{}
}
// never append an empty string
continue
}
if loggerMode &&
addr.Port != "" &&
addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort) &&
addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort) {
hostMap[net.JoinHostPort(addr.Host, addr.Port)] = struct{}{}
} else {
hostMap[addr.Host] = struct{}{}
}
}
// convert map to slice
sblockHosts := make([]string, 0, len(hostMap))
for host := range hostMap {
sblockHosts = append(sblockHosts, host)
}
return sblockHosts
}
func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
// ensure each entry in our list is unique
hostMap := make(map[string]struct{})
for _, addr := range sb.keys {
if addr.Host == "" {
continue
}
if addr.Scheme != "http" && addr.Port != httpPort {
hostMap[addr.Host] = struct{}{}
}
}
// convert map to slice
sblockHosts := make([]string, 0, len(hostMap))
for host := range hostMap {
sblockHosts = append(sblockHosts, host)
}
return sblockHosts
}
// hasHostCatchAllKey returns true if sb has a key that
// omits a host portion, i.e. it "catches all" hosts.
func (sb serverBlock) hasHostCatchAllKey() bool {
for _, addr := range sb.keys {
if addr.Host == "" {
return true
}
}
return false
}
// isAllHTTP returns true if all sb keys explicitly specify
// the http:// scheme
func (sb serverBlock) isAllHTTP() bool {
for _, addr := range sb.keys {
if addr.Scheme != "http" {
return false
}
}
return true
}
type (
// UnmarshalFunc is a function which can unmarshal Caddyfile
// tokens into zero or more config values using a Helper type.
// These are passed in a call to RegisterDirective.
UnmarshalFunc func(h Helper) ([]ConfigValue, error)
// UnmarshalHandlerFunc is like UnmarshalFunc, except the
// output of the unmarshaling is an HTTP handler. This
// function does not need to deal with HTTP request matching
// which is abstracted away. Since writing HTTP handlers
// with Caddyfile support is very common, this is a more
// convenient way to add a handler to the chain since a lot
// of the details common to HTTP handlers are taken care of
// for you. These are passed to a call to
// RegisterHandlerDirective.
UnmarshalHandlerFunc func(h Helper) (caddyhttp.MiddlewareHandler, error)
// UnmarshalGlobalFunc is a function which can unmarshal Caddyfile
// tokens from a global option. It is passed the tokens to parse and
// existing value from the previous instance of this global option
// (if any). It returns the value to associate with this global option.
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal any) (any, error)
)
var registeredDirectives = make(map[string]UnmarshalFunc)
var registeredGlobalOptions = make(map[string]UnmarshalGlobalFunc)
caddy-2.6.2/caddyconfig/httpcaddyfile/directives_test.go 0000664 0000000 0000000 00000003712 14350072374 0023442 0 ustar 00root root 0000000 0000000 package httpcaddyfile
import (
"reflect"
"sort"
"testing"
)
func TestHostsFromKeys(t *testing.T) {
for i, tc := range []struct {
keys []Address
expectNormalMode []string
expectLoggerMode []string
}{
{
[]Address{
{Original: "foo", Host: "foo"},
},
[]string{"foo"},
[]string{"foo"},
},
{
[]Address{
{Original: "foo", Host: "foo"},
{Original: "bar", Host: "bar"},
},
[]string{"bar", "foo"},
[]string{"bar", "foo"},
},
{
[]Address{
{Original: ":2015", Port: "2015"},
},
[]string{}, []string{},
},
{
[]Address{
{Original: ":443", Port: "443"},
},
[]string{}, []string{},
},
{
[]Address{
{Original: "foo", Host: "foo"},
{Original: ":2015", Port: "2015"},
},
[]string{}, []string{"foo"},
},
{
[]Address{
{Original: "example.com:2015", Host: "example.com", Port: "2015"},
},
[]string{"example.com"},
[]string{"example.com:2015"},
},
{
[]Address{
{Original: "example.com:80", Host: "example.com", Port: "80"},
},
[]string{"example.com"},
[]string{"example.com"},
},
{
[]Address{
{Original: "https://:2015/foo", Scheme: "https", Port: "2015", Path: "/foo"},
},
[]string{},
[]string{},
},
{
[]Address{
{Original: "https://example.com:2015/foo", Scheme: "https", Host: "example.com", Port: "2015", Path: "/foo"},
},
[]string{"example.com"},
[]string{"example.com:2015"},
},
} {
sb := serverBlock{keys: tc.keys}
// test in normal mode
actual := sb.hostsFromKeys(false)
sort.Strings(actual)
if !reflect.DeepEqual(tc.expectNormalMode, actual) {
t.Errorf("Test %d (loggerMode=false): Expected: %v Actual: %v", i, tc.expectNormalMode, actual)
}
// test in logger mode
actual = sb.hostsFromKeys(true)
sort.Strings(actual)
if !reflect.DeepEqual(tc.expectLoggerMode, actual) {
t.Errorf("Test %d (loggerMode=true): Expected: %v Actual: %v", i, tc.expectLoggerMode, actual)
}
}
}
caddy-2.6.2/caddyconfig/httpcaddyfile/httptype.go 0000664 0000000 0000000 00000141465 14350072374 0022133 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"encoding/json"
"fmt"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"go.uber.org/zap"
)
func init() {
caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}})
}
// App represents the configuration for a non-standard
// Caddy app module (e.g. third-party plugin) which was
// parsed from a global options block.
type App struct {
// The JSON key for the app being configured
Name string
// The raw app config as JSON
Value json.RawMessage
}
// ServerType can set up a config from an HTTP Caddyfile.
type ServerType struct {
}
// Setup makes a config from the tokens.
func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
options map[string]any) (*caddy.Config, []caddyconfig.Warning, error) {
var warnings []caddyconfig.Warning
gc := counter{new(int)}
state := make(map[string]any)
// load all the server blocks and associate them with a "pile" of config values
originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks))
for _, sblock := range inputServerBlocks {
for j, k := range sblock.Keys {
if j == 0 && strings.HasPrefix(k, "@") {
return nil, warnings, fmt.Errorf("cannot define a matcher outside of a site block: '%s'", k)
}
}
originalServerBlocks = append(originalServerBlocks, serverBlock{
block: sblock,
pile: make(map[string][]ConfigValue),
})
}
// apply any global options
var err error
originalServerBlocks, err = st.evaluateGlobalOptionsBlock(originalServerBlocks, options)
if err != nil {
return nil, warnings, err
}
// replace shorthand placeholders (which are convenient
// when writing a Caddyfile) with their actual placeholder
// identifiers or variable names
replacer := strings.NewReplacer(placeholderShorthands()...)
// these are placeholders that allow a user-defined final
// parameters, but we still want to provide a shorthand
// for those, so we use a regexp to replace
regexpReplacements := []struct {
search *regexp.Regexp
replace string
}{
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
}
for _, sb := range originalServerBlocks {
for _, segment := range sb.block.Segments {
for i := 0; i < len(segment); i++ {
// simple string replacements
segment[i].Text = replacer.Replace(segment[i].Text)
// complex regexp replacements
for _, r := range regexpReplacements {
segment[i].Text = r.search.ReplaceAllString(segment[i].Text, r.replace)
}
}
}
if len(sb.block.Keys) == 0 {
return nil, warnings, fmt.Errorf("server block without any key is global configuration, and if used, it must be first")
}
// extract matcher definitions
matcherDefs := make(map[string]caddy.ModuleMap)
for _, segment := range sb.block.Segments {
if dir := segment.Directive(); strings.HasPrefix(dir, matcherPrefix) {
d := sb.block.DispenseDirective(dir)
err := parseMatcherDefinitions(d, matcherDefs)
if err != nil {
return nil, warnings, err
}
}
}
// evaluate each directive ("segment") in this block
for _, segment := range sb.block.Segments {
dir := segment.Directive()
if strings.HasPrefix(dir, matcherPrefix) {
// matcher definitions were pre-processed
continue
}
dirFunc, ok := registeredDirectives[dir]
if !ok {
tkn := segment[0]
message := "%s:%d: unrecognized directive: %s"
if !sb.block.HasBraces {
message += "\nDid you mean to define a second site? If so, you must use curly braces around each site to separate their configurations."
}
return nil, warnings, fmt.Errorf(message, tkn.File, tkn.Line, dir)
}
h := Helper{
Dispenser: caddyfile.NewDispenser(segment),
options: options,
warnings: &warnings,
matcherDefs: matcherDefs,
parentBlock: sb.block,
groupCounter: gc,
State: state,
}
results, err := dirFunc(h)
if err != nil {
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
}
dir = normalizeDirectiveName(dir)
for _, result := range results {
result.directive = dir
sb.pile[result.Class] = append(sb.pile[result.Class], result)
}
}
}
// map
sbmap, err := st.mapAddressToServerBlocks(originalServerBlocks, options)
if err != nil {
return nil, warnings, err
}
// reduce
pairings := st.consolidateAddrMappings(sbmap)
// each pairing of listener addresses to list of server
// blocks is basically a server definition
servers, err := st.serversFromPairings(pairings, options, &warnings, gc)
if err != nil {
return nil, warnings, err
}
// now that each server is configured, make the HTTP app
httpApp := caddyhttp.App{
HTTPPort: tryInt(options["http_port"], &warnings),
HTTPSPort: tryInt(options["https_port"], &warnings),
GracePeriod: tryDuration(options["grace_period"], &warnings),
ShutdownDelay: tryDuration(options["shutdown_delay"], &warnings),
Servers: servers,
}
// then make the TLS app
tlsApp, warnings, err := st.buildTLSApp(pairings, options, warnings)
if err != nil {
return nil, warnings, err
}
// then make the PKI app
pkiApp, warnings, err := st.buildPKIApp(pairings, options, warnings)
if err != nil {
return nil, warnings, err
}
// extract any custom logs, and enforce configured levels
var customLogs []namedCustomLog
var hasDefaultLog bool
addCustomLog := func(ncl namedCustomLog) {
if ncl.name == "" {
return
}
if ncl.name == caddy.DefaultLoggerName {
hasDefaultLog = true
}
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
ncl.log.Level = zap.DebugLevel.CapitalString()
}
customLogs = append(customLogs, ncl)
}
// Apply global log options, when set
if options["log"] != nil {
for _, logValue := range options["log"].([]ConfigValue) {
addCustomLog(logValue.Value.(namedCustomLog))
}
}
if !hasDefaultLog {
// if the default log was not customized, ensure we
// configure it with any applicable options
if _, ok := options["debug"]; ok {
customLogs = append(customLogs, namedCustomLog{
name: caddy.DefaultLoggerName,
log: &caddy.CustomLog{Level: zap.DebugLevel.CapitalString()},
})
}
}
// Apply server-specific log options
for _, p := range pairings {
for _, sb := range p.serverBlocks {
for _, clVal := range sb.pile["custom_log"] {
addCustomLog(clVal.Value.(namedCustomLog))
}
}
}
// annnd the top-level config, then we're done!
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
// loop through the configured options, and if any of
// them are an httpcaddyfile App, then we insert them
// into the config as raw Caddy apps
for _, opt := range options {
if app, ok := opt.(App); ok {
cfg.AppsRaw[app.Name] = app.Value
}
}
// insert the standard Caddy apps into the config
if len(httpApp.Servers) > 0 {
cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
}
if !reflect.DeepEqual(tlsApp, &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}) {
cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
}
if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) {
cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings)
}
if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
"module",
storageCvtr.(caddy.Module).CaddyModule().ID.Name(),
&warnings)
}
if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
cfg.Admin = adminConfig
}
if len(customLogs) > 0 {
if cfg.Logging == nil {
cfg.Logging = &caddy.Logging{
Logs: make(map[string]*caddy.CustomLog),
}
}
for _, ncl := range customLogs {
if ncl.name != "" {
cfg.Logging.Logs[ncl.name] = ncl.log
}
// most users seem to prefer not writing access logs
// to the default log when they are directed to a
// file or have any other special customization
if ncl.name != caddy.DefaultLoggerName && len(ncl.log.Include) > 0 {
defaultLog, ok := cfg.Logging.Logs[caddy.DefaultLoggerName]
if !ok {
defaultLog = new(caddy.CustomLog)
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
}
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
}
}
}
return cfg, warnings, nil
}
// evaluateGlobalOptionsBlock evaluates the global options block,
// which is expected to be the first server block if it has zero
// keys. It returns the updated list of server blocks with the
// global options block removed, and updates options accordingly.
func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options map[string]any) ([]serverBlock, error) {
if len(serverBlocks) == 0 || len(serverBlocks[0].block.Keys) > 0 {
return serverBlocks, nil
}
for _, segment := range serverBlocks[0].block.Segments {
opt := segment.Directive()
var val any
var err error
disp := caddyfile.NewDispenser(segment)
optFunc, ok := registeredGlobalOptions[opt]
if !ok {
tkn := segment[0]
return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, opt)
}
val, err = optFunc(disp, options[opt])
if err != nil {
return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", opt, err)
}
// As a special case, fold multiple "servers" options together
// in an array instead of overwriting a possible existing value
if opt == "servers" {
existingOpts, ok := options[opt].([]serverOptions)
if !ok {
existingOpts = []serverOptions{}
}
serverOpts, ok := val.(serverOptions)
if !ok {
return nil, fmt.Errorf("unexpected type from 'servers' global options: %T", val)
}
options[opt] = append(existingOpts, serverOpts)
continue
}
// Additionally, fold multiple "log" options together into an
// array so that multiple loggers can be configured.
if opt == "log" {
existingOpts, ok := options[opt].([]ConfigValue)
if !ok {
existingOpts = []ConfigValue{}
}
logOpts, ok := val.([]ConfigValue)
if !ok {
return nil, fmt.Errorf("unexpected type from 'log' global options: %T", val)
}
options[opt] = append(existingOpts, logOpts...)
continue
}
options[opt] = val
}
// If we got "servers" options, we'll sort them by their listener address
if serverOpts, ok := options["servers"].([]serverOptions); ok {
sort.Slice(serverOpts, func(i, j int) bool {
return len(serverOpts[i].ListenerAddress) > len(serverOpts[j].ListenerAddress)
})
// Reject the config if there are duplicate listener address
seen := make(map[string]bool)
for _, entry := range serverOpts {
if _, alreadySeen := seen[entry.ListenerAddress]; alreadySeen {
return nil, fmt.Errorf("cannot have 'servers' global options with duplicate listener addresses: %s", entry.ListenerAddress)
}
seen[entry.ListenerAddress] = true
}
}
return serverBlocks[1:], nil
}
// serversFromPairings creates the servers for each pairing of addresses
// to server blocks. Each pairing is essentially a server definition.
func (st *ServerType) serversFromPairings(
pairings []sbAddrAssociation,
options map[string]any,
warnings *[]caddyconfig.Warning,
groupCounter counter,
) (map[string]*caddyhttp.Server, error) {
servers := make(map[string]*caddyhttp.Server)
defaultSNI := tryString(options["default_sni"], warnings)
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
}
autoHTTPS := "on"
if ah, ok := options["auto_https"].(string); ok {
autoHTTPS = ah
}
for i, p := range pairings {
// detect ambiguous site definitions: server blocks which
// have the same host bound to the same interface (listener
// address), otherwise their routes will improperly be added
// to the same server (see issue #4635)
for j, sblock1 := range p.serverBlocks {
for _, key := range sblock1.block.Keys {
for k, sblock2 := range p.serverBlocks {
if k == j {
continue
}
if sliceContains(sblock2.block.Keys, key) {
return nil, fmt.Errorf("ambiguous site definition: %s", key)
}
}
}
}
srv := &caddyhttp.Server{
Listen: p.addresses,
}
// handle the auto_https global option
if autoHTTPS != "on" {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
switch autoHTTPS {
case "off":
srv.AutoHTTPS.Disabled = true
case "disable_redirects":
srv.AutoHTTPS.DisableRedir = true
case "disable_certs":
srv.AutoHTTPS.DisableCerts = true
case "ignore_loaded_certs":
srv.AutoHTTPS.IgnoreLoadedCerts = true
}
}
// Using paths in site addresses is deprecated
// See ParseAddress() where parsing should later reject paths
// See https://github.com/caddyserver/caddy/pull/4728 for a full explanation
for _, sblock := range p.serverBlocks {
for _, addr := range sblock.keys {
if addr.Path != "" {
caddy.Log().Named("caddyfile").Warn("Using a path in a site address is deprecated; please use the 'handle' directive instead", zap.String("address", addr.String()))
}
}
}
// sort server blocks by their keys; this is important because
// only the first matching site should be evaluated, and we should
// attempt to match most specific site first (host and path), in
// case their matchers overlap; we do this somewhat naively by
// descending sort by length of host then path
sort.SliceStable(p.serverBlocks, func(i, j int) bool {
// TODO: we could pre-process the specificities for efficiency,
// but I don't expect many blocks will have THAT many keys...
var iLongestPath, jLongestPath string
var iLongestHost, jLongestHost string
var iWildcardHost, jWildcardHost bool
for _, addr := range p.serverBlocks[i].keys {
if strings.Contains(addr.Host, "*") || addr.Host == "" {
iWildcardHost = true
}
if specificity(addr.Host) > specificity(iLongestHost) {
iLongestHost = addr.Host
}
if specificity(addr.Path) > specificity(iLongestPath) {
iLongestPath = addr.Path
}
}
for _, addr := range p.serverBlocks[j].keys {
if strings.Contains(addr.Host, "*") || addr.Host == "" {
jWildcardHost = true
}
if specificity(addr.Host) > specificity(jLongestHost) {
jLongestHost = addr.Host
}
if specificity(addr.Path) > specificity(jLongestPath) {
jLongestPath = addr.Path
}
}
// catch-all blocks (blocks with no hostname) should always go
// last, even after blocks with wildcard hosts
if specificity(iLongestHost) == 0 {
return false
}
if specificity(jLongestHost) == 0 {
return true
}
if iWildcardHost != jWildcardHost {
// site blocks that have a key with a wildcard in the hostname
// must always be less specific than blocks without one; see
// https://github.com/caddyserver/caddy/issues/3410
return jWildcardHost && !iWildcardHost
}
if specificity(iLongestHost) == specificity(jLongestHost) {
return len(iLongestPath) > len(jLongestPath)
}
return specificity(iLongestHost) > specificity(jLongestHost)
})
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
// coming with a log directive
for _, sblock := range p.serverBlocks {
if len(sblock.pile["custom_log"]) != 0 {
srv.Logs = new(caddyhttp.ServerLogConfig)
break
}
}
// create a subroute for each site in the server block
for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
if err != nil {
return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err)
}
hosts := sblock.hostsFromKeys(false)
// emit warnings if user put unspecified IP addresses; they probably want the bind directive
for _, h := range hosts {
if h == "0.0.0.0" || h == "::" {
caddy.Log().Named("caddyfile").Warn("Site block has an unspecified IP address which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", zap.String("address", h))
}
}
// tls: connection policies
if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
// tls connection policies
for _, cpVal := range cpVals {
cp := cpVal.Value.(*caddytls.ConnectionPolicy)
// make sure the policy covers all hostnames from the block
for _, h := range hosts {
if h == defaultSNI {
hosts = append(hosts, "")
cp.DefaultSNI = defaultSNI
break
}
}
if len(hosts) > 0 {
cp.MatchersRaw = caddy.ModuleMap{
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
}
} else {
cp.DefaultSNI = defaultSNI
}
// only append this policy if it actually changes something
if !cp.SettingsEmpty() {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
hasCatchAllTLSConnPolicy = len(hosts) == 0
}
}
}
for _, addr := range sblock.keys {
// if server only uses HTTP port, auto-HTTPS will not apply
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
// exclude any hosts that were defined explicitly with "http://"
// in the key from automated cert management (issue #2998)
if addr.Scheme == "http" && addr.Host != "" {
if srv.AutoHTTPS == nil {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
}
if !sliceContains(srv.AutoHTTPS.Skip, addr.Host) {
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
}
}
}
// we'll need to remember if the address qualifies for auto-HTTPS, so we
// can add a TLS conn policy if necessary
if addr.Scheme == "https" ||
(addr.Scheme != "http" && addr.Host != "" && addr.Port != httpPort) {
addressQualifiesForTLS = true
}
// predict whether auto-HTTPS will add the conn policy for us; if so, we
// may not need to add one for this server
autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
(addr.Port == httpsPort || (addr.Port != httpPort && addr.Host != ""))
}
// Look for any config values that provide listener wrappers on the server block
for _, listenerConfig := range sblock.pile["listener_wrapper"] {
listenerWrapper, ok := listenerConfig.Value.(caddy.ListenerWrapper)
if !ok {
return nil, fmt.Errorf("config for a listener wrapper did not provide a value that implements caddy.ListenerWrapper")
}
jsonListenerWrapper := caddyconfig.JSONModuleObject(
listenerWrapper,
"wrapper",
listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
warnings)
srv.ListenerWrappersRaw = append(srv.ListenerWrappersRaw, jsonListenerWrapper)
}
// set up each handler directive, making sure to honor directive order
dirRoutes := sblock.pile["route"]
siteSubroute, err := buildSubroute(dirRoutes, groupCounter)
if err != nil {
return nil, err
}
// add the site block's route(s) to the server
srv.Routes = appendSubrouteToRouteList(srv.Routes, siteSubroute, matcherSetsEnc, p, warnings)
// if error routes are defined, add those too
if errorSubrouteVals, ok := sblock.pile["error_route"]; ok {
if srv.Errors == nil {
srv.Errors = new(caddyhttp.HTTPErrorConfig)
}
for _, val := range errorSubrouteVals {
sr := val.Value.(*caddyhttp.Subroute)
srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings)
}
}
// add log associations
// see https://github.com/caddyserver/caddy/issues/3310
sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog)
if sblock.hasHostCatchAllKey() {
// all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block
srv.Logs.DefaultLoggerName = ncl.name
} else {
// map each host to the user's desired logger name
for _, h := range sblockLogHosts {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
srv.Logs.LoggerNames[h] = ncl.name
}
}
}
if srv.Logs != nil && len(sblock.pile["custom_log"]) == 0 {
// server has access logs enabled, but this server block does not
// enable access logs; therefore, all hosts of this server block
// should not be access-logged
if len(hosts) == 0 {
// if the server block has a catch-all-hosts key, then we should
// not log reqs to any host unless it appears in the map
srv.Logs.SkipUnmappedHosts = true
}
srv.Logs.SkipHosts = append(srv.Logs.SkipHosts, sblockLogHosts...)
}
}
// a server cannot (natively) serve both HTTP and HTTPS at the
// same time, so make sure the configuration isn't in conflict
err := detectConflictingSchemes(srv, p.serverBlocks, options)
if err != nil {
return nil, err
}
// a catch-all TLS conn policy is necessary to ensure TLS can
// be offered to all hostnames of the server; even though only
// one policy is needed to enable TLS for the server, that
// policy might apply to only certain TLS handshakes; but when
// using the Caddyfile, user would expect all handshakes to at
// least have a matching connection policy, so here we append a
// catch-all/default policy if there isn't one already (it's
// important that it goes at the end) - see issue #3004:
// https://github.com/caddyserver/caddy/issues/3004
// TODO: maybe a smarter way to handle this might be to just make the
// auto-HTTPS logic at provision-time detect if there is any connection
// policy missing for any HTTPS-enabled hosts, if so, add it... maybe?
if addressQualifiesForTLS &&
!hasCatchAllTLSConnPolicy &&
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "") {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI})
}
// tidy things up a bit
srv.TLSConnPolicies, err = consolidateConnPolicies(srv.TLSConnPolicies)
if err != nil {
return nil, fmt.Errorf("consolidating TLS connection policies for server %d: %v", i, err)
}
srv.Routes = consolidateRoutes(srv.Routes)
servers[fmt.Sprintf("srv%d", i)] = srv
}
err := applyServerOptions(servers, options, warnings)
if err != nil {
return nil, err
}
return servers, nil
}
func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]any) error {
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
}
var httpOrHTTPS string
checkAndSetHTTP := func(addr Address) error {
if httpOrHTTPS == "HTTPS" {
errMsg := fmt.Errorf("server listening on %v is configured for HTTPS and cannot natively multiplex HTTP and HTTPS: %s",
srv.Listen, addr.Original)
if addr.Scheme == "" && addr.Host == "" {
errMsg = fmt.Errorf("%s (try specifying https:// in the address)", errMsg)
}
return errMsg
}
if len(srv.TLSConnPolicies) > 0 {
// any connection policies created for an HTTP server
// is a logical conflict, as it would enable HTTPS
return fmt.Errorf("server listening on %v is HTTP, but attempts to configure TLS connection policies", srv.Listen)
}
httpOrHTTPS = "HTTP"
return nil
}
checkAndSetHTTPS := func(addr Address) error {
if httpOrHTTPS == "HTTP" {
return fmt.Errorf("server listening on %v is configured for HTTP and cannot natively multiplex HTTP and HTTPS: %s",
srv.Listen, addr.Original)
}
httpOrHTTPS = "HTTPS"
return nil
}
for _, sblock := range serverBlocks {
for _, addr := range sblock.keys {
if addr.Scheme == "http" || addr.Port == httpPort {
if err := checkAndSetHTTP(addr); err != nil {
return err
}
} else if addr.Scheme == "https" || addr.Port == httpsPort || len(srv.TLSConnPolicies) > 0 {
if err := checkAndSetHTTPS(addr); err != nil {
return err
}
} else if addr.Host == "" {
if err := checkAndSetHTTP(addr); err != nil {
return err
}
}
}
}
return nil
}
// consolidateConnPolicies sorts any catch-all policy to the end, removes empty TLS connection
// policies, and combines equivalent ones for a cleaner overall output.
func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.ConnectionPolicies, error) {
// catch-all policies (those without any matcher) should be at the
// end, otherwise it nullifies any more specific policies
sort.SliceStable(cps, func(i, j int) bool {
return cps[j].MatchersRaw == nil && cps[i].MatchersRaw != nil
})
for i := 0; i < len(cps); i++ {
// compare it to the others
for j := 0; j < len(cps); j++ {
if j == i {
continue
}
// if they're exactly equal in every way, just keep one of them
if reflect.DeepEqual(cps[i], cps[j]) {
cps = append(cps[:j], cps[j+1:]...)
i--
break
}
// if they have the same matcher, try to reconcile each field: either they must
// be identical, or we have to be able to combine them safely
if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) {
if len(cps[i].ALPN) > 0 &&
len(cps[j].ALPN) > 0 &&
!reflect.DeepEqual(cps[i].ALPN, cps[j].ALPN) {
return nil, fmt.Errorf("two policies with same match criteria have conflicting ALPN: %v vs. %v",
cps[i].ALPN, cps[j].ALPN)
}
if len(cps[i].CipherSuites) > 0 &&
len(cps[j].CipherSuites) > 0 &&
!reflect.DeepEqual(cps[i].CipherSuites, cps[j].CipherSuites) {
return nil, fmt.Errorf("two policies with same match criteria have conflicting cipher suites: %v vs. %v",
cps[i].CipherSuites, cps[j].CipherSuites)
}
if cps[i].ClientAuthentication == nil &&
cps[j].ClientAuthentication != nil &&
!reflect.DeepEqual(cps[i].ClientAuthentication, cps[j].ClientAuthentication) {
return nil, fmt.Errorf("two policies with same match criteria have conflicting client auth configuration: %+v vs. %+v",
cps[i].ClientAuthentication, cps[j].ClientAuthentication)
}
if len(cps[i].Curves) > 0 &&
len(cps[j].Curves) > 0 &&
!reflect.DeepEqual(cps[i].Curves, cps[j].Curves) {
return nil, fmt.Errorf("two policies with same match criteria have conflicting curves: %v vs. %v",
cps[i].Curves, cps[j].Curves)
}
if cps[i].DefaultSNI != "" &&
cps[j].DefaultSNI != "" &&
cps[i].DefaultSNI != cps[j].DefaultSNI {
return nil, fmt.Errorf("two policies with same match criteria have conflicting default SNI: %s vs. %s",
cps[i].DefaultSNI, cps[j].DefaultSNI)
}
if cps[i].ProtocolMin != "" &&
cps[j].ProtocolMin != "" &&
cps[i].ProtocolMin != cps[j].ProtocolMin {
return nil, fmt.Errorf("two policies with same match criteria have conflicting min protocol: %s vs. %s",
cps[i].ProtocolMin, cps[j].ProtocolMin)
}
if cps[i].ProtocolMax != "" &&
cps[j].ProtocolMax != "" &&
cps[i].ProtocolMax != cps[j].ProtocolMax {
return nil, fmt.Errorf("two policies with same match criteria have conflicting max protocol: %s vs. %s",
cps[i].ProtocolMax, cps[j].ProtocolMax)
}
if cps[i].CertSelection != nil && cps[j].CertSelection != nil {
// merging fields other than AnyTag is not implemented
if !reflect.DeepEqual(cps[i].CertSelection.SerialNumber, cps[j].CertSelection.SerialNumber) ||
!reflect.DeepEqual(cps[i].CertSelection.SubjectOrganization, cps[j].CertSelection.SubjectOrganization) ||
cps[i].CertSelection.PublicKeyAlgorithm != cps[j].CertSelection.PublicKeyAlgorithm ||
!reflect.DeepEqual(cps[i].CertSelection.AllTags, cps[j].CertSelection.AllTags) {
return nil, fmt.Errorf("two policies with same match criteria have conflicting cert selections: %+v vs. %+v",
cps[i].CertSelection, cps[j].CertSelection)
}
}
// by now we've decided that we can merge the two -- we'll keep i and drop j
if len(cps[i].ALPN) == 0 && len(cps[j].ALPN) > 0 {
cps[i].ALPN = cps[j].ALPN
}
if len(cps[i].CipherSuites) == 0 && len(cps[j].CipherSuites) > 0 {
cps[i].CipherSuites = cps[j].CipherSuites
}
if cps[i].ClientAuthentication == nil && cps[j].ClientAuthentication != nil {
cps[i].ClientAuthentication = cps[j].ClientAuthentication
}
if len(cps[i].Curves) == 0 && len(cps[j].Curves) > 0 {
cps[i].Curves = cps[j].Curves
}
if cps[i].DefaultSNI == "" && cps[j].DefaultSNI != "" {
cps[i].DefaultSNI = cps[j].DefaultSNI
}
if cps[i].ProtocolMin == "" && cps[j].ProtocolMin != "" {
cps[i].ProtocolMin = cps[j].ProtocolMin
}
if cps[i].ProtocolMax == "" && cps[j].ProtocolMax != "" {
cps[i].ProtocolMax = cps[j].ProtocolMax
}
if cps[i].CertSelection == nil && cps[j].CertSelection != nil {
// if j is the only one with a policy, move it over to i
cps[i].CertSelection = cps[j].CertSelection
} else if cps[i].CertSelection != nil && cps[j].CertSelection != nil {
// if both have one, then combine AnyTag
for _, tag := range cps[j].CertSelection.AnyTag {
if !sliceContains(cps[i].CertSelection.AnyTag, tag) {
cps[i].CertSelection.AnyTag = append(cps[i].CertSelection.AnyTag, tag)
}
}
}
cps = append(cps[:j], cps[j+1:]...)
i--
break
}
}
}
return cps, nil
}
// appendSubrouteToRouteList appends the routes in subroute
// to the routeList, optionally qualified by matchers.
func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
subroute *caddyhttp.Subroute,
matcherSetsEnc []caddy.ModuleMap,
p sbAddrAssociation,
warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
// nothing to do if... there's nothing to do
if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
return routeList
}
// No need to wrap the handlers in a subroute if this is the only server block
// and there is no matcher for it (doing so would produce unnecessarily nested
// JSON), *unless* there is a host matcher within this site block; if so, then
// we still need to wrap in a subroute because otherwise the host matcher from
// the inside of the site block would be a top-level host matcher, which is
// subject to auto-HTTPS (cert management), and using a host matcher within
// a site block is a valid, common pattern for excluding domains from cert
// management, leading to unexpected behavior; see issue #5124.
wrapInSubroute := true
if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
var hasHostMatcher bool
outer:
for _, route := range subroute.Routes {
for _, ms := range route.MatcherSetsRaw {
for matcherName := range ms {
if matcherName == "host" {
hasHostMatcher = true
break outer
}
}
}
}
wrapInSubroute = hasHostMatcher
}
if wrapInSubroute {
route := caddyhttp.Route{
// the semantics of a site block in the Caddyfile dictate
// that only the first matching one is evaluated, since
// site blocks do not cascade nor inherit
Terminal: true,
}
if len(matcherSetsEnc) > 0 {
route.MatcherSetsRaw = matcherSetsEnc
}
if len(subroute.Routes) > 0 || subroute.Errors != nil {
route.HandlersRaw = []json.RawMessage{
caddyconfig.JSONModuleObject(subroute, "handler", "subroute", warnings),
}
}
if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 {
routeList = append(routeList, route)
}
} else {
routeList = append(routeList, subroute.Routes...)
}
return routeList
}
// buildSubroute turns the config values, which are expected to be routes
// into a clean and orderly subroute that has all the routes within it.
func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subroute, error) {
for _, val := range routes {
if !directiveIsOrdered(val.directive) {
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here", val.directive)
}
}
sortRoutes(routes)
subroute := new(caddyhttp.Subroute)
// some directives are mutually exclusive (only first matching
// instance should be evaluated); this is done by putting their
// routes in the same group
mutuallyExclusiveDirs := map[string]*struct {
count int
groupName string
}{
// as a special case, group rewrite directives so that they are mutually exclusive;
// this means that only the first matching rewrite will be evaluated, and that's
// probably a good thing, since there should never be a need to do more than one
// rewrite (I think?), and cascading rewrites smell bad... imagine these rewrites:
// rewrite /docs/json/* /docs/json/index.html
// rewrite /docs/* /docs/index.html
// (We use this on the Caddy website, or at least we did once.) The first rewrite's
// result is also matched by the second rewrite, making the first rewrite pointless.
// See issue #2959.
"rewrite": {},
// handle blocks are also mutually exclusive by definition
"handle": {},
// root just sets a variable, so if it was not mutually exclusive, intersecting
// root directives would overwrite previously-matched ones; they should not cascade
"root": {},
}
// we need to deterministically loop over each of these directives
// in order to keep the group numbers consistent
keys := make([]string, 0, len(mutuallyExclusiveDirs))
for k := range mutuallyExclusiveDirs {
keys = append(keys, k)
}
sort.Strings(keys)
for _, meDir := range keys {
info := mutuallyExclusiveDirs[meDir]
// see how many instances of the directive there are
for _, r := range routes {
if r.directive == meDir {
info.count++
if info.count > 1 {
break
}
}
}
// if there is more than one, put them in a group
// (special case: "rewrite" directive must always be in
// its own group--even if there is only one--because we
// do not want a rewrite to be consolidated into other
// adjacent routes that happen to have the same matcher,
// see caddyserver/caddy#3108 - because the implied
// intent of rewrite is to do an internal redirect,
// we can't assume that the request will continue to
// match the same matcher; anyway, giving a route a
// unique group name should keep it from consolidating)
if info.count > 1 || meDir == "rewrite" {
info.groupName = groupCounter.nextGroup()
}
}
// add all the routes piled in from directives
for _, r := range routes {
// put this route into a group if it is mutually exclusive
if info, ok := mutuallyExclusiveDirs[r.directive]; ok {
route := r.Value.(caddyhttp.Route)
route.Group = info.groupName
r.Value = route
}
switch route := r.Value.(type) {
case caddyhttp.Subroute:
// if a route-class config value is actually a Subroute handler
// with nothing but a list of routes, then it is the intention
// of the directive to keep these handlers together and in this
// same order, but not necessarily in a subroute (if it wanted
// to keep them in a subroute, the directive would have returned
// a route with a Subroute as its handler); this is useful to
// keep multiple handlers/routes together and in the same order
// so that the sorting procedure we did above doesn't reorder them
if route.Errors != nil {
// if error handlers are also set, this is confusing; it's
// probably supposed to be wrapped in a Route and encoded
// as a regular handler route... programmer error.
panic("found subroute with more than just routes; perhaps it should have been wrapped in a route?")
}
subroute.Routes = append(subroute.Routes, route.Routes...)
case caddyhttp.Route:
subroute.Routes = append(subroute.Routes, route)
}
}
subroute.Routes = consolidateRoutes(subroute.Routes)
return subroute, nil
}
// normalizeDirectiveName ensures directives that should be sorted
// at the same level are named the same before sorting happens.
func normalizeDirectiveName(directive string) string {
// As a special case, we want "handle_path" to be sorted
// at the same level as "handle", so we force them to use
// the same directive name after their parsing is complete.
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
if directive == "handle_path" {
directive = "handle"
}
return directive
}
// consolidateRoutes combines routes with the same properties
// (same matchers, same Terminal and Group settings) for a
// cleaner overall output.
func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList {
for i := 0; i < len(routes)-1; i++ {
if reflect.DeepEqual(routes[i].MatcherSetsRaw, routes[i+1].MatcherSetsRaw) &&
routes[i].Terminal == routes[i+1].Terminal &&
routes[i].Group == routes[i+1].Group {
// keep the handlers in the same order, then splice out repetitive route
routes[i].HandlersRaw = append(routes[i].HandlersRaw, routes[i+1].HandlersRaw...)
routes = append(routes[:i+1], routes[i+2:]...)
i--
}
}
return routes
}
func matcherSetFromMatcherToken(
tkn caddyfile.Token,
matcherDefs map[string]caddy.ModuleMap,
warnings *[]caddyconfig.Warning,
) (caddy.ModuleMap, bool, error) {
// matcher tokens can be wildcards, simple path matchers,
// or refer to a pre-defined matcher by some name
if tkn.Text == "*" {
// match all requests == no matchers, so nothing to do
return nil, true, nil
} else if strings.HasPrefix(tkn.Text, "/") {
// convenient way to specify a single path match
return caddy.ModuleMap{
"path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings),
}, true, nil
} else if strings.HasPrefix(tkn.Text, matcherPrefix) {
// pre-defined matcher
m, ok := matcherDefs[tkn.Text]
if !ok {
return nil, false, fmt.Errorf("unrecognized matcher name: %+v", tkn.Text)
}
return m, true, nil
}
return nil, false, nil
}
func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.ModuleMap, error) {
type hostPathPair struct {
hostm caddyhttp.MatchHost
pathm caddyhttp.MatchPath
}
// keep routes with common host and path matchers together
var matcherPairs []*hostPathPair
var catchAllHosts bool
for _, addr := range sblock.keys {
// choose a matcher pair that should be shared by this
// server block; if none exists yet, create one
var chosenMatcherPair *hostPathPair
for _, mp := range matcherPairs {
if (len(mp.pathm) == 0 && addr.Path == "") ||
(len(mp.pathm) == 1 && mp.pathm[0] == addr.Path) {
chosenMatcherPair = mp
break
}
}
if chosenMatcherPair == nil {
chosenMatcherPair = new(hostPathPair)
if addr.Path != "" {
chosenMatcherPair.pathm = []string{addr.Path}
}
matcherPairs = append(matcherPairs, chosenMatcherPair)
}
// if one of the keys has no host (i.e. is a catch-all for
// any hostname), then we need to null out the host matcher
// entirely so that it matches all hosts
if addr.Host == "" && !catchAllHosts {
chosenMatcherPair.hostm = nil
catchAllHosts = true
}
if catchAllHosts {
continue
}
// add this server block's keys to the matcher
// pair if it doesn't already exist
if addr.Host != "" {
var found bool
for _, h := range chosenMatcherPair.hostm {
if h == addr.Host {
found = true
break
}
}
if !found {
chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host)
}
}
}
// iterate each pairing of host and path matchers and
// put them into a map for JSON encoding
var matcherSets []map[string]caddyhttp.RequestMatcher
for _, mp := range matcherPairs {
matcherSet := make(map[string]caddyhttp.RequestMatcher)
if len(mp.hostm) > 0 {
matcherSet["host"] = mp.hostm
}
if len(mp.pathm) > 0 {
matcherSet["path"] = mp.pathm
}
if len(matcherSet) > 0 {
matcherSets = append(matcherSets, matcherSet)
}
}
// finally, encode each of the matcher sets
matcherSetsEnc := make([]caddy.ModuleMap, 0, len(matcherSets))
for _, ms := range matcherSets {
msEncoded, err := encodeMatcherSet(ms)
if err != nil {
return nil, fmt.Errorf("server block %v: %v", sblock.block.Keys, err)
}
matcherSetsEnc = append(matcherSetsEnc, msEncoded)
}
return matcherSetsEnc, nil
}
func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error {
for d.Next() {
// this is the "name" for "named matchers"
definitionName := d.Val()
if _, ok := matchers[definitionName]; ok {
return fmt.Errorf("matcher is defined more than once: %s", definitionName)
}
matchers[definitionName] = make(caddy.ModuleMap)
// given a matcher name and the tokens following it, parse
// the tokens as a matcher module and record it
makeMatcher := func(matcherName string, tokens []caddyfile.Token) error {
mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil {
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
}
err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens))
if err != nil {
return err
}
rm, ok := unm.(caddyhttp.RequestMatcher)
if !ok {
return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
return nil
}
// if the next token is quoted, we can assume it's not a matcher name
// and that it's probably an 'expression' matcher
if d.NextArg() {
if d.Token().Quoted() {
err := makeMatcher("expression", []caddyfile.Token{d.Token()})
if err != nil {
return err
}
continue
}
// if it wasn't quoted, then we need to rewind after calling
// d.NextArg() so the below properly grabs the matcher name
d.Prev()
}
// in case there are multiple instances of the same matcher, concatenate
// their tokens (we expect that UnmarshalCaddyfile should be able to
// handle more than one segment); otherwise, we'd overwrite other
// instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
}
for matcherName, tokens := range tokensByMatcherName {
err := makeMatcher(matcherName, tokens)
if err != nil {
return err
}
}
}
return nil
}
func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.ModuleMap, error) {
msEncoded := make(caddy.ModuleMap)
for matcherName, val := range matchers {
jsonBytes, err := json.Marshal(val)
if err != nil {
return nil, fmt.Errorf("marshaling matcher set %#v: %v", matchers, err)
}
msEncoded[matcherName] = jsonBytes
}
return msEncoded, nil
}
// placeholderShorthands returns a slice of old-new string pairs,
// where the left of the pair is a placeholder shorthand that may
// be used in the Caddyfile, and the right is the replacement.
func placeholderShorthands() []string {
return []string{
"{dir}", "{http.request.uri.path.dir}",
"{file}", "{http.request.uri.path.file}",
"{host}", "{http.request.host}",
"{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
"{method}", "{http.request.method}",
"{path}", "{http.request.uri.path}",
"{query}", "{http.request.uri.query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
"{uri}", "{http.request.uri}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
}
}
// WasReplacedPlaceholderShorthand checks if a token string was
// likely a replaced shorthand of the known Caddyfile placeholder
// replacement outputs. Useful to prevent some user-defined map
// output destinations from overlapping with one of the
// predefined shorthands.
func WasReplacedPlaceholderShorthand(token string) string {
prev := ""
for i, item := range placeholderShorthands() {
// only look at every 2nd item, which is the replacement
if i%2 == 0 {
prev = item
continue
}
if strings.Trim(token, "{}") == strings.Trim(item, "{}") {
// we return the original shorthand so it
// can be used for an error message
return prev
}
}
return ""
}
// tryInt tries to convert val to an integer. If it fails,
// it downgrades the error to a warning and returns 0.
func tryInt(val any, warnings *[]caddyconfig.Warning) int {
intVal, ok := val.(int)
if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not an integer type"})
}
return intVal
}
func tryString(val any, warnings *[]caddyconfig.Warning) string {
stringVal, ok := val.(string)
if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a string type"})
}
return stringVal
}
func tryDuration(val any, warnings *[]caddyconfig.Warning) caddy.Duration {
durationVal, ok := val.(caddy.Duration)
if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"})
}
return durationVal
}
// sliceContains returns true if needle is in haystack.
func sliceContains(haystack []string, needle string) bool {
for _, s := range haystack {
if s == needle {
return true
}
}
return false
}
// listenersUseAnyPortOtherThan returns true if there are any
// listeners in addresses that use a port which is not otherPort.
// Mostly borrowed from unexported method in caddyhttp package.
func listenersUseAnyPortOtherThan(addresses []string, otherPort string) bool {
otherPortInt, err := strconv.Atoi(otherPort)
if err != nil {
return false
}
for _, lnAddr := range addresses {
laddrs, err := caddy.ParseNetworkAddress(lnAddr)
if err != nil {
continue
}
if uint(otherPortInt) > laddrs.EndPort || uint(otherPortInt) < laddrs.StartPort {
return true
}
}
return false
}
// specificity returns len(s) minus any wildcards (*) and
// placeholders ({...}). Basically, it's a length count
// that penalizes the use of wildcards and placeholders.
// This is useful for comparing hostnames and paths.
// However, wildcards in paths are not a sure answer to
// the question of specificity. For example,
// '*.example.com' is clearly less specific than
// 'a.example.com', but is '/a' more or less specific
// than '/a*'?
func specificity(s string) int {
l := len(s) - strings.Count(s, "*")
for len(s) > 0 {
start := strings.Index(s, "{")
if start < 0 {
return l
}
end := strings.Index(s[start:], "}") + start + 1
if end <= start {
return l
}
l -= end - start
s = s[end:]
}
return l
}
type counter struct {
n *int
}
func (c counter) nextGroup() string {
name := fmt.Sprintf("group%d", *c.n)
*c.n++
return name
}
type namedCustomLog struct {
name string
log *caddy.CustomLog
}
// sbAddrAssociation is a mapping from a list of
// addresses to a list of server blocks that are
// served on those addresses.
type sbAddrAssociation struct {
addresses []string
serverBlocks []serverBlock
}
const matcherPrefix = "@"
// Interface guard
var _ caddyfile.ServerType = (*ServerType)(nil)
caddy-2.6.2/caddyconfig/httpcaddyfile/httptype_test.go 0000664 0000000 0000000 00000006213 14350072374 0023161 0 ustar 00root root 0000000 0000000 package httpcaddyfile
import (
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func TestMatcherSyntax(t *testing.T) {
for i, tc := range []struct {
input string
expectError bool
}{
{
input: `http://localhost
@debug {
query showdebug=1
}
`,
expectError: false,
},
{
input: `http://localhost
@debug {
query bad format
}
`,
expectError: true,
},
{
input: `http://localhost
@debug {
not {
path /somepath*
}
}
`,
expectError: false,
},
{
input: `http://localhost
@debug {
not path /somepath*
}
`,
expectError: false,
},
{
input: `http://localhost
@debug not path /somepath*
`,
expectError: false,
},
{
input: `@matcher {
path /matcher-not-allowed/outside-of-site-block/*
}
http://localhost
`,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue
}
}
}
func TestSpecificity(t *testing.T) {
for i, tc := range []struct {
input string
expect int
}{
{"", 0},
{"*", 0},
{"*.*", 1},
{"{placeholder}", 0},
{"/{placeholder}", 1},
{"foo", 3},
{"example.com", 11},
{"a.example.com", 13},
{"*.example.com", 12},
{"/foo", 4},
{"/foo*", 4},
{"{placeholder}.example.com", 12},
{"{placeholder.example.com", 24},
{"}.", 2},
{"}{", 2},
{"{}", 0},
{"{{{}}", 1},
} {
actual := specificity(tc.input)
if actual != tc.expect {
t.Errorf("Test %d (%s): Expected %d but got %d", i, tc.input, tc.expect, actual)
}
}
}
func TestGlobalOptions(t *testing.T) {
for i, tc := range []struct {
input string
expectError bool
}{
{
input: `
{
email test@example.com
}
:80
`,
expectError: false,
},
{
input: `
{
admin off
}
:80
`,
expectError: false,
},
{
input: `
{
admin 127.0.0.1:2020
}
:80
`,
expectError: false,
},
{
input: `
{
admin {
disabled false
}
}
:80
`,
expectError: true,
},
{
input: `
{
admin {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectError: false,
},
{
input: `
{
admin 127.0.0.1:2020 {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectError: false,
},
{
input: `
{
admin 192.168.1.1:2020 127.0.0.1:2020 {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectError: true,
},
{
input: `
{
admin off {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue
}
}
}
caddy-2.6.2/caddyconfig/httpcaddyfile/options.go 0000664 0000000 0000000 00000030070 14350072374 0021732 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"strconv"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
)
func init() {
RegisterGlobalOption("debug", parseOptTrue)
RegisterGlobalOption("http_port", parseOptHTTPPort)
RegisterGlobalOption("https_port", parseOptHTTPSPort)
RegisterGlobalOption("default_bind", parseOptStringList)
RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("shutdown_delay", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("storage", parseOptStorage)
RegisterGlobalOption("storage_clean_interval", parseOptDuration)
RegisterGlobalOption("renew_interval", parseOptDuration)
RegisterGlobalOption("ocsp_interval", parseOptDuration)
RegisterGlobalOption("acme_ca", parseOptSingleString)
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptACMEDNS)
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
RegisterGlobalOption("skip_install_trust", parseOptTrue)
RegisterGlobalOption("email", parseOptSingleString)
RegisterGlobalOption("admin", parseOptAdmin)
RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
RegisterGlobalOption("local_certs", parseOptTrue)
RegisterGlobalOption("key_type", parseOptSingleString)
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
RegisterGlobalOption("servers", parseServerOptions)
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
RegisterGlobalOption("log", parseLogOptions)
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
}
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) {
var httpPort int
for d.Next() {
var httpPortStr string
if !d.AllArgs(&httpPortStr) {
return 0, d.ArgErr()
}
var err error
httpPort, err = strconv.Atoi(httpPortStr)
if err != nil {
return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err)
}
}
return httpPort, nil
}
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) {
var httpsPort int
for d.Next() {
var httpsPortStr string
if !d.AllArgs(&httpsPortStr) {
return 0, d.ArgErr()
}
var err error
httpsPort, err = strconv.Atoi(httpsPortStr)
if err != nil {
return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err)
}
}
return httpsPort, nil
}
func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
newOrder := directiveOrder
for d.Next() {
// get directive name
if !d.Next() {
return nil, d.ArgErr()
}
dirName := d.Val()
if _, ok := registeredDirectives[dirName]; !ok {
return nil, d.Errf("%s is not a registered directive", dirName)
}
// get positional token
if !d.Next() {
return nil, d.ArgErr()
}
pos := d.Val()
// if directive exists, first remove it
for i, d := range newOrder {
if d == dirName {
newOrder = append(newOrder[:i], newOrder[i+1:]...)
break
}
}
// act on the positional
switch pos {
case "first":
newOrder = append([]string{dirName}, newOrder...)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case "last":
newOrder = append(newOrder, dirName)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case "before":
case "after":
default:
return nil, d.Errf("unknown positional '%s'", pos)
}
// get name of other directive
if !d.NextArg() {
return nil, d.ArgErr()
}
otherDir := d.Val()
if d.NextArg() {
return nil, d.ArgErr()
}
// insert directive into proper position
for i, d := range newOrder {
if d == otherDir {
if pos == "before" {
newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
} else if pos == "after" {
newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
}
break
}
}
}
directiveOrder = newOrder
return newOrder, nil
}
func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get storage module name
return nil, d.ArgErr()
}
modID := "caddy.storage." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
storage, ok := unm.(caddy.StorageConverter)
if !ok {
return nil, d.Errf("module %s is not a caddy.StorageConverter", modID)
}
return storage, nil
}
func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get duration value
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
return caddy.Duration(dur), nil
}
func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get DNS module name
return nil, d.ArgErr()
}
modID := "dns.providers." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
prov, ok := unm.(certmagic.ACMEDNSProvider)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.ACMEDNSProvider", modID, unm)
}
return prov, nil
}
func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
eab := new(acme.EAB)
for d.Next() {
if d.NextArg() {
return nil, d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "key_id":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.KeyID = d.Val()
case "mac_key":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.MACKey = d.Val()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
}
}
return eab, nil
}
func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) {
var issuers []certmagic.Issuer
if existing != nil {
issuers = existing.([]certmagic.Issuer)
}
for d.Next() { // consume option name
if !d.Next() { // get issuer module name
return nil, d.ArgErr()
}
modID := "tls.issuance." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
}
issuers = append(issuers, iss)
}
return issuers, nil
}
func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
return val, nil
}
func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name
val := d.RemainingArgs()
if len(val) == 0 {
return "", d.ArgErr()
}
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
adminCfg := new(caddy.AdminConfig)
for d.Next() {
if d.NextArg() {
listenAddress := d.Val()
if listenAddress == "off" {
adminCfg.Disabled = true
if d.Next() { // Do not accept any remaining options including block
return nil, d.Err("No more option is allowed after turning off admin config")
}
} else {
adminCfg.Listen = listenAddress
if d.NextArg() { // At most 1 arg is allowed
return nil, d.ArgErr()
}
}
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "enforce_origin":
adminCfg.EnforceOrigin = true
case "origins":
adminCfg.Origins = d.RemainingArgs()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
}
}
if adminCfg.Listen == "" && !adminCfg.Disabled {
adminCfg.Listen = caddy.DefaultAdminListen
}
return adminCfg, nil
}
func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
var ond *caddytls.OnDemandConfig
for d.Next() {
if d.NextArg() {
return nil, d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "ask":
if !d.NextArg() {
return nil, d.ArgErr()
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
ond.Ask = d.Val()
case "interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Interval = caddy.Duration(dur)
case "burst":
if !d.NextArg() {
return nil, d.ArgErr()
}
burst, err := strconv.Atoi(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Burst = burst
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
}
}
if ond == nil {
return nil, d.Err("expected at least one config parameter for on_demand_tls")
}
return ond, nil
}
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
if val != "off" && val != "disable_redirects" && val != "disable_certs" && val != "ignore_loaded_certs" {
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
}
return val, nil
}
func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) {
return unmarshalCaddyfileServerOptions(d)
}
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
var val string
if !d.AllArgs(&val) {
return nil, d.ArgErr()
}
if val != "off" {
return nil, d.Errf("invalid argument '%s'", val)
}
return certmagic.OCSPConfig{
DisableStapling: val == "off",
}, nil
}
// parseLogOptions parses the global log option. Syntax:
//
// log [name] {
// output ...
// format ...
// level
// include
// exclude
// }
//
// When the name argument is unspecified, this directive modifies the default
// logger.
func parseLogOptions(d *caddyfile.Dispenser, existingVal any) (any, error) {
currentNames := make(map[string]struct{})
if existingVal != nil {
innerVals, ok := existingVal.([]ConfigValue)
if !ok {
return nil, d.Errf("existing log values of unexpected type: %T", existingVal)
}
for _, rawVal := range innerVals {
val, ok := rawVal.Value.(namedCustomLog)
if !ok {
return nil, d.Errf("existing log value of unexpected type: %T", existingVal)
}
currentNames[val.name] = struct{}{}
}
}
var warnings []caddyconfig.Warning
// Call out the same parser that handles server-specific log configuration.
configValues, err := parseLogHelper(
Helper{
Dispenser: d,
warnings: &warnings,
},
currentNames,
)
if err != nil {
return nil, err
}
if len(warnings) > 0 {
return nil, d.Errf("warnings found in parsing global log options: %+v", warnings)
}
return configValues, nil
}
func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next()
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
}
caddy-2.6.2/caddyconfig/httpcaddyfile/options_test.go 0000664 0000000 0000000 00000002224 14350072374 0022771 0 ustar 00root root 0000000 0000000 package httpcaddyfile
import (
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
_ "github.com/caddyserver/caddy/v2/modules/logging"
)
func TestGlobalLogOptionSyntax(t *testing.T) {
for i, tc := range []struct {
input string
output string
expectError bool
}{
// NOTE: Additional test cases of successful Caddyfile parsing
// are present in: caddytest/integration/caddyfile_adapt/
{
input: `{
log default
}
`,
output: `{}`,
expectError: false,
},
{
input: `{
log example {
output file foo.log
}
log example {
format json
}
}
`,
expectError: true,
},
{
input: `{
log example /foo {
output file foo.log
}
}
`,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
out, _, err := adapter.Adapt([]byte(tc.input), nil)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %v", i, tc.expectError, err)
continue
}
if string(out) != tc.output {
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
}
}
}
caddy-2.6.2/caddyconfig/httpcaddyfile/pkiapp.go 0000664 0000000 0000000 00000012660 14350072374 0021530 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddypki"
)
func init() {
RegisterGlobalOption("pki", parsePKIApp)
}
// parsePKIApp parses the global log option. Syntax:
//
// pki {
// ca [] {
// name
// root_cn
// intermediate_cn
// root {
// cert
// key
// format
// }
// intermediate {
// cert
// key
// format
// }
// }
// }
//
// When the CA ID is unspecified, 'local' is assumed.
func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
for d.Next() {
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "ca":
pkiCa := new(caddypki.CA)
if d.NextArg() {
pkiCa.ID = d.Val()
if d.NextArg() {
return nil, d.ArgErr()
}
}
if pkiCa.ID == "" {
pkiCa.ID = caddypki.DefaultCAID
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "name":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Name = d.Val()
case "root_cn":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.RootCommonName = d.Val()
case "intermediate_cn":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.IntermediateCommonName = d.Val()
case "root":
if pkiCa.Root == nil {
pkiCa.Root = new(caddypki.KeyPair)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "cert":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.Certificate = d.Val()
case "key":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.PrivateKey = d.Val()
case "format":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.Format = d.Val()
default:
return nil, d.Errf("unrecognized pki ca root option '%s'", d.Val())
}
}
case "intermediate":
if pkiCa.Intermediate == nil {
pkiCa.Intermediate = new(caddypki.KeyPair)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "cert":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.Certificate = d.Val()
case "key":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.PrivateKey = d.Val()
case "format":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.Format = d.Val()
default:
return nil, d.Errf("unrecognized pki ca intermediate option '%s'", d.Val())
}
}
default:
return nil, d.Errf("unrecognized pki ca option '%s'", d.Val())
}
}
pki.CAs[pkiCa.ID] = pkiCa
default:
return nil, d.Errf("unrecognized pki option '%s'", d.Val())
}
}
}
return pki, nil
}
func (st ServerType) buildPKIApp(
pairings []sbAddrAssociation,
options map[string]any,
warnings []caddyconfig.Warning,
) (*caddypki.PKI, []caddyconfig.Warning, error) {
skipInstallTrust := false
if _, ok := options["skip_install_trust"]; ok {
skipInstallTrust = true
}
falseBool := false
// Load the PKI app configured via global options
var pkiApp *caddypki.PKI
unwrappedPki, ok := options["pki"].(*caddypki.PKI)
if ok {
pkiApp = unwrappedPki
} else {
pkiApp = &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
}
for _, ca := range pkiApp.CAs {
if skipInstallTrust {
ca.InstallTrust = &falseBool
}
pkiApp.CAs[ca.ID] = ca
}
// Add in the CAs configured via directives
for _, p := range pairings {
for _, sblock := range p.serverBlocks {
// find all the CAs that were defined and add them to the app config
// i.e. from any "acme_server" directives
for _, caCfgValue := range sblock.pile["pki.ca"] {
ca := caCfgValue.Value.(*caddypki.CA)
if skipInstallTrust {
ca.InstallTrust = &falseBool
}
// the CA might already exist from global options, so
// don't overwrite it in that case
if _, ok := pkiApp.CAs[ca.ID]; !ok {
pkiApp.CAs[ca.ID] = ca
}
}
}
}
// if there was no CAs defined in any of the servers,
// and we were requested to not install trust, then
// add one for the default/local CA to do so
if len(pkiApp.CAs) == 0 && skipInstallTrust {
ca := new(caddypki.CA)
ca.ID = caddypki.DefaultCAID
ca.InstallTrust = &falseBool
pkiApp.CAs[ca.ID] = ca
}
return pkiApp, warnings, nil
}
caddy-2.6.2/caddyconfig/httpcaddyfile/serveroptions.go 0000664 0000000 0000000 00000020531 14350072374 0023162 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"encoding/json"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dustin/go-humanize"
)
// serverOptions collects server config overrides parsed from Caddyfile global options
type serverOptions struct {
// If set, will only apply these options to servers that contain a
// listener address that matches exactly. If empty, will apply to all
// servers that were not already matched by another serverOptions.
ListenerAddress string
// These will all map 1:1 to the caddyhttp.Server struct
ListenerWrappersRaw []json.RawMessage
ReadTimeout caddy.Duration
ReadHeaderTimeout caddy.Duration
WriteTimeout caddy.Duration
IdleTimeout caddy.Duration
KeepAliveInterval caddy.Duration
MaxHeaderBytes int
Protocols []string
StrictSNIHost *bool
ShouldLogCredentials bool
Metrics *caddyhttp.Metrics
}
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
serverOpts := serverOptions{}
for d.Next() {
if d.NextArg() {
serverOpts.ListenerAddress = d.Val()
if d.NextArg() {
return nil, d.ArgErr()
}
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "listener_wrappers":
for nesting := d.Nesting(); d.NextBlock(nesting); {
modID := "caddy.listeners." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
if !ok {
return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
}
jsonListenerWrapper := caddyconfig.JSONModuleObject(
listenerWrapper,
"wrapper",
listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
nil,
)
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
}
case "timeouts":
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "read_body":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_body timeout duration: %v", err)
}
serverOpts.ReadTimeout = caddy.Duration(dur)
case "read_header":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_header timeout duration: %v", err)
}
serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
case "write":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing write timeout duration: %v", err)
}
serverOpts.WriteTimeout = caddy.Duration(dur)
case "idle":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing idle timeout duration: %v", err)
}
serverOpts.IdleTimeout = caddy.Duration(dur)
default:
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
}
}
case "keepalive_interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing keepalive interval duration: %v", err)
}
serverOpts.KeepAliveInterval = caddy.Duration(dur)
case "max_header_size":
var sizeStr string
if !d.AllArgs(&sizeStr) {
return nil, d.ArgErr()
}
size, err := humanize.ParseBytes(sizeStr)
if err != nil {
return nil, d.Errf("parsing max_header_size: %v", err)
}
serverOpts.MaxHeaderBytes = int(size)
case "log_credentials":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.ShouldLogCredentials = true
case "protocols":
protos := d.RemainingArgs()
for _, proto := range protos {
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
}
if sliceContains(serverOpts.Protocols, proto) {
return nil, d.Errf("protocol %s specified more than once", proto)
}
serverOpts.Protocols = append(serverOpts.Protocols, proto)
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
case "strict_sni_host":
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
case "metrics":
if d.NextArg() {
return nil, d.ArgErr()
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
serverOpts.Metrics = new(caddyhttp.Metrics)
// TODO: DEPRECATED. (August 2022)
case "protocol":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol sub-option will be removed soon")
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "allow_h2c":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: allow_h2c will be removed soon; use protocols option instead")
if d.NextArg() {
return nil, d.ArgErr()
}
if sliceContains(serverOpts.Protocols, "h2c") {
return nil, d.Errf("protocol h2c already specified")
}
serverOpts.Protocols = append(serverOpts.Protocols, "h2c")
case "strict_sni_host":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol > strict_sni_host in this position will be removed soon; move up to the servers block instead")
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
default:
return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
}
}
default:
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
}
}
}
return serverOpts, nil
}
// applyServerOptions sets the server options on the appropriate servers
func applyServerOptions(
servers map[string]*caddyhttp.Server,
options map[string]any,
warnings *[]caddyconfig.Warning,
) error {
serverOpts, ok := options["servers"].([]serverOptions)
if !ok {
return nil
}
for _, server := range servers {
// find the options that apply to this server
opts := func() *serverOptions {
for _, entry := range serverOpts {
if entry.ListenerAddress == "" {
return &entry
}
for _, listener := range server.Listen {
if entry.ListenerAddress == listener {
return &entry
}
}
}
return nil
}()
// if none apply, then move to the next server
if opts == nil {
continue
}
// set all the options
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
server.ReadTimeout = opts.ReadTimeout
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
server.WriteTimeout = opts.WriteTimeout
server.IdleTimeout = opts.IdleTimeout
server.KeepAliveInterval = opts.KeepAliveInterval
server.MaxHeaderBytes = opts.MaxHeaderBytes
server.Protocols = opts.Protocols
server.StrictSNIHost = opts.StrictSNIHost
server.Metrics = opts.Metrics
if opts.ShouldLogCredentials {
if server.Logs == nil {
server.Logs = &caddyhttp.ServerLogConfig{}
}
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
}
}
return nil
}
caddy-2.6.2/caddyconfig/httpcaddyfile/tlsapp.go 0000664 0000000 0000000 00000060647 14350072374 0021557 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
)
func (st ServerType) buildTLSApp(
pairings []sbAddrAssociation,
options map[string]any,
warnings []caddyconfig.Warning,
) (*caddytls.TLS, []caddyconfig.Warning, error) {
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
var certLoaders []caddytls.CertificateLoader
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
autoHTTPS := "on"
if ah, ok := options["auto_https"].(string); ok {
autoHTTPS = ah
}
// find all hosts that share a server block with a hostless
// key, so that they don't get forgotten/omitted by auto-HTTPS
// (since they won't appear in route matchers)
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
if autoHTTPS != "off" {
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
if addr.Host == "" {
// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
}
break
}
}
}
}
}
// a catch-all automation policy is used as a "default" for all subjects that
// don't have custom configuration explicitly associated with them; this
// is only to add if the global settings or defaults are non-empty
catchAllAP, err := newBaseAutomationPolicy(options, warnings, false)
if err != nil {
return nil, warnings, err
}
if catchAllAP != nil {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
}
for _, p := range pairings {
// avoid setting up TLS automation policies for a server that is HTTP-only
if !listenersUseAnyPortOtherThan(p.addresses, httpPort) {
continue
}
for _, sblock := range p.serverBlocks {
// check the scheme of all the site addresses,
// skip building AP if they all had http://
if sblock.isAllHTTP() {
continue
}
// get values that populate an automation policy for this block
ap, err := newBaseAutomationPolicy(options, warnings, true)
if err != nil {
return nil, warnings, err
}
sblockHosts := sblock.hostsFromKeys(false)
if len(sblockHosts) == 0 && catchAllAP != nil {
ap = catchAllAP
}
// on-demand tls
if _, ok := sblock.pile["tls.on_demand"]; ok {
ap.OnDemand = true
}
if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok {
ap.KeyType = keyTypeVals[0].Value.(string)
}
// certificate issuers
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
var issuers []certmagic.Issuer
for _, issuerVal := range issuerVals {
issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
}
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
// this more correctly implements an error check that was removed
// below; try it with this config:
//
// :443 {
// bind 127.0.0.1
// }
//
// :443 {
// bind ::1
// tls {
// issuer acme
// }
// }
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
}
ap.Issuers = issuers
}
// certificate managers
if certManagerVals, ok := sblock.pile["tls.cert_manager"]; ok {
for _, certManager := range certManagerVals {
certGetterName := certManager.Value.(caddy.Module).CaddyModule().ID.Name()
ap.ManagersRaw = append(ap.ManagersRaw, caddyconfig.JSONModuleObject(certManager.Value, "via", certGetterName, &warnings))
}
}
// custom bind host
for _, cfgVal := range sblock.pile["bind"] {
for _, iss := range ap.Issuers {
// if an issuer was already configured and it is NOT an ACME issuer,
// skip, since we intend to adjust only ACME issuers; ensure we
// include any issuer that embeds/wraps an underlying ACME issuer
var acmeIssuer *caddytls.ACMEIssuer
if acmeWrapper, ok := iss.(acmeCapable); ok {
acmeIssuer = acmeWrapper.GetACMEIssuer()
}
if acmeIssuer == nil {
continue
}
// proceed to configure the ACME issuer's bind host, without
// overwriting any existing settings
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.BindHost == "" {
// only binding to one host is supported
var bindHost string
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
bindHost = bindHosts[0]
}
acmeIssuer.Challenges.BindHost = bindHost
}
}
}
// we used to ensure this block is allowed to create an automation policy;
// doing so was forbidden if it has a key with no host (i.e. ":443")
// and if there is a different server block that also has a key with no
// host -- since a key with no host matches any host, we need its
// associated automation policy to have an empty Subjects list, i.e. no
// host filter, which is indistinguishable between the two server blocks
// because automation is not done in the context of a particular server...
// this is an example of a poor mapping from Caddyfile to JSON but that's
// the least-leaky abstraction I could figure out -- however, this check
// was preventing certain listeners, like those provided by plugins, from
// being used as desired (see the Tailscale listener plugin), so I removed
// the check: and I think since I originally wrote the check I added a new
// check above which *properly* detects this ambiguity without breaking the
// listener plugin; see the check above with a commented example config
if len(sblockHosts) == 0 && catchAllAP == nil {
// this server block has a key with no hosts, but there is not yet
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
}
// associate our new automation policy with this server block's hosts
ap.Subjects = sblock.hostsFromKeysNotHTTP(httpPort)
sort.Strings(ap.Subjects) // solely for deterministic test results
// if a combination of public and internal names were given
// for this same server block and no issuer was specified, we
// need to separate them out in the automation policies so
// that the internal names can use the internal issuer and
// the other names can use the default/public/ACME issuer
var ap2 *caddytls.AutomationPolicy
if len(ap.Issuers) == 0 {
var internal, external []string
for _, s := range ap.Subjects {
if !certmagic.SubjectQualifiesForCert(s) {
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
}
// we don't use certmagic.SubjectQualifiesForPublicCert() because of one nuance:
// names like *.*.tld that may not qualify for a public certificate are actually
// fine when used with OnDemand, since OnDemand (currently) does not obtain
// wildcards (if it ever does, there will be a separate config option to enable
// it that we would need to check here) since the hostname is known at handshake;
// and it is unexpected to switch to internal issuer when the user wants to get
// regular certificates on-demand for a class of certs like *.*.tld.
if subjectQualifiesForPublicCert(ap, s) {
external = append(external, s)
} else {
internal = append(internal, s)
}
}
if len(external) > 0 && len(internal) > 0 {
ap.Subjects = external
apCopy := *ap
ap2 = &apCopy
ap2.Subjects = internal
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
}
}
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
if ap2 != nil {
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
}
// certificate loaders
if clVals, ok := sblock.pile["tls.cert_loader"]; ok {
for _, clVal := range clVals {
certLoaders = append(certLoaders, clVal.Value.(caddytls.CertificateLoader))
}
}
}
}
// group certificate loaders by module name, then add to config
if len(certLoaders) > 0 {
loadersByName := make(map[string]caddytls.CertificateLoader)
for _, cl := range certLoaders {
name := caddy.GetModuleName(cl)
// ugh... technically, we may have multiple FileLoader and FolderLoader
// modules (because the tls directive returns one per occurrence), but
// the config structure expects only one instance of each kind of loader
// module, so we have to combine them... instead of enumerating each
// possible cert loader module in a type switch, we can use reflection,
// which works on any cert loaders that are slice types
if reflect.TypeOf(cl).Kind() == reflect.Slice {
combined := reflect.ValueOf(loadersByName[name])
if !combined.IsValid() {
combined = reflect.New(reflect.TypeOf(cl)).Elem()
}
clVal := reflect.ValueOf(cl)
for i := 0; i < clVal.Len(); i++ {
combined = reflect.Append(combined, clVal.Index(i))
}
loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
}
}
for certLoaderName, loaders := range loadersByName {
tlsApp.CertificatesRaw[certLoaderName] = caddyconfig.JSON(loaders, &warnings)
}
}
// set any of the on-demand options, for if/when on-demand TLS is enabled
if onDemand, ok := options["on_demand_tls"].(*caddytls.OnDemandConfig); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.OnDemand = onDemand
}
// set the storage clean interval if configured
if storageCleanInterval, ok := options["storage_clean_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.StorageCleanInterval = storageCleanInterval
}
// set the expired certificates renew interval if configured
if renewCheckInterval, ok := options["renew_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.RenewCheckInterval = renewCheckInterval
}
// set the OCSP check interval if configured
if ocspCheckInterval, ok := options["ocsp_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.OCSPCheckInterval = ocspCheckInterval
}
// set whether OCSP stapling should be disabled for manually-managed certificates
if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok {
tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling
}
// if any hostnames appear on the same server block as a key with
// no host, they will not be used with route matchers because the
// hostless key matches all hosts, therefore, it wouldn't be
// considered for auto-HTTPS, so we need to make sure those hosts
// are manually considered for managed certificates; we also need
// to make sure that any of these names which are internal-only
// get internal certificates by default rather than ACME
var al caddytls.AutomateLoader
internalAP := &caddytls.AutomationPolicy{
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
}
if autoHTTPS != "off" {
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.Subjects = append(internalAP.Subjects, h)
}
}
}
if len(al) > 0 {
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
}
if len(internalAP.Subjects) > 0 {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, internalAP)
}
// if there are any global options set for issuers (ACME ones in particular), make sure they
// take effect in every automation policy that does not have any issuers
if tlsApp.Automation != nil {
globalEmail := options["email"]
globalACMECA := options["acme_ca"]
globalACMECARoot := options["acme_ca_root"]
globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"]
globalPreferredChains := options["preferred_chains"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
if hasGlobalACMEDefaults {
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
ap := tlsApp.Automation.Policies[i]
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()
// 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)}
}
}
}
}
}
}
// finalize and verify policies; do cleanup
if tlsApp.Automation != nil {
for i, ap := range tlsApp.Automation.Policies {
// ensure all issuers have global defaults filled in
for j, issuer := range ap.Issuers {
err := fillInGlobalACMEDefaults(issuer, options)
if err != nil {
return nil, warnings, fmt.Errorf("filling in global issuer defaults for AP %d, issuer %d: %v", i, j, err)
}
}
// encode all issuer values we created, so they will be rendered in the output
if len(ap.Issuers) > 0 && ap.IssuersRaw == nil {
for _, iss := range ap.Issuers {
issuerName := iss.(caddy.Module).CaddyModule().ID.Name()
ap.IssuersRaw = append(ap.IssuersRaw, caddyconfig.JSONModuleObject(iss, "module", issuerName, &warnings))
}
}
}
// consolidate automation policies that are the exact same
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
// ensure automation policies don't overlap subjects (this should be
// an error at provision-time as well, but catch it in the adapt phase
// for convenience)
automationHostSet := make(map[string]struct{})
for _, ap := range tlsApp.Automation.Policies {
for _, s := range ap.Subjects {
if _, ok := automationHostSet[s]; ok {
return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
}
automationHostSet[s] = struct{}{}
}
}
// if nothing remains, remove any excess values to clean up the resulting config
if len(tlsApp.Automation.Policies) == 0 {
tlsApp.Automation.Policies = nil
}
if reflect.DeepEqual(tlsApp.Automation, new(caddytls.AutomationConfig)) {
tlsApp.Automation = nil
}
}
return tlsApp, warnings, nil
}
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) error {
acmeWrapper, ok := issuer.(acmeCapable)
if !ok {
return nil
}
acmeIssuer := acmeWrapper.GetACMEIssuer()
if acmeIssuer == nil {
return nil
}
globalEmail := options["email"]
globalACMECA := options["acme_ca"]
globalACMECARoot := options["acme_ca_root"]
globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"]
globalPreferredChains := options["preferred_chains"]
if globalEmail != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = globalEmail.(string)
}
if globalACMECA != nil && acmeIssuer.CA == "" {
acmeIssuer.CA = globalACMECA.(string)
}
if globalACMECARoot != nil && !sliceContains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
}
if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
DNS: &caddytls.DNSChallengeConfig{
ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil),
},
}
}
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
}
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
}
return nil
}
// newBaseAutomationPolicy returns a new TLS automation policy that gets
// its values from the global options map. It should be used as the base
// for any other automation policies. A nil policy (and no error) will be
// returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error).
func newBaseAutomationPolicy(options map[string]any, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
issuers, hasIssuers := options["cert_issuer"]
_, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"]
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
// if there are no global options related to automation policies
// set, then we can just return right away
if !hasGlobalAutomationOpts {
if always {
return new(caddytls.AutomationPolicy), nil
}
return nil, nil
}
ap := new(caddytls.AutomationPolicy)
if hasKeyType {
ap.KeyType = keyType.(string)
}
if hasIssuers && hasLocalCerts {
return nil, fmt.Errorf("global options are ambiguous: local_certs is confusing when combined with cert_issuer, because local_certs is also a specific kind of issuer")
}
if hasIssuers {
ap.Issuers = issuers.([]certmagic.Issuer)
} else if hasLocalCerts {
ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
}
if hasOCSPStapling {
ocspConfig := ocspStapling.(certmagic.OCSPConfig)
ap.DisableOCSPStapling = ocspConfig.DisableStapling
ap.OCSPOverrides = ocspConfig.ResponderOverrides
}
return ap, nil
}
// consolidateAutomationPolicies combines automation policies that are the same,
// for a cleaner overall output.
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
// sort from most specific to least specific; we depend on this ordering
sort.SliceStable(aps, func(i, j int) bool {
if automationPolicyIsSubset(aps[i], aps[j]) {
return true
}
if automationPolicyIsSubset(aps[j], aps[i]) {
return false
}
return len(aps[i].Subjects) > len(aps[j].Subjects)
})
emptyAPCount := 0
origLenAPs := len(aps)
// compute the number of empty policies (disregarding subjects) - see #4128
emptyAP := new(caddytls.AutomationPolicy)
for i := 0; i < len(aps); i++ {
emptyAP.Subjects = aps[i].Subjects
if reflect.DeepEqual(aps[i], emptyAP) {
emptyAPCount++
if !automationPolicyHasAllPublicNames(aps[i]) {
// if this automation policy has internal names, we might as well remove it
// so auto-https can implicitly use the internal issuer
aps = append(aps[:i], aps[i+1:]...)
i--
}
}
}
// If all policies are empty, we can return nil, as there is no need to set any policy
if emptyAPCount == origLenAPs {
return nil
}
// remove or combine duplicate policies
outer:
for i := 0; i < len(aps); i++ {
// compare only with next policies; we sorted by specificity so we must not delete earlier policies
for j := i + 1; j < len(aps); j++ {
// if they're exactly equal in every way, just keep one of them
if reflect.DeepEqual(aps[i], aps[j]) {
aps = append(aps[:j], aps[j+1:]...)
// must re-evaluate current i against next j; can't skip it!
// even if i decrements to -1, will be incremented to 0 immediately
i--
continue outer
}
// if the policy is the same, we can keep just one, but we have
// to be careful which one we keep; if only one has any hostnames
// defined, then we need to keep the one without any hostnames,
// otherwise the one without any subjects (a catch-all) would be
// eaten up by the one with subjects; and if both have subjects, we
// need to combine their lists
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
aps[i].MustStaple == aps[j].MustStaple &&
aps[i].KeyType == aps[j].KeyType &&
aps[i].OnDemand == aps[j].OnDemand &&
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
// later policy (at j) has no subjects ("catch-all"), so we can
// remove the identical-but-more-specific policy that comes first
// AS LONG AS it is not shadowed by another policy before it; e.g.
// if policy i is for example.com, policy i+1 is '*.com', and policy
// j is catch-all, we cannot remove policy i because that would
// cause example.com to be served by the less specific policy for
// '*.com', which might be different (yes we've seen this happen)
if automationPolicyShadows(i, aps) >= j {
aps = append(aps[:i], aps[i+1:]...)
i--
continue outer
}
} else {
// avoid repeated subjects
for _, subj := range aps[j].Subjects {
if !sliceContains(aps[i].Subjects, subj) {
aps[i].Subjects = append(aps[i].Subjects, subj)
}
}
aps = append(aps[:j], aps[j+1:]...)
j--
}
}
}
}
return aps
}
// automationPolicyIsSubset returns true if a's subjects are a subset
// of b's subjects.
func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
if len(b.Subjects) == 0 {
return true
}
if len(a.Subjects) == 0 {
return false
}
for _, aSubj := range a.Subjects {
var inSuperset bool
for _, bSubj := range b.Subjects {
if certmagic.MatchWildcard(aSubj, bSubj) {
inSuperset = true
break
}
}
if !inSuperset {
return false
}
}
return true
}
// automationPolicyShadows returns the index of a policy that aps[i] shadows;
// in other words, for all policies after position i, if that policy covers
// the same subjects but is less specific, that policy's position is returned,
// or -1 if no shadowing is found. For example, if policy i is for
// "foo.example.com" and policy i+2 is for "*.example.com", then i+2 will be
// returned, since that policy is shadowed by i, which is in front.
func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
for j := i + 1; j < len(aps); j++ {
if automationPolicyIsSubset(aps[i], aps[j]) {
return j
}
}
return -1
}
// 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).
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
return !certmagic.SubjectIsIP(subj) &&
!certmagic.SubjectIsInternal(subj) &&
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
}
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
for _, subj := range ap.Subjects {
if !subjectQualifiesForPublicCert(ap, subj) {
return false
}
}
return true
}
caddy-2.6.2/caddyconfig/httpcaddyfile/tlsapp_test.go 0000664 0000000 0000000 00000002323 14350072374 0022601 0 ustar 00root root 0000000 0000000 package httpcaddyfile
import (
"testing"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
func TestAutomationPolicyIsSubset(t *testing.T) {
for i, test := range []struct {
a, b []string
expect bool
}{
{
a: []string{"example.com"},
b: []string{},
expect: true,
},
{
a: []string{},
b: []string{"example.com"},
expect: false,
},
{
a: []string{"foo.example.com"},
b: []string{"*.example.com"},
expect: true,
},
{
a: []string{"foo.example.com"},
b: []string{"foo.example.com"},
expect: true,
},
{
a: []string{"foo.example.com"},
b: []string{"example.com"},
expect: false,
},
{
a: []string{"example.com", "foo.example.com"},
b: []string{"*.com", "*.*.com"},
expect: true,
},
{
a: []string{"example.com", "foo.example.com"},
b: []string{"*.com"},
expect: false,
},
} {
apA := &caddytls.AutomationPolicy{Subjects: test.a}
apB := &caddytls.AutomationPolicy{Subjects: test.b}
if actual := automationPolicyIsSubset(apA, apB); actual != test.expect {
t.Errorf("Test %d: Expected %t but got %t (A: %v B: %v)", i, test.expect, actual, test.a, test.b)
}
}
}
caddy-2.6.2/caddyconfig/httploader.go 0000664 0000000 0000000 00000012773 14350072374 0017573 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyconfig
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/caddyserver/caddy/v2"
)
func init() {
caddy.RegisterModule(HTTPLoader{})
}
// HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
// based on the Content-Type header of the HTTP response.
type HTTPLoader struct {
// The method for the request. Default: GET
Method string `json:"method,omitempty"`
// The URL of the request.
URL string `json:"url,omitempty"`
// HTTP headers to add to the request.
Headers http.Header `json:"header,omitempty"`
// Maximum time allowed for a complete connection and request.
Timeout caddy.Duration `json:"timeout,omitempty"`
TLS *struct {
// Present this instance's managed remote identity credentials to the server.
UseServerIdentity bool `json:"use_server_identity,omitempty"`
// PEM-encoded client certificate filename to present to the server.
ClientCertificateFile string `json:"client_certificate_file,omitempty"`
// PEM-encoded key to use with the client certificate.
ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"`
// 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"`
} `json:"tls,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.config_loaders.http",
New: func() caddy.Module { return new(HTTPLoader) },
}
}
// LoadConfig loads a Caddy config.
func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
repl := caddy.NewReplacer()
client, err := hl.makeClient(ctx)
if err != nil {
return nil, err
}
method := repl.ReplaceAll(hl.Method, "")
if method == "" {
method = http.MethodGet
}
url := repl.ReplaceAll(hl.URL, "")
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
for key, vals := range hl.Headers {
for _, val := range vals {
req.Header.Add(repl.ReplaceAll(key, ""), repl.ReplaceKnown(val, ""))
}
}
resp, err := doHttpCallWithRetries(ctx, client, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body)
if err != nil {
return nil, err
}
for _, warn := range warnings {
ctx.Logger().Warn(warn.String())
}
return result, nil
}
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
resp, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("problem calling http loader url: %v", err)
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
}
return resp, nil
}
func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
const maxAttempts = 10
// attempt up to 10 times
for i := 0; i < maxAttempts; i++ {
resp, err = attemptHttpCall(client, request)
if err != nil && i < maxAttempts-1 {
// wait 500ms before reattempting, or until context is done
select {
case <-time.After(time.Millisecond * 500):
case <-ctx.Done():
return resp, ctx.Err()
}
}
}
return resp, err
}
func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
client := &http.Client{
Timeout: time.Duration(hl.Timeout),
}
if hl.TLS != nil {
var tlsConfig *tls.Config
// client authentication
if hl.TLS.UseServerIdentity {
certs, err := ctx.IdentityCredentials(ctx.Logger())
if err != nil {
return nil, fmt.Errorf("getting server identity credentials: %v", err)
}
if tlsConfig == nil {
tlsConfig = new(tls.Config)
}
tlsConfig.Certificates = certs
} else if hl.TLS.ClientCertificateFile != "" && hl.TLS.ClientCertificateKeyFile != "" {
cert, err := tls.LoadX509KeyPair(hl.TLS.ClientCertificateFile, hl.TLS.ClientCertificateKeyFile)
if err != nil {
return nil, err
}
if tlsConfig == nil {
tlsConfig = new(tls.Config)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
// trusted server certs
if len(hl.TLS.RootCAPEMFiles) > 0 {
rootPool := x509.NewCertPool()
for _, pemFile := range hl.TLS.RootCAPEMFiles {
pemData, err := os.ReadFile(pemFile)
if err != nil {
return nil, fmt.Errorf("failed reading ca cert: %v", err)
}
rootPool.AppendCertsFromPEM(pemData)
}
if tlsConfig == nil {
tlsConfig = new(tls.Config)
}
tlsConfig.RootCAs = rootPool
}
client.Transport = &http.Transport{TLSClientConfig: tlsConfig}
}
return client, nil
}
var _ caddy.ConfigLoader = (*HTTPLoader)(nil)
caddy-2.6.2/caddyconfig/load.go 0000664 0000000 0000000 00000013365 14350072374 0016342 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyconfig
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"strings"
"sync"
"github.com/caddyserver/caddy/v2"
)
func init() {
caddy.RegisterModule(adminLoad{})
}
// adminLoad is a module that provides the /load endpoint
// for the Caddy admin API. The only reason it's not baked
// into the caddy package directly is because of the import
// of the caddyconfig package for its GetAdapter function.
// If the caddy package depends on the caddyconfig package,
// then the caddyconfig package will not be able to import
// the caddy package, and it can more easily cause backward
// edges in the dependency tree (i.e. import cycle).
// Fortunately, the admin API has first-class support for
// adding endpoints from modules.
type adminLoad struct{}
// CaddyModule returns the Caddy module information.
func (adminLoad) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "admin.api.load",
New: func() caddy.Module { return new(adminLoad) },
}
}
// Routes returns a route for the /load endpoint.
func (al adminLoad) Routes() []caddy.AdminRoute {
return []caddy.AdminRoute{
{
Pattern: "/load",
Handler: caddy.AdminHandlerFunc(al.handleLoad),
},
{
Pattern: "/adapt",
Handler: caddy.AdminHandlerFunc(al.handleAdapt),
},
}
}
// handleLoad replaces the entire current configuration with
// a new one provided in the response body. It supports config
// adapters through the use of the Content-Type header. A
// config that is identical to the currently-running config
// will be a no-op unless Cache-Control: must-revalidate is set.
func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return caddy.APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
_, err := io.Copy(buf, r.Body)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
}
}
body := buf.Bytes()
// if the config is formatted other than Caddy's native
// JSON, we need to adapt it before loading it
if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" {
result, warnings, err := adaptByContentType(ctHeader, body)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: err,
}
}
if len(warnings) > 0 {
respBody, err := json.Marshal(warnings)
if err != nil {
caddy.Log().Named("admin.api.load").Error(err.Error())
}
_, _ = w.Write(respBody)
}
body = result
}
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
err = caddy.Load(body, forceReload)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("loading config: %v", err),
}
}
caddy.Log().Named("admin.api").Info("load complete")
return nil
}
// handleAdapt adapts the given Caddy config to JSON and responds with the result.
func (adminLoad) handleAdapt(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return caddy.APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
_, err := io.Copy(buf, r.Body)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
}
}
result, warnings, err := adaptByContentType(r.Header.Get("Content-Type"), buf.Bytes())
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: err,
}
}
out := struct {
Warnings []Warning `json:"warnings,omitempty"`
Result json.RawMessage `json:"result"`
}{
Warnings: warnings,
Result: result,
}
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(out)
}
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contentType.
// If contentType is empty or ends with "/json", the input will be returned, as a no-op.
func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
// assume JSON as the default
if contentType == "" {
return body, nil, nil
}
ct, _, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, nil, caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("invalid Content-Type: %v", err),
}
}
// if already JSON, no need to adapt
if strings.HasSuffix(ct, "/json") {
return body, nil, nil
}
// adapter name should be suffix of MIME type
_, adapterName, slashFound := strings.Cut(ct, "/")
if !slashFound {
return nil, nil, fmt.Errorf("malformed Content-Type")
}
cfgAdapter := GetAdapter(adapterName)
if cfgAdapter == nil {
return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
}
result, warnings, err := cfgAdapter.Adapt(body, nil)
if err != nil {
return nil, nil, fmt.Errorf("adapting config using %s adapter: %v", adapterName, err)
}
return result, warnings, nil
}
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
caddy-2.6.2/caddytest/ 0000775 0000000 0000000 00000000000 14350072374 0014576 5 ustar 00root root 0000000 0000000 caddy-2.6.2/caddytest/a.caddy.localhost.crt 0000664 0000000 0000000 00000002607 14350072374 0020607 0 ustar 00root root 0000000 0000000 -----BEGIN CERTIFICATE-----
MIID5zCCAs8CFG4+w/pqR5AZQ+aVB330uRRRKMF0MA0GCSqGSIb3DQEBCwUAMIGv
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZBgNVBAoMEkxvY2FsIERldmVs
b3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxvcGVtZW50MRowGAYDVQQDDBFh
LmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9jYWwgRGV2ZWxvcGVtZW50MSAw
HgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2NhbDAeFw0yMDAzMTMxODUwMTda
Fw0zMDAzMTExODUwMTdaMIGvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZ
BgNVBAoMEkxvY2FsIERldmVsb3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxv
cGVtZW50MRowGAYDVQQDDBFhLmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9j
YWwgRGV2ZWxvcGVtZW50MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2Nh
bDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMd9pC9wF7j0459FndPs
Deud/rq41jEZFsVOVtjQgjS1A5ct6NfeMmSlq8i1F7uaTMPZjbOHzY6y6hzLc9+y
/VWNgyUC543HjXnNTnp9Xug6tBBxOxvRMw5mv2nAyzjBGDePPgN84xKhOXG2Wj3u
fOZ+VPVISefRNvjKfN87WLJ0B0HI9wplG5ASVdPQsWDY1cndrZgt2sxQ/3fjIno4
VvrgRWC9Penizgps/a0ZcFZMD/6HJoX/mSZVa1LjopwbMTXvyHCpXkth21E+rBt6
I9DMHerdioVQcX25CqPmAwePxPZSNGEQo/Qu32kzcmscmYxTtYBhDa+yLuHgGggI
j7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAP/94KPtkpYtkWADnhtzDmgQ6Q1pH
SubTUZdCwQtm6/LrvpT+uFNsOj4L3Mv3TVUnIQDmKd5VvR42W2MRBiTN2LQptgEn
C7g9BB+UA9kjL3DPk1pJMjzxLHohh0uNLi7eh4mAj8eNvjz9Z4qMWPQoVS0y7/ZK
cCBRKh2GkIqKm34ih6pX7xmMpPEQsFoTVPRHYJfYD1SZ8Iui+EN+7WqLuJWPsPXw
JM1HuZKn7pZmJU2MZZBsrupHGUvNMbBg2mFJcxt4D1VvU+p+a67PSjpFQ6dJG2re
pZoF+N1vMGAFkxe6UqhcC/bXDX+ILVQHJ+RNhzDO6DcWf8dRrC2LaJk3WA==
-----END CERTIFICATE-----
caddy-2.6.2/caddytest/a.caddy.localhost.key 0000664 0000000 0000000 00000003217 14350072374 0020605 0 ustar 00root root 0000000 0000000 -----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAx32kL3AXuPTjn0Wd0+wN653+urjWMRkWxU5W2NCCNLUDly3o
194yZKWryLUXu5pMw9mNs4fNjrLqHMtz37L9VY2DJQLnjceNec1Oen1e6Dq0EHE7
G9EzDma/acDLOMEYN48+A3zjEqE5cbZaPe585n5U9UhJ59E2+Mp83ztYsnQHQcj3
CmUbkBJV09CxYNjVyd2tmC3azFD/d+MiejhW+uBFYL096eLOCmz9rRlwVkwP/ocm
hf+ZJlVrUuOinBsxNe/IcKleS2HbUT6sG3oj0Mwd6t2KhVBxfbkKo+YDB4/E9lI0
YRCj9C7faTNyaxyZjFO1gGENr7Iu4eAaCAiPsQIDAQABAoIBAQDD/YFIBeWYlifn
e9risQDAIrp3sk7lb9O6Rwv1+Wxi4hBEABvJsYhq74VFK/3EF4UhyWR5JIvkjYyK
e6w887oGyoA05ZSe65XoO7fFidSrbbkoikZbPv3dQT7/ZCWEfdkQBNAVVyY0UGeC
e3hPbjYRsb5AOSQ694X9idqC6uhqcOrBDjITFrctUoP4S6l9A6a+mLSUIwiICcuh
mrNl+j0lzy7DMXRp/Z5Hyo5kuUlrC0dCLa1UHqtrrK7MR55AVEOihSNp1w+OC+vw
f0VjE4JUtO7RQEQUmD1tDfLXwNfMFeWaobB2W0WMvRg0IqoitiqPxsPHRm56OxfM
SRo/Q7QBAoGBAP8DapzBMuaIcJ7cE8Yl07ZGndWWf8buIKIItGF8rkEO3BXhrIke
EmpOi+ELtpbMOG0APhORZyQ58f4ZOVrqZfneNKtDiEZV4mJZaYUESm1pU+2Y6+y5
g4bpQSVKN0ow0xR+MH7qDYtSlsmBU7qAOz775L7BmMA1Bnu72aN/H1JBAoGBAMhD
OzqCSakHOjUbEd22rPwqWmcIyVyo04gaSmcVVT2dHbqR4/t0gX5a9D9U2qwyO6xi
/R+PXyMd32xIeVR2D/7SQ0x6dK68HXICLV8ofHZ5UQcHbxy5og4v/YxSZVTkN374
cEsUeyB0s/UPOHLktFU5hpIlON72/Rp7b+pNIwFxAoGAczpq+Qu/YTWzlcSh1r4O
7OT5uqI3eH7vFehTAV3iKxl4zxZa7NY+wfRd9kFhrr/2myIp6pOgBFl+hC+HoBIc
JAyIxf5M3GNAWOpH6MfojYmzV7/qktu8l8BcJGplk0t+hVsDtMUze4nFAqZCXBpH
Kw2M7bjyuZ78H/rgu6TcVUECgYEAo1M5ldE2U/VCApeuLX1TfWDpU8i1uK0zv3d5
oLKkT1i5KzTak3SEO9HgC1qf8PoS8tfUio26UICHe99rnHehOfivzEq+qNdgyF+A
M3BoeZMdgzcL5oh640k+Zte4LtDlddcWdhUhCepD7iPYrNNbQ3pkBwL2a9lRuOxc
7OC2IPECgYBH8f3OrwXjDltIG1dDvuDPNljxLZbFEFbQyVzMePYNftgZknAyGEdh
NW/LuWeTzstnmz/s6RE3jN5ZrrMa4sW77VA9+yU9QW2dkHqFyukQ4sfuNg6kDDNZ
+lqZYMCLw0M5P9fIbmnIYwey7tXkHfmzoCpnYHGQDN6hL0Bh0zGwmg==
-----END RSA PRIVATE KEY-----
caddy-2.6.2/caddytest/caddy.ca.cer 0000664 0000000 0000000 00000002263 14350072374 0016742 0 ustar 00root root 0000000 0000000 -----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkw
ODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU
7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl0
3WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45t
wOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNx
tdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTU
ApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAd
BgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS
2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5u
NY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkq
hkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfK
D66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEO
fG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnk
oNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZ
ks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdle
Ih6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
-----END CERTIFICATE----- caddy-2.6.2/caddytest/caddy.localhost.crt 0000664 0000000 0000000 00000002607 14350072374 0020370 0 ustar 00root root 0000000 0000000 -----BEGIN CERTIFICATE-----
MIID5zCCAs8CFFmAAFKV79uhzxc5qXbUw3oBNsYXMA0GCSqGSIb3DQEBCwUAMIGv
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZBgNVBAoMEkxvY2FsIERldmVs
b3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxvcGVtZW50MRowGAYDVQQDDBEq
LmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9jYWwgRGV2ZWxvcGVtZW50MSAw
HgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2NhbDAeFw0yMDAzMDIwODAxMTZa
Fw0zMDAyMjgwODAxMTZaMIGvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZ
BgNVBAoMEkxvY2FsIERldmVsb3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxv
cGVtZW50MRowGAYDVQQDDBEqLmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9j
YWwgRGV2ZWxvcGVtZW50MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2Nh
bDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJngfeirQkWaU8ihgIC5
SKpRQX/3koRjljDK/oCbhLs+wg592kIwVv06l7+mn7NSaNBloabjuA1GqyLRsNLL
ptrv0HvXa5qLx28+icsb2Ny3dJnQaj9w9PwjxQ1qZqEJfWRH1D8Vz9AmB+QSV/Gu
8e8alGFewlYZVfH1kbxoTT6QorF37TeA3bh1fgKFtzsGYKswcaZNdDBBHzLunCKZ
HU6U6L45hm+yLADj3mmDLafUeiVOt6MRLLoSD1eLRVSXGrNo+brJ87zkZntI9+W1
JxOBoXtZCwka7k2DlAtLihsrmBZA2ZC9yVeu/SQy3qb3iCNnTFTCyAnWeTCr6Tcq
6w8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOWfXqpAmD4C3wGiMeZAeaaS4hDAR
+JmN+avPDA6F6Bq7DB4NJuIwVUlaDL2s07w5VJJtW52aZVKoBlgHR5yG/XUli6J7
YUJRmdQJvHUSu26cmKvyoOaTrEYbmvtGICWtZc8uTlMf9wQZbJA4KyxTgEQJDXsZ
B2XFe+wVdhAgEpobYDROi+l/p8TL5z3U24LpwVTcJy5sEZVv7Wfs886IyxU8ORt8
VZNcDiH6V53OIGeiufIhia/mPe6jbLntfGZfIFxtCcow4IA/lTy1ned7K5fmvNNb
ZilxOQUk+wVK8genjdrZVAnAxsYLHJIb5yf9O7rr6fWciVMF3a0k5uNK1w==
-----END CERTIFICATE-----
caddy-2.6.2/caddytest/caddy.localhost.key 0000664 0000000 0000000 00000003213 14350072374 0020362 0 ustar 00root root 0000000 0000000 -----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAmeB96KtCRZpTyKGAgLlIqlFBf/eShGOWMMr+gJuEuz7CDn3a
QjBW/TqXv6afs1Jo0GWhpuO4DUarItGw0sum2u/Qe9drmovHbz6JyxvY3Ld0mdBq
P3D0/CPFDWpmoQl9ZEfUPxXP0CYH5BJX8a7x7xqUYV7CVhlV8fWRvGhNPpCisXft
N4DduHV+AoW3OwZgqzBxpk10MEEfMu6cIpkdTpTovjmGb7IsAOPeaYMtp9R6JU63
oxEsuhIPV4tFVJcas2j5usnzvORme0j35bUnE4Ghe1kLCRruTYOUC0uKGyuYFkDZ
kL3JV679JDLepveII2dMVMLICdZ5MKvpNyrrDwIDAQABAoIBAFcPK01zb6hfm12c
+k5aBiHOnUdgc/YRPg1XHEz5MEycQkDetZjTLrRQ7UBSbnKPgpu9lIsOtbhVLkgh
6XAqJroiCou2oruqr+hhsqZGmBiwdvj7cNF6ADGTr05az7v22YneFdinZ481pStF
sZocx+bm2+KHMV5zMSwXKyA0xtdJLxs2yklniDBxSZRppgppq1pDPprP5DkgKPfe
3ekUmbQd5bHmivhW8ItbJLuf82XSsMBZ9ZhKiKIlWlbKAgiSV3SqnUQb5fi7l8hG
yYZxbuCUIGFwKmEpUBBt/nyxrOlMiNtDh9JhrPmijTV3slq70pCLwLL/Ai2aeear
EVA5VhkCgYEAyAmxfPqc2P7BsDAp67/sA7OEPso9qM4WyuWiVdlX2gb9TLNLYbPX
Kk/UmpAIVzpoTAGY5Zp3wkvdD/ou8uUQsE8ioNn4S1a4G9XURH1wVhcEbUiAKI1S
QVBH9B/Pj3eIp5OTKwob0Wj7DNdxoH7ed/Eok0EaTWzOA8pCWADKv/MCgYEAxOzY
YsX7Nl+eyZr2+9unKyeAK/D1DCT/o99UUAHx72/xaBVP/06cfzpvKBNcF9iYc+fq
R1yIUIrDRoSmYKBq+Kb3+nOg1nrqih/NBTokbTiI4Q+/30OQt0Al1e7y9iNKqV8H
jYZItzluGNrWKedZbATwBwbVCY2jnNl6RMDnS3UCgYBxj3cwQUHLuoyQjjcuO80r
qLzZvIxWiXDNDKIk5HcIMlGYOmz/8U2kGp/SgxQJGQJeq8V2C0QTjGfaCyieAcaA
oNxCvptDgd6RBsoze5bLeNOtiqwe2WOp6n5+q5R0mOJ+Z7vzghCayGNFPgWmnH+F
TeW/+wSIkc0+v5L8TK7NWwKBgBrlWlyLO9deUfqpHqihhICBYaEexOlGuF+yZfqT
eW7BdFBJ8OYm33sFCR+JHV/oZlIWT8o1Wizd9vPPtEWoQ1P4wg/D8Si6GwSIeWEI
YudD/HX4x7T/rmlI6qIAg9CYW18sqoRq3c2gm2fro6qPfYgiWIItLbWjUcBfd7Ki
QjTtAoGARKdRv3jMWL84rlEx1nBRgL3pe9Dt+Uxzde2xT3ZeF+5Hp9NfU01qE6M6
1I6H64smqpetlsXmCEVKwBemP3pJa6avLKgIYiQvHAD/v4rs9mqgy1RTqtYyGNhR
1A/6dKkbiZ6wzePLLPasXVZxSKEviXf5gJooqumQVSVhCswyCZ0=
-----END RSA PRIVATE KEY-----
caddy-2.6.2/caddytest/caddytest.go 0000664 0000000 0000000 00000036737 14350072374 0017131 0 ustar 00root root 0000000 0000000 package caddytest
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/cookiejar"
"os"
"path"
"reflect"
"regexp"
"runtime"
"strings"
"testing"
"time"
"github.com/aryann/difflib"
"github.com/caddyserver/caddy/v2/caddyconfig"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
// plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard"
)
// Defaults store any configuration required to make the tests run
type Defaults struct {
// Port we expect caddy to listening on
AdminPort int
// Certificates we expect to be loaded before attempting to run the tests
Certifcates []string
// TestRequestTimeout is the time to wait for a http request to
TestRequestTimeout time.Duration
// LoadRequestTimeout is the time to wait for the config to be loaded against the caddy server
LoadRequestTimeout time.Duration
}
// Default testing values
var Default = Defaults{
AdminPort: 2999, // different from what a real server also running on a developer's machine might be
Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second,
}
var (
matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`)
matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
)
// Tester represents an instance of a test client.
type Tester struct {
Client *http.Client
configLoaded bool
t *testing.T
}
// NewTester will create a new testing client with an attached cookie jar
func NewTester(t *testing.T) *Tester {
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatalf("failed to create cookiejar: %s", err)
}
return &Tester{
Client: &http.Client{
Transport: CreateTestingTransport(),
Jar: jar,
Timeout: Default.TestRequestTimeout,
},
configLoaded: false,
t: t,
}
}
type configLoadError struct {
Response string
}
func (e configLoadError) Error() string { return e.Response }
func timeElapsed(start time.Time, name string) {
elapsed := time.Since(start)
log.Printf("%s took %s", name, elapsed)
}
// InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type.
func (tc *Tester) InitServer(rawConfig string, configType string) {
if err := tc.initServer(rawConfig, configType); err != nil {
tc.t.Logf("failed to load config: %s", err)
tc.t.Fail()
}
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
tc.t.Logf("failed ensuring config is running: %s", err)
tc.t.Fail()
}
}
// InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type.
func (tc *Tester) initServer(rawConfig string, configType string) error {
if testing.Short() {
tc.t.SkipNow()
return nil
}
err := validateTestPrerequisites()
if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil
}
tc.t.Cleanup(func() {
if tc.t.Failed() && tc.configLoaded {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
tc.t.Log("unable to read the current config")
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
var out bytes.Buffer
_ = json.Indent(&out, body, "", " ")
tc.t.Logf("----------- failed with config -----------\n%s", out.String())
}
})
rawConfig = prependCaddyFilePath(rawConfig)
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
start := time.Now()
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig))
if err != nil {
tc.t.Errorf("failed to create request. %s", err)
return err
}
if configType == "json" {
req.Header.Add("Content-Type", "application/json")
} else {
req.Header.Add("Content-Type", "text/"+configType)
}
res, err := client.Do(req)
if err != nil {
tc.t.Errorf("unable to contact caddy server. %s", err)
return err
}
timeElapsed(start, "caddytest: config load time")
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
tc.t.Errorf("unable to read response. %s", err)
return err
}
if res.StatusCode != 200 {
return configLoadError{Response: string(body)}
}
tc.configLoaded = true
return nil
}
func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error {
expectedBytes := []byte(prependCaddyFilePath(rawConfig))
if configType != "json" {
adapter := caddyconfig.GetAdapter(configType)
if adapter == nil {
return fmt.Errorf("adapter of config type is missing: %s", configType)
}
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
}
var expected any
err := json.Unmarshal(expectedBytes, &expected)
if err != nil {
return err
}
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
fetchConfig := func(client *http.Client) any {
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
return nil
}
defer resp.Body.Close()
actualBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
var actual any
err = json.Unmarshal(actualBytes, &actual)
if err != nil {
return nil
}
return actual
}
for retries := 10; retries > 0; retries-- {
if reflect.DeepEqual(expected, fetchConfig(client)) {
return nil
}
time.Sleep(10 * time.Millisecond)
}
tc.t.Errorf("POSTed configuration isn't active")
return errors.New("EnsureConfigRunning: POSTed configuration isn't active")
}
// validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running.
func validateTestPrerequisites() error {
// check certificates are found
for _, certName := range Default.Certifcates {
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
}
}
if isCaddyAdminRunning() != nil {
// start inprocess caddy server
os.Args = []string{"caddy", "run", "--config", "./test.init.config", "--adapter", "caddyfile"}
go func() {
caddycmd.Main()
}()
// wait for caddy to start serving the initial config
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(10 * time.Millisecond)
}
}
// one more time to return the error
return isCaddyAdminRunning()
}
func isCaddyAdminRunning() error {
// assert that caddy is running
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort)
}
resp.Body.Close()
return nil
}
func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1)
if !ok {
panic("unable to determine the current file path")
}
return path.Dir(filename)
}
// use the convention to replace /[certificatename].[crt|key] with the full path
// this helps reduce the noise in test configurations and also allow this
// to run in any path
func prependCaddyFilePath(rawConfig string) string {
r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1")
r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1")
return r
}
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
func CreateTestingTransport() *http.Transport {
dialer := net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second,
DualStack: true,
}
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
parts := strings.Split(addr, ":")
destAddr := fmt.Sprintf("127.0.0.1:%s", parts[1])
log.Printf("caddytest: redirecting the dialer from %s to %s", addr, destAddr)
return dialer.DialContext(ctx, network, destAddr)
}
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
}
}
// AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
tc := NewTester(t)
err := tc.initServer(rawConfig, configType)
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
}
}
// AssertRedirect makes a request and asserts the redirection happens
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
// using the existing client, we override the check redirect policy for this test
old := tc.Client.CheckRedirect
tc.Client.CheckRedirect = redirectPolicyFunc
defer func() { tc.Client.CheckRedirect = old }()
resp, err := tc.Client.Get(requestURI)
if err != nil {
tc.t.Errorf("failed to call server %s", err)
return nil
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
}
loc, err := resp.Location()
if err != nil {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
}
if loc == nil && expectedToLocation != "" {
tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI)
}
if loc != nil {
if expectedToLocation != loc.String() {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
}
}
return resp
}
// CompareAdapt adapts a config and then compares it against an expected result
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
t.Logf("unrecognized config adapter '%s'", adapterName)
return false
}
options := make(map[string]any)
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil {
t.Logf("adapting config using %s adapter: %v", adapterName, err)
return false
}
// prettify results to keep tests human-manageable
var prettyBuf bytes.Buffer
err = json.Indent(&prettyBuf, result, "", "\t")
if err != nil {
return false
}
result = prettyBuf.Bytes()
if len(warnings) > 0 {
for _, w := range warnings {
t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message)
}
}
diff := difflib.Diff(
strings.Split(expectedResponse, "\n"),
strings.Split(string(result), "\n"))
// scan for failure
failed := false
for _, d := range diff {
if d.Delta != difflib.Common {
failed = true
break
}
}
if failed {
for _, d := range diff {
switch d.Delta {
case difflib.Common:
fmt.Printf(" %s\n", d.Payload)
case difflib.LeftOnly:
fmt.Printf(" - %s\n", d.Payload)
case difflib.RightOnly:
fmt.Printf(" + %s\n", d.Payload)
}
}
return false
}
return true
}
// AssertAdapt adapts a config and then tests it against an expected result
func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) {
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
if !ok {
t.Fail()
}
}
// Generic request functions
func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
requestContentType := ""
for _, requestHeader := range requestHeaders {
arr := strings.SplitAfterN(requestHeader, ":", 2)
k := strings.TrimRight(arr[0], ":")
v := strings.TrimSpace(arr[1])
if k == "Content-Type" {
requestContentType = v
}
t.Logf("Request header: %s => %s", k, v)
req.Header.Set(k, v)
}
if requestContentType == "" {
t.Logf("Content-Type header not provided")
}
}
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.Client.Do(req)
if err != nil {
tc.t.Fatalf("failed to call server %s", err)
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode)
}
return resp
}
// AssertResponse request a URI and assert the status code and the body contains a string
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
tc.t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if body != expectedBody {
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return resp, body
}
// Verb specific test functions
// AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
caddy-2.6.2/caddytest/caddytest_test.go 0000664 0000000 0000000 00000001464 14350072374 0020155 0 ustar 00root root 0000000 0000000 package caddytest
import (
"strings"
"testing"
)
func TestReplaceCertificatePaths(t *testing.T) {
rawConfig := `a.caddy.localhost:9443 {
tls /caddy.localhost.crt /caddy.localhost.key {
}
redir / https://b.caddy.localhost:9443/version 301
respond /version 200 {
body "hello from a.caddy.localhost"
}
}`
r := prependCaddyFilePath(rawConfig)
if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.crt") {
t.Error("expected the /caddy.localhost.crt to be expanded to include the full path")
}
if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.key") {
t.Error("expected the /caddy.localhost.crt to be expanded to include the full path")
}
if !strings.Contains(r, "https://b.caddy.localhost:9443/version") {
t.Error("expected redirect uri to be unchanged")
}
}
caddy-2.6.2/caddytest/integration/ 0000775 0000000 0000000 00000000000 14350072374 0017121 5 ustar 00root root 0000000 0000000 caddy-2.6.2/caddytest/integration/autohttps_test.go 0000664 0000000 0000000 00000006324 14350072374 0022547 0 ustar 00root root 0000000 0000000 package integration
import (
"net/http"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
skip_install_trust
http_port 9080
https_port 9443
}
localhost
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
localhost:9443
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
localhost:1234
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
}
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"ingress_server": {
"listen": [
":9080",
":9443"
],
"routes": [
{
"match": [
{
"host": ["localhost"]
}
]
}
]
}
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
}
}
`, "json")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
}
http://:9080 {
respond "Foo"
}
http://baz.localhost:9080 {
respond "Baz"
}
bar.localhost {
respond "Bar"
}
`, "caddyfile")
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Baz")
}
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
}
http://:9080 {
respond "Foo"
}
bar.localhost {
respond "Bar"
}
`, "caddyfile")
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/ 0000775 0000000 0000000 00000000000 14350072374 0022216 5 ustar 00root root 0000000 0000000 caddy-2.6.2/caddytest/integration/caddyfile_adapt/auto_https_disable_redirects.txt 0000664 0000000 0000000 00000000616 14350072374 0030703 0 ustar 00root root 0000000 0000000 {
auto_https disable_redirects
}
localhost
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"automatic_https": {
"disable_redirects": true
}
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/auto_https_ignore_loaded_certs.txt 0000664 0000000 0000000 00000000631 14350072374 0031224 0 ustar 00root root 0000000 0000000 {
auto_https ignore_loaded_certs
}
localhost
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"automatic_https": {
"ignore_loaded_certificates": true
}
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/auto_https_off.txt 0000664 0000000 0000000 00000000651 14350072374 0026005 0 ustar 00root root 0000000 0000000 {
auto_https off
}
localhost
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{}
],
"automatic_https": {
"disable": true
}
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/bind_ipv6.txt 0000664 0000000 0000000 00000000512 14350072374 0024635 0 ustar 00root root 0000000 0000000 example.com {
bind tcp6/[::]
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
"tcp6/[::]:443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/encode_options.txt 0000664 0000000 0000000 00000002764 14350072374 0026000 0 ustar 00root root 0000000 0000000 :80
# All the options
encode gzip zstd {
minimum_length 256
match {
status 2xx 4xx 500
header Content-Type text/*
header Content-Type application/json*
header Content-Type application/javascript*
header Content-Type application/xhtml+xml*
header Content-Type application/atom+xml*
header Content-Type application/rss+xml*
header Content-Type image/svg+xml*
}
}
# Long way with a block for each encoding
encode {
zstd
gzip 5
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"encodings": {
"gzip": {},
"zstd": {}
},
"handler": "encode",
"match": {
"headers": {
"Content-Type": [
"text/*",
"application/json*",
"application/javascript*",
"application/xhtml+xml*",
"application/atom+xml*",
"application/rss+xml*",
"image/svg+xml*"
]
},
"status_code": [
2,
4,
500
]
},
"minimum_length": 256,
"prefer": [
"gzip",
"zstd"
]
},
{
"encodings": {
"gzip": {
"level": 5
},
"zstd": {}
},
"handler": "encode",
"prefer": [
"zstd",
"gzip"
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/error_example.txt 0000664 0000000 0000000 00000004531 14350072374 0025626 0 ustar 00root root 0000000 0000000 example.com {
root * /srv
# Trigger errors for certain paths
error /private* "Unauthorized" 403
error /hidden* "Not found" 404
# Handle the error by serving an HTML page
handle_errors {
rewrite * /{http.error.status_code}.html
file_server
}
file_server
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 403
}
],
"match": [
{
"path": [
"/private*"
]
}
]
},
{
"handle": [
{
"error": "Not found",
"handler": "error",
"status_code": 404
}
],
"match": [
{
"path": [
"/hidden*"
]
}
]
},
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group0",
"handle": [
{
"handler": "rewrite",
"uri": "/{http.error.status_code}.html"
}
]
},
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/expression_quotes.txt 0000664 0000000 0000000 00000004117 14350072374 0026561 0 ustar 00root root 0000000 0000000 example.com
@a expression {http.error.status_code} == 400
abort @a
@b expression {http.error.status_code} == "401"
abort @b
@c expression {http.error.status_code} == `402`
abort @c
@d expression "{http.error.status_code} == 403"
abort @d
@e expression `{http.error.status_code} == 404`
abort @e
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == 400"
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == \"401\""
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == `402`"
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == 403"
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == 404"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/file_server_disable_canonical_uris.txt 0000664 0000000 0000000 00000000573 14350072374 0032025 0 ustar 00root root 0000000 0000000 :80
file_server {
disable_canonical_uris
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"canonical_uris": false,
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/file_server_pass_thru.txt 0000664 0000000 0000000 00000000550 14350072374 0027354 0 ustar 00root root 0000000 0000000 :80
file_server {
pass_thru
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"pass_thru": true
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/file_server_precompressed.txt 0000664 0000000 0000000 00000001045 14350072374 0030217 0 ustar 00root root 0000000 0000000 :80
file_server {
precompressed zstd br gzip
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"precompressed": {
"br": {},
"gzip": {},
"zstd": {}
},
"precompressed_order": [
"zstd",
"br",
"gzip"
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/file_server_status.txt 0000664 0000000 0000000 00000003557 14350072374 0026701 0 ustar 00root root 0000000 0000000 localhost
root * /srv
handle /nope* {
file_server {
status 403
}
}
handle /custom-status* {
file_server {
status {env.CUSTOM_STATUS}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"status_code": "{env.CUSTOM_STATUS}"
}
]
}
]
}
],
"match": [
{
"path": [
"/custom-status*"
]
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"status_code": 403
}
]
}
]
}
],
"match": [
{
"path": [
"/nope*"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/forward_auth_authelia.txt 0000664 0000000 0000000 00000004713 14350072374 0027325 0 ustar 00root root 0000000 0000000 app.example.com {
forward_auth authelia:9091 {
uri /api/verify?rd=https://authelia.example.com
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}
reverse_proxy backend:8080
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"app.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
2
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"request": {
"set": {
"Remote-Email": [
"{http.reverse_proxy.header.Remote-Email}"
],
"Remote-Groups": [
"{http.reverse_proxy.header.Remote-Groups}"
],
"Remote-Name": [
"{http.reverse_proxy.header.Remote-Name}"
],
"Remote-User": [
"{http.reverse_proxy.header.Remote-User}"
]
}
}
}
]
}
]
}
],
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"X-Forwarded-Method": [
"{http.request.method}"
],
"X-Forwarded-Uri": [
"{http.request.uri}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/api/verify?rd=https://authelia.example.com"
},
"upstreams": [
{
"dial": "authelia:9091"
}
]
},
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "backend:8080"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.txt 0000664 0000000 0000000 00000003220 14350072374 0030463 0 ustar 00root root 0000000 0000000 :8881
forward_auth localhost:9000 {
uri /auth
copy_headers A>1 B C>3 {
D
E>5
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
2
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"request": {
"set": {
"1": [
"{http.reverse_proxy.header.A}"
],
"3": [
"{http.reverse_proxy.header.C}"
],
"5": [
"{http.reverse_proxy.header.E}"
],
"B": [
"{http.reverse_proxy.header.B}"
],
"D": [
"{http.reverse_proxy.header.D}"
]
}
}
}
]
}
]
}
],
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"X-Forwarded-Method": [
"{http.request.method}"
],
"X-Forwarded-Uri": [
"{http.request.uri}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/auth"
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options.txt 0000664 0000000 0000000 00000002233 14350072374 0025772 0 ustar 00root root 0000000 0000000 {
debug
http_port 8080
https_port 8443
grace_period 5s
shutdown_delay 10s
default_sni localhost
order root first
storage file_system {
root /data
}
acme_ca https://example.com
acme_ca_root /path/to/ca.crt
ocsp_stapling off
email test@example.com
admin off
on_demand_tls {
ask https://example.com
interval 30s
burst 20
}
local_certs
key_type ed25519
}
:80
----------
{
"admin": {
"disabled": true
},
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
},
"storage": {
"module": "file_system",
"root": "/data"
},
"apps": {
"http": {
"http_port": 8080,
"https_port": 8443,
"grace_period": 5000000000,
"shutdown_delay": 10000000000,
"servers": {
"srv0": {
"listen": [
":80"
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"module": "internal"
}
],
"key_type": "ed25519",
"disable_ocsp_stapling": true
}
],
"on_demand": {
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
}
},
"disable_ocsp_stapling": true
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options_acme.txt 0000664 0000000 0000000 00000003155 14350072374 0026763 0 ustar 00root root 0000000 0000000 {
debug
http_port 8080
https_port 8443
default_sni localhost
order root first
storage file_system {
root /data
}
acme_ca https://example.com
acme_eab {
key_id 4K2scIVbBpNd-78scadB2g
mac_key abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
}
acme_ca_root /path/to/ca.crt
email test@example.com
admin off
on_demand_tls {
ask https://example.com
interval 30s
burst 20
}
storage_clean_interval 7d
renew_interval 1d
ocsp_interval 2d
key_type ed25519
}
:80
----------
{
"admin": {
"disabled": true
},
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
},
"storage": {
"module": "file_system",
"root": "/data"
},
"apps": {
"http": {
"http_port": 8080,
"https_port": 8443,
"servers": {
"srv0": {
"listen": [
":80"
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"ca": "https://example.com",
"email": "test@example.com",
"external_account": {
"key_id": "4K2scIVbBpNd-78scadB2g",
"mac_key": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
},
"module": "acme",
"trusted_roots_pem_files": [
"/path/to/ca.crt"
]
}
],
"key_type": "ed25519"
}
],
"on_demand": {
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
},
"ocsp_interval": 172800000000000,
"renew_interval": 86400000000000,
"storage_clean_interval": 604800000000000
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options_admin.txt 0000664 0000000 0000000 00000002213 14350072374 0027140 0 ustar 00root root 0000000 0000000 {
debug
http_port 8080
https_port 8443
default_sni localhost
order root first
storage file_system {
root /data
}
acme_ca https://example.com
acme_ca_root /path/to/ca.crt
email test@example.com
admin {
origins localhost:2019 [::1]:2019 127.0.0.1:2019 192.168.10.128
}
on_demand_tls {
ask https://example.com
interval 30s
burst 20
}
local_certs
key_type ed25519
}
:80
----------
{
"admin": {
"listen": "localhost:2019",
"origins": [
"localhost:2019",
"[::1]:2019",
"127.0.0.1:2019",
"192.168.10.128"
]
},
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
},
"storage": {
"module": "file_system",
"root": "/data"
},
"apps": {
"http": {
"http_port": 8080,
"https_port": 8443,
"servers": {
"srv0": {
"listen": [
":80"
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"module": "internal"
}
],
"key_type": "ed25519"
}
],
"on_demand": {
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options_debug_with_access_log.txt 0000664 0000000 0000000 00000000764 14350072374 0032364 0 ustar 00root root 0000000 0000000 {
debug
}
:8881 {
log {
format console
}
}
----------
{
"logging": {
"logs": {
"default": {
"level": "DEBUG",
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"encoder": {
"format": "console"
},
"level": "DEBUG",
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options_default_bind.txt 0000664 0000000 0000000 00000001235 14350072374 0030473 0 ustar 00root root 0000000 0000000 {
default_bind tcp4/0.0.0.0 tcp6/[::]
}
example.com {
}
example.org:12345 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
"tcp4/0.0.0.0:12345",
"tcp6/[::]:12345"
],
"routes": [
{
"match": [
{
"host": [
"example.org"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
"tcp4/0.0.0.0:443",
"tcp6/[::]:443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options_log_and_site.txt 0000664 0000000 0000000 00000002072 14350072374 0030502 0 ustar 00root root 0000000 0000000 {
log {
output file caddy.log
include some-log-source
exclude admin.api admin2.api
}
log custom-logger {
output file caddy.log
level WARN
include custom-log-source
}
}
:8884 {
log {
format json
output file access.log
}
}
----------
{
"logging": {
"logs": {
"custom-logger": {
"writer": {
"filename": "caddy.log",
"output": "file"
},
"level": "WARN",
"include": [
"custom-log-source"
]
},
"default": {
"writer": {
"filename": "caddy.log",
"output": "file"
},
"include": [
"some-log-source"
],
"exclude": [
"admin.api",
"admin2.api",
"custom-log-source",
"http.log.access.log0"
]
},
"log0": {
"writer": {
"filename": "access.log",
"output": "file"
},
"encoder": {
"format": "json"
},
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options_log_basic.txt 0000664 0000000 0000000 00000000261 14350072374 0027773 0 ustar 00root root 0000000 0000000 {
log {
output file foo.log
}
}
----------
{
"logging": {
"logs": {
"default": {
"writer": {
"filename": "foo.log",
"output": "file"
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options_log_custom.txt 0000664 0000000 0000000 00000000714 14350072374 0030227 0 ustar 00root root 0000000 0000000 {
log custom-logger {
format filter {
wrap console
fields {
request>remote_ip ip_mask {
ipv4 24
ipv6 32
}
}
}
}
}
----------
{
"logging": {
"logs": {
"custom-logger": {
"encoder": {
"fields": {
"request\u003eremote_ip": {
"filter": "ip_mask",
"ipv4_cidr": 24,
"ipv6_cidr": 32
}
},
"format": "filter",
"wrap": {
"format": "console"
}
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options_log_multi.txt 0000664 0000000 0000000 00000000426 14350072374 0030047 0 ustar 00root root 0000000 0000000 {
log first {
output file foo.log
}
log second {
format json
}
}
----------
{
"logging": {
"logs": {
"first": {
"writer": {
"filename": "foo.log",
"output": "file"
}
},
"second": {
"encoder": {
"format": "json"
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.txt 0000664 0000000 0000000 00000001311 14350072374 0031351 0 ustar 00root root 0000000 0000000 {
preferred_chains smallest
}
example.com
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"module": "acme",
"preferred_chains": {
"smallest": true
}
},
{
"module": "zerossl",
"preferred_chains": {
"smallest": true
}
}
]
}
]
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_options_skip_install_trust.txt 0000664 0000000 0000000 00000005276 14350072374 0032021 0 ustar 00root root 0000000 0000000 {
skip_install_trust
pki {
ca {
name "Local"
root_cn "Custom Local Root Name"
intermediate_cn "Custom Local Intermediate Name"
root {
cert /path/to/cert.pem
key /path/to/key.pem
format pem_file
}
intermediate {
cert /path/to/cert.pem
key /path/to/key.pem
format pem_file
}
}
ca foo {
name "Foo"
root_cn "Custom Foo Root Name"
intermediate_cn "Custom Foo Intermediate Name"
}
}
}
a.example.com {
tls internal
}
acme.example.com {
acme_server {
ca foo
}
}
acme-bar.example.com {
acme_server {
ca bar
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme-bar.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "bar",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "foo",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"bar": {
"install_trust": false
},
"foo": {
"name": "Foo",
"root_common_name": "Custom Foo Root Name",
"intermediate_common_name": "Custom Foo Intermediate Name",
"install_trust": false
},
"local": {
"name": "Local",
"root_common_name": "Custom Local Root Name",
"intermediate_common_name": "Custom Local Intermediate Name",
"install_trust": false,
"root": {
"certificate": "/path/to/cert.pem",
"private_key": "/path/to/key.pem",
"format": "pem_file"
},
"intermediate": {
"certificate": "/path/to/cert.pem",
"private_key": "/path/to/key.pem",
"format": "pem_file"
}
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"acme-bar.example.com",
"acme.example.com"
]
},
{
"subjects": [
"a.example.com"
],
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_server_options_multi.txt 0000664 0000000 0000000 00000001704 14350072374 0030574 0 ustar 00root root 0000000 0000000 {
servers {
timeouts {
idle 90s
}
strict_sni_host insecure_off
}
servers :80 {
timeouts {
idle 60s
}
}
servers :443 {
timeouts {
idle 30s
}
strict_sni_host
}
}
foo.com {
}
http://bar.com {
}
:8080 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"idle_timeout": 30000000000,
"routes": [
{
"match": [
{
"host": [
"foo.com"
]
}
],
"terminal": true
}
],
"strict_sni_host": true
},
"srv1": {
"listen": [
":80"
],
"idle_timeout": 60000000000,
"routes": [
{
"match": [
{
"host": [
"bar.com"
]
}
],
"terminal": true
}
]
},
"srv2": {
"listen": [
":8080"
],
"idle_timeout": 90000000000,
"strict_sni_host": false
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/global_server_options_single.txt 0000664 0000000 0000000 00000001753 14350072374 0030727 0 ustar 00root root 0000000 0000000 {
servers {
listener_wrappers {
http_redirect
tls
}
timeouts {
read_body 30s
read_header 30s
write 30s
idle 30s
}
max_header_size 100MB
log_credentials
protocols h1 h2 h2c h3
strict_sni_host
}
}
foo.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"listener_wrappers": [
{
"wrapper": "http_redirect"
},
{
"wrapper": "tls"
}
],
"read_timeout": 30000000000,
"read_header_timeout": 30000000000,
"write_timeout": 30000000000,
"idle_timeout": 30000000000,
"max_header_bytes": 100000000,
"routes": [
{
"match": [
{
"host": [
"foo.com"
]
}
],
"terminal": true
}
],
"strict_sni_host": true,
"logs": {
"should_log_credentials": true
},
"protocols": [
"h1",
"h2",
"h2c",
"h3"
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/handle_path.txt 0000664 0000000 0000000 00000001357 14350072374 0025234 0 ustar 00root root 0000000 0000000 :80
handle_path /api/v1/* {
respond "API v1"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/api/v1/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/api/v1"
}
]
},
{
"handle": [
{
"body": "API v1",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/handle_path_sorting.txt 0000664 0000000 0000000 00000003054 14350072374 0026775 0 ustar 00root root 0000000 0000000 :80 {
handle /api/* {
respond "api"
}
handle_path /static/* {
respond "static"
}
handle {
respond "handle"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"group": "group3",
"match": [
{
"path": [
"/static/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/static"
}
]
},
{
"handle": [
{
"body": "static",
"handler": "static_response"
}
]
}
]
}
]
},
{
"group": "group3",
"match": [
{
"path": [
"/api/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "api",
"handler": "static_response"
}
]
}
]
}
]
},
{
"group": "group3",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "handle",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/header.txt 0000664 0000000 0000000 00000004704 14350072374 0024214 0 ustar 00root root 0000000 0000000 :80 {
header Denis "Ritchie"
header +Edsger "Dijkstra"
header ?John "von Neumann"
header -Wolfram
header {
Grace: "Hopper" # some users habitually suffix field names with a colon
+Ray "Solomonoff"
?Tim "Berners-Lee"
defer
}
@images path /images/*
header @images {
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
}
header {
+Link "Foo"
+Link "Bar"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/images/*"
]
}
],
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Cache-Control": [
"public, max-age=3600, stale-while-revalidate=86400"
]
}
}
}
]
},
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Denis": [
"Ritchie"
]
}
}
},
{
"handler": "headers",
"response": {
"add": {
"Edsger": [
"Dijkstra"
]
}
}
},
{
"handler": "headers",
"response": {
"require": {
"headers": {
"John": null
}
},
"set": {
"John": [
"von Neumann"
]
}
}
},
{
"handler": "headers",
"response": {
"deferred": true,
"delete": [
"Wolfram"
]
}
},
{
"handler": "headers",
"response": {
"add": {
"Ray": [
"Solomonoff"
]
},
"deferred": true,
"set": {
"Grace": [
"Hopper"
]
}
}
},
{
"handler": "headers",
"response": {
"require": {
"headers": {
"Tim": null
}
},
"set": {
"Tim": [
"Berners-Lee"
]
}
}
},
{
"handler": "headers",
"response": {
"add": {
"Link": [
"Foo",
"Bar"
]
}
}
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/http_only_hostnames.txt 0000664 0000000 0000000 00000001210 14350072374 0027052 0 ustar 00root root 0000000 0000000 # https://github.com/caddyserver/caddy/issues/3977
http://* {
respond "Hello, world!"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Hello, world!",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/http_only_on_any_address.txt 0000664 0000000 0000000 00000000742 14350072374 0030052 0 ustar 00root root 0000000 0000000 :80 {
respond /version 200 {
body "hello from localhost"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/version"
]
}
],
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/http_only_on_domain.txt 0000664 0000000 0000000 00000001474 14350072374 0027030 0 ustar 00root root 0000000 0000000 http://a.caddy.localhost {
respond /version 200 {
body "hello from localhost"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"a.caddy.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/http_only_on_hostless_block.txt 0000664 0000000 0000000 00000000511 14350072374 0030566 0 ustar 00root root 0000000 0000000 # Issue #4113
:80, http://example.com {
respond "foo"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"body": "foo",
"handler": "static_response"
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/http_only_on_localhost.txt 0000664 0000000 0000000 00000001450 14350072374 0027543 0 ustar 00root root 0000000 0000000 localhost:80 {
respond /version 200 {
body "hello from localhost"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/http_only_on_non_standard_port.txt 0000664 0000000 0000000 00000001624 14350072374 0031274 0 ustar 00root root 0000000 0000000 http://a.caddy.localhost:81 {
respond /version 200 {
body "hello from localhost"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":81"
],
"routes": [
{
"match": [
{
"host": [
"a.caddy.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"a.caddy.localhost"
]
}
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/https_on_domain.txt 0000664 0000000 0000000 00000001466 14350072374 0026153 0 ustar 00root root 0000000 0000000 a.caddy.localhost {
respond /version 200 {
body "hello from localhost"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.caddy.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/import_args_file.txt 0000664 0000000 0000000 00000001500 14350072374 0026300 0 ustar 00root root 0000000 0000000 example.com
import testdata/import_respond.txt Groot Rocket
import testdata/import_respond.txt you "the confused man"
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "'I am Groot', hears Rocket",
"handler": "static_response"
},
{
"body": "'I am you', hears the confused man",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/import_args_snippet.txt 0000664 0000000 0000000 00000002272 14350072374 0027052 0 ustar 00root root 0000000 0000000 (logging) {
log {
output file /var/log/caddy/{args.0}.access.log
}
}
a.example.com {
import logging a.example.com
}
b.example.com {
import logging b.example.com
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0",
"http.log.access.log1"
]
},
"log0": {
"writer": {
"filename": "/var/log/caddy/a.example.com.access.log",
"output": "file"
},
"include": [
"http.log.access.log0"
]
},
"log1": {
"writer": {
"filename": "/var/log/caddy/b.example.com.access.log",
"output": "file"
},
"include": [
"http.log.access.log1"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"b.example.com"
]
}
],
"terminal": true
}
],
"logs": {
"logger_names": {
"a.example.com": "log0",
"b.example.com": "log1"
}
}
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/import_args_snippet_env_placeholder.txt 0000664 0000000 0000000 00000000510 14350072374 0032255 0 ustar 00root root 0000000 0000000 (foo) {
respond {env.FOO}
}
:80 {
import foo
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"body": "{env.FOO}",
"handler": "static_response"
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.txt 0000664 0000000 0000000 00000003344 14350072374 0030304 0 ustar 00root root 0000000 0000000 http://localhost:2020 {
log
skip_log /first-hidden*
skip_log /second-hidden*
respond 200
}
:2020 {
respond 418
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":2020"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"skip_log": true
}
],
"match": [
{
"path": [
"/second-hidden*"
]
}
]
},
{
"handle": [
{
"handler": "vars",
"skip_log": true
}
],
"match": [
{
"path": [
"/first-hidden*"
]
}
]
},
{
"handle": [
{
"handler": "static_response",
"status_code": 200
}
]
}
]
}
],
"terminal": true
},
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 418
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
},
"logs": {
"logger_names": {
"localhost:2020": ""
},
"skip_unmapped_hosts": true
}
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/log_filters.txt 0000664 0000000 0000000 00000004224 14350072374 0025272 0 ustar 00root root 0000000 0000000 :80
log {
output stdout
format filter {
wrap console
fields {
uri query {
replace foo REDACTED
delete bar
hash baz
}
request>headers>Authorization replace REDACTED
request>headers>Server delete
request>headers>Cookie cookie {
replace foo REDACTED
delete bar
hash baz
}
request>remote_ip ip_mask {
ipv4 24
ipv6 32
}
request>headers>Regexp regexp secret REDACTED
request>headers>Hash hash
}
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"writer": {
"output": "stdout"
},
"encoder": {
"fields": {
"request\u003eheaders\u003eAuthorization": {
"filter": "replace",
"value": "REDACTED"
},
"request\u003eheaders\u003eCookie": {
"actions": [
{
"name": "foo",
"type": "replace",
"value": "REDACTED"
},
{
"name": "bar",
"type": "delete"
},
{
"name": "baz",
"type": "hash"
}
],
"filter": "cookie"
},
"request\u003eheaders\u003eHash": {
"filter": "hash"
},
"request\u003eheaders\u003eRegexp": {
"filter": "regexp",
"regexp": "secret",
"value": "REDACTED"
},
"request\u003eheaders\u003eServer": {
"filter": "delete"
},
"request\u003eremote_ip": {
"filter": "ip_mask",
"ipv4_cidr": 24,
"ipv6_cidr": 32
},
"uri": {
"actions": [
{
"parameter": "foo",
"type": "replace",
"value": "REDACTED"
},
{
"parameter": "bar",
"type": "delete"
},
{
"parameter": "baz",
"type": "hash"
}
],
"filter": "query"
}
},
"format": "filter",
"wrap": {
"format": "console"
}
},
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/log_roll_days.txt 0000664 0000000 0000000 00000001305 14350072374 0025607 0 ustar 00root root 0000000 0000000 :80
log {
output file /var/log/access.log {
roll_size 1gb
roll_uncompressed
roll_local_time
roll_keep 5
roll_keep_for 90d
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"writer": {
"filename": "/var/log/access.log",
"output": "file",
"roll_gzip": false,
"roll_keep": 5,
"roll_keep_days": 90,
"roll_local_time": true,
"roll_size_mb": 954
},
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/log_skip_hosts.txt 0000664 0000000 0000000 00000001761 14350072374 0026013 0 ustar 00root root 0000000 0000000 one.example.com {
log
}
two.example.com {
}
three.example.com {
}
example.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"three.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"one.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"two.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
],
"logs": {
"logger_names": {
"one.example.com": ""
},
"skip_hosts": [
"three.example.com",
"two.example.com",
"example.com"
]
}
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/map_and_vars_with_raw_types.txt 0000664 0000000 0000000 00000005015 14350072374 0030542 0 ustar 00root root 0000000 0000000 example.com
map {host} {my_placeholder} {magic_number} {
# Should output boolean "true" and an integer
example.com true 3
# Should output a string and null
foo.example.com "string value"
# Should output two strings (quoted int)
(.*)\.example.com "${1} subdomain" "5"
# Should output null and a string (quoted int)
~.*\.net$ - `7`
# Should output a float and the string "false"
~.*\.xyz$ 123.456 "false"
# Should output two strings, second being escaped quote
default "unknown domain" \"""
}
vars foo bar
vars {
abc true
def 1
ghi 2.3
jkl "mn op"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"defaults": [
"unknown domain",
"\""
],
"destinations": [
"{my_placeholder}",
"{magic_number}"
],
"handler": "map",
"mappings": [
{
"input": "example.com",
"outputs": [
true,
3
]
},
{
"input": "foo.example.com",
"outputs": [
"string value",
null
]
},
{
"input": "(.*)\\.example.com",
"outputs": [
"${1} subdomain",
"5"
]
},
{
"input_regexp": ".*\\.net$",
"outputs": [
null,
"7"
]
},
{
"input_regexp": ".*\\.xyz$",
"outputs": [
123.456,
"false"
]
}
],
"source": "{http.request.host}"
},
{
"foo": "bar",
"handler": "vars"
},
{
"abc": true,
"def": 1,
"ghi": 2.3,
"handler": "vars",
"jkl": "mn op"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/matcher_syntax.txt 0000664 0000000 0000000 00000011230 14350072374 0026005 0 ustar 00root root 0000000 0000000 :80 {
@matcher {
method GET
}
respond @matcher "get"
@matcher2 method POST
respond @matcher2 "post"
@matcher3 not method PUT
respond @matcher3 "not put"
@matcher4 vars "{http.request.uri}" "/vars-matcher"
respond @matcher4 "from vars matcher"
@matcher5 vars_regexp static "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
respond @matcher5 "from vars_regexp matcher with name"
@matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
respond @matcher6 "from vars_regexp matcher without name"
@matcher7 `path('/foo*') && method('GET')`
respond @matcher7 "inline expression matcher shortcut"
@matcher8 {
header Foo bar
header Foo foobar
header Bar foo
}
respond @matcher8 "header matcher merging values of the same field"
@matcher9 {
query foo=bar foo=baz bar=foo
query bar=baz
}
respond @matcher9 "query matcher merging pairs with the same keys"
@matcher10 {
header !Foo
header Bar foo
}
respond @matcher10 "header matcher with null field matcher"
@matcher11 remote_ip private_ranges
respond @matcher11 "remote_ip matcher with private ranges"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"method": [
"GET"
]
}
],
"handle": [
{
"body": "get",
"handler": "static_response"
}
]
},
{
"match": [
{
"method": [
"POST"
]
}
],
"handle": [
{
"body": "post",
"handler": "static_response"
}
]
},
{
"match": [
{
"not": [
{
"method": [
"PUT"
]
}
]
}
],
"handle": [
{
"body": "not put",
"handler": "static_response"
}
]
},
{
"match": [
{
"vars": {
"{http.request.uri}": [
"/vars-matcher"
]
}
}
],
"handle": [
{
"body": "from vars matcher",
"handler": "static_response"
}
]
},
{
"match": [
{
"vars_regexp": {
"{http.request.uri}": {
"name": "static",
"pattern": "\\.([a-f0-9]{6})\\.(css|js)$"
}
}
}
],
"handle": [
{
"body": "from vars_regexp matcher with name",
"handler": "static_response"
}
]
},
{
"match": [
{
"vars_regexp": {
"{http.request.uri}": {
"pattern": "\\.([a-f0-9]{6})\\.(css|js)$"
}
}
}
],
"handle": [
{
"body": "from vars_regexp matcher without name",
"handler": "static_response"
}
]
},
{
"match": [
{
"expression": "path('/foo*') \u0026\u0026 method('GET')"
}
],
"handle": [
{
"body": "inline expression matcher shortcut",
"handler": "static_response"
}
]
},
{
"match": [
{
"header": {
"Bar": [
"foo"
],
"Foo": [
"bar",
"foobar"
]
}
}
],
"handle": [
{
"body": "header matcher merging values of the same field",
"handler": "static_response"
}
]
},
{
"match": [
{
"query": {
"bar": [
"foo",
"baz"
],
"foo": [
"bar",
"baz"
]
}
}
],
"handle": [
{
"body": "query matcher merging pairs with the same keys",
"handler": "static_response"
}
]
},
{
"match": [
{
"header": {
"Bar": [
"foo"
],
"Foo": null
}
}
],
"handle": [
{
"body": "header matcher with null field matcher",
"handler": "static_response"
}
]
},
{
"match": [
{
"remote_ip": {
"ranges": [
"192.168.0.0/16",
"172.16.0.0/12",
"10.0.0.0/8",
"127.0.0.1/8",
"fd00::/8",
"::1"
]
}
}
],
"handle": [
{
"body": "remote_ip matcher with private ranges",
"handler": "static_response"
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/matchers_in_route.txt 0000664 0000000 0000000 00000000623 14350072374 0026472 0 ustar 00root root 0000000 0000000 :80 {
route {
# unused matchers should not panic
# see https://github.com/caddyserver/caddy/issues/3745
@matcher1 path /path1
@matcher2 path /path2
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "subroute"
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/method_directive.txt 0000664 0000000 0000000 00000000442 14350072374 0026275 0 ustar 00root root 0000000 0000000 :8080 {
method FOO
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "rewrite",
"method": "FOO"
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/metrics_disable_om.txt 0000664 0000000 0000000 00000000657 14350072374 0026613 0 ustar 00root root 0000000 0000000 :80 {
metrics /metrics {
disable_openmetrics
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/metrics"
]
}
],
"handle": [
{
"disable_openmetrics": true,
"handler": "metrics"
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/metrics_syntax.txt 0000664 0000000 0000000 00000000556 14350072374 0026041 0 ustar 00root root 0000000 0000000 :80 {
metrics /metrics
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/metrics"
]
}
],
"handle": [
{
"handler": "metrics"
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/not_block_merging.txt 0000664 0000000 0000000 00000001134 14350072374 0026440 0 ustar 00root root 0000000 0000000 :80
@test {
not {
header Abc "123"
header Bcd "123"
}
}
respond @test 403
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"not": [
{
"header": {
"Abc": [
"123"
],
"Bcd": [
"123"
]
}
}
]
}
],
"handle": [
{
"handler": "static_response",
"status_code": 403
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.txt 0000664 0000000 0000000 00000004677 14350072374 0030157 0 ustar 00root root 0000000 0000000 :8886
route {
# Add trailing slash for directory requests
@canonicalPath {
file {
try_files {path}/index.php
}
not path */
}
redir @canonicalPath {http.request.orig_uri.path}/ 308
# If the requested file does not exist, try index files
@indexFiles {
file {
try_files {path} {path}/index.php index.php
split_path .php
}
}
rewrite @indexFiles {http.matchers.file.relative}
# Proxy PHP files to the FastCGI responder
@phpFiles {
path *.php
}
reverse_proxy @phpFiles 127.0.0.1:9000 {
transport fastcgi {
split .php
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8886"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "127.0.0.1:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
}
]
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/php_fastcgi_handle_response.txt 0000664 0000000 0000000 00000004766 14350072374 0030514 0 ustar 00root root 0000000 0000000 :8881 {
php_fastcgi app:9000 {
env FOO bar
@error status 4xx
handle_response @error {
root * /errors
rewrite * /{http.reverse_proxy.status_code}.html
file_server
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
]
},
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
],
"split_path": [
".php"
]
}
}
],
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
]
},
{
"match": [
{
"path": [
"*.php"
]
}
],
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
4
]
},
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/errors"
}
]
},
{
"group": "group0",
"handle": [
{
"handler": "rewrite",
"uri": "/{http.reverse_proxy.status_code}.html"
}
]
},
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
],
"handler": "reverse_proxy",
"transport": {
"env": {
"FOO": "bar"
},
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "app:9000"
}
]
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/php_fastcgi_index_off.txt 0000664 0000000 0000000 00000002352 14350072374 0027271 0 ustar 00root root 0000000 0000000 :8884
php_fastcgi localhost:9000 {
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
index off
dial_timeout 3s
read_timeout 10s
write_timeout 20s
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"match": [
{
"path": [
"*.php",
"*.php5"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"selection_policy": {
"policy": "random"
}
},
"transport": {
"dial_timeout": 3000000000,
"env": {
"VAR1": "value1",
"VAR2": "value2"
},
"protocol": "fastcgi",
"read_timeout": 10000000000,
"root": "/var/www",
"split_path": [
".php",
".php5"
],
"write_timeout": 20000000000
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.txt 0000664 0000000 0000000 00000005501 14350072374 0026752 0 ustar 00root root 0000000 0000000 :8884
# the use of a host matcher here should cause this
# site block to be wrapped in a subroute, even though
# the site block does not have a hostname; this is
# to prevent auto-HTTPS from picking up on this host
# matcher because it is not a key on the site block
@test host example.com
php_fastcgi @test localhost:9000
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
}
]
}
],
"match": [
{
"host": [
"example.com"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/php_fastcgi_subdirectives.txt 0000664 0000000 0000000 00000004005 14350072374 0030200 0 ustar 00root root 0000000 0000000 :8884
php_fastcgi localhost:9000 {
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
index index.php5
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php5"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
]
},
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php5",
"index.php5"
],
"split_path": [
".php",
".php5"
]
}
}
],
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
]
},
{
"match": [
{
"path": [
"*.php",
"*.php5"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"selection_policy": {
"policy": "random"
}
},
"transport": {
"env": {
"VAR1": "value1",
"VAR2": "value2"
},
"protocol": "fastcgi",
"root": "/var/www",
"split_path": [
".php",
".php5"
]
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override.txt 0000664 0000000 0000000 00000004277 14350072374 0031237 0 ustar 00root root 0000000 0000000 :8884
php_fastcgi localhost:9000 {
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
try_files {path} {path}/index.php =404
dial_timeout 3s
read_timeout 10s
write_timeout 20s
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
]
},
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"=404"
],
"split_path": [
".php",
".php5"
]
}
}
],
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
]
},
{
"match": [
{
"path": [
"*.php",
"*.php5"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"selection_policy": {
"policy": "random"
}
},
"transport": {
"dial_timeout": 3000000000,
"env": {
"VAR1": "value1",
"VAR2": "value2"
},
"protocol": "fastcgi",
"read_timeout": 10000000000,
"root": "/var/www",
"split_path": [
".php",
".php5"
],
"write_timeout": 20000000000
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/portless_upstream.txt 0000664 0000000 0000000 00000003411 14350072374 0026551 0 ustar 00root root 0000000 0000000 whoami.example.com {
reverse_proxy whoami
}
app.example.com {
reverse_proxy app:80
}
unix.example.com {
reverse_proxy unix//path/to/socket
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"whoami.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "whoami:80"
}
]
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"unix.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "unix//path/to/socket"
}
]
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"app.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "app:80"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/request_body.txt 0000664 0000000 0000000 00000001133 14350072374 0025462 0 ustar 00root root 0000000 0000000 localhost
request_body {
max_size 1MB
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "request_body",
"max_size": 1000000
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/request_header.txt 0000664 0000000 0000000 00000002634 14350072374 0025764 0 ustar 00root root 0000000 0000000 :80
@matcher path /something*
request_header @matcher Denis "Ritchie"
request_header +Edsger "Dijkstra"
request_header -Wolfram
@images path /images/*
request_header @images Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/something*"
]
}
],
"handle": [
{
"handler": "headers",
"request": {
"set": {
"Denis": [
"Ritchie"
]
}
}
}
]
},
{
"match": [
{
"path": [
"/images/*"
]
}
],
"handle": [
{
"handler": "headers",
"request": {
"set": {
"Cache-Control": [
"public, max-age=3600, stale-while-revalidate=86400"
]
}
}
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"add": {
"Edsger": [
"Dijkstra"
]
}
}
},
{
"handler": "headers",
"request": {
"delete": [
"Wolfram"
]
}
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/reverse_proxy_dynamic_upstreams.txt 0000664 0000000 0000000 00000003700 14350072374 0031502 0 ustar 00root root 0000000 0000000 :8884 {
reverse_proxy {
dynamic a foo 9000
}
reverse_proxy {
dynamic a {
name foo
port 9000
refresh 5m
resolvers 8.8.8.8 8.8.4.4
dial_timeout 2s
dial_fallback_delay 300ms
}
}
}
:8885 {
reverse_proxy {
dynamic srv _api._tcp.example.com
}
reverse_proxy {
dynamic srv {
service api
proto tcp
name example.com
refresh 5m
resolvers 8.8.8.8 8.8.4.4
dial_timeout 1s
dial_fallback_delay -1s
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"dynamic_upstreams": {
"name": "foo",
"port": "9000",
"source": "a"
},
"handler": "reverse_proxy"
},
{
"dynamic_upstreams": {
"dial_fallback_delay": 300000000,
"dial_timeout": 2000000000,
"name": "foo",
"port": "9000",
"refresh": 300000000000,
"resolver": {
"addresses": [
"8.8.8.8",
"8.8.4.4"
]
},
"source": "a"
},
"handler": "reverse_proxy"
}
]
}
]
},
"srv1": {
"listen": [
":8885"
],
"routes": [
{
"handle": [
{
"dynamic_upstreams": {
"name": "_api._tcp.example.com",
"source": "srv"
},
"handler": "reverse_proxy"
},
{
"dynamic_upstreams": {
"dial_fallback_delay": -1000000000,
"dial_timeout": 1000000000,
"name": "example.com",
"proto": "tcp",
"refresh": 300000000000,
"resolver": {
"addresses": [
"8.8.8.8",
"8.8.4.4"
]
},
"service": "api",
"source": "srv"
},
"handler": "reverse_proxy"
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/reverse_proxy_empty_non_http_transport.txt 0000664 0000000 0000000 00000000732 14350072374 0033140 0 ustar 00root root 0000000 0000000 :8884
reverse_proxy 127.0.0.1:65535 {
transport fastcgi
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi"
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/reverse_proxy_h2c_shorthand.txt 0000664 0000000 0000000 00000001523 14350072374 0030502 0 ustar 00root root 0000000 0000000 :8884
reverse_proxy h2c://localhost:8080
reverse_proxy unix+h2c//run/app.sock
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "localhost:8080"
}
]
},
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "unix//run/app.sock"
}
]
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.txt 0000664 0000000 0000000 00000012217 14350072374 0031127 0 ustar 00root root 0000000 0000000 :8884
reverse_proxy 127.0.0.1:65535 {
@500 status 500
replace_status @500 400
@all status 2xx 3xx 4xx 5xx
replace_status @all {http.error.status_code}
replace_status {http.error.status_code}
@accel header X-Accel-Redirect *
handle_response @accel {
respond "Header X-Accel-Redirect!"
}
@another {
header X-Another *
}
handle_response @another {
respond "Header X-Another!"
}
@401 status 401
handle_response @401 {
respond "Status 401!"
}
handle_response {
respond "Any! This should be last in the JSON!"
}
@403 {
status 403
}
handle_response @403 {
respond "Status 403!"
}
@multi {
status 401 403
status 404
header Foo *
header Bar *
}
handle_response @multi {
respond "Headers Foo, Bar AND statuses 401, 403 and 404!"
}
@200 status 200
handle_response @200 {
copy_response_headers {
include Foo Bar
}
respond "Copied headers from the response"
}
@201 status 201
handle_response @201 {
header Foo "Copying the response"
copy_response 404
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
500
]
},
"status_code": 400
},
{
"match": {
"status_code": [
2,
3,
4,
5
]
},
"status_code": "{http.error.status_code}"
},
{
"match": {
"headers": {
"X-Accel-Redirect": [
"*"
]
}
},
"routes": [
{
"handle": [
{
"body": "Header X-Accel-Redirect!",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"headers": {
"X-Another": [
"*"
]
}
},
"routes": [
{
"handle": [
{
"body": "Header X-Another!",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"status_code": [
401
]
},
"routes": [
{
"handle": [
{
"body": "Status 401!",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"status_code": [
403
]
},
"routes": [
{
"handle": [
{
"body": "Status 403!",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"headers": {
"Bar": [
"*"
],
"Foo": [
"*"
]
},
"status_code": [
401,
403,
404
]
},
"routes": [
{
"handle": [
{
"body": "Headers Foo, Bar AND statuses 401, 403 and 404!",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"status_code": [
200
]
},
"routes": [
{
"handle": [
{
"handler": "copy_response_headers",
"include": [
"Foo",
"Bar"
]
},
{
"body": "Copied headers from the response",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"status_code": [
201
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Foo": [
"Copying the response"
]
}
}
},
{
"handler": "copy_response",
"status_code": 404
}
]
}
]
},
{
"status_code": "{http.error.status_code}"
},
{
"routes": [
{
"handle": [
{
"body": "Any! This should be last in the JSON!",
"handler": "static_response"
}
]
}
]
}
],
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/reverse_proxy_health_headers.txt 0000664 0000000 0000000 00000001730 14350072374 0030714 0 ustar 00root root 0000000 0000000 :8884
reverse_proxy 127.0.0.1:65535 {
health_headers {
Host example.com
X-Header-Key 95ca39e3cbe7
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
X-Empty-Value
}
health_uri /health
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"headers": {
"Host": [
"example.com"
],
"X-Empty-Value": [
""
],
"X-Header-Key": [
"95ca39e3cbe7"
],
"X-Header-Keys": [
"VbG4NZwWnipo",
"335Q9/MhqcNU3s2TO"
]
},
"uri": "/health"
}
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/reverse_proxy_health_path_query.txt 0000664 0000000 0000000 00000002200 14350072374 0031453 0 ustar 00root root 0000000 0000000 # Health with query in the uri
:8443 {
reverse_proxy localhost:54321 {
health_uri /health?ready=1
health_status 2xx
}
}
# Health without query in the uri
:8444 {
reverse_proxy localhost:54321 {
health_uri /health
health_status 200
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8443"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"expect_status": 2,
"uri": "/health?ready=1"
}
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
]
}
]
},
"srv1": {
"listen": [
":8444"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"expect_status": 200,
"uri": "/health"
}
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance.txt 0000664 0000000 0000000 00000001771 14350072374 0030345 0 ustar 00root root 0000000 0000000 :8884
reverse_proxy 127.0.0.1:65535 {
lb_policy first
lb_retries 5
lb_try_duration 10s
lb_try_interval 500ms
lb_retry_match {
path /foo*
method POST
}
lb_retry_match path /bar*
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"retries": 5,
"retry_match": [
{
"method": [
"POST"
],
"path": [
"/foo*"
]
},
{
"path": [
"/bar*"
]
}
],
"selection_policy": {
"policy": "first"
},
"try_duration": 10000000000,
"try_interval": 500000000
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/reverse_proxy_options.txt 0000664 0000000 0000000 00000005501 14350072374 0027447 0 ustar 00root root 0000000 0000000
https://example.com {
reverse_proxy /path https://localhost:54321 {
header_up Host {upstream_hostport}
header_up Foo bar
method GET
rewrite /rewritten?uri={uri}
buffer_requests
transport http {
read_buffer 10MB
write_buffer 20MB
max_response_header 30MB
dial_timeout 3s
dial_fallback_delay 5s
response_header_timeout 8s
expect_continue_timeout 9s
resolvers 8.8.8.8 8.8.4.4
versions h2c 2
compression off
max_conns_per_host 5
keepalive_idle_conns_per_host 2
keepalive_interval 30s
tls_renegotiation freely
tls_except_ports 8181 8182
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"buffer_requests": true,
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"Foo": [
"bar"
],
"Host": [
"{http.reverse_proxy.upstream.hostport}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/rewritten?uri={http.request.uri}"
},
"transport": {
"compression": false,
"dial_fallback_delay": 5000000000,
"dial_timeout": 3000000000,
"expect_continue_timeout": 9000000000,
"keep_alive": {
"max_idle_conns_per_host": 2,
"probe_interval": 30000000000
},
"max_conns_per_host": 5,
"max_response_header_size": 30000000,
"protocol": "http",
"read_buffer_size": 10000000,
"resolver": {
"addresses": [
"8.8.8.8",
"8.8.4.4"
]
},
"response_header_timeout": 8000000000,
"tls": {
"except_ports": [
"8181",
"8182"
],
"renegotiation": "freely"
},
"versions": [
"h2c",
"2"
],
"write_buffer_size": 20000000
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
],
"match": [
{
"path": [
"/path"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies.txt 0000664 0000000 0000000 00000001563 14350072374 0031223 0 ustar 00root root 0000000 0000000 :8884
reverse_proxy 127.0.0.1:65535 {
trusted_proxies 127.0.0.1
}
reverse_proxy 127.0.0.1:65535 {
trusted_proxies private_ranges
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"trusted_proxies": [
"127.0.0.1"
],
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
},
{
"handler": "reverse_proxy",
"trusted_proxies": [
"192.168.0.0/16",
"172.16.0.0/12",
"10.0.0.0/8",
"127.0.0.1/8",
"fd00::/8",
"::1"
],
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/reverse_proxy_upstream_placeholder.txt 0000664 0000000 0000000 00000003262 14350072374 0032160 0 ustar 00root root 0000000 0000000 :8884 {
map {host} {upstream} {
foo.example.com 1.2.3.4
default 2.3.4.5
}
# Upstream placeholder with a port should retain the port
reverse_proxy {upstream}:80
}
:8885 {
map {host} {upstream} {
foo.example.com 1.2.3.4:8080
default 2.3.4.5:8080
}
# Upstream placeholder with no port should not have a port joined
reverse_proxy {upstream}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"defaults": [
"2.3.4.5"
],
"destinations": [
"{upstream}"
],
"handler": "map",
"mappings": [
{
"input": "foo.example.com",
"outputs": [
"1.2.3.4"
]
}
],
"source": "{http.request.host}"
},
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "{upstream}:80"
}
]
}
]
}
]
},
"srv1": {
"listen": [
":8885"
],
"routes": [
{
"handle": [
{
"defaults": [
"2.3.4.5:8080"
],
"destinations": [
"{upstream}"
],
"handler": "map",
"mappings": [
{
"input": "foo.example.com",
"outputs": [
"1.2.3.4:8080"
]
}
],
"source": "{http.request.host}"
},
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "{upstream}"
}
]
}
]
}
]
}
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.txt 0000664 0000000 0000000 00000001414 14350072374 0032412 0 ustar 00root root 0000000 0000000 localhost:80
respond * "{header.content-type} {labels.0} {query.p} {path.0} {re.name.0}"
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "{http.request.header.content-type} {http.request.host.labels.0} {http.request.uri.query.p} {http.request.uri.path.0} {http.regexp.name.0}",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/site_block_sorting.txt 0000664 0000000 0000000 00000005555 14350072374 0026654 0 ustar 00root root 0000000 0000000 # https://caddy.community/t/caddy-suddenly-directs-my-site-to-the-wrong-directive/11597/2
abcdef {
respond "abcdef"
}
abcdefg {
respond "abcdefg"
}
abc {
respond "abc"
}
abcde, http://abcde {
respond "abcde"
}
:443, ab {
respond "443 or ab"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"abcdefg"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "abcdefg",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"abcdef"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "abcdef",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"abcde"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "abcde",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"abc"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "abc",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "443 or ab",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"abcde"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "abcde",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"certificates": {
"automate": [
"ab"
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/sort_directives_with_any_matcher_first.txt 0000664 0000000 0000000 00000001264 14350072374 0033006 0 ustar 00root root 0000000 0000000 :80
respond 200
@untrusted not remote_ip 10.1.1.0/24
respond @untrusted 401
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"not": [
{
"remote_ip": {
"ranges": [
"10.1.1.0/24"
]
}
}
]
}
],
"handle": [
{
"handler": "static_response",
"status_code": 401
}
]
},
{
"handle": [
{
"handler": "static_response",
"status_code": 200
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/sort_directives_within_handle.txt 0000664 0000000 0000000 00000005176 14350072374 0031075 0 ustar 00root root 0000000 0000000 *.example.com {
@foo host foo.example.com
handle @foo {
handle_path /strip* {
respond "this should be first"
}
handle {
respond "this should be second"
}
}
handle {
respond "this should be last"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"*.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group5",
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/strip"
}
]
},
{
"handle": [
{
"body": "this should be first",
"handler": "static_response"
}
]
}
]
}
],
"match": [
{
"path": [
"/strip*"
]
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "this should be second",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
],
"match": [
{
"host": [
"foo.example.com"
]
}
]
},
{
"group": "group5",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "this should be last",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/sort_vars_in_reverse.txt 0000664 0000000 0000000 00000001362 14350072374 0027224 0 ustar 00root root 0000000 0000000 :80
vars /foobar foo last
vars /foo foo middle
vars * foo first
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"foo": "first",
"handler": "vars"
}
]
},
{
"match": [
{
"path": [
"/foo"
]
}
],
"handle": [
{
"foo": "middle",
"handler": "vars"
}
]
},
{
"match": [
{
"path": [
"/foobar"
]
}
],
"handle": [
{
"foo": "last",
"handler": "vars"
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_acme_preferred_chains.txt 0000664 0000000 0000000 00000001330 14350072374 0030126 0 ustar 00root root 0000000 0000000 localhost
tls {
issuer acme {
preferred_chains {
any_common_name "Generic CA 1" "Generic CA 2"
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"localhost"
],
"issuers": [
{
"module": "acme",
"preferred_chains": {
"any_common_name": [
"Generic CA 1",
"Generic CA 2"
]
}
}
]
}
]
}
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_1.txt 0000664 0000000 0000000 00000002016 14350072374 0030127 0 ustar 00root root 0000000 0000000 {
local_certs
}
*.tld, *.*.tld {
tls {
on_demand
}
}
foo.tld, www.foo.tld {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"foo.tld",
"www.foo.tld"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.tld",
"*.*.tld"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"foo.tld",
"www.foo.tld"
],
"issuers": [
{
"module": "internal"
}
]
},
{
"subjects": [
"*.*.tld",
"*.tld"
],
"issuers": [
{
"module": "internal"
}
],
"on_demand": true
},
{
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_10.txt 0000664 0000000 0000000 00000001421 14350072374 0030206 0 ustar 00root root 0000000 0000000 # example from issue #4667
{
auto_https off
}
https://, example.com {
tls test.crt test.key
respond "Hello World"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"handle": [
{
"body": "Hello World",
"handler": "static_response"
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": [
"cert0"
]
}
}
],
"automatic_https": {
"disable": true
}
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "test.crt",
"key": "test.key",
"tags": [
"cert0"
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_2.txt 0000664 0000000 0000000 00000002133 14350072374 0030130 0 ustar 00root root 0000000 0000000 # issue #3953
{
cert_issuer zerossl api_key
}
example.com {
tls {
on_demand
key_type rsa2048
}
}
http://example.net {
}
:1234 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":1234"
]
},
"srv1": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv2": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"example.net"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"api_key": "api_key",
"module": "zerossl"
}
],
"key_type": "rsa2048",
"on_demand": true
},
{
"issuers": [
{
"api_key": "api_key",
"module": "zerossl"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.txt 0000664 0000000 0000000 00000002251 14350072374 0030132 0 ustar 00root root 0000000 0000000 # https://caddy.community/t/caddyfile-having-individual-sites-differ-from-global-options/11297
{
local_certs
}
a.example.com {
tls internal
}
b.example.com {
tls abc@example.com
}
c.example.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"b.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"c.example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"b.example.com"
],
"issuers": [
{
"email": "abc@example.com",
"module": "acme"
},
{
"email": "abc@example.com",
"module": "zerossl"
}
]
},
{
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.txt 0000664 0000000 0000000 00000004203 14350072374 0030132 0 ustar 00root root 0000000 0000000 {
email my.email@example.com
}
:82 {
redir https://example.com{uri}
}
:83 {
redir https://example.com{uri}
}
:84 {
redir https://example.com{uri}
}
abc.de {
redir https://example.com{uri}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"abc.de"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"https://example.com{http.request.uri}"
]
},
"status_code": 302
}
]
}
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":82"
],
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"https://example.com{http.request.uri}"
]
},
"status_code": 302
}
]
}
]
},
"srv2": {
"listen": [
":83"
],
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"https://example.com{http.request.uri}"
]
},
"status_code": 302
}
]
}
]
},
"srv3": {
"listen": [
":84"
],
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"https://example.com{http.request.uri}"
]
},
"status_code": 302
}
]
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"email": "my.email@example.com",
"module": "acme"
},
{
"email": "my.email@example.com",
"module": "zerossl"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_5.txt 0000664 0000000 0000000 00000001271 14350072374 0030135 0 ustar 00root root 0000000 0000000 a.example.com {
}
b.example.com {
}
:443 {
tls {
on_demand
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"b.example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"a.example.com",
"b.example.com"
]
},
{
"on_demand": true
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_6.txt 0000664 0000000 0000000 00000002663 14350072374 0030144 0 ustar 00root root 0000000 0000000 # (this Caddyfile is contrived, but based on issue #4161)
example.com {
tls {
ca https://foobar
}
}
example.com:8443 {
tls {
ca https://foobar
}
}
example.com:8444 {
tls {
ca https://foobar
}
}
example.com:8445 {
tls {
ca https://foobar
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":8443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv2": {
"listen": [
":8444"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv3": {
"listen": [
":8445"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"ca": "https://foobar",
"module": "acme"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_7.txt 0000664 0000000 0000000 00000001520 14350072374 0030134 0 ustar 00root root 0000000 0000000 # (this Caddyfile is contrived, but based on issues #4176 and #4198)
http://example.com {
}
https://example.com {
tls internal
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.txt 0000664 0000000 0000000 00000002422 14350072374 0030137 0 ustar 00root root 0000000 0000000 # (this Caddyfile is contrived, but based on issues #4176 and #4198)
http://example.com {
}
https://example.com {
tls abc@example.com
}
http://localhost:8081 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv2": {
"listen": [
":8081"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
}
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"email": "abc@example.com",
"module": "acme"
},
{
"email": "abc@example.com",
"module": "zerossl"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_9.txt 0000664 0000000 0000000 00000001525 14350072374 0030143 0 ustar 00root root 0000000 0000000 # example from issue #4640
http://foo:8447, http://127.0.0.1:8447 {
reverse_proxy 127.0.0.1:8080
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8447"
],
"routes": [
{
"match": [
{
"host": [
"foo",
"127.0.0.1"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "127.0.0.1:8080"
}
]
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"foo",
"127.0.0.1"
]
}
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.txt0000664 0000000 0000000 00000001416 14350072374 0034451 0 ustar 00root root 0000000 0000000 {
email foo@bar
}
localhost {
}
example.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"email": "foo@bar",
"module": "acme"
},
{
"email": "foo@bar",
"module": "zerossl"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file.txt 0000664 0000000 0000000 00000004057 14350072374 0030162 0 ustar 00root root 0000000 0000000 localhost
respond "hello from localhost"
tls {
client_auth {
mode request
trusted_ca_cert_file ../caddy.ca.cer
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"localhost"
]
},
"client_authentication": {
"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=="
],
"mode": "request"
}
},
{}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert.txt 0000664 0000000 0000000 00000006207 14350072374 0030520 0 ustar 00root root 0000000 0000000 localhost
respond "hello from localhost"
tls {
client_auth {
mode request
trusted_ca_cert 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": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"localhost"
]
},
"client_authentication": {
"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=="
],
"mode": "request"
}
},
{}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_conn_policy_consolidate.txt 0000664 0000000 0000000 00000003551 14350072374 0030545 0 ustar 00root root 0000000 0000000 # https://github.com/caddyserver/caddy/issues/3906
a.a {
tls internal
respond 403
}
http://b.b https://b.b:8443 {
tls internal
respond 404
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.a"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 403
}
]
}
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"b.b"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 404
}
]
}
]
}
],
"terminal": true
}
]
},
"srv2": {
"listen": [
":8443"
],
"routes": [
{
"match": [
{
"host": [
"b.b"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 404
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"a.a",
"b.b"
],
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_internal_options.txt 0000664 0000000 0000000 00000001250 14350072374 0027226 0 ustar 00root root 0000000 0000000 a.example.com {
tls {
issuer internal {
ca foo
lifetime 24h
sign_with_root
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"a.example.com"
],
"issuers": [
{
"ca": "foo",
"lifetime": 86400000000000,
"module": "internal",
"sign_with_root": true
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tls_propagation_timeout.txt 0000664 0000000 0000000 00000002441 14350072374 0027733 0 ustar 00root root 0000000 0000000 localhost
respond "hello from localhost"
tls {
issuer acme {
propagation_delay 5m10s
propagation_timeout 10m20s
}
issuer zerossl {
propagation_delay 5m30s
propagation_timeout -1
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"localhost"
],
"issuers": [
{
"challenges": {
"dns": {
"propagation_delay": 310000000000,
"propagation_timeout": 620000000000
}
},
"module": "acme"
},
{
"challenges": {
"dns": {
"propagation_delay": 330000000000,
"propagation_timeout": -1
}
},
"module": "zerossl"
}
]
}
]
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt/tracing.txt 0000664 0000000 0000000 00000000641 14350072374 0024407 0 ustar 00root root 0000000 0000000 :80 {
tracing /myhandler {
span my-span
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/myhandler"
]
}
],
"handle": [
{
"handler": "tracing",
"span": "my-span"
}
]
}
]
}
}
}
}
} caddy-2.6.2/caddytest/integration/caddyfile_adapt_test.go 0000664 0000000 0000000 00000002712 14350072374 0023606 0 ustar 00root root 0000000 0000000 package integration
import (
jsonMod "encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestCaddyfileAdaptToJSON(t *testing.T) {
// load the list of test files from the dir
files, err := os.ReadDir("./caddyfile_adapt")
if err != nil {
t.Errorf("failed to read caddyfile_adapt dir: %s", err)
}
// prep a regexp to fix strings on windows
winNewlines := regexp.MustCompile(`\r?\n`)
for _, f := range files {
if f.IsDir() {
continue
}
// read the test file
filename := f.Name()
data, err := os.ReadFile("./caddyfile_adapt/" + filename)
if err != nil {
t.Errorf("failed to read %s dir: %s", filename, err)
}
// split the Caddyfile (first) and JSON (second) parts
// (append newline to Caddyfile to match formatter expectations)
parts := strings.Split(string(data), "----------")
caddyfile, json := strings.TrimSpace(parts[0])+"\n", strings.TrimSpace(parts[1])
// replace windows newlines in the json with unix newlines
json = winNewlines.ReplaceAllString(json, "\n")
// replace os-specific default path for file_server's hide field
replacePath, _ := jsonMod.Marshal(fmt.Sprint(".", string(filepath.Separator), "Caddyfile"))
json = strings.ReplaceAll(json, `"./Caddyfile"`, string(replacePath))
// run the test
ok := caddytest.CompareAdapt(t, filename, caddyfile, "caddyfile", json)
if !ok {
t.Errorf("failed to adapt %s", filename)
}
}
}
caddy-2.6.2/caddytest/integration/caddyfile_test.go 0000664 0000000 0000000 00000004636 14350072374 0022444 0 ustar 00root root 0000000 0000000 package integration
import (
"net/http"
"net/url"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestRespond(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost")
}
func TestRedirect(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
redir / http://localhost:9080/hello 301
respond /hello 200 {
body "hello from localhost"
}
}
`, "caddyfile")
// act and assert
tester.AssertRedirect("http://localhost:9080/", "http://localhost:9080/hello", 301)
// follow redirect
tester.AssertGetResponse("http://localhost:9080/", 200, "hello from localhost")
}
func TestDuplicateHosts(t *testing.T) {
// act and assert
caddytest.AssertLoadError(t,
`
localhost:9080 {
}
localhost:9080 {
}
`,
"caddyfile",
"ambiguous site definition")
}
func TestReadCookie(t *testing.T) {
localhost, _ := url.Parse("http://localhost")
cookie := http.Cookie{
Name: "clientname",
Value: "caddytest",
}
// arrange
tester := caddytest.NewTester(t)
tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie})
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
templates {
root testdata
}
file_server {
root testdata
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/cookie.html", 200, "Cookie.ClientName caddytest
")
}
func TestReplIndex(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
templates {
root testdata
}
file_server {
root testdata
index "index.{host}.html"
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/", 200, "")
}
caddy-2.6.2/caddytest/integration/handler_test.go 0000664 0000000 0000000 00000001014 14350072374 0022120 0 ustar 00root root 0000000 0000000 package integration
import (
"net/http"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestBrowse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
file_server browse
}
`, "caddyfile")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
if err != nil {
t.Fail()
return
}
tester.AssertResponseCode(req, 200)
}
caddy-2.6.2/caddytest/integration/map_test.go 0000664 0000000 0000000 00000006435 14350072374 0021274 0 ustar 00root root 0000000 0000000 package integration
import (
"bytes"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestMap(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
map {http.request.method} {dest-1} {dest-2} {
default unknown1 unknown2
~G(.)(.) G${1}${2}-called
POST post-called foobar
}
respond /version 200 {
body "hello from localhost {dest-1} {dest-2}"
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost GET-called unknown2")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called foobar")
}
func TestMapRespondWithDefault(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
localhost:9080 {
map {http.request.method} {dest-name} {
default unknown
GET get-called
}
respond /version 200 {
body "hello from localhost {dest-name}"
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown")
}
func TestMapAsJSON(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9080"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "map",
"source": "{http.request.method}",
"destinations": ["{dest-name}"],
"defaults": ["unknown"],
"mappings": [
{
"input": "GET",
"outputs": ["get-called"]
},
{
"input": "POST",
"outputs": ["post-called"]
}
]
}
]
},
{
"handle": [
{
"body": "hello from localhost {dest-name}",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": ["/version"]
}
]
}
]
}
],
"match": [
{
"host": ["localhost"]
}
],
"terminal": true
}
]
}
}
}
}
}`, "json")
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
}
caddy-2.6.2/caddytest/integration/reverseproxy_test.go 0000664 0000000 0000000 00000024166 14350072374 0023275 0 ustar 00root root 0000000 0000000 package integration
import (
"fmt"
"net"
"net/http"
"os"
"runtime"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestSRVReverseProxy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"lookup_srv": "srv.host.service.consul"
}
]
}
]
}
]
}
}
}
}
}
`, "json")
}
func TestSRVWithDial(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "tcp/address.to.upstream:80",
"lookup_srv": "srv.host.service.consul"
}
]
}
]
}
]
}
}
}
}
}
`, "json", `upstream: specifying dial address is incompatible with lookup_srv: 0: {\"dial\": \"tcp/address.to.upstream:80\", \"lookup_srv\": \"srv.host.service.consul\"}`)
}
func TestDialWithPlaceholderUnix(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
f, err := os.CreateTemp("", "*.sock")
if err != nil {
t.Errorf("failed to create TempFile: %s", err)
return
}
// a hack to get a file name within a valid path to use as socket
socketName := f.Name()
os.Remove(f.Name())
server := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello, World!"))
}),
}
unixListener, err := net.Listen("unix", socketName)
if err != nil {
t.Errorf("failed to listen on the socket: %s", err)
return
}
go server.Serve(unixListener)
t.Cleanup(func() {
server.Close()
})
runtime.Gosched() // Allow other goroutines to run
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "unix/{http.request.header.X-Caddy-Upstream-Dial}"
}
]
}
]
}
]
}
}
}
}
}
`, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", nil)
if err != nil {
t.Fail()
return
}
req.Header.Set("X-Caddy-Upstream-Dial", socketName)
tester.AssertResponse(req, 200, "Hello, World!")
}
func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "static_response",
"body": "Hello, World!"
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
}
},
"srv1": {
"listen": [
":9080"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "{http.request.header.X-Caddy-Upstream-Dial}"
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
}
}
}
}
}
}
`, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
if err != nil {
t.Fail()
return
}
req.Header.Set("X-Caddy-Upstream-Dial", "localhost:8080")
tester.AssertResponse(req, 200, "Hello, World!")
}
func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "static_response",
"body": "Hello, World!"
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
}
},
"srv1": {
"listen": [
":9080"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "tcp/{http.request.header.X-Caddy-Upstream-Dial}:8080"
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
}
}
}
}
}
}
`, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
if err != nil {
t.Fail()
return
}
req.Header.Set("X-Caddy-Upstream-Dial", "localhost")
tester.AssertResponse(req, 200, "Hello, World!")
}
func TestSRVWithActiveHealthcheck(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"path": "/ok"
}
},
"upstreams": [
{
"lookup_srv": "srv.host.service.consul"
}
]
}
]
}
]
}
}
}
}
}
`, "json", `upstream: lookup_srv is incompatible with active health checks: 0: {\"dial\": \"\", \"lookup_srv\": \"srv.host.service.consul\"}`)
}
func TestReverseProxyHealthCheck(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:2020 {
respond "Hello, World!"
}
http://localhost:2021 {
respond "ok"
}
http://localhost:9080 {
reverse_proxy {
to localhost:2020
health_uri /health
health_port 2021
health_interval 10ms
health_timeout 100ms
}
}
`, "caddyfile")
time.Sleep(100 * time.Millisecond) // TODO: for some reason this test seems particularly flaky, getting 503 when it should be 200, unless we wait
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
tester := caddytest.NewTester(t)
f, err := os.CreateTemp("", "*.sock")
if err != nil {
t.Errorf("failed to create TempFile: %s", err)
return
}
// a hack to get a file name within a valid path to use as socket
socketName := f.Name()
os.Remove(f.Name())
server := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.URL.Path, "/health") {
w.Write([]byte("ok"))
return
}
w.Write([]byte("Hello, World!"))
}),
}
unixListener, err := net.Listen("unix", socketName)
if err != nil {
t.Errorf("failed to listen on the socket: %s", err)
return
}
go server.Serve(unixListener)
t.Cleanup(func() {
server.Close()
})
runtime.Gosched() // Allow other goroutines to run
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy {
to unix/%s
health_uri /health
health_port 2021
health_interval 2s
health_timeout 5s
}
}
`, socketName), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
tester := caddytest.NewTester(t)
f, err := os.CreateTemp("", "*.sock")
if err != nil {
t.Errorf("failed to create TempFile: %s", err)
return
}
// a hack to get a file name within a valid path to use as socket
socketName := f.Name()
os.Remove(f.Name())
server := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.URL.Path, "/health") {
w.Write([]byte("ok"))
return
}
w.Write([]byte("Hello, World!"))
}),
}
unixListener, err := net.Listen("unix", socketName)
if err != nil {
t.Errorf("failed to listen on the socket: %s", err)
return
}
go server.Serve(unixListener)
t.Cleanup(func() {
server.Close()
})
runtime.Gosched() // Allow other goroutines to run
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy {
to unix/%s
health_uri /health
health_interval 2s
health_timeout 5s
}
}
`, socketName), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
caddy-2.6.2/caddytest/integration/sni_test.go 0000664 0000000 0000000 00000013344 14350072374 0021305 0 ustar 00root root 0000000 0000000 package integration
import (
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestDefaultSNI(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"match": {
"sni": [
"127.0.0.1"
]
}
},
{
"default_sni": "*.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/caddy.localhost.crt",
"key": "/caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost")
}
func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"a.caddy.localhost",
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost",
"match": {
"sni": [
"a.caddy.localhost",
"127.0.0.1",
""
]
}
},
{
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a")
}
func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost")
}
func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
caddytest.AssertAdapt(t, `
{
skip_install_trust
default_sni a.caddy.localhost
}
:80 {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile", `{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/version"
]
}
],
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
]
}
]
}
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
}
}`)
}
caddy-2.6.2/caddytest/integration/stream_test.go 0000664 0000000 0000000 00000023730 14350072374 0022007 0 ustar 00root root 0000000 0000000 package integration
import (
"compress/gzip"
"context"
"crypto/rand"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2/caddytest"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
// (see https://github.com/caddyserver/caddy/issues/3556 for use case)
func TestH2ToH2CStream(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"compression": false,
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
],
"match": [
{
"path": [
"/tov2ray"
]
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
expectedBody := "some data to be echoed"
// start the server
server := testH2ToH2CStreamServeH2C(t)
go server.ListenAndServe()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
defer cancel()
server.Shutdown(ctx)
}()
r, w := io.Pipe()
req := &http.Request{
Method: "PUT",
Body: io.NopCloser(r),
URL: &url.URL{
Scheme: "https",
Host: "127.0.0.1:9443",
Path: "/tov2ray",
},
Proto: "HTTP/2",
ProtoMajor: 2,
ProtoMinor: 0,
Header: make(http.Header),
}
// Disable any compression method from server.
req.Header.Set("Accept-Encoding", "identity")
resp := tester.AssertResponseCode(req, http.StatusOK)
if resp.StatusCode != http.StatusOK {
return
}
go func() {
fmt.Fprint(w, expectedBody)
w.Close()
}()
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if !strings.Contains(body, expectedBody) {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
}
func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
h2s := &http2.Server{}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rstring, err := httputil.DumpRequest(r, false)
if err == nil {
t.Logf("h2c server received req: %s", rstring)
}
// We only accept HTTP/2!
if r.ProtoMajor != 2 {
t.Error("Not a HTTP/2 request, rejected!")
w.WriteHeader(http.StatusInternalServerError)
return
}
if r.Host != "127.0.0.1:9443" {
t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound)
return
}
if !strings.HasPrefix(r.URL.Path, "/tov2ray") {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(200)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
buf := make([]byte, 4*1024)
for {
n, err := r.Body.Read(buf)
if n > 0 {
w.Write(buf[:n])
}
if err != nil {
if err == io.EOF {
r.Body.Close()
}
break
}
}
})
server := &http.Server{
Addr: "127.0.0.1:54321",
Handler: h2c.NewHandler(handler, h2s),
}
return server
}
// (see https://github.com/caddyserver/caddy/issues/3606 for use case)
func TestH2ToH1ChunkedResponse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"encodings": {
"gzip": {}
},
"handler": "encode"
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
],
"match": [
{
"path": [
"/tov2ray"
]
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": [
"cert0"
]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
}
}
`, "json")
// need a large body here to trigger caddy's compression, larger than gzip.miniLength
expectedBody, err := GenerateRandomString(1024)
if err != nil {
t.Fatalf("generate expected body failed, err: %s", err)
}
// start the server
server := testH2ToH1ChunkedResponseServeH1(t)
go server.ListenAndServe()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
defer cancel()
server.Shutdown(ctx)
}()
r, w := io.Pipe()
req := &http.Request{
Method: "PUT",
Body: io.NopCloser(r),
URL: &url.URL{
Scheme: "https",
Host: "127.0.0.1:9443",
Path: "/tov2ray",
},
Proto: "HTTP/2",
ProtoMajor: 2,
ProtoMinor: 0,
Header: make(http.Header),
}
// underlying transport will automaticlly add gzip
// req.Header.Set("Accept-Encoding", "gzip")
go func() {
fmt.Fprint(w, expectedBody)
w.Close()
}()
resp := tester.AssertResponseCode(req, http.StatusOK)
if resp.StatusCode != http.StatusOK {
return
}
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if body != expectedBody {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
}
func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host != "127.0.0.1:9443" {
t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound)
return
}
if !strings.HasPrefix(r.URL.Path, "/tov2ray") {
w.WriteHeader(http.StatusNotFound)
return
}
defer r.Body.Close()
bytes, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("unable to read the response body %s", err)
}
n := len(bytes)
var writer io.Writer
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
gw, err := gzip.NewWriterLevel(w, 5)
if err != nil {
t.Error("can't return gzip data")
w.WriteHeader(http.StatusInternalServerError)
return
}
defer gw.Close()
writer = gw
w.Header().Set("Content-Encoding", "gzip")
w.Header().Del("Content-Length")
w.WriteHeader(200)
} else {
writer = w
}
if n > 0 {
writer.Write(bytes[:])
}
})
server := &http.Server{
Addr: "127.0.0.1:54321",
Handler: handler,
}
return server
}
// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}
// GenerateRandomString returns a securely generated random string.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
bytes, err := GenerateRandomBytes(n)
if err != nil {
return "", err
}
for i, b := range bytes {
bytes[i] = letters[b%byte(len(letters))]
}
return string(bytes), nil
}
caddy-2.6.2/caddytest/integration/test.init.config 0000664 0000000 0000000 00000000032 14350072374 0022224 0 ustar 00root root 0000000 0000000 {
admin localhost:2999
}
caddy-2.6.2/caddytest/integration/testdata/ 0000775 0000000 0000000 00000000000 14350072374 0020732 5 ustar 00root root 0000000 0000000 caddy-2.6.2/caddytest/integration/testdata/cookie.html 0000664 0000000 0000000 00000000063 14350072374 0023070 0 ustar 00root root 0000000 0000000 Cookie.ClientName {{.Cookie "clientname"}}