caddy-2.6.2/000077500000000000000000000000001435007237400126125ustar00rootroot00000000000000caddy-2.6.2/.editorconfig000066400000000000000000000001261435007237400152660ustar00rootroot00000000000000[*] end_of_line = lf [caddytest/integration/caddyfile_adapt/*.txt] indent_style = tabcaddy-2.6.2/.gitattributes000066400000000000000000000000201435007237400154750ustar00rootroot00000000000000*.go text eol=lfcaddy-2.6.2/AUTHORS000066400000000000000000000006171435007237400136660ustar00rootroot00000000000000# 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 Light Code Labs Ardan Labs caddy-2.6.2/LICENSE000066400000000000000000000261361435007237400136270ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. caddy-2.6.2/README.md000066400000000000000000000256761435007237400141110ustar00rootroot00000000000000

Caddy

a project


Every site on HTTPS

Caddy is an extensible server platform that uses TLS by default.


@caddyserver on Twitter Caddy Forum
Caddy on Sourcegraph Cloudsmith

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)

Powered by
CertMagic

## [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.go000066400000000000000000001232611435007237400142360ustar00rootroot00000000000000// 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.go000066400000000000000000000130021435007237400152640ustar00rootroot00000000000000// 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.go000066400000000000000000000735131435007237400142360ustar00rootroot00000000000000// 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: // // " " // // where is the absolute path in the config and is the expected hash of // the config at that path. If the hash in the ifMatchHeader doesn't match // the hash of the config, then an APIError with status 412 will be returned. func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error { switch method { case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodConnect, http.MethodTrace: return fmt.Errorf("method not allowed") } currentCtxMu.Lock() defer currentCtxMu.Unlock() if ifMatchHeader != "" { // expect the first and last character to be quotes if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' { return APIError{ HTTPStatus: http.StatusBadRequest, Err: fmt.Errorf("malformed If-Match header; expect quoted string"), } } // read out the parts parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1]) if len(parts) != 2 { return APIError{ HTTPStatus: http.StatusBadRequest, Err: fmt.Errorf("malformed If-Match header; expect format \" \""), } } // get the current hash of the config // at the given path hash := etagHasher() err := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash) if err != nil { return err } if hex.EncodeToString(hash.Sum(nil)) != parts[1] { return APIError{ HTTPStatus: http.StatusPreconditionFailed, Err: fmt.Errorf("If-Match header did not match current config hash"), } } } err := unsyncedConfigAccess(method, path, input, nil) if err != nil { return err } // the mutation is complete, so encode the entire config as JSON newCfg, err := json.Marshal(rawCfg[rawConfigKey]) if err != nil { return APIError{ HTTPStatus: http.StatusBadRequest, Err: fmt.Errorf("encoding new config: %v", err), } } // if nothing changed, no need to do a whole reload unless the client forces it if !forceReload && bytes.Equal(rawCfgJSON, newCfg) { Log().Info("config is unchanged") return errSameConfig } // find any IDs in this config and index them idx := make(map[string]string) err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx) if err != nil { return APIError{ HTTPStatus: http.StatusInternalServerError, Err: fmt.Errorf("indexing config: %v", err), } } // load this new config; if it fails, we need to revert to // our old representation of caddy's actual config err = unsyncedDecodeAndRun(newCfg, true) if err != nil { if len(rawCfgJSON) > 0 { // restore old config state to keep it consistent // with what caddy is still running; we need to // unmarshal it again because it's likely that // pointers deep in our rawCfg map were modified var oldCfg any err2 := json.Unmarshal(rawCfgJSON, &oldCfg) if err2 != nil { err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2) } rawCfg[rawConfigKey] = oldCfg } return fmt.Errorf("loading new config: %v", err) } // success, so update our stored copy of the encoded // config to keep it consistent with what caddy is now // running (storing an encoded copy is not strictly // necessary, but avoids an extra json.Marshal for // each config change) rawCfgJSON = newCfg rawCfgIndex = idx return nil } // readConfig traverses the current config to path // and writes its JSON encoding to out. func readConfig(path string, out io.Writer) error { currentCtxMu.RLock() defer currentCtxMu.RUnlock() return unsyncedConfigAccess(http.MethodGet, path, nil, out) } // indexConfigObjects recursively searches ptr for object fields named // "@id" and maps that ID value to the full configPath in the index. // This function is NOT safe for concurrent access; obtain a write lock // on currentCtxMu. func indexConfigObjects(ptr any, configPath string, index map[string]string) error { switch val := ptr.(type) { case map[string]any: for k, v := range val { if k == idKey { switch idVal := v.(type) { case string: index[idVal] = configPath case float64: // all JSON numbers decode as float64 index[fmt.Sprintf("%v", idVal)] = configPath default: return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey) } continue } // traverse this object property recursively err := indexConfigObjects(val[k], path.Join(configPath, k), index) if err != nil { return err } } case []any: // traverse each element of the array recursively for i := range val { err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index) if err != nil { return err } } } return nil } // unsyncedDecodeAndRun removes any meta fields (like @id tags) // from cfgJSON, decodes the result into a *Config, and runs // it as the new config, replacing any other current config. // It does NOT update the raw config state, as this is a // lower-level function; most callers will want to use Load // instead. A write lock on currentCtxMu is required! If // allowPersist is false, it will not be persisted to disk, // even if it is configured to. func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { // remove any @id fields from the JSON, which would cause // loading to break since the field wouldn't be recognized strippedCfgJSON := RemoveMetaFields(cfgJSON) var newCfg *Config err := strictUnmarshalJSON(strippedCfgJSON, &newCfg) if err != nil { return err } // prevent recursive config loads; that is a user error, and // although frequent config loads should be safe, we cannot // guarantee that in the presence of third party plugins, nor // do we want this error to go unnoticed (we assume it was a // pulled config if we're not allowed to persist it) if !allowPersist && newCfg != nil && newCfg.Admin != nil && newCfg.Admin.Config != nil && newCfg.Admin.Config.LoadRaw != nil && newCfg.Admin.Config.LoadDelay <= 0 { return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_delay") } // run the new config and start all its apps ctx, err := run(newCfg, true) if err != nil { return err } // swap old context (including its config) with the new one oldCtx := currentCtx currentCtx = ctx // Stop, Cleanup each old app unsyncedStop(oldCtx) // autosave a non-nil config, if not disabled if allowPersist && newCfg != nil && (newCfg.Admin == nil || newCfg.Admin.Config == nil || newCfg.Admin.Config.Persist == nil || *newCfg.Admin.Config.Persist) { dir := filepath.Dir(ConfigAutosavePath) err := os.MkdirAll(dir, 0700) if err != nil { Log().Error("unable to create folder for config autosave", zap.String("dir", dir), zap.Error(err)) } else { err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600) if err == nil { Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath)) } else { Log().Error("unable to autosave config", zap.String("file", ConfigAutosavePath), zap.Error(err)) } } } return nil } // run runs newCfg and starts all its apps if // start is true. If any errors happen, cleanup // is performed if any modules were provisioned; // apps that were started already will be stopped, // so this function should not leak resources if // an error is returned. However, if no error is // returned and start == false, you should cancel // the config if you are not going to start it, // so that each provisioned module will be // cleaned up. // // This is a low-level function; most callers // will want to use Run instead, which also // updates the config's raw state. func run(newCfg *Config, start bool) (Context, error) { // because we will need to roll back any state // modifications if this function errors, we // keep a single error value and scope all // sub-operations to their own functions to // ensure this error value does not get // overridden or missed when it should have // been set by a short assignment var err error if newCfg == nil { newCfg = new(Config) } // create a context within which to load // modules - essentially our new config's // execution environment; be sure that // cleanup occurs when we return if there // was an error; if no error, it will get // cleaned up on next config cycle ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg}) defer func() { if err != nil { // if there were any errors during startup, // we should cancel the new context we created // since the associated config won't be used; // this will cause all modules that were newly // provisioned to clean themselves up cancel() // also undo any other state changes we made if currentCtx.cfg != nil { certmagic.Default.Storage = currentCtx.cfg.storage } } }() newCfg.cancelFunc = cancel // clean up later // set up logging before anything bad happens if newCfg.Logging == nil { newCfg.Logging = new(Logging) } err = newCfg.Logging.openLogs(ctx) if err != nil { return ctx, err } // start the admin endpoint (and stop any prior one) if start { err = replaceLocalAdminServer(newCfg) if err != nil { return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err) } } // prepare the new config for use newCfg.apps = make(map[string]App) // set up global storage and make it CertMagic's default storage, too err = func() error { if newCfg.StorageRaw != nil { val, err := ctx.LoadModule(newCfg, "StorageRaw") if err != nil { return fmt.Errorf("loading storage module: %v", err) } stor, err := val.(StorageConverter).CertMagicStorage() if err != nil { return fmt.Errorf("creating storage value: %v", err) } newCfg.storage = stor } if newCfg.storage == nil { newCfg.storage = DefaultStorage } certmagic.Default.Storage = newCfg.storage return nil }() if err != nil { return ctx, err } // Load and Provision each app and their submodules err = func() error { for appName := range newCfg.AppsRaw { if _, err := ctx.App(appName); err != nil { return err } } return nil }() if err != nil { return ctx, err } if !start { return ctx, nil } // Provision any admin routers which may need to access // some of the other apps at runtime err = newCfg.Admin.provisionAdminRouters(ctx) if err != nil { return ctx, err } // Start err = func() error { started := make([]string, 0, len(newCfg.apps)) for name, a := range newCfg.apps { err := a.Start() if err != nil { // an app failed to start, so we need to stop // all other apps that were already started for _, otherAppName := range started { err2 := newCfg.apps[otherAppName].Stop() if err2 != nil { err = fmt.Errorf("%v; additionally, aborting app %s: %v", err, otherAppName, err2) } } return fmt.Errorf("%s app module: start: %v", name, err) } started = append(started, name) } return nil }() if err != nil { return ctx, err } // now that the user's config is running, finish setting up anything else, // such as remote admin endpoint, config loader, etc. return ctx, finishSettingUp(ctx, newCfg) } // finishSettingUp should be run after all apps have successfully started. func finishSettingUp(ctx Context, cfg *Config) error { // establish this server's identity (only after apps are loaded // so that cert management of this endpoint doesn't prevent user's // servers from starting which likely also use HTTP/HTTPS ports; // but before remote management which may depend on these creds) err := manageIdentity(ctx, cfg) if err != nil { return fmt.Errorf("provisioning remote admin endpoint: %v", err) } // replace any remote admin endpoint err = replaceRemoteAdminServer(ctx, cfg) if err != nil { return fmt.Errorf("provisioning remote admin endpoint: %v", err) } // if dynamic config is requested, set that up and run it if cfg != nil && cfg.Admin != nil && cfg.Admin.Config != nil && cfg.Admin.Config.LoadRaw != nil { val, err := ctx.LoadModule(cfg.Admin.Config, "LoadRaw") if err != nil { return fmt.Errorf("loading config loader module: %s", err) } logger := Log().Named("config_loader").With( zap.String("module", val.(Module).CaddyModule().ID.Name()), zap.Int("load_delay", int(cfg.Admin.Config.LoadDelay))) runLoadedConfig := func(config []byte) error { logger.Info("applying dynamically-loaded config") err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false) if errors.Is(err, errSameConfig) { return err } if err != nil { logger.Error("failed to run dynamically-loaded config", zap.Error(err)) return err } logger.Info("successfully applied dynamically-loaded config") return nil } if cfg.Admin.Config.LoadDelay > 0 { go func() { // the loop is here to iterate ONLY if there is an error, a no-op config load, // or an unchanged config; in which case we simply wait the delay and try again for { timer := time.NewTimer(time.Duration(cfg.Admin.Config.LoadDelay)) select { case <-timer.C: loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx) if err != nil { logger.Error("failed loading dynamic config; will retry", zap.Error(err)) continue } if loadedConfig == nil { logger.Info("dynamically-loaded config was nil; will retry") continue } err = runLoadedConfig(loadedConfig) if errors.Is(err, errSameConfig) { logger.Info("dynamically-loaded config was unchanged; will retry") continue } case <-ctx.Done(): if !timer.Stop() { <-timer.C } logger.Info("stopping dynamic config loading") } break } }() } else { // if no LoadDelay is provided, will load config synchronously loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx) if err != nil { return fmt.Errorf("loading dynamic config from %T: %v", val, err) } // do this in a goroutine so current config can finish being loaded; otherwise deadlock go func() { _ = runLoadedConfig(loadedConfig) }() } } return nil } // ConfigLoader is a type that can load a Caddy config. If // the return value is non-nil, it must be valid Caddy JSON; // if nil or with non-nil error, it is considered to be a // no-op load and may be retried later. type ConfigLoader interface { LoadConfig(Context) ([]byte, error) } // Stop stops running the current configuration. // It is the antithesis of Run(). This function // will log any errors that occur during the // stopping of individual apps and continue to // stop the others. Stop should only be called // if not replacing with a new config. func Stop() error { currentCtxMu.Lock() defer currentCtxMu.Unlock() unsyncedStop(currentCtx) currentCtx = Context{} rawCfgJSON = nil rawCfgIndex = nil rawCfg[rawConfigKey] = nil return nil } // unsyncedStop stops cfg from running, but has // no locking around cfg. It is a no-op if cfg is // nil. If any app returns an error when stopping, // it is logged and the function continues stopping // the next app. This function assumes all apps in // cfg were successfully started first. func unsyncedStop(ctx Context) { if ctx.cfg == nil { return } // stop each app for name, a := range ctx.cfg.apps { err := a.Stop() if err != nil { log.Printf("[ERROR] stop %s: %v", name, err) } } // clean up all modules ctx.cfg.cancelFunc() } // Validate loads, provisions, and validates // cfg, but does not start running it. func Validate(cfg *Config) error { _, err := run(cfg, false) if err == nil { cfg.cancelFunc() // call Cleanup on all modules } return err } // exitProcess exits the process as gracefully as possible, // but it always exits, even if there are errors doing so. // It stops all apps, cleans up external locks, removes any // PID file, and shuts down admin endpoint(s) in a goroutine. // Errors are logged along the way, and an appropriate exit // code is emitted. func exitProcess(ctx context.Context, logger *zap.Logger) { // let the rest of the program know we're quitting atomic.StoreInt32(exiting, 1) // give the OS or service/process manager our 2 weeks' notice: we quit if err := notify.Stopping(); err != nil { Log().Error("unable to notify service manager of stopping state", zap.Error(err)) } if logger == nil { logger = Log() } logger.Warn("exiting; byeee!! 👋") exitCode := ExitCodeSuccess // stop all apps if err := Stop(); err != nil { logger.Error("failed to stop apps", zap.Error(err)) exitCode = ExitCodeFailedQuit } // clean up certmagic locks certmagic.CleanUpOwnLocks(ctx, logger) // remove pidfile if pidfile != "" { err := os.Remove(pidfile) if err != nil { logger.Error("cleaning up PID file:", zap.String("pidfile", pidfile), zap.Error(err)) exitCode = ExitCodeFailedQuit } } // shut down admin endpoint(s) in goroutines so that // if this function was called from an admin handler, // it has a chance to return gracefully // use goroutine so that we can finish responding to API request go func() { defer func() { logger = logger.With(zap.Int("exit_code", exitCode)) if exitCode == ExitCodeSuccess { logger.Info("shutdown complete") } else { logger.Error("unclean shutdown") } os.Exit(exitCode) }() if remoteAdminServer != nil { err := stopAdminServer(remoteAdminServer) if err != nil { exitCode = ExitCodeFailedQuit logger.Error("failed to stop remote admin server gracefully", zap.Error(err)) } } if localAdminServer != nil { err := stopAdminServer(localAdminServer) if err != nil { exitCode = ExitCodeFailedQuit logger.Error("failed to stop local admin server gracefully", zap.Error(err)) } } }() } var exiting = new(int32) // accessed atomically // Exiting returns true if the process is exiting. // EXPERIMENTAL API: subject to change or removal. func Exiting() bool { return atomic.LoadInt32(exiting) == 1 } // Duration can be an integer or a string. An integer is // interpreted as nanoseconds. If a string, it is a Go // time.Duration value such as `300ms`, `1.5h`, or `2h45m`; // valid units are `ns`, `us`/`µs`, `ms`, `s`, `m`, `h`, and `d`. type Duration time.Duration // UnmarshalJSON satisfies json.Unmarshaler. func (d *Duration) UnmarshalJSON(b []byte) error { if len(b) == 0 { return io.EOF } var dur time.Duration var err error if b[0] == byte('"') && b[len(b)-1] == byte('"') { dur, err = ParseDuration(strings.Trim(string(b), `"`)) } else { err = json.Unmarshal(b, &dur) } *d = Duration(dur) return err } // ParseDuration parses a duration string, adding // support for the "d" unit meaning number of days, // where a day is assumed to be 24h. The maximum // input string length is 1024. func ParseDuration(s string) (time.Duration, error) { if len(s) > 1024 { return 0, fmt.Errorf("parsing duration: input string too long") } var inNumber bool var numStart int for i := 0; i < len(s); i++ { ch := s[i] if ch == 'd' { daysStr := s[numStart:i] days, err := strconv.ParseFloat(daysStr, 64) if err != nil { return 0, err } hours := days * 24.0 hoursStr := strconv.FormatFloat(hours, 'f', -1, 64) s = s[:numStart] + hoursStr + "h" + s[i+1:] i-- continue } if !inNumber { numStart = i } inNumber = (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == '+' } return time.ParseDuration(s) } // InstanceID returns the UUID for this instance, and generates one if it // does not already exist. The UUID is stored in the local data directory, // regardless of storage configuration, since each instance is intended to // have its own unique ID. func InstanceID() (uuid.UUID, error) { uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid") uuidFileBytes, err := os.ReadFile(uuidFilePath) if os.IsNotExist(err) { uuid, err := uuid.NewRandom() if err != nil { return uuid, err } err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0600) return uuid, err } else if err != nil { return [16]byte{}, err } return uuid.ParseBytes(uuidFileBytes) } // CustomVersion is an optional string that overrides Caddy's // reported version. It can be helpful when downstream packagers // need to manually set Caddy's version. If no other version // information is available, the short form version (see // Version()) will be set to CustomVersion, and the full version // will include CustomVersion at the beginning. // // Set this variable during `go build` with `-ldflags`: // // -ldflags '-X github.com/caddyserver/caddy/v2.CustomVersion=v2.6.2' // // for example. var CustomVersion string // Version returns the Caddy version in a simple/short form, and // a full version string. The short form will not have spaces and // is intended for User-Agent strings and similar, but may be // omitting valuable information. Note that Caddy must be compiled // in a special way to properly embed complete version information. // First this function tries to get the version from the embedded // build info provided by go.mod dependencies; then it tries to // get info from embedded VCS information, which requires having // built Caddy from a git repository. If no version is available, // this function returns "(devel)" because Go uses that, but for // the simple form we change it to "unknown". If still no version // is available (e.g. no VCS repo), then it will use CustomVersion; // CustomVersion is always prepended to the full version string. // // See relevant Go issues: https://github.com/golang/go/issues/29228 // and https://github.com/golang/go/issues/50603. // // This function is experimental and subject to change or removal. func Version() (simple, full string) { // the currently-recommended way to build Caddy involves // building it as a dependency so we can extract version // information from go.mod tooling; once the upstream // Go issues are fixed, we should just be able to use // bi.Main... hopefully. var module *debug.Module bi, ok := debug.ReadBuildInfo() if ok { // find the Caddy module in the dependency list for _, dep := range bi.Deps { if dep.Path == ImportPath { module = dep break } } } if module != nil { simple, full = module.Version, module.Version if module.Sum != "" { full += " " + module.Sum } if module.Replace != nil { full += " => " + module.Replace.Path if module.Replace.Version != "" { simple = module.Replace.Version + "_custom" full += "@" + module.Replace.Version } if module.Replace.Sum != "" { full += " " + module.Replace.Sum } } } if full == "" { var vcsRevision string var vcsTime time.Time var vcsModified bool for _, setting := range bi.Settings { switch setting.Key { case "vcs.revision": vcsRevision = setting.Value case "vcs.time": vcsTime, _ = time.Parse(time.RFC3339, setting.Value) case "vcs.modified": vcsModified, _ = strconv.ParseBool(setting.Value) } } if vcsRevision != "" { var modified string if vcsModified { modified = "+modified" } full = fmt.Sprintf("%s%s (%s)", vcsRevision, modified, vcsTime.Format(time.RFC822)) simple = vcsRevision // use short checksum for simple, if hex-only if _, err := hex.DecodeString(simple); err == nil { simple = simple[:8] } // append date to simple since it can be convenient // to know the commit date as part of the version if !vcsTime.IsZero() { simple += "-" + vcsTime.Format("20060102") } } } if full == "" { if CustomVersion != "" { full = CustomVersion } else { full = "unknown" } } else if CustomVersion != "" { full = CustomVersion + " " + full } if simple == "" || simple == "(devel)" { if CustomVersion != "" { simple = CustomVersion } else { simple = "unknown" } } return } // ActiveContext returns the currently-active context. // This function is experimental and might be changed // or removed in the future. func ActiveContext() Context { currentCtxMu.RLock() defer currentCtxMu.RUnlock() return currentCtx } // CtxKey is a value type for use with context.WithValue. type CtxKey string // This group of variables pertains to the current configuration. var ( // currentCtxMu protects everything in this var block. currentCtxMu sync.RWMutex // currentCtx is the root context for the currently-running // configuration, which can be accessed through this value. // If the Config contained in this value is not nil, then // a config is currently active/running. currentCtx Context // rawCfg is the current, generic-decoded configuration; // we initialize it as a map with one field ("config") // to maintain parity with the API endpoint and to avoid // the special case of having to access/mutate the variable // directly without traversing into it. rawCfg = map[string]any{ rawConfigKey: nil, } // rawCfgJSON is the JSON-encoded form of rawCfg. Keeping // this around avoids an extra Marshal call during changes. rawCfgJSON []byte // rawCfgIndex is the map of user-assigned ID to expanded // path, for converting /id/ paths to /config/ paths. rawCfgIndex map[string]string ) // errSameConfig is returned if the new config is the same // as the old one. This isn't usually an actual, actionable // error; it's mostly a sentinel value. var errSameConfig = errors.New("config is unchanged") // ImportPath is the package import path for Caddy core. // This identifier may be removed in the future. const ImportPath = "github.com/caddyserver/caddy/v2" caddy-2.6.2/caddy_test.go000066400000000000000000000032031435007237400152620ustar00rootroot00000000000000// 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 ( "testing" "time" ) func TestParseDuration(t *testing.T) { const day = 24 * time.Hour for i, tc := range []struct { input string expect time.Duration }{ { input: "3h", expect: 3 * time.Hour, }, { input: "1d", expect: day, }, { input: "1d30m", expect: day + 30*time.Minute, }, { input: "1m2d", expect: time.Minute + day*2, }, { input: "1m2d30s", expect: time.Minute + day*2 + 30*time.Second, }, { input: "1d2d", expect: 3 * day, }, { input: "1.5d", expect: time.Duration(1.5 * float64(day)), }, { input: "4m1.25d", expect: 4*time.Minute + time.Duration(1.25*float64(day)), }, { input: "-1.25d12h", expect: time.Duration(-1.25*float64(day)) - 12*time.Hour, }, } { actual, err := ParseDuration(tc.input) if err != nil { t.Errorf("Test %d ('%s'): Got error: %v", i, tc.input, err) continue } if actual != tc.expect { t.Errorf("Test %d ('%s'): Expected=%s Actual=%s", i, tc.input, tc.expect, actual) } } } caddy-2.6.2/caddyconfig/000077500000000000000000000000001435007237400150645ustar00rootroot00000000000000caddy-2.6.2/caddyconfig/caddyfile/000077500000000000000000000000001435007237400170105ustar00rootroot00000000000000caddy-2.6.2/caddyconfig/caddyfile/adapter.go000066400000000000000000000111471435007237400207630ustar00rootroot00000000000000// 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 caddyfile import ( "bytes" "encoding/json" "fmt" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" ) // Adapter adapts Caddyfile to Caddy JSON. type Adapter struct { ServerType ServerType } // Adapt converts the Caddyfile config in body to Caddy JSON. func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconfig.Warning, error) { if a.ServerType == nil { return nil, nil, fmt.Errorf("no server type") } if options == nil { options = make(map[string]any) } filename, _ := options["filename"].(string) if filename == "" { filename = "Caddyfile" } serverBlocks, err := Parse(filename, body) if err != nil { return nil, nil, err } cfg, warnings, err := a.ServerType.Setup(serverBlocks, options) if err != nil { return nil, warnings, err } // lint check: see if input was properly formatted; sometimes messy files files parse // successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry) if warning, different := formattingDifference(filename, body); different { warnings = append(warnings, warning) } result, err := json.Marshal(cfg) return result, warnings, err } // formattingDifference returns a warning and true if the formatted version // is any different from the input; empty warning and false otherwise. // TODO: also perform this check on imported files func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) { // replace windows-style newlines to normalize comparison normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1) formatted := Format(normalizedBody) if bytes.Equal(formatted, normalizedBody) { return caddyconfig.Warning{}, false } // find where the difference is line := 1 for i, ch := range normalizedBody { if i >= len(formatted) || ch != formatted[i] { break } if ch == '\n' { line++ } } return caddyconfig.Warning{ File: filename, Line: line, Message: "Caddyfile input is not formatted; run the 'caddy fmt' command to fix inconsistencies", }, true } // Unmarshaler is a type that can unmarshal // Caddyfile tokens to set itself up for a // JSON encoding. The goal of an unmarshaler // is not to set itself up for actual use, // but to set itself up for being marshaled // into JSON. Caddyfile-unmarshaled values // will not be used directly; they will be // encoded as JSON and then used from that. // Implementations must be able to support // multiple segments (instances of their // directive or batch of tokens); typically // this means wrapping all token logic in // a loop: `for d.Next() { ... }`. type Unmarshaler interface { UnmarshalCaddyfile(d *Dispenser) error } // ServerType is a type that can evaluate a Caddyfile and set up a caddy config. type ServerType interface { // Setup takes the server blocks which // contain tokens, as well as options // (e.g. CLI flags) and creates a Caddy // config, along with any warnings or // an error. Setup([]ServerBlock, map[string]any) (*caddy.Config, []caddyconfig.Warning, error) } // UnmarshalModule instantiates a module with the given ID and invokes // UnmarshalCaddyfile on the new value using the immediate next segment // of d as input. In other words, d's next token should be the first // token of the module's Caddyfile input. // // This function is used when the next segment of Caddyfile tokens // belongs to another Caddy module. The returned value is often // type-asserted to the module's associated type for practical use // when setting up a config. func UnmarshalModule(d *Dispenser, moduleID string) (Unmarshaler, error) { mod, err := caddy.GetModule(moduleID) if err != nil { return nil, d.Errf("getting module named '%s': %v", moduleID, err) } inst := mod.New() unm, ok := inst.(Unmarshaler) if !ok { return nil, d.Errf("module %s is not a Caddyfile unmarshaler; is %T", mod.ID, inst) } err = unm.UnmarshalCaddyfile(d.NewFromNextSegment()) if err != nil { return nil, err } return unm, nil } // Interface guard var _ caddyconfig.Adapter = (*Adapter)(nil) caddy-2.6.2/caddyconfig/caddyfile/dispenser.go000066400000000000000000000353401435007237400213400ustar00rootroot00000000000000// 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 caddyfile import ( "errors" "fmt" "io" "log" "strconv" "strings" ) // Dispenser is a type that dispenses tokens, similarly to a lexer, // except that it can do so with some notion of structure. An empty // Dispenser is invalid; call NewDispenser to make a proper instance. type Dispenser struct { tokens []Token cursor int nesting int } // NewDispenser returns a Dispenser filled with the given tokens. func NewDispenser(tokens []Token) *Dispenser { return &Dispenser{ tokens: tokens, cursor: -1, } } // NewTestDispenser parses input into tokens and creates a new // Dispenser for test purposes only; any errors are fatal. func NewTestDispenser(input string) *Dispenser { tokens, err := allTokens("Testfile", []byte(input)) if err != nil && err != io.EOF { log.Fatalf("getting all tokens from input: %v", err) } return NewDispenser(tokens) } // Next loads the next token. Returns true if a token // was loaded; false otherwise. If false, all tokens // have been consumed. func (d *Dispenser) Next() bool { if d.cursor < len(d.tokens)-1 { d.cursor++ return true } return false } // Prev moves to the previous token. It does the inverse // of Next(), except this function may decrement the cursor // to -1 so that the next call to Next() points to the // first token; this allows dispensing to "start over". This // method returns true if the cursor ends up pointing to a // valid token. func (d *Dispenser) Prev() bool { if d.cursor > -1 { d.cursor-- return d.cursor > -1 } return false } // NextArg loads the next token if it is on the same // line and if it is not a block opening (open curly // brace). Returns true if an argument token was // loaded; false otherwise. If false, all tokens on // the line have been consumed except for potentially // a block opening. It handles imported tokens // correctly. func (d *Dispenser) NextArg() bool { if !d.nextOnSameLine() { return false } if d.Val() == "{" { // roll back; a block opening is not an argument d.cursor-- return false } return true } // nextOnSameLine advances the cursor if the next // token is on the same line of the same file. func (d *Dispenser) nextOnSameLine() bool { if d.cursor < 0 { d.cursor++ return true } if d.cursor >= len(d.tokens) { return false } if d.cursor < len(d.tokens)-1 && d.tokens[d.cursor].File == d.tokens[d.cursor+1].File && d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line { d.cursor++ return true } return false } // NextLine loads the next token only if it is not on the same // line as the current token, and returns true if a token was // loaded; false otherwise. If false, there is not another token // or it is on the same line. It handles imported tokens correctly. func (d *Dispenser) NextLine() bool { if d.cursor < 0 { d.cursor++ return true } if d.cursor >= len(d.tokens) { return false } if d.cursor < len(d.tokens)-1 && (d.tokens[d.cursor].File != d.tokens[d.cursor+1].File || d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) { d.cursor++ return true } return false } // NextBlock can be used as the condition of a for loop // to load the next token as long as it opens a block or // is already in a block nested more than initialNestingLevel. // In other words, a loop over NextBlock() will iterate // all tokens in the block assuming the next token is an // open curly brace, until the matching closing brace. // The open and closing brace tokens for the outer-most // block will be consumed internally and omitted from // the iteration. // // Proper use of this method looks like this: // // for nesting := d.Nesting(); d.NextBlock(nesting); { // } // // However, in simple cases where it is known that the // Dispenser is new and has not already traversed state // by a loop over NextBlock(), this will do: // // for d.NextBlock(0) { // } // // As with other token parsing logic, a loop over // NextBlock() should be contained within a loop over // Next(), as it is usually prudent to skip the initial // token. func (d *Dispenser) NextBlock(initialNestingLevel int) bool { if d.nesting > initialNestingLevel { if !d.Next() { return false // should be EOF error } if d.Val() == "}" && !d.nextOnSameLine() { d.nesting-- } else if d.Val() == "{" && !d.nextOnSameLine() { d.nesting++ } return d.nesting > initialNestingLevel } if !d.nextOnSameLine() { // block must open on same line return false } if d.Val() != "{" { d.cursor-- // roll back if not opening brace return false } d.Next() // consume open curly brace if d.Val() == "}" { return false // open and then closed right away } d.nesting++ return true } // Nesting returns the current nesting level. Necessary // if using NextBlock() func (d *Dispenser) Nesting() int { return d.nesting } // Val gets the text of the current token. If there is no token // loaded, it returns empty string. func (d *Dispenser) Val() string { if d.cursor < 0 || d.cursor >= len(d.tokens) { return "" } return d.tokens[d.cursor].Text } // ValRaw gets the raw text of the current token (including quotes). // If there is no token loaded, it returns empty string. func (d *Dispenser) ValRaw() string { if d.cursor < 0 || d.cursor >= len(d.tokens) { return "" } quote := d.tokens[d.cursor].wasQuoted if quote > 0 { return string(quote) + d.tokens[d.cursor].Text + string(quote) // string literal } return d.tokens[d.cursor].Text } // ScalarVal gets value of the current token, converted to the closest // scalar type. If there is no token loaded, it returns nil. func (d *Dispenser) ScalarVal() any { if d.cursor < 0 || d.cursor >= len(d.tokens) { return nil } quote := d.tokens[d.cursor].wasQuoted text := d.tokens[d.cursor].Text if quote > 0 { return text // string literal } if num, err := strconv.Atoi(text); err == nil { return num } if num, err := strconv.ParseFloat(text, 64); err == nil { return num } if bool, err := strconv.ParseBool(text); err == nil { return bool } return text } // Line gets the line number of the current token. // If there is no token loaded, it returns 0. func (d *Dispenser) Line() int { if d.cursor < 0 || d.cursor >= len(d.tokens) { return 0 } return d.tokens[d.cursor].Line } // File gets the filename where the current token originated. func (d *Dispenser) File() string { if d.cursor < 0 || d.cursor >= len(d.tokens) { return "" } return d.tokens[d.cursor].File } // Args is a convenience function that loads the next arguments // (tokens on the same line) into an arbitrary number of strings // pointed to in targets. If there are not enough argument tokens // available to fill targets, false is returned and the remaining // targets are left unchanged. If all the targets are filled, // then true is returned. func (d *Dispenser) Args(targets ...*string) bool { for i := 0; i < len(targets); i++ { if !d.NextArg() { return false } *targets[i] = d.Val() } return true } // AllArgs is like Args, but if there are more argument tokens // available than there are targets, false is returned. The // number of available argument tokens must match the number of // targets exactly to return true. func (d *Dispenser) AllArgs(targets ...*string) bool { if !d.Args(targets...) { return false } if d.NextArg() { d.Prev() return false } return true } // CountRemainingArgs counts the amount of remaining arguments // (tokens on the same line) without consuming the tokens. func (d *Dispenser) CountRemainingArgs() int { count := 0 for d.NextArg() { count++ } for i := 0; i < count; i++ { d.Prev() } return count } // RemainingArgs loads any more arguments (tokens on the same line) // into a slice and returns them. Open curly brace tokens also indicate // the end of arguments, and the curly brace is not included in // the return value nor is it loaded. func (d *Dispenser) RemainingArgs() []string { var args []string for d.NextArg() { args = append(args, d.Val()) } return args } // RemainingArgsRaw loads any more arguments (tokens on the same line, // retaining quotes) into a slice and returns them. Open curly brace // tokens also indicate the end of arguments, and the curly brace is // not included in the return value nor is it loaded. func (d *Dispenser) RemainingArgsRaw() []string { var args []string for d.NextArg() { args = append(args, d.ValRaw()) } return args } // NewFromNextSegment returns a new dispenser with a copy of // the tokens from the current token until the end of the // "directive" whether that be to the end of the line or // the end of a block that starts at the end of the line; // in other words, until the end of the segment. func (d *Dispenser) NewFromNextSegment() *Dispenser { return NewDispenser(d.NextSegment()) } // NextSegment returns a copy of the tokens from the current // token until the end of the line or block that starts at // the end of the line. func (d *Dispenser) NextSegment() Segment { tkns := Segment{d.Token()} for d.NextArg() { tkns = append(tkns, d.Token()) } var openedBlock bool for nesting := d.Nesting(); d.NextBlock(nesting); { if !openedBlock { // because NextBlock() consumes the initial open // curly brace, we rewind here to append it, since // our case is special in that we want the new // dispenser to have all the tokens including // surrounding curly braces d.Prev() tkns = append(tkns, d.Token()) d.Next() openedBlock = true } tkns = append(tkns, d.Token()) } if openedBlock { // include closing brace tkns = append(tkns, d.Token()) // do not consume the closing curly brace; the // next iteration of the enclosing loop will // call Next() and consume it } return tkns } // Token returns the current token. func (d *Dispenser) Token() Token { if d.cursor < 0 || d.cursor >= len(d.tokens) { return Token{} } return d.tokens[d.cursor] } // Reset sets d's cursor to the beginning, as // if this was a new and unused dispenser. func (d *Dispenser) Reset() { d.cursor = -1 d.nesting = 0 } // ArgErr returns an argument error, meaning that another // argument was expected but not found. In other words, // a line break or open curly brace was encountered instead of // an argument. func (d *Dispenser) ArgErr() error { if d.Val() == "{" { return d.Err("Unexpected token '{', expecting argument") } return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val()) } // SyntaxErr creates a generic syntax error which explains what was // found and what was expected. func (d *Dispenser) SyntaxErr(expected string) error { msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected) return errors.New(msg) } // EOFErr returns an error indicating that the dispenser reached // the end of the input when searching for the next token. func (d *Dispenser) EOFErr() error { return d.Errf("Unexpected EOF") } // Err generates a custom parse-time error with a message of msg. func (d *Dispenser) Err(msg string) error { return d.Errf(msg) } // Errf is like Err, but for formatted error messages func (d *Dispenser) Errf(format string, args ...any) error { return d.WrapErr(fmt.Errorf(format, args...)) } // WrapErr takes an existing error and adds the Caddyfile file and line number. func (d *Dispenser) WrapErr(err error) error { return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err) } // Delete deletes the current token and returns the updated slice // of tokens. The cursor is not advanced to the next token. // Because deletion modifies the underlying slice, this method // should only be called if you have access to the original slice // of tokens and/or are using the slice of tokens outside this // Dispenser instance. If you do not re-assign the slice with the // return value of this method, inconsistencies in the token // array will become apparent (or worse, hide from you like they // did me for 3 and a half freaking hours late one night). func (d *Dispenser) Delete() []Token { if d.cursor >= 0 && d.cursor <= len(d.tokens)-1 { d.tokens = append(d.tokens[:d.cursor], d.tokens[d.cursor+1:]...) d.cursor-- } return d.tokens } // numLineBreaks counts how many line breaks are in the token // value given by the token index tknIdx. It returns 0 if the // token does not exist or there are no line breaks. func (d *Dispenser) numLineBreaks(tknIdx int) int { if tknIdx < 0 || tknIdx >= len(d.tokens) { return 0 } return strings.Count(d.tokens[tknIdx].Text, "\n") } // isNewLine determines whether the current token is on a different // line (higher line number) than the previous token. It handles imported // tokens correctly. If there isn't a previous token, it returns true. func (d *Dispenser) isNewLine() bool { if d.cursor < 1 { return true } if d.cursor > len(d.tokens)-1 { return false } prev := d.tokens[d.cursor-1] curr := d.tokens[d.cursor] // If the previous token is from a different file, // we can assume it's from a different line if prev.File != curr.File { return true } // The previous token may contain line breaks if // it was quoted and spanned multiple lines. e.g: // // dir "foo // bar // baz" prevLineBreaks := d.numLineBreaks(d.cursor - 1) // If the previous token (incl line breaks) ends // on a line earlier than the current token, // then the current token is on a new line return prev.Line+prevLineBreaks < curr.Line } // isNextOnNewLine determines whether the current token is on a different // line (higher line number) than the next token. It handles imported // tokens correctly. If there isn't a next token, it returns true. func (d *Dispenser) isNextOnNewLine() bool { if d.cursor < 0 { return false } if d.cursor >= len(d.tokens)-1 { return true } curr := d.tokens[d.cursor] next := d.tokens[d.cursor+1] // If the next token is from a different file, // we can assume it's from a different line if curr.File != next.File { return true } // The current token may contain line breaks if // it was quoted and spanned multiple lines. e.g: // // dir "foo // bar // baz" currLineBreaks := d.numLineBreaks(d.cursor) // If the current token (incl line breaks) ends // on a line earlier than the next token, // then the next token is on a new line return curr.Line+currLineBreaks < next.Line } caddy-2.6.2/caddyconfig/caddyfile/dispenser_test.go000066400000000000000000000211331435007237400223720ustar00rootroot00000000000000// 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 caddyfile import ( "errors" "reflect" "strings" "testing" ) func TestDispenser_Val_Next(t *testing.T) { input := `host:port dir1 arg1 dir2 arg2 arg3 dir3` d := NewTestDispenser(input) if val := d.Val(); val != "" { t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val) } assertNext := func(shouldLoad bool, expectedCursor int, expectedVal string) { if loaded := d.Next(); loaded != shouldLoad { t.Errorf("Next(): Expected %v but got %v instead (val '%s')", shouldLoad, loaded, d.Val()) } if d.cursor != expectedCursor { t.Errorf("Expected cursor to be %d, but was %d", expectedCursor, d.cursor) } if d.nesting != 0 { t.Errorf("Nesting should be 0, was %d instead", d.nesting) } if val := d.Val(); val != expectedVal { t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) } } assertNext(true, 0, "host:port") assertNext(true, 1, "dir1") assertNext(true, 2, "arg1") assertNext(true, 3, "dir2") assertNext(true, 4, "arg2") assertNext(true, 5, "arg3") assertNext(true, 6, "dir3") // Note: This next test simply asserts existing behavior. // If desired, we may wish to empty the token value after // reading past the EOF. Open an issue if you want this change. assertNext(false, 6, "dir3") } func TestDispenser_NextArg(t *testing.T) { input := `dir1 arg1 dir2 arg2 arg3 dir3` d := NewTestDispenser(input) assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) { if d.Next() != shouldLoad { t.Errorf("Next(): Should load token but got false instead (val: '%s')", d.Val()) } if d.cursor != expectedCursor { t.Errorf("Next(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor) } if val := d.Val(); val != expectedVal { t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) } } assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) { if !d.NextArg() { t.Error("NextArg(): Should load next argument but got false instead") } if d.cursor != expectedCursor { t.Errorf("NextArg(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor) } if val := d.Val(); val != expectedVal { t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) } if !loadAnother { if d.NextArg() { t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val()) } if d.cursor != expectedCursor { t.Errorf("NextArg(): Expected cursor to remain at %d, but it was %d", expectedCursor, d.cursor) } } } assertNext(true, "dir1", 0) assertNextArg("arg1", false, 1) assertNext(true, "dir2", 2) assertNextArg("arg2", true, 3) assertNextArg("arg3", false, 4) assertNext(true, "dir3", 5) assertNext(false, "dir3", 5) } func TestDispenser_NextLine(t *testing.T) { input := `host:port dir1 arg1 dir2 arg2 arg3` d := NewTestDispenser(input) assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) { if d.NextLine() != shouldLoad { t.Errorf("NextLine(): Should load token but got false instead (val: '%s')", d.Val()) } if d.cursor != expectedCursor { t.Errorf("NextLine(): Expected cursor to be %d, instead was %d", expectedCursor, d.cursor) } if val := d.Val(); val != expectedVal { t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) } } assertNextLine(true, "host:port", 0) assertNextLine(true, "dir1", 1) assertNextLine(false, "dir1", 1) d.Next() // arg1 assertNextLine(true, "dir2", 3) assertNextLine(false, "dir2", 3) d.Next() // arg2 assertNextLine(false, "arg2", 4) d.Next() // arg3 assertNextLine(false, "arg3", 5) } func TestDispenser_NextBlock(t *testing.T) { input := `foobar1 { sub1 arg1 sub2 } foobar2 { }` d := NewTestDispenser(input) assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) { if loaded := d.NextBlock(0); loaded != shouldLoad { t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded) } if d.cursor != expectedCursor { t.Errorf("NextBlock(): Expected cursor to be %d, was %d", expectedCursor, d.cursor) } if d.nesting != expectedNesting { t.Errorf("NextBlock(): Nesting should be %d, not %d", expectedNesting, d.nesting) } } assertNextBlock(false, -1, 0) d.Next() // foobar1 assertNextBlock(true, 2, 1) assertNextBlock(true, 3, 1) assertNextBlock(true, 4, 1) assertNextBlock(false, 5, 0) d.Next() // foobar2 assertNextBlock(false, 8, 0) // empty block is as if it didn't exist } func TestDispenser_Args(t *testing.T) { var s1, s2, s3 string input := `dir1 arg1 arg2 arg3 dir2 arg4 arg5 dir3 arg6 arg7 dir4` d := NewTestDispenser(input) d.Next() // dir1 // As many strings as arguments if all := d.Args(&s1, &s2, &s3); !all { t.Error("Args(): Expected true, got false") } if s1 != "arg1" { t.Errorf("Args(): Expected s1 to be 'arg1', got '%s'", s1) } if s2 != "arg2" { t.Errorf("Args(): Expected s2 to be 'arg2', got '%s'", s2) } if s3 != "arg3" { t.Errorf("Args(): Expected s3 to be 'arg3', got '%s'", s3) } d.Next() // dir2 // More strings than arguments if all := d.Args(&s1, &s2, &s3); all { t.Error("Args(): Expected false, got true") } if s1 != "arg4" { t.Errorf("Args(): Expected s1 to be 'arg4', got '%s'", s1) } if s2 != "arg5" { t.Errorf("Args(): Expected s2 to be 'arg5', got '%s'", s2) } if s3 != "arg3" { t.Errorf("Args(): Expected s3 to be unchanged ('arg3'), instead got '%s'", s3) } // (quick cursor check just for kicks and giggles) if d.cursor != 6 { t.Errorf("Cursor should be 6, but is %d", d.cursor) } d.Next() // dir3 // More arguments than strings if all := d.Args(&s1); !all { t.Error("Args(): Expected true, got false") } if s1 != "arg6" { t.Errorf("Args(): Expected s1 to be 'arg6', got '%s'", s1) } d.Next() // dir4 // No arguments or strings if all := d.Args(); !all { t.Error("Args(): Expected true, got false") } // No arguments but at least one string if all := d.Args(&s1); all { t.Error("Args(): Expected false, got true") } } func TestDispenser_RemainingArgs(t *testing.T) { input := `dir1 arg1 arg2 arg3 dir2 arg4 arg5 dir3 arg6 { arg7 dir4` d := NewTestDispenser(input) d.Next() // dir1 args := d.RemainingArgs() if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(args, expected) { t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args) } d.Next() // dir2 args = d.RemainingArgs() if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(args, expected) { t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args) } d.Next() // dir3 args = d.RemainingArgs() if expected := []string{"arg6"}; !reflect.DeepEqual(args, expected) { t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args) } d.Next() // { d.Next() // arg7 d.Next() // dir4 args = d.RemainingArgs() if len(args) != 0 { t.Errorf("RemainingArgs(): Expected %v, got %v", []string{}, args) } } func TestDispenser_ArgErr_Err(t *testing.T) { input := `dir1 { } dir2 arg1 arg2` d := NewTestDispenser(input) d.cursor = 1 // { if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "{") { t.Errorf("ArgErr(): Expected an error message with { in it, but got '%v'", err) } d.cursor = 5 // arg2 if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "arg2") { t.Errorf("ArgErr(): Expected an error message with 'arg2' in it; got '%v'", err) } err := d.Err("foobar") if err == nil { t.Fatalf("Err(): Expected an error, got nil") } if !strings.Contains(err.Error(), "Testfile:3") { t.Errorf("Expected error message with filename:line in it; got '%v'", err) } if !strings.Contains(err.Error(), "foobar") { t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err) } var ErrBarIsFull = errors.New("bar is full") bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull) if !errors.Is(bookingError, ErrBarIsFull) { t.Errorf("Errf(): should be able to unwrap the error chain") } } caddy-2.6.2/caddyconfig/caddyfile/formatter.go000066400000000000000000000112031435007237400213370ustar00rootroot00000000000000// 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 caddyfile import ( "bytes" "io" "unicode" ) // Format formats the input Caddyfile to a standard, nice-looking // appearance. It works by reading each rune of the input and taking // control over all the bracing and whitespace that is written; otherwise, // words, comments, placeholders, and escaped characters are all treated // literally and written as they appear in the input. func Format(input []byte) []byte { input = bytes.TrimSpace(input) out := new(bytes.Buffer) rdr := bytes.NewReader(input) var ( last rune // the last character that was written to the result space = true // whether current/previous character was whitespace (beginning of input counts as space) beginningOfLine = true // whether we are at beginning of line openBrace bool // whether current word/token is or started with open curly brace openBraceWritten bool // if openBrace, whether that brace was written or not openBraceSpace bool // whether there was a non-newline space before open brace newLines int // count of newlines consumed comment bool // whether we're in a comment quoted bool // whether we're in a quoted segment escaped bool // whether current char is escaped nesting int // indentation level ) write := func(ch rune) { out.WriteRune(ch) last = ch } indent := func() { for tabs := nesting; tabs > 0; tabs-- { write('\t') } } nextLine := func() { write('\n') beginningOfLine = true } for { ch, _, err := rdr.ReadRune() if err != nil { if err == io.EOF { break } panic(err) } if comment { if ch == '\n' { comment = false space = true nextLine() continue } else { write(ch) continue } } if !escaped && ch == '\\' { if space { write(' ') space = false } write(ch) escaped = true continue } if escaped { write(ch) escaped = false continue } if quoted { if ch == '"' { quoted = false } write(ch) continue } if space && ch == '"' { quoted = true } if unicode.IsSpace(ch) { space = true if ch == '\n' { newLines++ } continue } spacePrior := space space = false ////////////////////////////////////////////////////////// // I find it helpful to think of the formatting loop in two // main sections; by the time we reach this point, we // know we are in a "regular" part of the file: we know // the character is not a space, not in a literal segment // like a comment or quoted, it's not escaped, etc. ////////////////////////////////////////////////////////// if ch == '#' { comment = true } if openBrace && spacePrior && !openBraceWritten { if nesting == 0 && last == '}' { nextLine() nextLine() } openBrace = false if beginningOfLine { indent() } else if !openBraceSpace { write(' ') } write('{') openBraceWritten = true nextLine() newLines = 0 // prevent infinite nesting from ridiculous inputs (issue #4169) if nesting < 10 { nesting++ } } switch { case ch == '{': openBrace = true openBraceWritten = false openBraceSpace = spacePrior && !beginningOfLine if openBraceSpace { write(' ') } continue case ch == '}' && (spacePrior || !openBrace): if last != '\n' { nextLine() } if nesting > 0 { nesting-- } indent() write('}') newLines = 0 continue } if newLines > 2 { newLines = 2 } for i := 0; i < newLines; i++ { nextLine() } newLines = 0 if beginningOfLine { indent() } if nesting == 0 && last == '}' && beginningOfLine { nextLine() nextLine() } if !beginningOfLine && spacePrior { write(' ') } if openBrace && !openBraceWritten { write('{') openBraceWritten = true } write(ch) beginningOfLine = false } // the Caddyfile does not need any leading or trailing spaces, but... trimmedResult := bytes.TrimSpace(out.Bytes()) // ...Caddyfiles should, however, end with a newline because // newlines are significant to the syntax of the file return append(trimmedResult, '\n') } caddy-2.6.2/caddyconfig/caddyfile/formatter_fuzz.go000066400000000000000000000014441435007237400224230ustar00rootroot00000000000000// 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. //go:build gofuzz package caddyfile import "bytes" func FuzzFormat(input []byte) int { formatted := Format(input) if bytes.Equal(formatted, Format(formatted)) { return 1 } return 0 } caddy-2.6.2/caddyconfig/caddyfile/formatter_test.go000066400000000000000000000077711435007237400224150ustar00rootroot00000000000000// 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 caddyfile import ( "strings" "testing" ) func TestFormatter(t *testing.T) { for i, tc := range []struct { description string input string expect string }{ { description: "very simple", input: `abc def g hi jkl mn`, expect: `abc def g hi jkl mn`, }, { description: "basic indentation, line breaks, and nesting", input: ` a b c { d } e { f } g { h { i } } j { k { l } } m { n { o } p { q r s } } { { t u v w } }`, expect: `a b c { d } e { f } g { h { i } } j { k { l } } m { n { o } p { q r s } } { { t u v w } }`, }, { description: "block spacing", input: `a{ b } c{ d }`, expect: `a { b } c { d }`, }, { description: "advanced spacing", input: `abc { def }ghi{ jkl mno pqr}`, expect: `abc { def } ghi { jkl mno pqr }`, }, { description: "env var placeholders", input: `{$A} b { {$C} } d { {$E} } { {$F} } `, expect: `{$A} b { {$C} } d { {$E} } { {$F} }`, }, { description: "env var placeholders with port", input: `:{$PORT}`, expect: `:{$PORT}`, }, { description: "comments", input: `#a "\n" #b { c } d { e#f # g } h { # i }`, expect: `#a "\n" #b { c } d { e#f # g } h { # i }`, }, { description: "quotes and escaping", input: `"a \"b\" "#c d e { "f" } g { "h" } i { "foo bar" } j { "\"k\" l m" }`, expect: `"a \"b\" "#c d e { "f" } g { "h" } i { "foo bar" } j { "\"k\" l m" }`, }, { description: "bad nesting (too many open)", input: `a { { }`, expect: `a { { } `, }, { description: "bad nesting (too many close)", input: `a { { }}}`, expect: `a { { } } } `, }, { description: "json", input: `foo bar "{\"key\":34}" `, expect: `foo bar "{\"key\":34}"`, }, { description: "escaping after spaces", input: `foo \"literal\"`, expect: `foo \"literal\"`, }, { description: "simple placeholders as standalone tokens", input: `foo {bar}`, expect: `foo {bar}`, }, { description: "simple placeholders within tokens", input: `foo{bar} foo{bar}baz`, expect: `foo{bar} foo{bar}baz`, }, { description: "placeholders and malformed braces", input: `foo{bar} foo{ bar}baz`, expect: `foo{bar} foo { bar } baz`, }, { description: "hash within string is not a comment", input: `redir / /some/#/path`, expect: `redir / /some/#/path`, }, { description: "brace does not fold into comment above", input: `# comment { foo }`, expect: `# comment { foo }`, }, { description: "matthewpi/vscode-caddyfile-support#13", input: `{ email {$ACMEEMAIL} #debug } block { } `, expect: `{ email {$ACMEEMAIL} #debug } block { } `, }, { description: "matthewpi/vscode-caddyfile-support#13 - bad formatting", input: `{ email {$ACMEEMAIL} #debug } block { } `, expect: `{ email {$ACMEEMAIL} #debug } block { } `, }, } { // the formatter should output a trailing newline, // even if the tests aren't written to expect that if !strings.HasSuffix(tc.expect, "\n") { tc.expect += "\n" } actual := Format([]byte(tc.input)) if string(actual) != tc.expect { t.Errorf("\n[TEST %d: %s]\n====== EXPECTED ======\n%s\n====== ACTUAL ======\n%s^^^^^^^^^^^^^^^^^^^^^", i, tc.description, string(tc.expect), string(actual)) } } } caddy-2.6.2/caddyconfig/caddyfile/importgraph.go000066400000000000000000000050371435007237400217000ustar00rootroot00000000000000// 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 caddyfile import ( "fmt" ) type adjacency map[string][]string type importGraph struct { nodes map[string]bool edges adjacency } func (i *importGraph) addNode(name string) { if i.nodes == nil { i.nodes = make(map[string]bool) } if _, exists := i.nodes[name]; exists { return } i.nodes[name] = true } func (i *importGraph) addNodes(names []string) { for _, name := range names { i.addNode(name) } } func (i *importGraph) removeNode(name string) { delete(i.nodes, name) } func (i *importGraph) removeNodes(names []string) { for _, name := range names { i.removeNode(name) } } func (i *importGraph) addEdge(from, to string) error { if !i.exists(from) || !i.exists(to) { return fmt.Errorf("one of the nodes does not exist") } if i.willCycle(to, from) { return fmt.Errorf("a cycle of imports exists between %s and %s", from, to) } if i.areConnected(from, to) { // if connected, there's nothing to do return nil } if i.nodes == nil { i.nodes = make(map[string]bool) } if i.edges == nil { i.edges = make(adjacency) } i.edges[from] = append(i.edges[from], to) return nil } func (i *importGraph) addEdges(from string, tos []string) error { for _, to := range tos { err := i.addEdge(from, to) if err != nil { return err } } return nil } func (i *importGraph) areConnected(from, to string) bool { al, ok := i.edges[from] if !ok { return false } for _, v := range al { if v == to { return true } } return false } func (i *importGraph) willCycle(from, to string) bool { collector := make(map[string]bool) var visit func(string) visit = func(start string) { if !collector[start] { collector[start] = true for _, v := range i.edges[start] { visit(v) } } } for _, v := range i.edges[from] { visit(v) } for k := range collector { if to == k { return true } } return false } func (i *importGraph) exists(key string) bool { _, exists := i.nodes[key] return exists } caddy-2.6.2/caddyconfig/caddyfile/lexer.go000066400000000000000000000103111435007237400204520ustar00rootroot00000000000000// 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 caddyfile import ( "bufio" "bytes" "io" "unicode" ) type ( // lexer is a utility which can get values, token by // token, from a Reader. A token is a word, and tokens // are separated by whitespace. A word can be enclosed // in quotes if it contains whitespace. lexer struct { reader *bufio.Reader token Token line int skippedLines int } // Token represents a single parsable unit. Token struct { File string Line int Text string wasQuoted rune // enclosing quote character, if any inSnippet bool snippetName string } ) // load prepares the lexer to scan an input for tokens. // It discards any leading byte order mark. func (l *lexer) load(input io.Reader) error { l.reader = bufio.NewReader(input) l.line = 1 // discard byte order mark, if present firstCh, _, err := l.reader.ReadRune() if err != nil { return err } if firstCh != 0xFEFF { err := l.reader.UnreadRune() if err != nil { return err } } return nil } // next loads the next token into the lexer. // A token is delimited by whitespace, unless // the token starts with a quotes character (") // in which case the token goes until the closing // quotes (the enclosing quotes are not included). // Inside quoted strings, quotes may be escaped // with a preceding \ character. No other chars // may be escaped. The rest of the line is skipped // if a "#" character is read in. Returns true if // a token was loaded; false otherwise. func (l *lexer) next() bool { var val []rune var comment, quoted, btQuoted, escaped bool makeToken := func(quoted rune) bool { l.token.Text = string(val) l.token.wasQuoted = quoted return true } for { ch, _, err := l.reader.ReadRune() if err != nil { if len(val) > 0 { return makeToken(0) } if err == io.EOF { return false } panic(err) } if !escaped && !btQuoted && ch == '\\' { escaped = true continue } if quoted || btQuoted { if quoted && escaped { // all is literal in quoted area, // so only escape quotes if ch != '"' { val = append(val, '\\') } escaped = false } else { if quoted && ch == '"' { return makeToken('"') } if btQuoted && ch == '`' { return makeToken('`') } } if ch == '\n' { l.line += 1 + l.skippedLines l.skippedLines = 0 } val = append(val, ch) continue } if unicode.IsSpace(ch) { if ch == '\r' { continue } if ch == '\n' { if escaped { l.skippedLines++ escaped = false } else { l.line += 1 + l.skippedLines l.skippedLines = 0 } comment = false } if len(val) > 0 { return makeToken(0) } continue } if ch == '#' && len(val) == 0 { comment = true } if comment { continue } if len(val) == 0 { l.token = Token{Line: l.line} if ch == '"' { quoted = true continue } if ch == '`' { btQuoted = true continue } } if escaped { val = append(val, '\\') escaped = false } val = append(val, ch) } } // Tokenize takes bytes as input and lexes it into // a list of tokens that can be parsed as a Caddyfile. // Also takes a filename to fill the token's File as // the source of the tokens, which is important to // determine relative paths for `import` directives. func Tokenize(input []byte, filename string) ([]Token, error) { l := lexer{} if err := l.load(bytes.NewReader(input)); err != nil { return nil, err } var tokens []Token for l.next() { l.token.File = filename tokens = append(tokens, l.token) } return tokens, nil } func (t Token) Quoted() bool { return t.wasQuoted > 0 } caddy-2.6.2/caddyconfig/caddyfile/lexer_fuzz.go000066400000000000000000000014561435007237400215420ustar00rootroot00000000000000// 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. //go:build gofuzz package caddyfile func FuzzTokenize(input []byte) int { tokens, err := Tokenize(input, "Caddyfile") if err != nil { return 0 } if len(tokens) == 0 { return -1 } return 1 } caddy-2.6.2/caddyconfig/caddyfile/lexer_test.go000066400000000000000000000147241435007237400215250ustar00rootroot00000000000000// 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 caddyfile import ( "testing" ) type lexerTestCase struct { input []byte expected []Token } func TestLexer(t *testing.T) { testCases := []lexerTestCase{ { input: []byte(`host:123`), expected: []Token{ {Line: 1, Text: "host:123"}, }, }, { input: []byte(`host:123 directive`), expected: []Token{ {Line: 1, Text: "host:123"}, {Line: 3, Text: "directive"}, }, }, { input: []byte(`host:123 { directive }`), expected: []Token{ {Line: 1, Text: "host:123"}, {Line: 1, Text: "{"}, {Line: 2, Text: "directive"}, {Line: 3, Text: "}"}, }, }, { input: []byte(`host:123 { directive }`), expected: []Token{ {Line: 1, Text: "host:123"}, {Line: 1, Text: "{"}, {Line: 1, Text: "directive"}, {Line: 1, Text: "}"}, }, }, { input: []byte(`host:123 { #comment directive # comment foobar # another comment }`), expected: []Token{ {Line: 1, Text: "host:123"}, {Line: 1, Text: "{"}, {Line: 3, Text: "directive"}, {Line: 5, Text: "foobar"}, {Line: 6, Text: "}"}, }, }, { input: []byte(`host:123 { # hash inside string is not a comment redir / /some/#/path }`), expected: []Token{ {Line: 1, Text: "host:123"}, {Line: 1, Text: "{"}, {Line: 3, Text: "redir"}, {Line: 3, Text: "/"}, {Line: 3, Text: "/some/#/path"}, {Line: 4, Text: "}"}, }, }, { input: []byte("# comment at beginning of file\n# comment at beginning of line\nhost:123"), expected: []Token{ {Line: 3, Text: "host:123"}, }, }, { input: []byte(`a "quoted value" b foobar`), expected: []Token{ {Line: 1, Text: "a"}, {Line: 1, Text: "quoted value"}, {Line: 1, Text: "b"}, {Line: 2, Text: "foobar"}, }, }, { input: []byte(`A "quoted \"value\" inside" B`), expected: []Token{ {Line: 1, Text: "A"}, {Line: 1, Text: `quoted "value" inside`}, {Line: 1, Text: "B"}, }, }, { input: []byte("An escaped \"newline\\\ninside\" quotes"), expected: []Token{ {Line: 1, Text: "An"}, {Line: 1, Text: "escaped"}, {Line: 1, Text: "newline\\\ninside"}, {Line: 2, Text: "quotes"}, }, }, { input: []byte("An escaped newline\\\noutside quotes"), expected: []Token{ {Line: 1, Text: "An"}, {Line: 1, Text: "escaped"}, {Line: 1, Text: "newline"}, {Line: 1, Text: "outside"}, {Line: 1, Text: "quotes"}, }, }, { input: []byte("line1\\\nescaped\nline2\nline3"), expected: []Token{ {Line: 1, Text: "line1"}, {Line: 1, Text: "escaped"}, {Line: 3, Text: "line2"}, {Line: 4, Text: "line3"}, }, }, { input: []byte("line1\\\nescaped1\\\nescaped2\nline4\nline5"), expected: []Token{ {Line: 1, Text: "line1"}, {Line: 1, Text: "escaped1"}, {Line: 1, Text: "escaped2"}, {Line: 4, Text: "line4"}, {Line: 5, Text: "line5"}, }, }, { input: []byte(`"unescapable\ in quotes"`), expected: []Token{ {Line: 1, Text: `unescapable\ in quotes`}, }, }, { input: []byte(`"don't\escape"`), expected: []Token{ {Line: 1, Text: `don't\escape`}, }, }, { input: []byte(`"don't\\escape"`), expected: []Token{ {Line: 1, Text: `don't\\escape`}, }, }, { input: []byte(`un\escapable`), expected: []Token{ {Line: 1, Text: `un\escapable`}, }, }, { input: []byte(`A "quoted value with line break inside" { foobar }`), expected: []Token{ {Line: 1, Text: "A"}, {Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"}, {Line: 2, Text: "{"}, {Line: 3, Text: "foobar"}, {Line: 4, Text: "}"}, }, }, { input: []byte(`"C:\php\php-cgi.exe"`), expected: []Token{ {Line: 1, Text: `C:\php\php-cgi.exe`}, }, }, { input: []byte(`empty "" string`), expected: []Token{ {Line: 1, Text: `empty`}, {Line: 1, Text: ``}, {Line: 1, Text: `string`}, }, }, { input: []byte("skip those\r\nCR characters"), expected: []Token{ {Line: 1, Text: "skip"}, {Line: 1, Text: "those"}, {Line: 2, Text: "CR"}, {Line: 2, Text: "characters"}, }, }, { input: []byte("\xEF\xBB\xBF:8080"), // test with leading byte order mark expected: []Token{ {Line: 1, Text: ":8080"}, }, }, { input: []byte("simple `backtick quoted` string"), expected: []Token{ {Line: 1, Text: `simple`}, {Line: 1, Text: `backtick quoted`}, {Line: 1, Text: `string`}, }, }, { input: []byte("multiline `backtick\nquoted\n` string"), expected: []Token{ {Line: 1, Text: `multiline`}, {Line: 1, Text: "backtick\nquoted\n"}, {Line: 3, Text: `string`}, }, }, { input: []byte("nested `\"quotes inside\" backticks` string"), expected: []Token{ {Line: 1, Text: `nested`}, {Line: 1, Text: `"quotes inside" backticks`}, {Line: 1, Text: `string`}, }, }, { input: []byte("reverse-nested \"`backticks` inside\" quotes"), expected: []Token{ {Line: 1, Text: `reverse-nested`}, {Line: 1, Text: "`backticks` inside"}, {Line: 1, Text: `quotes`}, }, }, } for i, testCase := range testCases { actual, err := Tokenize(testCase.input, "") if err != nil { t.Errorf("%v", err) } lexerCompare(t, i, testCase.expected, actual) } } func lexerCompare(t *testing.T, n int, expected, actual []Token) { if len(expected) != len(actual) { t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) } for i := 0; i < len(actual) && i < len(expected); i++ { if actual[i].Line != expected[i].Line { t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d", n, i, expected[i].Text, expected[i].Line, actual[i].Line) break } if actual[i].Text != expected[i].Text { t.Errorf("Test case %d token %d: expected text '%s' but was '%s'", n, i, expected[i].Text, actual[i].Text) break } } } caddy-2.6.2/caddyconfig/caddyfile/parse.go000066400000000000000000000430031435007237400204510ustar00rootroot00000000000000// 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 caddyfile import ( "bytes" "fmt" "io" "os" "path/filepath" "strconv" "strings" "github.com/caddyserver/caddy/v2" "go.uber.org/zap" ) // Parse parses the input just enough to group tokens, in // order, by server block. No further parsing is performed. // Server blocks are returned in the order in which they appear. // Directives that do not appear in validDirectives will cause // an error. If you do not want to check for valid directives, // pass in nil instead. // // Environment variables in {$ENVIRONMENT_VARIABLE} notation // will be replaced before parsing begins. func Parse(filename string, input []byte) ([]ServerBlock, error) { // unfortunately, we must copy the input because parsing must // remain a read-only operation, but we have to expand environment // variables before we parse, which changes the underlying array (#4422) inputCopy := make([]byte, len(input)) copy(inputCopy, input) tokens, err := allTokens(filename, inputCopy) if err != nil { return nil, err } p := parser{ Dispenser: NewDispenser(tokens), importGraph: importGraph{ nodes: make(map[string]bool), edges: make(adjacency), }, } return p.parseAll() } // allTokens lexes the entire input, but does not parse it. // It returns all the tokens from the input, unstructured // and in order. It may mutate input as it expands env vars. func allTokens(filename string, input []byte) ([]Token, error) { inputCopy, err := replaceEnvVars(input) if err != nil { return nil, err } tokens, err := Tokenize(inputCopy, filename) if err != nil { return nil, err } return tokens, nil } // replaceEnvVars replaces all occurrences of environment variables. // It mutates the underlying array and returns the updated slice. func replaceEnvVars(input []byte) ([]byte, error) { var offset int for { begin := bytes.Index(input[offset:], spanOpen) if begin < 0 { break } begin += offset // make beginning relative to input, not offset end := bytes.Index(input[begin+len(spanOpen):], spanClose) if end < 0 { break } end += begin + len(spanOpen) // make end relative to input, not begin // get the name; if there is no name, skip it envString := input[begin+len(spanOpen) : end] if len(envString) == 0 { offset = end + len(spanClose) continue } // split the string into a key and an optional default envParts := strings.SplitN(string(envString), envVarDefaultDelimiter, 2) // do a lookup for the env var, replace with the default if not found envVarValue, found := os.LookupEnv(envParts[0]) if !found && len(envParts) == 2 { envVarValue = envParts[1] } // get the value of the environment variable // note that this causes one-level deep chaining envVarBytes := []byte(envVarValue) // splice in the value input = append(input[:begin], append(envVarBytes, input[end+len(spanClose):]...)...) // continue at the end of the replacement offset = begin + len(envVarBytes) } return input, nil } type parser struct { *Dispenser block ServerBlock // current server block being parsed eof bool // if we encounter a valid EOF in a hard place definedSnippets map[string][]Token nesting int importGraph importGraph } func (p *parser) parseAll() ([]ServerBlock, error) { var blocks []ServerBlock for p.Next() { err := p.parseOne() if err != nil { return blocks, err } if len(p.block.Keys) > 0 || len(p.block.Segments) > 0 { blocks = append(blocks, p.block) } if p.nesting > 0 { return blocks, p.EOFErr() } } return blocks, nil } func (p *parser) parseOne() error { p.block = ServerBlock{} return p.begin() } func (p *parser) begin() error { if len(p.tokens) == 0 { return nil } err := p.addresses() if err != nil { return err } if p.eof { // this happens if the Caddyfile consists of only // a line of addresses and nothing else return nil } if ok, name := p.isSnippet(); ok { if p.definedSnippets == nil { p.definedSnippets = map[string][]Token{} } if _, found := p.definedSnippets[name]; found { return p.Errf("redeclaration of previously declared snippet %s", name) } // consume all tokens til matched close brace tokens, err := p.snippetTokens() if err != nil { return err } // Just as we need to track which file the token comes from, we need to // keep track of which snippets do the tokens come from. This is helpful // in tracking import cycles across files/snippets by namespacing them. Without // this we end up with false-positives in cycle-detection. for k, v := range tokens { v.inSnippet = true v.snippetName = name tokens[k] = v } p.definedSnippets[name] = tokens // empty block keys so we don't save this block as a real server. p.block.Keys = nil return nil } return p.blockContents() } func (p *parser) addresses() error { var expectingAnother bool for { tkn := p.Val() // special case: import directive replaces tokens during parse-time if tkn == "import" && p.isNewLine() { err := p.doImport() if err != nil { return err } continue } // Open brace definitely indicates end of addresses if tkn == "{" { if expectingAnother { return p.Errf("Expected another address but had '%s' - check for extra comma", tkn) } // Mark this server block as being defined with braces. // This is used to provide a better error message when // the user may have tried to define two server blocks // without having used braces, which are required in // that case. p.block.HasBraces = true break } // Users commonly forget to place a space between the address and the '{' if strings.HasSuffix(tkn, "{") { return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", tkn) } if tkn != "" { // empty token possible if user typed "" // Trailing comma indicates another address will follow, which // may possibly be on the next line if tkn[len(tkn)-1] == ',' { tkn = tkn[:len(tkn)-1] expectingAnother = true } else { expectingAnother = false // but we may still see another one on this line } // If there's a comma here, it's probably because they didn't use a space // between their two domains, e.g. "foo.com,bar.com", which would not be // parsed as two separate site addresses. if strings.Contains(tkn, ",") { return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", tkn) } p.block.Keys = append(p.block.Keys, tkn) } // Advance token and possibly break out of loop or return error hasNext := p.Next() if expectingAnother && !hasNext { return p.EOFErr() } if !hasNext { p.eof = true break // EOF } if !expectingAnother && p.isNewLine() { break } } return nil } func (p *parser) blockContents() error { errOpenCurlyBrace := p.openCurlyBrace() if errOpenCurlyBrace != nil { // single-server configs don't need curly braces p.cursor-- } err := p.directives() if err != nil { return err } // only look for close curly brace if there was an opening if errOpenCurlyBrace == nil { err = p.closeCurlyBrace() if err != nil { return err } } return nil } // directives parses through all the lines for directives // and it expects the next token to be the first // directive. It goes until EOF or closing curly brace // which ends the server block. func (p *parser) directives() error { for p.Next() { // end of server block if p.Val() == "}" { // p.nesting has already been decremented break } // special case: import directive replaces tokens during parse-time if p.Val() == "import" { err := p.doImport() if err != nil { return err } p.cursor-- // cursor is advanced when we continue, so roll back one more continue } // normal case: parse a directive as a new segment // (a "segment" is a line which starts with a directive // and which ends at the end of the line or at the end of // the block that is opened at the end of the line) if err := p.directive(); err != nil { return err } } return nil } // doImport swaps out the import directive and its argument // (a total of 2 tokens) with the tokens in the specified file // or globbing pattern. When the function returns, the cursor // is on the token before where the import directive was. In // other words, call Next() to access the first token that was // imported. func (p *parser) doImport() error { // syntax checks if !p.NextArg() { return p.ArgErr() } importPattern := p.Val() if importPattern == "" { return p.Err("Import requires a non-empty filepath") } // grab remaining args as placeholder replacements args := p.RemainingArgs() // add args to the replacer repl := caddy.NewEmptyReplacer() for index, arg := range args { repl.Set("args."+strconv.Itoa(index), arg) } // splice out the import directive and its arguments // (2 tokens, plus the length of args) tokensBefore := p.tokens[:p.cursor-1-len(args)] tokensAfter := p.tokens[p.cursor+1:] var importedTokens []Token var nodes []string // first check snippets. That is a simple, non-recursive replacement if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil { importedTokens = p.definedSnippets[importPattern] if len(importedTokens) > 0 { // just grab the first one nodes = append(nodes, fmt.Sprintf("%s:%s", importedTokens[0].File, importedTokens[0].snippetName)) } } else { // make path relative to the file of the _token_ being processed rather // than current working directory (issue #867) and then use glob to get // list of matching filenames absFile, err := filepath.Abs(p.Dispenser.File()) if err != nil { return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.File(), err) } var matches []string var globPattern string if !filepath.IsAbs(importPattern) { globPattern = filepath.Join(filepath.Dir(absFile), importPattern) } else { globPattern = importPattern } if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 || (strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) { // See issue #2096 - a pattern with many glob expansions can hang for too long return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern) } matches, err = filepath.Glob(globPattern) if err != nil { return p.Errf("Failed to use import pattern %s: %v", importPattern, err) } if len(matches) == 0 { if strings.ContainsAny(globPattern, "*?[]") { caddy.Log().Warn("No files matching import glob pattern", zap.String("pattern", importPattern)) } else { return p.Errf("File to import not found: %s", importPattern) } } // collect all the imported tokens for _, importFile := range matches { newTokens, err := p.doSingleImport(importFile) if err != nil { return err } importedTokens = append(importedTokens, newTokens...) } nodes = matches } nodeName := p.File() if p.Token().inSnippet { nodeName += fmt.Sprintf(":%s", p.Token().snippetName) } p.importGraph.addNode(nodeName) p.importGraph.addNodes(nodes) if err := p.importGraph.addEdges(nodeName, nodes); err != nil { p.importGraph.removeNodes(nodes) return err } // copy the tokens so we don't overwrite p.definedSnippets tokensCopy := make([]Token, len(importedTokens)) copy(tokensCopy, importedTokens) // run the argument replacer on the tokens for index, token := range tokensCopy { token.Text = repl.ReplaceKnown(token.Text, "") tokensCopy[index] = token } // splice the imported tokens in the place of the import statement // and rewind cursor so Next() will land on first imported token p.tokens = append(tokensBefore, append(tokensCopy, tokensAfter...)...) p.cursor -= len(args) + 1 return nil } // doSingleImport lexes the individual file at importFile and returns // its tokens or an error, if any. func (p *parser) doSingleImport(importFile string) ([]Token, error) { file, err := os.Open(importFile) if err != nil { return nil, p.Errf("Could not import %s: %v", importFile, err) } defer file.Close() if info, err := file.Stat(); err != nil { return nil, p.Errf("Could not import %s: %v", importFile, err) } else if info.IsDir() { return nil, p.Errf("Could not import %s: is a directory", importFile) } input, err := io.ReadAll(file) if err != nil { return nil, p.Errf("Could not read imported file %s: %v", importFile, err) } importedTokens, err := allTokens(importFile, input) if err != nil { return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err) } // Tack the file path onto these tokens so errors show the imported file's name // (we use full, absolute path to avoid bugs: issue #1892) filename, err := filepath.Abs(importFile) if err != nil { return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err) } for i := 0; i < len(importedTokens); i++ { importedTokens[i].File = filename } return importedTokens, nil } // directive collects tokens until the directive's scope // closes (either end of line or end of curly brace block). // It expects the currently-loaded token to be a directive // (or } that ends a server block). The collected tokens // are loaded into the current server block for later use // by directive setup functions. func (p *parser) directive() error { // a segment is a list of tokens associated with this directive var segment Segment // the directive itself is appended as a relevant token segment = append(segment, p.Token()) for p.Next() { if p.Val() == "{" { p.nesting++ if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 { return p.Err("Unexpected next token after '{' on same line") } } else if p.Val() == "{}" { if p.isNextOnNewLine() && p.Token().wasQuoted == 0 { return p.Err("Unexpected '{}' at end of line") } } else if p.isNewLine() && p.nesting == 0 { p.cursor-- // read too far break } else if p.Val() == "}" && p.nesting > 0 { p.nesting-- } else if p.Val() == "}" && p.nesting == 0 { return p.Err("Unexpected '}' because no matching opening brace") } else if p.Val() == "import" && p.isNewLine() { if err := p.doImport(); err != nil { return err } p.cursor-- // cursor is advanced when we continue, so roll back one more continue } segment = append(segment, p.Token()) } p.block.Segments = append(p.block.Segments, segment) if p.nesting > 0 { return p.EOFErr() } return nil } // openCurlyBrace expects the current token to be an // opening curly brace. This acts like an assertion // because it returns an error if the token is not // a opening curly brace. It does NOT advance the token. func (p *parser) openCurlyBrace() error { if p.Val() != "{" { return p.SyntaxErr("{") } return nil } // closeCurlyBrace expects the current token to be // a closing curly brace. This acts like an assertion // because it returns an error if the token is not // a closing curly brace. It does NOT advance the token. func (p *parser) closeCurlyBrace() error { if p.Val() != "}" { return p.SyntaxErr("}") } return nil } func (p *parser) isSnippet() (bool, string) { keys := p.block.Keys // A snippet block is a single key with parens. Nothing else qualifies. if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") { return true, strings.TrimSuffix(keys[0][1:], ")") } return false, "" } // read and store everything in a block for later replay. func (p *parser) snippetTokens() ([]Token, error) { // snippet must have curlies. err := p.openCurlyBrace() if err != nil { return nil, err } nesting := 1 // count our own nesting in snippets tokens := []Token{} for p.Next() { if p.Val() == "}" { nesting-- if nesting == 0 { break } } if p.Val() == "{" { nesting++ } tokens = append(tokens, p.tokens[p.cursor]) } // make sure we're matched up if nesting != 0 { return nil, p.SyntaxErr("}") } return tokens, nil } // ServerBlock associates any number of keys from the // head of the server block with tokens, which are // grouped by segments. type ServerBlock struct { HasBraces bool Keys []string Segments []Segment } // DispenseDirective returns a dispenser that contains // all the tokens in the server block. func (sb ServerBlock) DispenseDirective(dir string) *Dispenser { var tokens []Token for _, seg := range sb.Segments { if len(seg) > 0 && seg[0].Text == dir { tokens = append(tokens, seg...) } } return NewDispenser(tokens) } // Segment is a list of tokens which begins with a directive // and ends at the end of the directive (either at the end of // the line, or at the end of a block it opens). type Segment []Token // Directive returns the directive name for the segment. // The directive name is the text of the first token. func (s Segment) Directive() string { if len(s) > 0 { return s[0].Text } return "" } // spanOpen and spanClose are used to bound spans that // contain the name of an environment variable. var ( spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'} envVarDefaultDelimiter = ":" ) caddy-2.6.2/caddyconfig/caddyfile/parse_test.go000066400000000000000000000407651435007237400215240ustar00rootroot00000000000000// 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 caddyfile import ( "bytes" "os" "path/filepath" "testing" ) func TestAllTokens(t *testing.T) { input := []byte("a b c\nd e") expected := []string{"a", "b", "c", "d", "e"} tokens, err := allTokens("TestAllTokens", input) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(tokens) != len(expected) { t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens)) } for i, val := range expected { if tokens[i].Text != val { t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text) } } } func TestParseOneAndImport(t *testing.T) { testParseOne := func(input string) (ServerBlock, error) { p := testParser(input) p.Next() // parseOne doesn't call Next() to start, so we must err := p.parseOne() return p.block, err } for i, test := range []struct { input string shouldErr bool keys []string numTokens []int // number of tokens to expect in each segment }{ {`localhost`, false, []string{ "localhost", }, []int{}}, {`localhost dir1`, false, []string{ "localhost", }, []int{1}}, {`localhost:1234 dir1 foo bar`, false, []string{ "localhost:1234", }, []int{3}, }, {`localhost { dir1 }`, false, []string{ "localhost", }, []int{1}}, {`localhost:1234 { dir1 foo bar dir2 }`, false, []string{ "localhost:1234", }, []int{3, 1}}, {`http://localhost https://localhost dir1 foo bar`, false, []string{ "http://localhost", "https://localhost", }, []int{3}}, {`http://localhost https://localhost { dir1 foo bar }`, false, []string{ "http://localhost", "https://localhost", }, []int{3}}, {`http://localhost, https://localhost { dir1 foo bar }`, false, []string{ "http://localhost", "https://localhost", }, []int{3}}, {`http://localhost, { }`, true, []string{ "http://localhost", }, []int{}}, {`host1:80, http://host2.com dir1 foo bar dir2 baz`, false, []string{ "host1:80", "http://host2.com", }, []int{3, 2}}, {`http://host1.com, http://host2.com, https://host3.com`, false, []string{ "http://host1.com", "http://host2.com", "https://host3.com", }, []int{}}, {`http://host1.com:1234, https://host2.com dir1 foo { bar baz } dir2`, false, []string{ "http://host1.com:1234", "https://host2.com", }, []int{6, 1}}, {`127.0.0.1 dir1 { bar baz } dir2 { foo bar }`, false, []string{ "127.0.0.1", }, []int{5, 5}}, {`localhost dir1 { foo`, true, []string{ "localhost", }, []int{3}}, {`localhost dir1 { }`, false, []string{ "localhost", }, []int{3}}, {`localhost dir1 { } }`, true, []string{ "localhost", }, []int{}}, {`localhost{ dir1 }`, true, []string{}, []int{}}, {`localhost dir1 { nested { foo } } dir2 foo bar`, false, []string{ "localhost", }, []int{7, 3}}, {``, false, []string{}, []int{}}, {`localhost dir1 arg1 import testdata/import_test1.txt`, false, []string{ "localhost", }, []int{2, 3, 1}}, {`import testdata/import_test2.txt`, false, []string{ "host1", }, []int{1, 2}}, {`import testdata/not_found.txt`, true, []string{}, []int{}}, {`""`, false, []string{}, []int{}}, {``, false, []string{}, []int{}}, // Unexpected next token after '{' on same line {`localhost dir1 { a b }`, true, []string{"localhost"}, []int{}}, // Workaround with quotes {`localhost dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}}, // Unexpected '{}' at end of line {`localhost dir1 {}`, true, []string{"localhost"}, []int{}}, // Workaround with quotes {`localhost dir1 "{}"`, false, []string{"localhost"}, []int{2}}, // import with args {`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}}, {`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}}, {`import testdata/import_args*.txt a b`, false, []string{"a"}, []int{2}}, // test cases found by fuzzing! {`import }{$"`, true, []string{}, []int{}}, {`import /*/*.txt`, true, []string{}, []int{}}, {`import /???/?*?o`, true, []string{}, []int{}}, {`import /??`, true, []string{}, []int{}}, {`import /[a-z]`, true, []string{}, []int{}}, {`import {$}`, true, []string{}, []int{}}, {`import {%}`, true, []string{}, []int{}}, {`import {$$}`, true, []string{}, []int{}}, {`import {%%}`, true, []string{}, []int{}}, } { result, err := testParseOne(test.input) if test.shouldErr && err == nil { t.Errorf("Test %d: Expected an error, but didn't get one", i) } if !test.shouldErr && err != nil { t.Errorf("Test %d: Expected no error, but got: %v", i, err) } // t.Logf("%+v\n", result) if len(result.Keys) != len(test.keys) { t.Errorf("Test %d: Expected %d keys, got %d", i, len(test.keys), len(result.Keys)) continue } for j, addr := range result.Keys { if addr != test.keys[j] { t.Errorf("Test %d, key %d: Expected '%s', but was '%s'", i, j, test.keys[j], addr) } } if len(result.Segments) != len(test.numTokens) { t.Errorf("Test %d: Expected %d segments, had %d", i, len(test.numTokens), len(result.Segments)) continue } for j, seg := range result.Segments { if len(seg) != test.numTokens[j] { t.Errorf("Test %d, segment %d: Expected %d tokens, counted %d", i, j, test.numTokens[j], len(seg)) continue } } } } func TestRecursiveImport(t *testing.T) { testParseOne := func(input string) (ServerBlock, error) { p := testParser(input) p.Next() // parseOne doesn't call Next() to start, so we must err := p.parseOne() return p.block, err } isExpected := func(got ServerBlock) bool { if len(got.Keys) != 1 || got.Keys[0] != "localhost" { t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys) return false } if len(got.Segments) != 2 { t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments)) return false } if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 2 { t.Errorf("got unexpected tokens: %v", got.Segments) return false } return true } recursiveFile1, err := filepath.Abs("testdata/recursive_import_test1") if err != nil { t.Fatal(err) } recursiveFile2, err := filepath.Abs("testdata/recursive_import_test2") if err != nil { t.Fatal(err) } // test relative recursive import err = os.WriteFile(recursiveFile1, []byte( `localhost dir1 import recursive_import_test2`), 0644) if err != nil { t.Fatal(err) } defer os.Remove(recursiveFile1) err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0644) if err != nil { t.Fatal(err) } defer os.Remove(recursiveFile2) // import absolute path result, err := testParseOne("import " + recursiveFile1) if err != nil { t.Fatal(err) } if !isExpected(result) { t.Error("absolute+relative import failed") } // import relative path result, err = testParseOne("import testdata/recursive_import_test1") if err != nil { t.Fatal(err) } if !isExpected(result) { t.Error("relative+relative import failed") } // test absolute recursive import err = os.WriteFile(recursiveFile1, []byte( `localhost dir1 import `+recursiveFile2), 0644) if err != nil { t.Fatal(err) } // import absolute path result, err = testParseOne("import " + recursiveFile1) if err != nil { t.Fatal(err) } if !isExpected(result) { t.Error("absolute+absolute import failed") } // import relative path result, err = testParseOne("import testdata/recursive_import_test1") if err != nil { t.Fatal(err) } if !isExpected(result) { t.Error("relative+absolute import failed") } } func TestDirectiveImport(t *testing.T) { testParseOne := func(input string) (ServerBlock, error) { p := testParser(input) p.Next() // parseOne doesn't call Next() to start, so we must err := p.parseOne() return p.block, err } isExpected := func(got ServerBlock) bool { if len(got.Keys) != 1 || got.Keys[0] != "localhost" { t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys) return false } if len(got.Segments) != 2 { t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments)) return false } if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 8 { t.Errorf("got unexpected tokens: %v", got.Segments) return false } return true } directiveFile, err := filepath.Abs("testdata/directive_import_test") if err != nil { t.Fatal(err) } err = os.WriteFile(directiveFile, []byte(`prop1 1 prop2 2`), 0644) if err != nil { t.Fatal(err) } defer os.Remove(directiveFile) // import from existing file result, err := testParseOne(`localhost dir1 proxy { import testdata/directive_import_test transparent }`) if err != nil { t.Fatal(err) } if !isExpected(result) { t.Error("directive import failed") } // import from nonexistent file _, err = testParseOne(`localhost dir1 proxy { import testdata/nonexistent_file transparent }`) if err == nil { t.Fatal("expected error when importing a nonexistent file") } } func TestParseAll(t *testing.T) { for i, test := range []struct { input string shouldErr bool keys [][]string // keys per server block, in order }{ {`localhost`, false, [][]string{ {"localhost"}, }}, {`localhost:1234`, false, [][]string{ {"localhost:1234"}, }}, {`localhost:1234 { } localhost:2015 { }`, false, [][]string{ {"localhost:1234"}, {"localhost:2015"}, }}, {`localhost:1234, http://host2`, false, [][]string{ {"localhost:1234", "http://host2"}, }}, {`localhost:1234, http://host2,`, true, [][]string{}}, {`http://host1.com, http://host2.com { } https://host3.com, https://host4.com { }`, false, [][]string{ {"http://host1.com", "http://host2.com"}, {"https://host3.com", "https://host4.com"}, }}, {`import testdata/import_glob*.txt`, false, [][]string{ {"glob0.host0"}, {"glob0.host1"}, {"glob1.host0"}, {"glob2.host0"}, }}, {`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches {`import notfound/file.conf`, true, [][]string{}}, // but a specific file should // recursive self-import {`import testdata/import_recursive0.txt`, true, [][]string{}}, {`import testdata/import_recursive3.txt import testdata/import_recursive1.txt`, true, [][]string{}}, // cyclic imports {`(A) { import A } :80 import A `, true, [][]string{}}, {`(A) { import B } (B) { import A } :80 import A `, true, [][]string{}}, } { p := testParser(test.input) blocks, err := p.parseAll() if test.shouldErr && err == nil { t.Errorf("Test %d: Expected an error, but didn't get one", i) } if !test.shouldErr && err != nil { t.Errorf("Test %d: Expected no error, but got: %v", i, err) } if len(blocks) != len(test.keys) { t.Errorf("Test %d: Expected %d server blocks, got %d", i, len(test.keys), len(blocks)) continue } for j, block := range blocks { if len(block.Keys) != len(test.keys[j]) { t.Errorf("Test %d: Expected %d keys in block %d, got %d", i, len(test.keys[j]), j, len(block.Keys)) continue } for k, addr := range block.Keys { if addr != test.keys[j][k] { t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'", i, j, k, test.keys[j][k], addr) } } } } } func TestEnvironmentReplacement(t *testing.T) { os.Setenv("FOOBAR", "foobar") os.Setenv("CHAINED", "$FOOBAR") for i, test := range []struct { input string expect string }{ { input: "", expect: "", }, { input: "foo", expect: "foo", }, { input: "{$NOT_SET}", expect: "", }, { input: "foo{$NOT_SET}bar", expect: "foobar", }, { input: "{$FOOBAR}", expect: "foobar", }, { input: "foo {$FOOBAR} bar", expect: "foo foobar bar", }, { input: "foo{$FOOBAR}bar", expect: "foofoobarbar", }, { input: "foo\n{$FOOBAR}\nbar", expect: "foo\nfoobar\nbar", }, { input: "{$FOOBAR} {$FOOBAR}", expect: "foobar foobar", }, { input: "{$FOOBAR}{$FOOBAR}", expect: "foobarfoobar", }, { input: "{$CHAINED}", expect: "$FOOBAR", // should not chain env expands }, { input: "{$FOO:default}", expect: "default", }, { input: "foo{$BAR:bar}baz", expect: "foobarbaz", }, { input: "foo{$BAR:$FOOBAR}baz", expect: "foo$FOOBARbaz", // should not chain env expands }, { input: "{$FOOBAR", expect: "{$FOOBAR", }, { input: "{$LONGER_NAME $FOOBAR}", expect: "", }, { input: "{$}", expect: "{$}", }, { input: "{$$}", expect: "", }, { input: "{$", expect: "{$", }, { input: "}{$", expect: "}{$", }, } { actual, err := replaceEnvVars([]byte(test.input)) if err != nil { t.Fatal(err) } if !bytes.Equal(actual, []byte(test.expect)) { t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual) } } } func TestSnippets(t *testing.T) { p := testParser(` (common) { gzip foo errors stderr } http://example.com { import common } `) blocks, err := p.parseAll() if err != nil { t.Fatal(err) } if len(blocks) != 1 { t.Fatalf("Expect exactly one server block. Got %d.", len(blocks)) } if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual { t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual) } if len(blocks[0].Segments) != 2 { t.Fatalf("Server block should have tokens from import, got: %+v", blocks[0]) } if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual { t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) } if actual, expected := blocks[0].Segments[1][1].Text, "stderr"; expected != actual { t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) } } func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) { file, err := os.CreateTemp("", t.Name()) if err != nil { panic(err) // get a stack trace so we know where this was called from. } if _, err := file.WriteString(str); err != nil { panic(err) } if err := file.Close(); err != nil { panic(err) } return file.Name() } func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) { fileName := writeStringToTempFileOrDie(t, ` http://example.com { # This isn't an import directive, it's just an arg with value 'import' basicauth / import password } `) // Parse the root file that imports the other one. p := testParser(`import ` + fileName) blocks, err := p.parseAll() if err != nil { t.Fatal(err) } auth := blocks[0].Segments[0] line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text if line != "basicauth / import password" { // Previously, it would be changed to: // basicauth / import /path/to/test/dir/password // referencing a file that (probably) doesn't exist and changing the // password! t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line) } } func TestSnippetAcrossMultipleFiles(t *testing.T) { // Make the derived Caddyfile that expects (common) to be defined. fileName := writeStringToTempFileOrDie(t, ` http://example.com { import common } `) // Parse the root file that defines (common) and then imports the other one. p := testParser(` (common) { gzip foo } import ` + fileName + ` `) blocks, err := p.parseAll() if err != nil { t.Fatal(err) } if len(blocks) != 1 { t.Fatalf("Expect exactly one server block. Got %d.", len(blocks)) } if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual { t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual) } if len(blocks[0].Segments) != 1 { t.Fatalf("Server block should have tokens from import") } if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual { t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) } } func testParser(input string) parser { return parser{Dispenser: NewTestDispenser(input)} } caddy-2.6.2/caddyconfig/caddyfile/testdata/000077500000000000000000000000001435007237400206215ustar00rootroot00000000000000caddy-2.6.2/caddyconfig/caddyfile/testdata/import_args0.txt000066400000000000000000000000101435007237400237570ustar00rootroot00000000000000{args.0}caddy-2.6.2/caddyconfig/caddyfile/testdata/import_args1.txt000066400000000000000000000000211435007237400237620ustar00rootroot00000000000000{args.0} {args.1}caddy-2.6.2/caddyconfig/caddyfile/testdata/import_glob0.txt000066400000000000000000000000541435007237400237560ustar00rootroot00000000000000glob0.host0 { dir2 arg1 } glob0.host1 { } caddy-2.6.2/caddyconfig/caddyfile/testdata/import_glob1.txt000066400000000000000000000000411435007237400237530ustar00rootroot00000000000000glob1.host0 { dir1 dir2 arg1 } caddy-2.6.2/caddyconfig/caddyfile/testdata/import_glob2.txt000066400000000000000000000000331435007237400237550ustar00rootroot00000000000000glob2.host0 { dir2 arg1 } caddy-2.6.2/caddyconfig/caddyfile/testdata/import_recursive0.txt000066400000000000000000000000341435007237400250400ustar00rootroot00000000000000import import_recursive0.txtcaddy-2.6.2/caddyconfig/caddyfile/testdata/import_recursive1.txt000066400000000000000000000000341435007237400250410ustar00rootroot00000000000000import import_recursive2.txtcaddy-2.6.2/caddyconfig/caddyfile/testdata/import_recursive2.txt000066400000000000000000000000341435007237400250420ustar00rootroot00000000000000import import_recursive3.txtcaddy-2.6.2/caddyconfig/caddyfile/testdata/import_recursive3.txt000066400000000000000000000000341435007237400250430ustar00rootroot00000000000000import import_recursive1.txtcaddy-2.6.2/caddyconfig/caddyfile/testdata/import_test1.txt000066400000000000000000000000231435007237400240070ustar00rootroot00000000000000dir2 arg1 arg2 dir3caddy-2.6.2/caddyconfig/caddyfile/testdata/import_test2.txt000066400000000000000000000000321435007237400240100ustar00rootroot00000000000000host1 { dir1 dir2 arg1 }caddy-2.6.2/caddyconfig/configadapters.go000066400000000000000000000104241435007237400204050ustar00rootroot00000000000000// 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 ( "encoding/json" "fmt" "github.com/caddyserver/caddy/v2" ) // Adapter is a type which can adapt a configuration to Caddy JSON. // It returns the results and any warnings, or an error. type Adapter interface { Adapt(body []byte, options map[string]any) ([]byte, []Warning, error) } // Warning represents a warning or notice related to conversion. type Warning struct { File string `json:"file,omitempty"` Line int `json:"line,omitempty"` Directive string `json:"directive,omitempty"` Message string `json:"message,omitempty"` } func (w Warning) String() string { var directive string if w.Directive != "" { directive = fmt.Sprintf(" (%s)", w.Directive) } return fmt.Sprintf("%s:%d%s: %s", w.File, w.Line, directive, w.Message) } // JSON encodes val as JSON, returning it as a json.RawMessage. Any // marshaling errors (which are highly unlikely with correct code) // are converted to warnings. This is convenient when filling config // structs that require a json.RawMessage, without having to worry // about errors. func JSON(val any, warnings *[]Warning) json.RawMessage { b, err := json.Marshal(val) if err != nil { if warnings != nil { *warnings = append(*warnings, Warning{Message: err.Error()}) } return nil } return b } // JSONModuleObject is like JSON(), except it marshals val into a JSON object // with an added key named fieldName with the value fieldVal. This is useful // for encoding module values where the module name has to be described within // the object by a certain key; for example, `"handler": "file_server"` for a // file server HTTP handler (fieldName="handler" and fieldVal="file_server"). // The val parameter must encode into a map[string]any (i.e. it must be // a struct or map). Any errors are converted into warnings. func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage { // encode to a JSON object first enc, err := json.Marshal(val) if err != nil { if warnings != nil { *warnings = append(*warnings, Warning{Message: err.Error()}) } return nil } // then decode the object var tmp map[string]any err = json.Unmarshal(enc, &tmp) if err != nil { if warnings != nil { *warnings = append(*warnings, Warning{Message: err.Error()}) } return nil } // so we can easily add the module's field with its appointed value tmp[fieldName] = fieldVal // then re-marshal as JSON result, err := json.Marshal(tmp) if err != nil { if warnings != nil { *warnings = append(*warnings, Warning{Message: err.Error()}) } return nil } return result } // RegisterAdapter registers a config adapter with the given name. // This should usually be done at init-time. It panics if the // adapter cannot be registered successfully. func RegisterAdapter(name string, adapter Adapter) { if _, ok := configAdapters[name]; ok { panic(fmt.Errorf("%s: already registered", name)) } configAdapters[name] = adapter caddy.RegisterModule(adapterModule{name, adapter}) } // GetAdapter returns the adapter with the given name, // or nil if one with that name is not registered. func GetAdapter(name string) Adapter { return configAdapters[name] } // adapterModule is a wrapper type that can turn any config // adapter into a Caddy module, which has the benefit of being // counted with other modules, even though they do not // technically extend the Caddy configuration structure. // See caddyserver/caddy#3132. type adapterModule struct { name string Adapter } func (am adapterModule) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: caddy.ModuleID("caddy.adapters." + am.name), New: func() caddy.Module { return am }, } } var configAdapters = make(map[string]Adapter) caddy-2.6.2/caddyconfig/httpcaddyfile/000077500000000000000000000000001435007237400177105ustar00rootroot00000000000000caddy-2.6.2/caddyconfig/httpcaddyfile/addresses.go000066400000000000000000000333411435007237400222200ustar00rootroot00000000000000// 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 ( "fmt" "net" "net/netip" "reflect" "sort" "strconv" "strings" "unicode" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/certmagic" ) // mapAddressToServerBlocks returns a map of listener address to list of server // blocks that will be served on that address. To do this, each server block is // expanded so that each one is considered individually, although keys of a // server block that share the same address stay grouped together so the config // isn't repeated unnecessarily. For example, this Caddyfile: // // example.com { // bind 127.0.0.1 // } // www.example.com, example.net/path, localhost:9999 { // bind 127.0.0.1 1.2.3.4 // } // // has two server blocks to start with. But expressed in this Caddyfile are // actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999, // and 127.0.0.1:9999. This is because the bind directive is applied to each // key of its server block (specifying the host part), and each key may have // a different port. And we definitely need to be sure that a site which is // bound to be served on a specific interface is not served on others just // because that is more convenient: it would be a potential security risk // if the difference between interfaces means private vs. public. // // So what this function does for the example above is iterate each server // block, and for each server block, iterate its keys. For the first, it // finds one key (example.com) and determines its listener address // (127.0.0.1:443 - because of 'bind' and automatic HTTPS). It then adds // the listener address to the map value returned by this function, with // the first server block as one of its associations. // // It then iterates each key on the second server block and associates them // with one or more listener addresses. Indeed, each key in this block has // two listener addresses because of the 'bind' directive. Once we know // which addresses serve which keys, we can create a new server block for // each address containing the contents of the server block and only those // specific keys of the server block which use that address. // // It is possible and even likely that some keys in the returned map have // the exact same list of server blocks (i.e. they are identical). This // happens when multiple hosts are declared with a 'bind' directive and // the resulting listener addresses are not shared by any other server // block (or the other server blocks are exactly identical in their token // contents). This happens with our example above because 1.2.3.4:443 // and 1.2.3.4:9999 are used exclusively with the second server block. This // repetition may be undesirable, so call consolidateAddrMappings() to map // multiple addresses to the same lists of server blocks (a many:many mapping). // (Doing this is essentially a map-reduce technique.) func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock, options map[string]any) (map[string][]serverBlock, error) { sbmap := make(map[string][]serverBlock) for i, sblock := range originalServerBlocks { // within a server block, we need to map all the listener addresses // implied by the server block to the keys of the server block which // will be served by them; this has the effect of treating each // key of a server block as its own, but without having to repeat its // contents in cases where multiple keys really can be served together addrToKeys := make(map[string][]string) for j, key := range sblock.block.Keys { // a key can have multiple listener addresses if there are multiple // arguments to the 'bind' directive (although they will all have // the same port, since the port is defined by the key or is implicit // through automatic HTTPS) addrs, err := st.listenerAddrsForServerBlockKey(sblock, key, options) if err != nil { return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key, err) } // associate this key with each listener address it is served on for _, addr := range addrs { addrToKeys[addr] = append(addrToKeys[addr], key) } } // make a slice of the map keys so we can iterate in sorted order addrs := make([]string, 0, len(addrToKeys)) for k := range addrToKeys { addrs = append(addrs, k) } sort.Strings(addrs) // now that we know which addresses serve which keys of this // server block, we iterate that mapping and create a list of // new server blocks for each address where the keys of the // server block are only the ones which use the address; but // the contents (tokens) are of course the same for _, addr := range addrs { keys := addrToKeys[addr] // parse keys so that we only have to do it once parsedKeys := make([]Address, 0, len(keys)) for _, key := range keys { addr, err := ParseAddress(key) if err != nil { return nil, fmt.Errorf("parsing key '%s': %v", key, err) } parsedKeys = append(parsedKeys, addr.Normalize()) } sbmap[addr] = append(sbmap[addr], serverBlock{ block: caddyfile.ServerBlock{ Keys: keys, Segments: sblock.block.Segments, }, pile: sblock.pile, keys: parsedKeys, }) } } return sbmap, nil } // consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of // single listener addresses to lists of server blocks. Since multiple addresses may serve // identical sites (server block contents), this function turns a 1:many mapping into a // many:many mapping. Server block contents (tokens) must be exactly identical so that // reflect.DeepEqual returns true in order for the addresses to be combined. Identical // entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each // association from multiple addresses to multiple server blocks; i.e. each element of // the returned slice) becomes a server definition in the output JSON. func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]serverBlock) []sbAddrAssociation { sbaddrs := make([]sbAddrAssociation, 0, len(addrToServerBlocks)) for addr, sblocks := range addrToServerBlocks { // we start with knowing that at least this address // maps to these server blocks a := sbAddrAssociation{ addresses: []string{addr}, serverBlocks: sblocks, } // now find other addresses that map to identical // server blocks and add them to our list of // addresses, while removing them from the map for otherAddr, otherSblocks := range addrToServerBlocks { if addr == otherAddr { continue } if reflect.DeepEqual(sblocks, otherSblocks) { a.addresses = append(a.addresses, otherAddr) delete(addrToServerBlocks, otherAddr) } } sort.Strings(a.addresses) sbaddrs = append(sbaddrs, a) } // sort them by their first address (we know there will always be at least one) // to avoid problems with non-deterministic ordering (makes tests flaky) sort.Slice(sbaddrs, func(i, j int) bool { return sbaddrs[i].addresses[0] < sbaddrs[j].addresses[0] }) return sbaddrs } // listenerAddrsForServerBlockKey essentially converts the Caddyfile // site addresses to Caddy listener addresses for each server block. func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string, options map[string]any) ([]string, error) { addr, err := ParseAddress(key) if err != nil { return nil, fmt.Errorf("parsing key: %v", err) } addr = addr.Normalize() // figure out the HTTP and HTTPS ports; either // use defaults, or override with user config httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort) if hport, ok := options["http_port"]; ok { httpPort = strconv.Itoa(hport.(int)) } if hsport, ok := options["https_port"]; ok { httpsPort = strconv.Itoa(hsport.(int)) } // default port is the HTTPS port lnPort := httpsPort if addr.Port != "" { // port explicitly defined lnPort = addr.Port } else if addr.Scheme == "http" { // port inferred from scheme lnPort = httpPort } // error if scheme and port combination violate convention if (addr.Scheme == "http" && lnPort == httpsPort) || (addr.Scheme == "https" && lnPort == httpPort) { return nil, fmt.Errorf("[%s] scheme and port violate convention", key) } // the bind directive specifies hosts (and potentially network), but is optional lnHosts := make([]string, 0, len(sblock.pile["bind"])) for _, cfgVal := range sblock.pile["bind"] { lnHosts = append(lnHosts, cfgVal.Value.([]string)...) } if len(lnHosts) == 0 { if defaultBind, ok := options["default_bind"].([]string); ok { lnHosts = defaultBind } else { lnHosts = []string{""} } } // use a map to prevent duplication listeners := make(map[string]struct{}) for _, lnHost := range lnHosts { // normally we would simply append the port, // but if lnHost is IPv6, we need to ensure it // is enclosed in [ ]; net.JoinHostPort does // this for us, but lnHost might also have a // network type in front (e.g. "tcp/") leading // to "[tcp/::1]" which causes parsing failures // later; what we need is "tcp/[::1]", so we have // to split the network and host, then re-combine network, host, ok := strings.Cut(lnHost, "/") if !ok { host = network network = "" } host = strings.Trim(host, "[]") // IPv6 networkAddr := caddy.JoinNetworkAddress(network, host, lnPort) addr, err := caddy.ParseNetworkAddress(networkAddr) if err != nil { return nil, fmt.Errorf("parsing network address: %v", err) } listeners[addr.String()] = struct{}{} } // now turn map into list listenersList := make([]string, 0, len(listeners)) for lnStr := range listeners { listenersList = append(listenersList, lnStr) } sort.Strings(listenersList) return listenersList, nil } // Address represents a site address. It contains // the original input value, and the component // parts of an address. The component parts may be // updated to the correct values as setup proceeds, // but the original value should never be changed. // // The Host field must be in a normalized form. type Address struct { Original, Scheme, Host, Port, Path string } // ParseAddress parses an address string into a structured format with separate // scheme, host, port, and path portions, as well as the original input string. func ParseAddress(str string) (Address, error) { const maxLen = 4096 if len(str) > maxLen { str = str[:maxLen] } remaining := strings.TrimSpace(str) a := Address{Original: remaining} // extract scheme splitScheme := strings.SplitN(remaining, "://", 2) switch len(splitScheme) { case 0: return a, nil case 1: remaining = splitScheme[0] case 2: a.Scheme = splitScheme[0] remaining = splitScheme[1] } // extract host and port hostSplit := strings.SplitN(remaining, "/", 2) if len(hostSplit) > 0 { host, port, err := net.SplitHostPort(hostSplit[0]) if err != nil { host, port, err = net.SplitHostPort(hostSplit[0] + ":") if err != nil { host = hostSplit[0] } } a.Host = host a.Port = port } if len(hostSplit) == 2 { // all that remains is the path a.Path = "/" + hostSplit[1] } // make sure port is valid if a.Port != "" { if portNum, err := strconv.Atoi(a.Port); err != nil { return Address{}, fmt.Errorf("invalid port '%s': %v", a.Port, err) } else if portNum < 0 || portNum > 65535 { return Address{}, fmt.Errorf("port %d is out of range", portNum) } } return a, nil } // String returns a human-readable form of a. It will // be a cleaned-up and filled-out URL string. func (a Address) String() string { if a.Host == "" && a.Port == "" { return "" } scheme := a.Scheme if scheme == "" { if a.Port == strconv.Itoa(certmagic.HTTPSPort) { scheme = "https" } else { scheme = "http" } } s := scheme if s != "" { s += "://" } if a.Port != "" && ((scheme == "https" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort)) || (scheme == "http" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort))) { s += net.JoinHostPort(a.Host, a.Port) } else { s += a.Host } if a.Path != "" { s += a.Path } return s } // Normalize returns a normalized version of a. func (a Address) Normalize() Address { path := a.Path // ensure host is normalized if it's an IP address host := strings.TrimSpace(a.Host) if ip, err := netip.ParseAddr(host); err == nil { if ip.Is6() && !ip.Is4() && !ip.Is4In6() { host = ip.String() } } return Address{ Original: a.Original, Scheme: lowerExceptPlaceholders(a.Scheme), Host: lowerExceptPlaceholders(host), Port: a.Port, Path: path, } } // lowerExceptPlaceholders lowercases s except within // placeholders (substrings in non-escaped '{ }' spans). // See https://github.com/caddyserver/caddy/issues/3264 func lowerExceptPlaceholders(s string) string { var sb strings.Builder var escaped, inPlaceholder bool for _, ch := range s { if ch == '\\' && !escaped { escaped = true sb.WriteRune(ch) continue } if ch == '{' && !escaped { inPlaceholder = true } if ch == '}' && inPlaceholder && !escaped { inPlaceholder = false } if inPlaceholder { sb.WriteRune(ch) } else { sb.WriteRune(unicode.ToLower(ch)) } escaped = false } return sb.String() } caddy-2.6.2/caddyconfig/httpcaddyfile/addresses_fuzz.go000066400000000000000000000014661435007237400233010ustar00rootroot00000000000000// 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. //go:build gofuzz package httpcaddyfile func FuzzParseAddress(data []byte) int { addr, err := ParseAddress(string(data)) if err != nil { if addr == (Address{}) { return 1 } return 0 } return 1 } caddy-2.6.2/caddyconfig/httpcaddyfile/addresses_test.go000066400000000000000000000164521435007237400232630ustar00rootroot00000000000000package httpcaddyfile import ( "testing" ) func TestParseAddress(t *testing.T) { for i, test := range []struct { input string scheme, host, port, path string shouldErr bool }{ {``, "", "", "", "", false}, {`localhost`, "", "localhost", "", "", false}, {`localhost:1234`, "", "localhost", "1234", "", false}, {`localhost:`, "", "localhost", "", "", false}, {`0.0.0.0`, "", "0.0.0.0", "", "", false}, {`127.0.0.1:1234`, "", "127.0.0.1", "1234", "", false}, {`:1234`, "", "", "1234", "", false}, {`[::1]`, "", "::1", "", "", false}, {`[::1]:1234`, "", "::1", "1234", "", false}, {`:`, "", "", "", "", false}, {`:http`, "", "", "", "", true}, {`:https`, "", "", "", "", true}, {`localhost:http`, "", "", "", "", true}, // using service name in port is verboten, as of Go 1.12.8 {`localhost:https`, "", "", "", "", true}, {`http://localhost:https`, "", "", "", "", true}, // conflict {`http://localhost:http`, "", "", "", "", true}, // repeated scheme {`host:https/path`, "", "", "", "", true}, {`http://localhost:443`, "http", "localhost", "443", "", false}, // NOTE: not conventional {`https://localhost:80`, "https", "localhost", "80", "", false}, // NOTE: not conventional {`http://localhost`, "http", "localhost", "", "", false}, {`https://localhost`, "https", "localhost", "", "", false}, {`http://{env.APP_DOMAIN}`, "http", "{env.APP_DOMAIN}", "", "", false}, {`{env.APP_DOMAIN}:80`, "", "{env.APP_DOMAIN}", "80", "", false}, {`{env.APP_DOMAIN}/path`, "", "{env.APP_DOMAIN}", "", "/path", false}, {`example.com/{env.APP_PATH}`, "", "example.com", "", "/{env.APP_PATH}", false}, {`http://127.0.0.1`, "http", "127.0.0.1", "", "", false}, {`https://127.0.0.1`, "https", "127.0.0.1", "", "", false}, {`http://[::1]`, "http", "::1", "", "", false}, {`http://localhost:1234`, "http", "localhost", "1234", "", false}, {`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false}, {`http://[::1]:1234`, "http", "::1", "1234", "", false}, {``, "", "", "", "", false}, {`::1`, "", "::1", "", "", false}, {`localhost::`, "", "localhost::", "", "", false}, {`#$%@`, "", "#$%@", "", "", false}, // don't want to presume what the hostname could be {`host/path`, "", "host", "", "/path", false}, {`http://host/`, "http", "host", "", "/", false}, {`//asdf`, "", "", "", "//asdf", false}, {`:1234/asdf`, "", "", "1234", "/asdf", false}, {`http://host/path`, "http", "host", "", "/path", false}, {`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false}, {`host:80/path`, "", "host", "80", "/path", false}, {`/path`, "", "", "", "/path", false}, } { actual, err := ParseAddress(test.input) if err != nil && !test.shouldErr { t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err) } if err == nil && test.shouldErr { t.Errorf("Test %d (%s): Expected error, but had none (%#v)", i, test.input, actual) } if !test.shouldErr && actual.Original != test.input { t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original) } if actual.Scheme != test.scheme { t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme) } if actual.Host != test.host { t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host) } if actual.Port != test.port { t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port) } if actual.Path != test.path { t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path) } } } func TestAddressString(t *testing.T) { for i, test := range []struct { addr Address expected string }{ {Address{Scheme: "http", Host: "host", Port: "1234", Path: "/path"}, "http://host:1234/path"}, {Address{Scheme: "", Host: "host", Port: "", Path: ""}, "http://host"}, {Address{Scheme: "", Host: "host", Port: "80", Path: ""}, "http://host"}, {Address{Scheme: "", Host: "host", Port: "443", Path: ""}, "https://host"}, {Address{Scheme: "https", Host: "host", Port: "443", Path: ""}, "https://host"}, {Address{Scheme: "https", Host: "host", Port: "", Path: ""}, "https://host"}, {Address{Scheme: "", Host: "host", Port: "80", Path: "/path"}, "http://host/path"}, {Address{Scheme: "http", Host: "", Port: "1234", Path: ""}, "http://:1234"}, {Address{Scheme: "", Host: "", Port: "", Path: ""}, ""}, } { actual := test.addr.String() if actual != test.expected { t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual) } } } func TestKeyNormalization(t *testing.T) { testCases := []struct { input string expect Address }{ { input: "example.com", expect: Address{ Host: "example.com", }, }, { input: "http://host:1234/path", expect: Address{ Scheme: "http", Host: "host", Port: "1234", Path: "/path", }, }, { input: "HTTP://A/ABCDEF", expect: Address{ Scheme: "http", Host: "a", Path: "/ABCDEF", }, }, { input: "A/ABCDEF", expect: Address{ Host: "a", Path: "/ABCDEF", }, }, { input: "A:2015/Path", expect: Address{ Host: "a", Port: "2015", Path: "/Path", }, }, { input: "sub.{env.MY_DOMAIN}", expect: Address{ Host: "sub.{env.MY_DOMAIN}", }, }, { input: "sub.ExAmPle", expect: Address{ Host: "sub.example", }, }, { input: "sub.\\{env.MY_DOMAIN\\}", expect: Address{ Host: "sub.\\{env.my_domain\\}", }, }, { input: "sub.{env.MY_DOMAIN}.com", expect: Address{ Host: "sub.{env.MY_DOMAIN}.com", }, }, { input: ":80", expect: Address{ Port: "80", }, }, { input: ":443", expect: Address{ Port: "443", }, }, { input: ":1234", expect: Address{ Port: "1234", }, }, { input: "", expect: Address{}, }, { input: ":", expect: Address{}, }, { input: "[::]", expect: Address{ Host: "::", }, }, { input: "127.0.0.1", expect: Address{ Host: "127.0.0.1", }, }, { input: "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:1234", expect: Address{ Host: "2001:db8:85a3:8d3:1319:8a2e:370:7348", Port: "1234", }, }, { // IPv4 address in IPv6 form (#4381) input: "[::ffff:cff4:e77d]:1234", expect: Address{ Host: "::ffff:cff4:e77d", Port: "1234", }, }, { input: "::ffff:cff4:e77d", expect: Address{ Host: "::ffff:cff4:e77d", }, }, } for i, tc := range testCases { addr, err := ParseAddress(tc.input) if err != nil { t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err) continue } actual := addr.Normalize() if actual.Scheme != tc.expect.Scheme { t.Errorf("Test %d: Input '%s': Expected Scheme='%s' but got Scheme='%s'", i, tc.input, tc.expect.Scheme, actual.Scheme) } if actual.Host != tc.expect.Host { t.Errorf("Test %d: Input '%s': Expected Host='%s' but got Host='%s'", i, tc.input, tc.expect.Host, actual.Host) } if actual.Port != tc.expect.Port { t.Errorf("Test %d: Input '%s': Expected Port='%s' but got Port='%s'", i, tc.input, tc.expect.Port, actual.Port) } if actual.Path != tc.expect.Path { t.Errorf("Test %d: Input '%s': Expected Path='%s' but got Path='%s'", i, tc.input, tc.expect.Path, actual.Path) } } } caddy-2.6.2/caddyconfig/httpcaddyfile/builtins.go000066400000000000000000000611651435007237400221010ustar00rootroot00000000000000// 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/base64" "encoding/pem" "fmt" "html" "net/http" "os" "reflect" "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/caddytls" "github.com/caddyserver/certmagic" "github.com/mholt/acmez/acme" "go.uber.org/zap/zapcore" ) func init() { RegisterDirective("bind", parseBind) RegisterDirective("tls", parseTLS) RegisterHandlerDirective("root", parseRoot) RegisterHandlerDirective("vars", parseVars) RegisterHandlerDirective("redir", parseRedir) RegisterHandlerDirective("respond", parseRespond) RegisterHandlerDirective("abort", parseAbort) RegisterHandlerDirective("error", parseError) RegisterHandlerDirective("route", parseRoute) RegisterHandlerDirective("handle", parseHandle) RegisterDirective("handle_errors", parseHandleErrors) RegisterDirective("log", parseLog) RegisterHandlerDirective("skip_log", parseSkipLog) } // parseBind parses the bind directive. Syntax: // // bind func parseBind(h Helper) ([]ConfigValue, error) { var lnHosts []string for h.Next() { lnHosts = append(lnHosts, h.RemainingArgs()...) } return h.NewBindAddresses(lnHosts), nil } // parseTLS parses the tls directive. Syntax: // // tls [|internal]|[ ] { // protocols [] // ciphers // curves // client_auth { // mode [request|require|verify_if_given|require_and_verify] // trusted_ca_cert // trusted_ca_cert_file // trusted_leaf_cert // trusted_leaf_cert_file // } // alpn // load // ca // ca_root // dns [...] // on_demand // eab // issuer [...] // get_certificate [...] // insecure_secrets_log // } func parseTLS(h Helper) ([]ConfigValue, error) { cp := new(caddytls.ConnectionPolicy) var fileLoader caddytls.FileLoader var folderLoader caddytls.FolderLoader var certSelector caddytls.CustomCertSelectionPolicy var acmeIssuer *caddytls.ACMEIssuer var keyType string var internalIssuer *caddytls.InternalIssuer var issuers []certmagic.Issuer var certManagers []certmagic.Manager var onDemand bool for h.Next() { // file certificate loader firstLine := h.RemainingArgs() switch len(firstLine) { case 0: case 1: if firstLine[0] == "internal" { internalIssuer = new(caddytls.InternalIssuer) } else if !strings.Contains(firstLine[0], "@") { return nil, h.Err("single argument must either be 'internal' or an email address") } else { if acmeIssuer == nil { acmeIssuer = new(caddytls.ACMEIssuer) } acmeIssuer.Email = firstLine[0] } case 2: certFilename := firstLine[0] keyFilename := firstLine[1] // tag this certificate so if multiple certs match, specifically // this one that the user has provided will be used, see #2588: // https://github.com/caddyserver/caddy/issues/2588 ... but we // must be careful about how we do this; being careless will // lead to failed handshakes // // we need to remember which cert files we've seen, since we // must load each cert only once; otherwise, they each get a // different tag... since a cert loaded twice has the same // bytes, it will overwrite the first one in the cache, and // only the last cert (and its tag) will survive, so any conn // policy that is looking for any tag other than the last one // to be loaded won't find it, and TLS handshakes will fail // (see end of issue #3004) // // tlsCertTags maps certificate filenames to their tag. // This is used to remember which tag is used for each // certificate files, since we need to avoid loading // the same certificate files more than once, overwriting // previous tags tlsCertTags, ok := h.State["tlsCertTags"].(map[string]string) if !ok { tlsCertTags = make(map[string]string) h.State["tlsCertTags"] = tlsCertTags } tag, ok := tlsCertTags[certFilename] if !ok { // haven't seen this cert file yet, let's give it a tag // and add a loader for it tag = fmt.Sprintf("cert%d", len(tlsCertTags)) fileLoader = append(fileLoader, caddytls.CertKeyFilePair{ Certificate: certFilename, Key: keyFilename, Tags: []string{tag}, }) // remember this for next time we see this cert file tlsCertTags[certFilename] = tag } certSelector.AnyTag = append(certSelector.AnyTag, tag) default: return nil, h.ArgErr() } var hasBlock bool for nesting := h.Nesting(); h.NextBlock(nesting); { hasBlock = true switch h.Val() { case "protocols": args := h.RemainingArgs() if len(args) == 0 { return nil, h.SyntaxErr("one or two protocols") } if len(args) > 0 { if _, ok := caddytls.SupportedProtocols[args[0]]; !ok { return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0]) } cp.ProtocolMin = args[0] } if len(args) > 1 { if _, ok := caddytls.SupportedProtocols[args[1]]; !ok { return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1]) } cp.ProtocolMax = args[1] } case "ciphers": for h.NextArg() { if !caddytls.CipherSuiteNameSupported(h.Val()) { return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val()) } cp.CipherSuites = append(cp.CipherSuites, h.Val()) } case "curves": for h.NextArg() { if _, ok := caddytls.SupportedCurves[h.Val()]; !ok { return nil, h.Errf("Wrong curve name or curve not supported: '%s'", h.Val()) } cp.Curves = append(cp.Curves, h.Val()) } case "client_auth": cp.ClientAuthentication = &caddytls.ClientAuthentication{} for nesting := h.Nesting(); h.NextBlock(nesting); { subdir := h.Val() switch subdir { case "mode": if !h.Args(&cp.ClientAuthentication.Mode) { return nil, h.ArgErr() } if h.NextArg() { return nil, h.ArgErr() } case "trusted_ca_cert", "trusted_leaf_cert": if !h.NextArg() { return nil, h.ArgErr() } if subdir == "trusted_ca_cert" { cp.ClientAuthentication.TrustedCACerts = append(cp.ClientAuthentication.TrustedCACerts, h.Val()) } else { cp.ClientAuthentication.TrustedLeafCerts = append(cp.ClientAuthentication.TrustedLeafCerts, h.Val()) } case "trusted_ca_cert_file", "trusted_leaf_cert_file": if !h.NextArg() { return nil, h.ArgErr() } filename := h.Val() certDataPEM, err := os.ReadFile(filename) if err != nil { return nil, err } block, _ := pem.Decode(certDataPEM) if block == nil || block.Type != "CERTIFICATE" { return nil, h.Errf("no CERTIFICATE pem block found in %s", h.Val()) } if subdir == "trusted_ca_cert_file" { cp.ClientAuthentication.TrustedCACerts = append(cp.ClientAuthentication.TrustedCACerts, base64.StdEncoding.EncodeToString(block.Bytes)) } else { cp.ClientAuthentication.TrustedLeafCerts = append(cp.ClientAuthentication.TrustedLeafCerts, base64.StdEncoding.EncodeToString(block.Bytes)) } default: return nil, h.Errf("unknown subdirective for client_auth: %s", subdir) } } case "alpn": args := h.RemainingArgs() if len(args) == 0 { return nil, h.ArgErr() } cp.ALPN = args case "load": folderLoader = append(folderLoader, h.RemainingArgs()...) case "ca": arg := h.RemainingArgs() if len(arg) != 1 { return nil, h.ArgErr() } if acmeIssuer == nil { acmeIssuer = new(caddytls.ACMEIssuer) } acmeIssuer.CA = arg[0] case "key_type": arg := h.RemainingArgs() if len(arg) != 1 { return nil, h.ArgErr() } keyType = arg[0] case "eab": arg := h.RemainingArgs() if len(arg) != 2 { return nil, h.ArgErr() } if acmeIssuer == nil { acmeIssuer = new(caddytls.ACMEIssuer) } acmeIssuer.ExternalAccount = &acme.EAB{ KeyID: arg[0], MACKey: arg[1], } case "issuer": if !h.NextArg() { return nil, h.ArgErr() } modName := h.Val() modID := "tls.issuance." + modName unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) if err != nil { return nil, err } issuer, ok := unm.(certmagic.Issuer) if !ok { return nil, h.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm) } issuers = append(issuers, issuer) case "get_certificate": if !h.NextArg() { return nil, h.ArgErr() } modName := h.Val() modID := "tls.get_certificate." + modName unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) if err != nil { return nil, err } certManager, ok := unm.(certmagic.Manager) if !ok { return nil, h.Errf("module %s (%T) is not a certmagic.CertificateManager", modID, unm) } certManagers = append(certManagers, certManager) case "dns": if !h.NextArg() { return nil, h.ArgErr() } provName := h.Val() if acmeIssuer == nil { acmeIssuer = new(caddytls.ACMEIssuer) } if acmeIssuer.Challenges == nil { acmeIssuer.Challenges = new(caddytls.ChallengesConfig) } if acmeIssuer.Challenges.DNS == nil { acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) } modID := "dns.providers." + provName unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) if err != nil { return nil, err } acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings) case "resolvers": args := h.RemainingArgs() if len(args) == 0 { return nil, h.ArgErr() } if acmeIssuer == nil { acmeIssuer = new(caddytls.ACMEIssuer) } if acmeIssuer.Challenges == nil { acmeIssuer.Challenges = new(caddytls.ChallengesConfig) } if acmeIssuer.Challenges.DNS == nil { acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) } acmeIssuer.Challenges.DNS.Resolvers = args case "dns_challenge_override_domain": arg := h.RemainingArgs() if len(arg) != 1 { return nil, h.ArgErr() } if acmeIssuer == nil { acmeIssuer = new(caddytls.ACMEIssuer) } if acmeIssuer.Challenges == nil { acmeIssuer.Challenges = new(caddytls.ChallengesConfig) } if acmeIssuer.Challenges.DNS == nil { acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) } acmeIssuer.Challenges.DNS.OverrideDomain = arg[0] case "ca_root": arg := h.RemainingArgs() if len(arg) != 1 { return nil, h.ArgErr() } if acmeIssuer == nil { acmeIssuer = new(caddytls.ACMEIssuer) } acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, arg[0]) case "on_demand": if h.NextArg() { return nil, h.ArgErr() } onDemand = true case "insecure_secrets_log": if !h.NextArg() { return nil, h.ArgErr() } cp.InsecureSecretsLog = h.Val() default: return nil, h.Errf("unknown subdirective: %s", h.Val()) } } // a naked tls directive is not allowed if len(firstLine) == 0 && !hasBlock { return nil, h.ArgErr() } } // begin building the final config values configVals := []ConfigValue{} // certificate loaders if len(fileLoader) > 0 { configVals = append(configVals, ConfigValue{ Class: "tls.cert_loader", Value: fileLoader, }) } if len(folderLoader) > 0 { configVals = append(configVals, ConfigValue{ Class: "tls.cert_loader", Value: folderLoader, }) } // some tls subdirectives are shortcuts that implicitly configure issuers, and the // user can also configure issuers explicitly using the issuer subdirective; the // logic to support both would likely be complex, or at least unintuitive if len(issuers) > 0 && (acmeIssuer != nil || internalIssuer != nil) { return nil, h.Err("cannot mix issuer subdirective (explicit issuers) with other issuer-specific subdirectives (implicit issuers)") } if acmeIssuer != nil && internalIssuer != nil { return nil, h.Err("cannot create both ACME and internal certificate issuers") } // now we should either have: explicitly-created issuers, or an implicitly-created // ACME or internal issuer, or no issuers at all switch { case len(issuers) > 0: for _, issuer := range issuers { configVals = append(configVals, ConfigValue{ Class: "tls.cert_issuer", Value: issuer, }) } case acmeIssuer != nil: // implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one defaultIssuers := caddytls.DefaultIssuers() // if a CA endpoint was set, override multiple implicit issuers since it's a specific one if acmeIssuer.CA != "" { defaultIssuers = []certmagic.Issuer{acmeIssuer} } for _, issuer := range defaultIssuers { switch iss := issuer.(type) { case *caddytls.ACMEIssuer: issuer = acmeIssuer case *caddytls.ZeroSSLIssuer: iss.ACMEIssuer = acmeIssuer } configVals = append(configVals, ConfigValue{ Class: "tls.cert_issuer", Value: issuer, }) } case internalIssuer != nil: configVals = append(configVals, ConfigValue{ Class: "tls.cert_issuer", Value: internalIssuer, }) } // certificate key type if keyType != "" { configVals = append(configVals, ConfigValue{ Class: "tls.key_type", Value: keyType, }) } // on-demand TLS if onDemand { configVals = append(configVals, ConfigValue{ Class: "tls.on_demand", Value: true, }) } for _, certManager := range certManagers { configVals = append(configVals, ConfigValue{ Class: "tls.cert_manager", Value: certManager, }) } // custom certificate selection if len(certSelector.AnyTag) > 0 { cp.CertSelection = &certSelector } // connection policy -- always add one, to ensure that TLS // is enabled, because this directive was used (this is // needed, for instance, when a site block has a key of // just ":5000" - i.e. no hostname, and only on-demand TLS // is enabled) configVals = append(configVals, ConfigValue{ Class: "tls.connection_policy", Value: cp, }) return configVals, nil } // parseRoot parses the root directive. Syntax: // // root [] func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) { var root string for h.Next() { if !h.NextArg() { return nil, h.ArgErr() } root = h.Val() if h.NextArg() { return nil, h.ArgErr() } } return caddyhttp.VarsMiddleware{"root": root}, nil } // parseVars parses the vars directive. See its UnmarshalCaddyfile method for syntax. func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) { v := new(caddyhttp.VarsMiddleware) err := v.UnmarshalCaddyfile(h.Dispenser) return v, err } // parseRedir parses the redir directive. Syntax: // // redir [] [] // // 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.go000066400000000000000000000077771435007237400231510ustar00rootroot00000000000000package 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.go000066400000000000000000000430271435007237400224060ustar00rootroot00000000000000// 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.go000066400000000000000000000037121435007237400234420ustar00rootroot00000000000000package 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.go000066400000000000000000001414651435007237400221330ustar00rootroot00000000000000// 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.go000066400000000000000000000062131435007237400231610ustar00rootroot00000000000000package 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.go000066400000000000000000000300701435007237400217320ustar00rootroot00000000000000// 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.go000066400000000000000000000022241435007237400227710ustar00rootroot00000000000000package 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.go000066400000000000000000000126601435007237400215300ustar00rootroot00000000000000// 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.go000066400000000000000000000205311435007237400231620ustar00rootroot00000000000000// 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.go000066400000000000000000000606471435007237400215570ustar00rootroot00000000000000// 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.go000066400000000000000000000023231435007237400226010ustar00rootroot00000000000000package 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.go000066400000000000000000000127731435007237400175730ustar00rootroot00000000000000// 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.go000066400000000000000000000133651435007237400163420ustar00rootroot00000000000000// 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/000077500000000000000000000000001435007237400145765ustar00rootroot00000000000000caddy-2.6.2/caddytest/a.caddy.localhost.crt000066400000000000000000000026071435007237400206070ustar00rootroot00000000000000-----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.key000066400000000000000000000032171435007237400206050ustar00rootroot00000000000000-----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.cer000066400000000000000000000022631435007237400167420ustar00rootroot00000000000000-----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.crt000066400000000000000000000026071435007237400203700ustar00rootroot00000000000000-----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.key000066400000000000000000000032131435007237400203620ustar00rootroot00000000000000-----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.go000066400000000000000000000367371435007237400171310ustar00rootroot00000000000000package 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.go000066400000000000000000000014641435007237400201550ustar00rootroot00000000000000package 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/000077500000000000000000000000001435007237400171215ustar00rootroot00000000000000caddy-2.6.2/caddytest/integration/autohttps_test.go000066400000000000000000000063241435007237400225470ustar00rootroot00000000000000package 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/000077500000000000000000000000001435007237400222165ustar00rootroot00000000000000caddy-2.6.2/caddytest/integration/caddyfile_adapt/auto_https_disable_redirects.txt000066400000000000000000000006161435007237400307030ustar00rootroot00000000000000{ 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.txt000066400000000000000000000006311435007237400312240ustar00rootroot00000000000000{ 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.txt000066400000000000000000000006511435007237400260050ustar00rootroot00000000000000{ 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.txt000066400000000000000000000005121435007237400246350ustar00rootroot00000000000000example.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.txt000066400000000000000000000027641435007237400260000ustar00rootroot00000000000000: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.txt000066400000000000000000000045311435007237400256260ustar00rootroot00000000000000example.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.txt000066400000000000000000000041171435007237400265610ustar00rootroot00000000000000example.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.txt000066400000000000000000000005731435007237400320250ustar00rootroot00000000000000: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.txt000066400000000000000000000005501435007237400273540ustar00rootroot00000000000000: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.txt000066400000000000000000000010451435007237400302170ustar00rootroot00000000000000: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.txt000066400000000000000000000035571435007237400267010ustar00rootroot00000000000000localhost 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.txt000066400000000000000000000047131435007237400273250ustar00rootroot00000000000000app.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.txt000066400000000000000000000032201435007237400304630ustar00rootroot00000000000000: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.txt000066400000000000000000000022331435007237400257720ustar00rootroot00000000000000{ 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.txt000066400000000000000000000031551435007237400267630ustar00rootroot00000000000000{ 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.txt000066400000000000000000000022131435007237400271400ustar00rootroot00000000000000{ 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.txt000066400000000000000000000007641435007237400323640ustar00rootroot00000000000000{ 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.txt000066400000000000000000000012351435007237400304730ustar00rootroot00000000000000{ 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.txt000066400000000000000000000020721435007237400305020ustar00rootroot00000000000000{ 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.txt000066400000000000000000000002611435007237400277730ustar00rootroot00000000000000{ 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.txt000066400000000000000000000007141435007237400302270ustar00rootroot00000000000000{ 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.txt000066400000000000000000000004261435007237400300470ustar00rootroot00000000000000{ 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.txt000066400000000000000000000013111435007237400313510ustar00rootroot00000000000000{ 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.txt000066400000000000000000000052761435007237400320210ustar00rootroot00000000000000{ 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.txt000066400000000000000000000017041435007237400305740ustar00rootroot00000000000000{ 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.txt000066400000000000000000000017531435007237400307270ustar00rootroot00000000000000{ 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.txt000066400000000000000000000013571435007237400252340ustar00rootroot00000000000000: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.txt000066400000000000000000000030541435007237400267750ustar00rootroot00000000000000: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.txt000066400000000000000000000047041435007237400242140ustar00rootroot00000000000000: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.txt000066400000000000000000000012101435007237400270520ustar00rootroot00000000000000# 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.txt000066400000000000000000000007421435007237400300520ustar00rootroot00000000000000: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.txt000066400000000000000000000014741435007237400270300ustar00rootroot00000000000000http://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.txt000066400000000000000000000005111435007237400305660ustar00rootroot00000000000000# 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.txt000066400000000000000000000014501435007237400275430ustar00rootroot00000000000000localhost: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.txt000066400000000000000000000016241435007237400312740ustar00rootroot00000000000000http://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.txt000066400000000000000000000014661435007237400261530ustar00rootroot00000000000000a.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.txt000066400000000000000000000015001435007237400263000ustar00rootroot00000000000000example.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.txt000066400000000000000000000022721435007237400270520ustar00rootroot00000000000000(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.txt000066400000000000000000000005101435007237400322550ustar00rootroot00000000000000(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.txt000066400000000000000000000033441435007237400303040ustar00rootroot00000000000000http://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.txt000066400000000000000000000042241435007237400252720ustar00rootroot00000000000000: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.txt000066400000000000000000000013051435007237400256070ustar00rootroot00000000000000: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.txt000066400000000000000000000017611435007237400260130ustar00rootroot00000000000000one.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.txt000066400000000000000000000050151435007237400305420ustar00rootroot00000000000000example.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.txt000066400000000000000000000112301435007237400260050ustar00rootroot00000000000000: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.txt000066400000000000000000000006231435007237400264720ustar00rootroot00000000000000: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.txt000066400000000000000000000004421435007237400262750ustar00rootroot00000000000000: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.txt000066400000000000000000000006571435007237400266130ustar00rootroot00000000000000: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.txt000066400000000000000000000005561435007237400260410ustar00rootroot00000000000000: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.txt000066400000000000000000000011341435007237400264400ustar00rootroot00000000000000: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.txt000066400000000000000000000046771435007237400301570ustar00rootroot00000000000000: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.txt000066400000000000000000000047661435007237400305140ustar00rootroot00000000000000: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.txt000066400000000000000000000023521435007237400272710ustar00rootroot00000000000000: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.txt000066400000000000000000000055011435007237400267520ustar00rootroot00000000000000: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.txt000066400000000000000000000040051435007237400302000ustar00rootroot00000000000000: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.txt000066400000000000000000000042771435007237400312370ustar00rootroot00000000000000: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.txt000066400000000000000000000034111435007237400265510ustar00rootroot00000000000000whoami.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.txt000066400000000000000000000011331435007237400254620ustar00rootroot00000000000000localhost 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.txt000066400000000000000000000026341435007237400257640ustar00rootroot00000000000000: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.txt000066400000000000000000000037001435007237400315020ustar00rootroot00000000000000: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.txt000066400000000000000000000007321435007237400331400ustar00rootroot00000000000000: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.txt000066400000000000000000000015231435007237400305020ustar00rootroot00000000000000: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.txt000066400000000000000000000122171435007237400311270ustar00rootroot00000000000000: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.txt000066400000000000000000000017301435007237400307140ustar00rootroot00000000000000: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.txt000066400000000000000000000022001435007237400314530ustar00rootroot00000000000000# 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.txt000066400000000000000000000017711435007237400303450ustar00rootroot00000000000000: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.txt000066400000000000000000000055011435007237400274470ustar00rootroot00000000000000 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.txt000066400000000000000000000015631435007237400312230ustar00rootroot00000000000000: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.txt000066400000000000000000000032621435007237400321600ustar00rootroot00000000000000: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.txt000066400000000000000000000014141435007237400324120ustar00rootroot00000000000000localhost: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.txt000066400000000000000000000055551435007237400266540ustar00rootroot00000000000000# 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.txt000066400000000000000000000012641435007237400330060ustar00rootroot00000000000000: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.txt000066400000000000000000000051761435007237400310750ustar00rootroot00000000000000*.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.txt000066400000000000000000000013621435007237400272240ustar00rootroot00000000000000: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.txt000066400000000000000000000013301435007237400301260ustar00rootroot00000000000000localhost 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.txt000066400000000000000000000020161435007237400301270ustar00rootroot00000000000000{ 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.txt000066400000000000000000000014211435007237400302060ustar00rootroot00000000000000# 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.txt000066400000000000000000000021331435007237400301300ustar00rootroot00000000000000# 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.txt000066400000000000000000000022511435007237400301320ustar00rootroot00000000000000# 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.txt000066400000000000000000000042031435007237400301320ustar00rootroot00000000000000{ 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.txt000066400000000000000000000012711435007237400301350ustar00rootroot00000000000000a.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.txt000066400000000000000000000026631435007237400301440ustar00rootroot00000000000000# (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.txt000066400000000000000000000015201435007237400301340ustar00rootroot00000000000000# (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.txt000066400000000000000000000024221435007237400301370ustar00rootroot00000000000000# (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.txt000066400000000000000000000015251435007237400301430ustar00rootroot00000000000000# 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.txt000066400000000000000000000014161435007237400344510ustar00rootroot00000000000000{ 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.txt000066400000000000000000000040571435007237400301620ustar00rootroot00000000000000localhost 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.txt000066400000000000000000000062071435007237400305200ustar00rootroot00000000000000localhost 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.txt000066400000000000000000000035511435007237400305450ustar00rootroot00000000000000# 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.txt000066400000000000000000000012501435007237400272260ustar00rootroot00000000000000a.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.txt000066400000000000000000000024411435007237400277330ustar00rootroot00000000000000localhost 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.txt000066400000000000000000000006411435007237400244070ustar00rootroot00000000000000: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.go000066400000000000000000000027121435007237400236060ustar00rootroot00000000000000package 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.go000066400000000000000000000046361435007237400224440ustar00rootroot00000000000000package 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.go000066400000000000000000000010141435007237400221200ustar00rootroot00000000000000package 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.go000066400000000000000000000064351435007237400212740ustar00rootroot00000000000000package 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.go000066400000000000000000000241661435007237400232750ustar00rootroot00000000000000package 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.go000066400000000000000000000133441435007237400213050ustar00rootroot00000000000000package 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.go000066400000000000000000000237301435007237400220070ustar00rootroot00000000000000package 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.config000066400000000000000000000000321435007237400222240ustar00rootroot00000000000000{ admin localhost:2999 } caddy-2.6.2/caddytest/integration/testdata/000077500000000000000000000000001435007237400207325ustar00rootroot00000000000000caddy-2.6.2/caddytest/integration/testdata/cookie.html000066400000000000000000000000631435007237400230700ustar00rootroot00000000000000

Cookie.ClientName {{.Cookie "clientname"}}

caddy-2.6.2/caddytest/integration/testdata/import_respond.txt000066400000000000000000000000511435007237400245330ustar00rootroot00000000000000respond "'I am {args.0}', hears {args.1}"caddy-2.6.2/caddytest/integration/testdata/index.localhost.html000066400000000000000000000000001435007237400247040ustar00rootroot00000000000000caddy-2.6.2/cmd/000077500000000000000000000000001435007237400133555ustar00rootroot00000000000000caddy-2.6.2/cmd/caddy/000077500000000000000000000000001435007237400144415ustar00rootroot00000000000000caddy-2.6.2/cmd/caddy/main.go000066400000000000000000000026761435007237400157270ustar00rootroot00000000000000// 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 main is the entry point of the Caddy application. // Most of Caddy's functionality is provided through modules, // which can be plugged in by adding their import below. // // There is no need to modify the Caddy source code to customize your // builds. You can easily build a custom Caddy with these simple steps: // // 1. Copy this file (main.go) into a new folder // 2. Edit the imports below to include the modules you want plugged in // 3. Run `go mod init caddy` // 4. Run `go install` or `go build` - you now have a custom binary! // // Or you can use xcaddy which does it all for you as a command: // https://github.com/caddyserver/xcaddy package main import ( caddycmd "github.com/caddyserver/caddy/v2/cmd" // plug in Caddy modules here _ "github.com/caddyserver/caddy/v2/modules/standard" ) func main() { caddycmd.Main() } caddy-2.6.2/cmd/caddy/setcap.sh000077500000000000000000000001471435007237400162610ustar00rootroot00000000000000#!/bin/sh # USAGE: go run -exec ./setcap.sh sudo setcap cap_net_bind_service=+ep "$1" "$@" caddy-2.6.2/cmd/cobra.go000066400000000000000000000075461435007237400150060ustar00rootroot00000000000000package caddycmd import ( "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ Use: "caddy", Long: `Caddy is an extensible server platform written in Go. At its core, Caddy merely manages configuration. Modules are plugged in statically at compile-time to provide useful functionality. Caddy's standard distribution includes common modules to serve HTTP, TLS, and PKI applications, including the automation of certificates. To run Caddy, use: - 'caddy run' to run Caddy in the foreground (recommended). - 'caddy start' to start Caddy in the background; only do this if you will be keeping the terminal window open until you run 'caddy stop' to close the server. When Caddy is started, it opens a locally-bound administrative socket to which configuration can be POSTed via a restful HTTP API (see https://caddyserver.com/docs/api). Caddy's native configuration format is JSON. However, config adapters can be used to convert other config formats to JSON when Caddy receives its configuration. The Caddyfile is a built-in config adapter that is popular for hand-written configurations due to its straightforward syntax (see https://caddyserver.com/docs/caddyfile). Many third-party adapters are available (see https://caddyserver.com/docs/config-adapters). Use 'caddy adapt' to see how a config translates to JSON. For convenience, the CLI can act as an HTTP client to give Caddy its initial configuration for you. If a file named Caddyfile is in the current working directory, it will do this automatically. Otherwise, you can use the --config flag to specify the path to a config file. Some special-purpose subcommands build and load a configuration file for you directly from command line input; for example: - caddy file-server - caddy reverse-proxy - caddy respond These commands disable the administration endpoint because their configuration is specified solely on the command line. In general, the most common way to run Caddy is simply: $ caddy run Or, with a configuration file: $ caddy run --config caddy.json If running interactively in a terminal, running Caddy in the background may be more convenient: $ caddy start ... $ caddy stop This allows you to run other commands while Caddy stays running. Be sure to stop Caddy before you close the terminal! Depending on the system, Caddy may need permission to bind to low ports. One way to do this on Linux is to use setcap: $ sudo setcap cap_net_bind_service=+ep $(which caddy) Remember to run that command again after replacing the binary. See the Caddy website for tutorials, configuration structure, syntax, and module documentation: https://caddyserver.com/docs/ Custom Caddy builds are available on the Caddy download page at: https://caddyserver.com/download The xcaddy command can be used to build Caddy from source with or without additional plugins: https://github.com/caddyserver/xcaddy Where possible, Caddy should be installed using officially-supported package installers: https://caddyserver.com/docs/install Instructions for running Caddy in production are also available: https://caddyserver.com/docs/running `, Example: ` $ caddy run $ caddy run --config caddy.json $ caddy reload --config caddy.json $ caddy stop`, // kind of annoying to have all the help text printed out if // caddy has an error provisioning its modules, for instance... SilenceUsage: true, } const fullDocsFooter = `Full documentation is available at: https://caddyserver.com/docs/command-line` func init() { rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter) } func caddyCmdToCoral(caddyCmd Command) *cobra.Command { cmd := &cobra.Command{ Use: caddyCmd.Name, Short: caddyCmd.Short, Long: caddyCmd.Long, RunE: func(cmd *cobra.Command, _ []string) error { fls := cmd.Flags() _, err := caddyCmd.Func(Flags{fls}) return err }, } cmd.Flags().AddGoFlagSet(caddyCmd.Flags) return cmd } caddy-2.6.2/cmd/commandfuncs.go000066400000000000000000000524771435007237400164000ustar00rootroot00000000000000// 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 caddycmd import ( "bytes" "context" "crypto/rand" "encoding/json" "errors" "fmt" "io" "log" "net" "net/http" "os" "os/exec" "runtime" "runtime/debug" "strings" "github.com/aryann/difflib" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "go.uber.org/zap" ) func cmdStart(fl Flags) (int, error) { startCmdConfigFlag := fl.String("config") startCmdConfigAdapterFlag := fl.String("adapter") startCmdPidfileFlag := fl.String("pidfile") startCmdWatchFlag := fl.Bool("watch") startCmdEnvfileFlag := fl.String("envfile") // open a listener to which the child process will connect when // it is ready to confirm that it has successfully started ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("opening listener for success confirmation: %v", err) } defer ln.Close() // craft the command with a pingback address and with a // pipe for its stdin, so we can tell it our confirmation // code that we expect so that some random port scan at // the most unfortunate time won't fool us into thinking // the child succeeded (i.e. the alternative is to just // wait for any connection on our listener, but better to // ensure it's the process we're expecting - we can be // sure by giving it some random bytes and having it echo // them back to us) cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) if startCmdConfigFlag != "" { cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag) } if startCmdEnvfileFlag != "" { cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag) } if startCmdConfigAdapterFlag != "" { cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag) } if startCmdWatchFlag { cmd.Args = append(cmd.Args, "--watch") } if startCmdPidfileFlag != "" { cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag) } stdinpipe, err := cmd.StdinPipe() if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("creating stdin pipe: %v", err) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr // generate the random bytes we'll send to the child process expect := make([]byte, 32) _, err = rand.Read(expect) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err) } // begin writing the confirmation bytes to the child's // stdin; use a goroutine since the child hasn't been // started yet, and writing synchronously would result // in a deadlock go func() { _, _ = stdinpipe.Write(expect) stdinpipe.Close() }() // start the process err = cmd.Start() if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err) } // there are two ways we know we're done: either // the process will connect to our listener, or // it will exit with an error success, exit := make(chan struct{}), make(chan error) // in one goroutine, we await the success of the child process go func() { for { conn, err := ln.Accept() if err != nil { if !errors.Is(err, net.ErrClosed) { log.Println(err) } break } err = handlePingbackConn(conn, expect) if err == nil { close(success) break } log.Println(err) } }() // in another goroutine, we await the failure of the child process go func() { err := cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks exit <- err // sending on separate line ensures select won't trigger until after Wait unblocks }() // when one of the goroutines unblocks, we're done and can exit select { case <-success: fmt.Printf("Successfully started Caddy (pid=%d) - Caddy is running in the background\n", cmd.Process.Pid) case err := <-exit: return caddy.ExitCodeFailedStartup, fmt.Errorf("caddy process exited with error: %v", err) } return caddy.ExitCodeSuccess, nil } func cmdRun(fl Flags) (int, error) { caddy.TrapSignals() runCmdConfigFlag := fl.String("config") runCmdConfigAdapterFlag := fl.String("adapter") runCmdResumeFlag := fl.Bool("resume") runCmdLoadEnvfileFlag := fl.String("envfile") runCmdPrintEnvFlag := fl.Bool("environ") runCmdWatchFlag := fl.Bool("watch") runCmdPidfileFlag := fl.String("pidfile") runCmdPingbackFlag := fl.String("pingback") // load all additional envs as soon as possible if runCmdLoadEnvfileFlag != "" { if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("loading additional environment variables: %v", err) } } // if we are supposed to print the environment, do that first if runCmdPrintEnvFlag { printEnvironment() } // load the config, depending on flags var config []byte var err error if runCmdResumeFlag { config, err = os.ReadFile(caddy.ConfigAutosavePath) if os.IsNotExist(err) { // not a bad error; just can't resume if autosave file doesn't exist caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath)) runCmdResumeFlag = false } else if err != nil { return caddy.ExitCodeFailedStartup, err } else { if runCmdConfigFlag == "" { caddy.Log().Info("resuming from last configuration", zap.String("autosave_file", caddy.ConfigAutosavePath)) } else { // if they also specified a config file, user should be aware that we're not // using it (doing so could lead to data/config loss by overwriting!) caddy.Log().Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration", zap.String("autosave_file", caddy.ConfigAutosavePath)) } } } // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive var configFile string if !runCmdResumeFlag { config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } } // run the initial config err = caddy.Load(config, true) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err) } caddy.Log().Info("serving initial configuration") // if we are to report to another process the successful start // of the server, do so now by echoing back contents of stdin if runCmdPingbackFlag != "" { confirmationBytes, err := io.ReadAll(os.Stdin) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("reading confirmation bytes from stdin: %v", err) } conn, err := net.Dial("tcp", runCmdPingbackFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("dialing confirmation address: %v", err) } defer conn.Close() _, err = conn.Write(confirmationBytes) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err) } } // if enabled, reload config file automatically on changes // (this better only be used in dev!) if runCmdWatchFlag { go watchConfigFile(configFile, runCmdConfigAdapterFlag) } // create pidfile if runCmdPidfileFlag != "" { err := caddy.PIDFile(runCmdPidfileFlag) if err != nil { caddy.Log().Error("unable to write PID file", zap.String("pidfile", runCmdPidfileFlag), zap.Error(err)) } } // warn if the environment does not provide enough information about the disk hasXDG := os.Getenv("XDG_DATA_HOME") != "" && os.Getenv("XDG_CONFIG_HOME") != "" && os.Getenv("XDG_CACHE_HOME") != "" switch runtime.GOOS { case "windows": if os.Getenv("HOME") == "" && os.Getenv("USERPROFILE") == "" && !hasXDG { caddy.Log().Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy") } case "plan9": if os.Getenv("home") == "" && !hasXDG { caddy.Log().Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy") } default: if os.Getenv("HOME") == "" && !hasXDG { caddy.Log().Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy") } } select {} } func cmdStop(fl Flags) (int, error) { addrFlag := fl.String("address") configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/stop", nil, nil) if err != nil { caddy.Log().Warn("failed using API to stop instance", zap.Error(err)) return caddy.ExitCodeFailedStartup, err } defer resp.Body.Close() return caddy.ExitCodeSuccess, nil } func cmdReload(fl Flags) (int, error) { configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") addrFlag := fl.String("address") forceFlag := fl.Bool("force") // get the config in caddy's native format config, configFile, err := LoadConfig(configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } if configFile == "" { return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load") } adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } // optionally force a config reload headers := make(http.Header) if forceFlag { headers.Set("Cache-Control", "must-revalidate") } resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config)) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err) } defer resp.Body.Close() return caddy.ExitCodeSuccess, nil } func cmdVersion(_ Flags) (int, error) { _, full := caddy.Version() fmt.Println(full) return caddy.ExitCodeSuccess, nil } func cmdBuildInfo(_ Flags) (int, error) { bi, ok := debug.ReadBuildInfo() if !ok { return caddy.ExitCodeFailedStartup, fmt.Errorf("no build information") } fmt.Println(bi) return caddy.ExitCodeSuccess, nil } func cmdListModules(fl Flags) (int, error) { packages := fl.Bool("packages") versions := fl.Bool("versions") skipStandard := fl.Bool("skip-standard") printModuleInfo := func(mi moduleInfo) { fmt.Print(mi.caddyModuleID) if versions && mi.goModule != nil { fmt.Print(" " + mi.goModule.Version) } if packages && mi.goModule != nil { fmt.Print(" " + mi.goModule.Path) if mi.goModule.Replace != nil { fmt.Print(" => " + mi.goModule.Replace.Path) } } if mi.err != nil { fmt.Printf(" [%v]", mi.err) } fmt.Println() } // organize modules by whether they come with the standard distribution standard, nonstandard, unknown, err := getModules() if err != nil { // oh well, just print the module IDs and exit for _, m := range caddy.Modules() { fmt.Println(m) } return caddy.ExitCodeSuccess, nil } // Standard modules (always shipped with Caddy) if !skipStandard { if len(standard) > 0 { for _, mod := range standard { printModuleInfo(mod) } } fmt.Printf("\n Standard modules: %d\n", len(standard)) } // Non-standard modules (third party plugins) if len(nonstandard) > 0 { if len(standard) > 0 && !skipStandard { fmt.Println() } for _, mod := range nonstandard { printModuleInfo(mod) } } fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard)) // Unknown modules (couldn't get Caddy module info) if len(unknown) > 0 { if (len(standard) > 0 && !skipStandard) || len(nonstandard) > 0 { fmt.Println() } for _, mod := range unknown { printModuleInfo(mod) } } fmt.Printf("\n Unknown modules: %d\n", len(unknown)) return caddy.ExitCodeSuccess, nil } func cmdEnviron(_ Flags) (int, error) { printEnvironment() return caddy.ExitCodeSuccess, nil } func cmdAdaptConfig(fl Flags) (int, error) { adaptCmdInputFlag := fl.String("config") adaptCmdAdapterFlag := fl.String("adapter") adaptCmdPrettyFlag := fl.Bool("pretty") adaptCmdValidateFlag := fl.Bool("validate") // if no input file was specified, try a default // Caddyfile if the Caddyfile adapter is plugged in if adaptCmdInputFlag == "" && caddyconfig.GetAdapter("caddyfile") != nil { _, err := os.Stat("Caddyfile") if err == nil { // default Caddyfile exists adaptCmdInputFlag = "Caddyfile" caddy.Log().Info("using adjacent Caddyfile") } else if !os.IsNotExist(err) { // default Caddyfile exists, but error accessing it return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing default Caddyfile: %v", err) } } if adaptCmdInputFlag == "" { return caddy.ExitCodeFailedStartup, fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)") } if adaptCmdAdapterFlag == "" { return caddy.ExitCodeFailedStartup, fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)") } cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag) if cfgAdapter == nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag) } input, err := os.ReadFile(adaptCmdInputFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("reading input file: %v", err) } opts := map[string]any{"filename": adaptCmdInputFlag} adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts) if err != nil { return caddy.ExitCodeFailedStartup, err } if adaptCmdPrettyFlag { var prettyBuf bytes.Buffer err = json.Indent(&prettyBuf, adaptedConfig, "", "\t") if err != nil { return caddy.ExitCodeFailedStartup, err } adaptedConfig = prettyBuf.Bytes() } // print result to stdout fmt.Println(string(adaptedConfig)) // print warnings to stderr for _, warn := range warnings { msg := warn.Message if warn.Directive != "" { msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) } caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg, zap.String("file", warn.File), zap.Int("line", warn.Line)) } // validate output if requested if adaptCmdValidateFlag { var cfg *caddy.Config err = json.Unmarshal(adaptedConfig, &cfg) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err) } err = caddy.Validate(cfg) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("validation: %v", err) } } return caddy.ExitCodeSuccess, nil } func cmdValidateConfig(fl Flags) (int, error) { validateCmdConfigFlag := fl.String("config") validateCmdAdapterFlag := fl.String("adapter") input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } input = caddy.RemoveMetaFields(input) var cfg *caddy.Config err = json.Unmarshal(input, &cfg) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err) } err = caddy.Validate(cfg) if err != nil { return caddy.ExitCodeFailedStartup, err } fmt.Println("Valid configuration") return caddy.ExitCodeSuccess, nil } func cmdFmt(fl Flags) (int, error) { formatCmdConfigFile := fl.Arg(0) if formatCmdConfigFile == "" { formatCmdConfigFile = "Caddyfile" } // as a special case, read from stdin if the file name is "-" if formatCmdConfigFile == "-" { input, err := io.ReadAll(os.Stdin) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("reading stdin: %v", err) } fmt.Print(string(caddyfile.Format(input))) return caddy.ExitCodeSuccess, nil } input, err := os.ReadFile(formatCmdConfigFile) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("reading input file: %v", err) } output := caddyfile.Format(input) if fl.Bool("overwrite") { if err := os.WriteFile(formatCmdConfigFile, output, 0600); err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err) } } else if fl.Bool("diff") { diff := difflib.Diff( strings.Split(string(input), "\n"), strings.Split(string(output), "\n")) 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) } } } else { fmt.Print(string(output)) } return caddy.ExitCodeSuccess, nil } // AdminAPIRequest makes an API request according to the CLI flags given, // with the given HTTP method and request URI. If body is non-nil, it will // be assumed to be Content-Type application/json. The caller should close // the response body. Should only be used by Caddy CLI commands which // need to interact with a running instance of Caddy via the admin API. func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) { parsedAddr, err := caddy.ParseNetworkAddress(adminAddr) if err != nil || parsedAddr.PortRangeSize() > 1 { return nil, fmt.Errorf("invalid admin address %s: %v", adminAddr, err) } origin := "http://" + parsedAddr.JoinHostPort(0) if parsedAddr.IsUnixNetwork() { origin = "http://unixsocket" // hack so that http.NewRequest() is happy } // form the request req, err := http.NewRequest(method, origin+uri, body) if err != nil { return nil, fmt.Errorf("making request: %v", err) } if parsedAddr.IsUnixNetwork() { // When listening on a unix socket, the admin endpoint doesn't // accept any Host header because there is no host:port for // a unix socket's address. The server's host check is fairly // strict for security reasons, so we don't allow just any // Host header. For unix sockets, the Host header must be // empty. Unfortunately, Go makes it impossible to make HTTP // requests with an empty Host header... except with this one // weird trick. (Hopefully they don't fix it. It's already // hard enough to use HTTP over unix sockets.) // // An equivalent curl command would be something like: // $ curl --unix-socket caddy.sock http:/:$REQUEST_URI req.URL.Host = " " req.Host = "" } else { req.Header.Set("Origin", origin) } if body != nil { req.Header.Set("Content-Type", "application/json") } for k, v := range headers { req.Header[k] = v } // make an HTTP client that dials our network type, since admin // endpoints aren't always TCP, which is what the default transport // expects; reuse is not of particular concern here client := http.Client{ Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial(parsedAddr.Network, parsedAddr.JoinHostPort(0)) }, }, } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("performing request: %v", err) } // if it didn't work, let the user know if resp.StatusCode >= 400 { respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10)) if err != nil { return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err) } return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody) } return resp, nil } // DetermineAdminAPIAddress determines which admin API endpoint address should // be used based on the inputs. By priority: if `address` is specified, then // it is returned; if `config` is specified, then that config will be used for // finding the admin address; if `configFile` (and `configAdapter`) are specified, // then that config will be loaded to find the admin address; otherwise, the // default admin listen address will be returned. func DetermineAdminAPIAddress(address string, config []byte, configFile, configAdapter string) (string, error) { // Prefer the address if specified and non-empty if address != "" { return address, nil } // Try to load the config from file if specified, with the given adapter name if configFile != "" { var loadedConfigFile string var err error // use the provided loaded config if non-empty // otherwise, load it from the specified file/adapter loadedConfig := config if len(loadedConfig) == 0 { // get the config in caddy's native format loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter) if err != nil { return "", err } if loadedConfigFile == "" { return "", fmt.Errorf("no config file to load; either use --config flag or ensure Caddyfile exists in current directory") } } // get the address of the admin listener from the config if len(loadedConfig) > 0 { var tmpStruct struct { Admin caddy.AdminConfig `json:"admin"` } err := json.Unmarshal(loadedConfig, &tmpStruct) if err != nil { return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err) } if tmpStruct.Admin.Listen != "" { return tmpStruct.Admin.Listen, nil } } } // Fallback to the default listen address otherwise return caddy.DefaultAdminListen, nil } type moduleInfo struct { caddyModuleID string goModule *debug.Module err error } caddy-2.6.2/cmd/commands.go000066400000000000000000000436161435007237400155170ustar00rootroot00000000000000// 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 caddycmd import ( "flag" "fmt" "os" "regexp" "strings" "github.com/caddyserver/caddy/v2" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" ) // Command represents a subcommand. Name, Func, // and Short are required. type Command struct { // The name of the subcommand. Must conform to the // format described by the RegisterCommand() godoc. // Required. Name string // Func is a function that executes a subcommand using // the parsed flags. It returns an exit code and any // associated error. // Required. Func CommandFunc // Usage is a brief message describing the syntax of // the subcommand's flags and args. Use [] to indicate // optional parameters and <> to enclose literal values // intended to be replaced by the user. Do not prefix // the string with "caddy" or the name of the command // since these will be prepended for you; only include // the actual parameters for this command. Usage string // Short is a one-line message explaining what the // command does. Should not end with punctuation. // Required. Short string // Long is the full help text shown to the user. // Will be trimmed of whitespace on both ends before // being printed. Long string // Flags is the flagset for command. Flags *flag.FlagSet } // CommandFunc is a command's function. It runs the // command and returns the proper exit code along with // any error that occurred. type CommandFunc func(Flags) (int, error) // Commands returns a list of commands initialised by // RegisterCommand func Commands() map[string]Command { return commands } var commands = make(map[string]Command) func init() { RegisterCommand(Command{ Name: "start", Func: cmdStart, Usage: "[--config [--adapter ]] [--envfile ] [--watch] [--pidfile ]", Short: "Starts the Caddy process in the background and then returns", Long: ` Starts the Caddy process, optionally bootstrapped with an initial config file. This command unblocks after the server starts running or fails to run. If --envfile is specified, an environment file with environment variables in the KEY=VALUE format will be loaded into the Caddy process. On Windows, the spawned child process will remain attached to the terminal, so closing the window will forcefully stop Caddy; to avoid forgetting this, try using 'caddy run' instead to keep it in the foreground.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("start", flag.ExitOnError) fs.String("config", "", "Configuration file") fs.String("envfile", "", "Environment file to load") fs.String("adapter", "", "Name of config adapter to apply") fs.String("pidfile", "", "Path of file to which to write process ID") fs.Bool("watch", false, "Reload changed config file automatically") return fs }(), }) RegisterCommand(Command{ Name: "run", Func: cmdRun, Usage: "[--config [--adapter ]] [--envfile ] [--environ] [--resume] [--watch] [--pidfile ]", Short: `Starts the Caddy process and blocks indefinitely`, Long: ` Starts the Caddy process, optionally bootstrapped with an initial config file, and blocks indefinitely until the server is stopped; i.e. runs Caddy in "daemon" mode (foreground). If a config file is specified, it will be applied immediately after the process is running. If the config file is not in Caddy's native JSON format, you can specify an adapter with --adapter to adapt the given config file to Caddy's native format. The config adapter must be a registered module. Any warnings will be printed to the log, but beware that any adaptation without errors will immediately be used. If you want to review the results of the adaptation first, use the 'adapt' subcommand. As a special case, if the current working directory has a file called "Caddyfile" and the caddyfile config adapter is plugged in (default), then that file will be loaded and used to configure Caddy, even without any command line flags. If --envfile is specified, an environment file with environment variables in the KEY=VALUE format will be loaded into the Caddy process. If --environ is specified, the environment as seen by the Caddy process will be printed before starting. This is the same as the environ command but does not quit after printing, and can be useful for troubleshooting. The --resume flag will override the --config flag if there is a config auto- save file. It is not an error if --resume is used and no autosave file exists. If --watch is specified, the config file will be loaded automatically after changes. ⚠️ This can make unintentional config changes easier; only use this option in a local development environment.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("run", flag.ExitOnError) fs.String("config", "", "Configuration file") fs.String("adapter", "", "Name of config adapter to apply") fs.String("envfile", "", "Environment file to load") fs.Bool("environ", false, "Print environment") fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)") fs.Bool("watch", false, "Watch config file for changes and reload it automatically") fs.String("pidfile", "", "Path of file to which to write process ID") fs.String("pingback", "", "Echo confirmation bytes to this address on success") return fs }(), }) RegisterCommand(Command{ Name: "stop", Func: cmdStop, Usage: "[--address ] [--config [--adapter ]]", Short: "Gracefully stops a started Caddy process", Long: ` Stops the background Caddy process as gracefully as possible. It requires that the admin API is enabled and accessible, since it will use the API's /stop endpoint. The address of this request can be customized using the --address flag, or from the given --config, if not the default.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("stop", flag.ExitOnError) fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default") fs.String("config", "", "Configuration file to use to parse the admin address, if --address is not used") fs.String("adapter", "", "Name of config adapter to apply (when --config is used)") return fs }(), }) RegisterCommand(Command{ Name: "reload", Func: cmdReload, Usage: "--config [--adapter ] [--address ]", Short: "Changes the config of the running Caddy instance", Long: ` Gives the running Caddy instance a new configuration. This has the same effect as POSTing a document to the /load API endpoint, but is convenient for simple workflows revolving around config files. Since the admin endpoint is configurable, the endpoint configuration is loaded from the --address flag if specified; otherwise it is loaded from the given config file; otherwise the default is assumed.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("reload", flag.ExitOnError) fs.String("config", "", "Configuration file (required)") fs.String("adapter", "", "Name of config adapter to apply") fs.String("address", "", "Address of the administration listener, if different from config") fs.Bool("force", false, "Force config reload, even if it is the same") return fs }(), }) RegisterCommand(Command{ Name: "version", Func: cmdVersion, Short: "Prints the version", Long: ` Prints the version of this Caddy binary. Version information must be embedded into the binary at compile-time in order for Caddy to display anything useful with this command. If Caddy is built from within a version control repository, the Go command will embed the revision hash if available. However, if Caddy is built in the way specified by our online documentation (or by using xcaddy), more detailed version information is printed as given by Go modules. For more details about the full version string, see the Go module documentation: https://go.dev/doc/modules/version-numbers `, }) RegisterCommand(Command{ Name: "list-modules", Func: cmdListModules, Usage: "[--packages] [--versions]", Short: "Lists the installed Caddy modules", Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("list-modules", flag.ExitOnError) fs.Bool("packages", false, "Print package paths") fs.Bool("versions", false, "Print version information") fs.Bool("skip-standard", false, "Skip printing standard modules") return fs }(), }) RegisterCommand(Command{ Name: "build-info", Func: cmdBuildInfo, Short: "Prints information about this build", }) RegisterCommand(Command{ Name: "environ", Func: cmdEnviron, Short: "Prints the environment", Long: ` Prints the environment as seen by this Caddy process. The environment includes variables set in the system. If your Caddy configuration uses environment variables (e.g. "{env.VARIABLE}") then this command can be useful for verifying that the variables will have the values you expect in your config. Note that environments may be different depending on how you run Caddy. Environments for Caddy instances started by service managers such as systemd are often different than the environment inherited from your shell or terminal. You can also print the environment the same time you use "caddy run" by adding the "--environ" flag. Environments may contain sensitive data. `, }) RegisterCommand(Command{ Name: "adapt", Func: cmdAdaptConfig, Usage: "--config [--adapter ] [--pretty] [--validate]", Short: "Adapts a configuration to Caddy's native JSON", Long: ` Adapts a configuration to Caddy's native JSON format and writes the output to stdout, along with any warnings to stderr. If --pretty is specified, the output will be formatted with indentation for human readability. If --validate is used, the adapted config will be checked for validity. If the config is invalid, an error will be printed to stderr and a non- zero exit status will be returned.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("adapt", flag.ExitOnError) fs.String("config", "", "Configuration file to adapt (required)") fs.String("adapter", "caddyfile", "Name of config adapter") fs.Bool("pretty", false, "Format the output for human readability") fs.Bool("validate", false, "Validate the output") return fs }(), }) RegisterCommand(Command{ Name: "validate", Func: cmdValidateConfig, Usage: "--config [--adapter ]", Short: "Tests whether a configuration file is valid", Long: ` Loads and provisions the provided config, but does not start running it. This reveals any errors with the configuration through the loading and provisioning stages.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("validate", flag.ExitOnError) fs.String("config", "", "Input configuration file") fs.String("adapter", "", "Name of config adapter") return fs }(), }) RegisterCommand(Command{ Name: "fmt", Func: cmdFmt, Usage: "[--overwrite] []", Short: "Formats a Caddyfile", Long: ` Formats the Caddyfile by adding proper indentation and spaces to improve human readability. It prints the result to stdout. If --overwrite is specified, the output will be written to the config file directly instead of printing it. If --diff is specified, the output will be compared against the input, and lines will be prefixed with '-' and '+' where they differ. Note that unchanged lines are prefixed with two spaces for alignment, and that this is not a valid patch format. If you wish you use stdin instead of a regular file, use - as the path. When reading from stdin, the --overwrite flag has no effect: the result is always printed to stdout.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("fmt", flag.ExitOnError) fs.Bool("overwrite", false, "Overwrite the input file with the results") fs.Bool("diff", false, "Print the differences between the input file and the formatted output") return fs }(), }) RegisterCommand(Command{ Name: "upgrade", Func: cmdUpgrade, Short: "Upgrade Caddy (EXPERIMENTAL)", Long: ` Downloads an updated Caddy binary with the same modules/plugins at the latest versions. EXPERIMENTAL: May be changed or removed.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("upgrade", flag.ExitOnError) fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it") return fs }(), }) RegisterCommand(Command{ Name: "add-package", Func: cmdAddPackage, Usage: "", Short: "Adds Caddy packages (EXPERIMENTAL)", Long: ` Downloads an updated Caddy binary with the specified packages (module/plugin) added. Retains existing packages. Returns an error if the any of packages are already included. EXPERIMENTAL: May be changed or removed. `, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("add-package", flag.ExitOnError) fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it") return fs }(), }) RegisterCommand(Command{ Name: "remove-package", Func: cmdRemovePackage, Usage: "", Short: "Removes Caddy packages (EXPERIMENTAL)", Long: ` Downloads an updated Caddy binaries without the specified packages (module/plugin). Returns an error if any of the packages are not included. EXPERIMENTAL: May be changed or removed. `, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("remove-package", flag.ExitOnError) fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it") return fs }(), }) RegisterCommand(Command{ Name: "manpage", Func: func(fl Flags) (int, error) { dir := strings.TrimSpace(fl.String("directory")) if dir == "" { return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required") } if err := os.MkdirAll(dir, 0755); err != nil { return caddy.ExitCodeFailedQuit, err } if err := doc.GenManTree(rootCmd, &doc.GenManHeader{ Title: "Caddy", Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections }, dir); err != nil { return caddy.ExitCodeFailedQuit, err } return caddy.ExitCodeSuccess, nil }, Usage: "--directory ", Short: "Generates the manual pages for Caddy commands", Long: ` Generates the manual pages for Caddy commands into the designated directory tagged into section 8 (System Administration). The manual page files are generated into the directory specified by the argument of --directory. If the directory does not exist, it will be created. `, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("manpage", flag.ExitOnError) fs.String("directory", "", "The output directory where the manpages are generated") return fs }(), }) // source: https://github.com/spf13/cobra/blob/main/shell_completions.md rootCmd.AddCommand(&cobra.Command{ Use: "completion [bash|zsh|fish|powershell]", Short: "Generate completion script", Long: fmt.Sprintf(`To load completions: Bash: $ source <(%[1]s completion bash) # To load completions for each session, execute once: # Linux: $ %[1]s completion bash > /etc/bash_completion.d/%[1]s # macOS: $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s Zsh: # If shell completion is not already enabled in your environment, # you will need to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc # To load completions for each session, execute once: $ %[1]s completion zsh > "${fpath[1]}/_%[1]s" # You will need to start a new shell for this setup to take effect. fish: $ %[1]s completion fish | source # To load completions for each session, execute once: $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish PowerShell: PS> %[1]s completion powershell | Out-String | Invoke-Expression # To load completions for every new session, run: PS> %[1]s completion powershell > %[1]s.ps1 # and source this file from your PowerShell profile. `, rootCmd.Root().Name()), DisableFlagsInUseLine: true, ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, Args: cobra.ExactValidArgs(1), RunE: func(cmd *cobra.Command, args []string) error { switch args[0] { case "bash": return cmd.Root().GenBashCompletion(os.Stdout) case "zsh": return cmd.Root().GenZshCompletion(os.Stdout) case "fish": return cmd.Root().GenFishCompletion(os.Stdout, true) case "powershell": return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) default: return fmt.Errorf("unrecognized shell: %s", args[0]) } }, }) } // RegisterCommand registers the command cmd. // cmd.Name must be unique and conform to the // following format: // // - lowercase // - alphanumeric and hyphen characters only // - cannot start or end with a hyphen // - hyphen cannot be adjacent to another hyphen // // This function panics if the name is already registered, // if the name does not meet the described format, or if // any of the fields are missing from cmd. // // This function should be used in init(). func RegisterCommand(cmd Command) { if cmd.Name == "" { panic("command name is required") } if cmd.Func == nil { panic("command function missing") } if cmd.Short == "" { panic("command short string is required") } if _, exists := commands[cmd.Name]; exists { panic("command already registered: " + cmd.Name) } if !commandNameRegex.MatchString(cmd.Name) { panic("invalid command name") } rootCmd.AddCommand(caddyCmdToCoral(cmd)) } var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`) caddy-2.6.2/cmd/main.go000066400000000000000000000311061435007237400146310ustar00rootroot00000000000000// 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 caddycmd import ( "bufio" "bytes" "flag" "fmt" "io" "log" "net" "os" "path/filepath" "runtime" "runtime/debug" "strconv" "strings" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/certmagic" "github.com/spf13/pflag" "go.uber.org/zap" ) func init() { // set a fitting User-Agent for ACME requests version, _ := caddy.Version() cleanModVersion := strings.TrimPrefix(version, "v") ua := "Caddy/" + cleanModVersion if uaEnv, ok := os.LookupEnv("USERAGENT"); ok { ua = uaEnv + " " + ua } certmagic.UserAgent = ua // by using Caddy, user indicates agreement to CA terms // (very important, as Caddy is often non-interactive // and thus ACME account creation will fail!) certmagic.DefaultACME.Agreed = true } // Main implements the main function of the caddy command. // Call this if Caddy is to be the main() of your program. func Main() { if len(os.Args) == 0 { fmt.Printf("[FATAL] no arguments provided by OS; args[0] must be command\n") os.Exit(caddy.ExitCodeFailedStartup) } if err := rootCmd.Execute(); err != nil { os.Exit(1) } } // handlePingbackConn reads from conn and ensures it matches // the bytes in expect, or returns an error if it doesn't. func handlePingbackConn(conn net.Conn, expect []byte) error { defer conn.Close() confirmationBytes, err := io.ReadAll(io.LimitReader(conn, 32)) if err != nil { return err } if !bytes.Equal(confirmationBytes, expect) { return fmt.Errorf("wrong confirmation: %x", confirmationBytes) } return nil } // LoadConfig loads the config from configFile and adapts it // using adapterName. If adapterName is specified, configFile // must be also. If no configFile is specified, it tries // loading a default config file. The lack of a config file is // not treated as an error, but false will be returned if // there is no config available. It prints any warnings to stderr, // and returns the resulting JSON config bytes along with // the name of the loaded config file (if any). func LoadConfig(configFile, adapterName string) ([]byte, string, error) { // specifying an adapter without a config file is ambiguous if adapterName != "" && configFile == "" { return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)") } // load initial config and adapter var config []byte var cfgAdapter caddyconfig.Adapter var err error if configFile != "" { if configFile == "-" { config, err = io.ReadAll(os.Stdin) } else { config, err = os.ReadFile(configFile) } if err != nil { return nil, "", fmt.Errorf("reading config file: %v", err) } caddy.Log().Info("using provided configuration", zap.String("config_file", configFile), zap.String("config_adapter", adapterName)) } else if adapterName == "" { // as a special case when no config file or adapter // is specified, see if the Caddyfile adapter is // plugged in, and if so, try using a default Caddyfile cfgAdapter = caddyconfig.GetAdapter("caddyfile") if cfgAdapter != nil { config, err = os.ReadFile("Caddyfile") if os.IsNotExist(err) { // okay, no default Caddyfile; pretend like this never happened cfgAdapter = nil } else if err != nil { // default Caddyfile exists, but error reading it return nil, "", fmt.Errorf("reading default Caddyfile: %v", err) } else { // success reading default Caddyfile configFile = "Caddyfile" caddy.Log().Info("using adjacent Caddyfile") } } } // as a special case, if a config file called "Caddyfile" was // specified, and no adapter is specified, assume caddyfile adapter // for convenience if strings.HasPrefix(filepath.Base(configFile), "Caddyfile") && filepath.Ext(configFile) != ".json" && adapterName == "" { adapterName = "caddyfile" } // load config adapter if adapterName != "" { cfgAdapter = caddyconfig.GetAdapter(adapterName) if cfgAdapter == nil { return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName) } } // adapt config if cfgAdapter != nil { adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]any{ "filename": configFile, }) if err != nil { return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err) } for _, warn := range warnings { msg := warn.Message if warn.Directive != "" { msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) } caddy.Log().Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line)) } config = adaptedConfig } return config, configFile, nil } // watchConfigFile watches the config file at filename for changes // and reloads the config if the file was updated. This function // blocks indefinitely; it only quits if the poller has errors for // long enough time. The filename passed in must be the actual // config file used, not one to be discovered. func watchConfigFile(filename, adapterName string) { defer func() { if err := recover(); err != nil { log.Printf("[PANIC] watching config file: %v\n%s", err, debug.Stack()) } }() // make our logger; since config reloads can change the // default logger, we need to get it dynamically each time logger := func() *zap.Logger { return caddy.Log(). Named("watcher"). With(zap.String("config_file", filename)) } // get the initial timestamp on the config file info, err := os.Stat(filename) if err != nil { logger().Error("cannot watch config file", zap.Error(err)) return } lastModified := info.ModTime() logger().Info("watching config file for changes") // if the file disappears or something, we can // stop polling if the error lasts long enough var lastErr time.Time finalError := func(err error) bool { if lastErr.IsZero() { lastErr = time.Now() return false } if time.Since(lastErr) > 30*time.Second { logger().Error("giving up watching config file; too many errors", zap.Error(err)) return true } return false } // begin poller //nolint:staticcheck for range time.Tick(1 * time.Second) { // get the file info info, err := os.Stat(filename) if err != nil { if finalError(err) { return } continue } lastErr = time.Time{} // no error, so clear any memory of one // if it hasn't changed, nothing to do if !info.ModTime().After(lastModified) { continue } logger().Info("config file changed; reloading") // remember this timestamp lastModified = info.ModTime() // load the contents of the file config, _, err := LoadConfig(filename, adapterName) if err != nil { logger().Error("unable to load latest config", zap.Error(err)) continue } // apply the updated config err = caddy.Load(config, false) if err != nil { logger().Error("applying latest config", zap.Error(err)) continue } } } // Flags wraps a FlagSet so that typed values // from flags can be easily retrieved. type Flags struct { *pflag.FlagSet } // String returns the string representation of the // flag given by name. It panics if the flag is not // in the flag set. func (f Flags) String(name string) string { return f.FlagSet.Lookup(name).Value.String() } // Bool returns the boolean representation of the // flag given by name. It returns false if the flag // is not a boolean type. It panics if the flag is // not in the flag set. func (f Flags) Bool(name string) bool { val, _ := strconv.ParseBool(f.String(name)) return val } // Int returns the integer representation of the // flag given by name. It returns 0 if the flag // is not an integer type. It panics if the flag is // not in the flag set. func (f Flags) Int(name string) int { val, _ := strconv.ParseInt(f.String(name), 0, strconv.IntSize) return int(val) } // Float64 returns the float64 representation of the // flag given by name. It returns false if the flag // is not a float64 type. It panics if the flag is // not in the flag set. func (f Flags) Float64(name string) float64 { val, _ := strconv.ParseFloat(f.String(name), 64) return val } // Duration returns the duration representation of the // flag given by name. It returns false if the flag // is not a duration type. It panics if the flag is // not in the flag set. func (f Flags) Duration(name string) time.Duration { val, _ := caddy.ParseDuration(f.String(name)) return val } func loadEnvFromFile(envFile string) error { file, err := os.Open(envFile) if err != nil { return fmt.Errorf("reading environment file: %v", err) } defer file.Close() envMap, err := parseEnvFile(file) if err != nil { return fmt.Errorf("parsing environment file: %v", err) } for k, v := range envMap { if err := os.Setenv(k, v); err != nil { return fmt.Errorf("setting environment variables: %v", err) } } // Update the storage paths to ensure they have the proper // value after loading a specified env file. caddy.ConfigAutosavePath = filepath.Join(caddy.AppConfigDir(), "autosave.json") caddy.DefaultStorage = &certmagic.FileStorage{Path: caddy.AppDataDir()} return nil } // parseEnvFile parses an env file from KEY=VALUE format. // It's pretty naive. Limited value quotation is supported, // but variable and command expansions are not supported. func parseEnvFile(envInput io.Reader) (map[string]string, error) { envMap := make(map[string]string) scanner := bufio.NewScanner(envInput) var lineNumber int for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) lineNumber++ // skip empty lines and lines starting with comment if line == "" || strings.HasPrefix(line, "#") { continue } // split line into key and value before, after, isCut := strings.Cut(line, "=") if !isCut { return nil, fmt.Errorf("can't parse line %d; line should be in KEY=VALUE format", lineNumber) } key, val := before, after // sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here key = strings.TrimPrefix(key, "export ") // validate key and value if key == "" { return nil, fmt.Errorf("missing or empty key on line %d", lineNumber) } if strings.Contains(key, " ") { return nil, fmt.Errorf("invalid key on line %d: contains whitespace: %s", lineNumber, key) } if strings.HasPrefix(val, " ") || strings.HasPrefix(val, "\t") { return nil, fmt.Errorf("invalid value on line %d: whitespace before value: '%s'", lineNumber, val) } // remove any trailing comment after value if commentStart, _, found := strings.Cut(val, "#"); found { val = strings.TrimRight(commentStart, " \t") } // quoted value: support newlines if strings.HasPrefix(val, `"`) { for !(strings.HasSuffix(line, `"`) && !strings.HasSuffix(line, `\"`)) { val = strings.ReplaceAll(val, `\"`, `"`) if !scanner.Scan() { break } lineNumber++ line = strings.ReplaceAll(scanner.Text(), `\"`, `"`) val += "\n" + line } val = strings.TrimPrefix(val, `"`) val = strings.TrimSuffix(val, `"`) } envMap[key] = val } if err := scanner.Err(); err != nil { return nil, err } return envMap, nil } func printEnvironment() { _, version := caddy.Version() fmt.Printf("caddy.HomeDir=%s\n", caddy.HomeDir()) fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir()) fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir()) fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath) fmt.Printf("caddy.Version=%s\n", version) fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS) fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH) fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler) fmt.Printf("runtime.NumCPU=%d\n", runtime.NumCPU()) fmt.Printf("runtime.GOMAXPROCS=%d\n", runtime.GOMAXPROCS(0)) fmt.Printf("runtime.Version=%s\n", runtime.Version()) cwd, err := os.Getwd() if err != nil { cwd = fmt.Sprintf("", err) } fmt.Printf("os.Getwd=%s\n\n", cwd) for _, v := range os.Environ() { fmt.Println(v) } } // StringSlice is a flag.Value that enables repeated use of a string flag. type StringSlice []string func (ss StringSlice) String() string { return "[" + strings.Join(ss, ", ") + "]" } func (ss *StringSlice) Set(value string) error { *ss = append(*ss, value) return nil } // Interface guard var _ flag.Value = (*StringSlice)(nil) caddy-2.6.2/cmd/main_test.go000066400000000000000000000055371435007237400157010ustar00rootroot00000000000000package caddycmd import ( "reflect" "strings" "testing" ) func TestParseEnvFile(t *testing.T) { for i, tc := range []struct { input string expect map[string]string shouldErr bool }{ { input: `KEY=value`, expect: map[string]string{ "KEY": "value", }, }, { input: ` KEY=value OTHER_KEY=Some Value `, expect: map[string]string{ "KEY": "value", "OTHER_KEY": "Some Value", }, }, { input: ` KEY=value INVALID KEY=asdf OTHER_KEY=Some Value `, shouldErr: true, }, { input: ` KEY=value SIMPLE_QUOTED="quoted value" OTHER_KEY=Some Value `, expect: map[string]string{ "KEY": "value", "SIMPLE_QUOTED": "quoted value", "OTHER_KEY": "Some Value", }, }, { input: ` KEY=value NEWLINES="foo bar" OTHER_KEY=Some Value `, expect: map[string]string{ "KEY": "value", "NEWLINES": "foo\n\tbar", "OTHER_KEY": "Some Value", }, }, { input: ` KEY=value ESCAPED="\"escaped quotes\" here" OTHER_KEY=Some Value `, expect: map[string]string{ "KEY": "value", "ESCAPED": "\"escaped quotes\"\nhere", "OTHER_KEY": "Some Value", }, }, { input: ` export KEY=value OTHER_KEY=Some Value `, expect: map[string]string{ "KEY": "value", "OTHER_KEY": "Some Value", }, }, { input: ` =value OTHER_KEY=Some Value `, shouldErr: true, }, { input: ` EMPTY= OTHER_KEY=Some Value `, expect: map[string]string{ "EMPTY": "", "OTHER_KEY": "Some Value", }, }, { input: ` EMPTY="" OTHER_KEY=Some Value `, expect: map[string]string{ "EMPTY": "", "OTHER_KEY": "Some Value", }, }, { input: ` KEY=value #OTHER_KEY=Some Value `, expect: map[string]string{ "KEY": "value", }, }, { input: ` KEY=value COMMENT=foo bar # some comment here OTHER_KEY=Some Value `, expect: map[string]string{ "KEY": "value", "COMMENT": "foo bar", "OTHER_KEY": "Some Value", }, }, { input: ` KEY=value WHITESPACE= foo OTHER_KEY=Some Value `, shouldErr: true, }, { input: ` KEY=value WHITESPACE=" foo bar " OTHER_KEY=Some Value `, expect: map[string]string{ "KEY": "value", "WHITESPACE": " foo bar ", "OTHER_KEY": "Some Value", }, }, } { actual, err := parseEnvFile(strings.NewReader(tc.input)) if err != nil && !tc.shouldErr { t.Errorf("Test %d: Got error but shouldn't have: %v", i, err) } if err == nil && tc.shouldErr { t.Errorf("Test %d: Did not get error but should have", i) } if tc.shouldErr { continue } if !reflect.DeepEqual(tc.expect, actual) { t.Errorf("Test %d: Expected %v but got %v", i, tc.expect, actual) } } } caddy-2.6.2/cmd/packagesfuncs.go000066400000000000000000000221661435007237400165300ustar00rootroot00000000000000// 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 caddycmd import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "os/exec" "reflect" "runtime" "runtime/debug" "strings" "github.com/caddyserver/caddy/v2" "go.uber.org/zap" ) func cmdUpgrade(fl Flags) (int, error) { _, nonstandard, _, err := getModules() if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err) } pluginPkgs, err := getPluginPackages(nonstandard) if err != nil { return caddy.ExitCodeFailedStartup, err } return upgradeBuild(pluginPkgs, fl) } func cmdAddPackage(fl Flags) (int, error) { if len(fl.Args()) == 0 { return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified") } _, nonstandard, _, err := getModules() if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err) } pluginPkgs, err := getPluginPackages(nonstandard) if err != nil { return caddy.ExitCodeFailedStartup, err } for _, arg := range fl.Args() { if _, ok := pluginPkgs[arg]; ok { return caddy.ExitCodeFailedStartup, fmt.Errorf("package is already added") } pluginPkgs[arg] = struct{}{} } return upgradeBuild(pluginPkgs, fl) } func cmdRemovePackage(fl Flags) (int, error) { if len(fl.Args()) == 0 { return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified") } _, nonstandard, _, err := getModules() if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err) } pluginPkgs, err := getPluginPackages(nonstandard) if err != nil { return caddy.ExitCodeFailedStartup, err } for _, arg := range fl.Args() { if _, ok := pluginPkgs[arg]; !ok { // package does not exist return caddy.ExitCodeFailedStartup, fmt.Errorf("package is not added") } delete(pluginPkgs, arg) } return upgradeBuild(pluginPkgs, fl) } func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) { l := caddy.Log() thisExecPath, err := os.Executable() if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err) } thisExecStat, err := os.Stat(thisExecPath) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err) } l.Info("this executable will be replaced", zap.String("path", thisExecPath)) // build the request URL to download this custom build qs := url.Values{ "os": {runtime.GOOS}, "arch": {runtime.GOARCH}, } for pkg := range pluginPkgs { qs.Add("p", pkg) } // initiate the build resp, err := downloadBuild(qs) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: %v", err) } defer resp.Body.Close() // back up the current binary, in case something goes wrong we can replace it backupExecPath := thisExecPath + ".tmp" l.Info("build acquired; backing up current executable", zap.String("current_path", thisExecPath), zap.String("backup_path", backupExecPath)) err = os.Rename(thisExecPath, backupExecPath) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err) } defer func() { if err != nil { err2 := os.Rename(backupExecPath, thisExecPath) if err2 != nil { l.Error("restoring original executable failed; will need to be restored manually", zap.String("backup_path", backupExecPath), zap.String("original_path", thisExecPath), zap.Error(err2)) } } }() // download the file; do this in a closure to close reliably before we execute it err = writeCaddyBinary(thisExecPath, &resp.Body, thisExecStat) if err != nil { return caddy.ExitCodeFailedStartup, err } l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath)) // use the new binary to print out version and module info fmt.Print("\nModule versions:\n\n") if err = listModules(thisExecPath); err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute 'caddy list-modules': %v", err) } fmt.Println("\nVersion:") if err = showVersion(thisExecPath); err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute 'caddy version': %v", err) } fmt.Println() // clean up the backup file if !fl.Bool("keep-backup") { if err = removeCaddyBinary(backupExecPath); err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err) } } else { l.Info("skipped cleaning up the backup file", zap.String("backup_path", backupExecPath)) } l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath)) return caddy.ExitCodeSuccess, nil } func getModules() (standard, nonstandard, unknown []moduleInfo, err error) { bi, ok := debug.ReadBuildInfo() if !ok { err = fmt.Errorf("no build info") return } for _, modID := range caddy.Modules() { modInfo, err := caddy.GetModule(modID) if err != nil { // that's weird, shouldn't happen unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err}) continue } // to get the Caddy plugin's version info, we need to know // the package that the Caddy module's value comes from; we // can use reflection but we need a non-pointer value (I'm // not sure why), and since New() should return a pointer // value, we need to dereference it first iface := any(modInfo.New()) if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr { iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface() } modPkgPath := reflect.TypeOf(iface).PkgPath() // now we find the Go module that the Caddy module's package // belongs to; we assume the Caddy module package path will // be prefixed by its Go module path, and we will choose the // longest matching prefix in case there are nested modules var matched *debug.Module for _, dep := range bi.Deps { if strings.HasPrefix(modPkgPath, dep.Path) { if matched == nil || len(dep.Path) > len(matched.Path) { matched = dep } } } caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched} if strings.HasPrefix(modPkgPath, caddy.ImportPath) { standard = append(standard, caddyModGoMod) } else { nonstandard = append(nonstandard, caddyModGoMod) } } return } func listModules(path string) error { cmd := exec.Command(path, "list-modules", "--versions", "--skip-standard") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func showVersion(path string) error { cmd := exec.Command(path, "version") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func downloadBuild(qs url.Values) (*http.Response, error) { l := caddy.Log() l.Info("requesting build", zap.String("os", qs.Get("os")), zap.String("arch", qs.Get("arch")), zap.Strings("packages", qs["p"])) resp, err := http.Get(fmt.Sprintf("%s?%s", downloadPath, qs.Encode())) if err != nil { return nil, fmt.Errorf("secure request failed: %v", err) } if resp.StatusCode >= 400 { var details struct { StatusCode int `json:"status_code"` Error struct { Message string `json:"message"` ID string `json:"id"` } `json:"error"` } err2 := json.NewDecoder(resp.Body).Decode(&details) if err2 != nil { return nil, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2) } return nil, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID) } return resp, nil } func getPluginPackages(modules []moduleInfo) (map[string]struct{}, error) { pluginPkgs := make(map[string]struct{}) for _, mod := range modules { if mod.goModule.Replace != nil { return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s", mod.goModule.Path, mod.goModule.Replace.Path) } pluginPkgs[mod.goModule.Path] = struct{}{} } return pluginPkgs, nil } func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) error { l := caddy.Log() destFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileInfo.Mode()) if err != nil { return fmt.Errorf("unable to open destination file: %v", err) } defer destFile.Close() l.Info("downloading binary", zap.String("destination", path)) _, err = io.Copy(destFile, *body) if err != nil { return fmt.Errorf("unable to download file: %v", err) } err = destFile.Sync() if err != nil { return fmt.Errorf("syncing downloaded file to device: %v", err) } return nil } const downloadPath = "https://caddyserver.com/api/download" caddy-2.6.2/cmd/removebinary.go000066400000000000000000000016661435007237400164170ustar00rootroot00000000000000// 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. //go:build !windows package caddycmd import ( "os" ) // removeCaddyBinary removes the Caddy binary at the given path. // // On any non-Windows OS, this simply calls os.Remove, since they should // probably not exhibit any issue with processes deleting themselves. func removeCaddyBinary(path string) error { return os.Remove(path) } caddy-2.6.2/cmd/removebinary_windows.go000066400000000000000000000027021435007237400201610ustar00rootroot00000000000000// 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 caddycmd import ( "os" "path/filepath" "syscall" ) // removeCaddyBinary removes the Caddy binary at the given path. // // On Windows, this uses a syscall to indirectly remove the file, // because otherwise we get an "Access is denied." error when trying // to delete the binary while Caddy is still running and performing // the upgrade. "cmd.exe /C" executes a command specified by the // following arguments, i.e. "del" which will run as a separate process, // which avoids the "Access is denied." error. func removeCaddyBinary(path string) error { var sI syscall.StartupInfo var pI syscall.ProcessInformation argv, err := syscall.UTF16PtrFromString(filepath.Join(os.Getenv("windir"), "system32", "cmd.exe") + " /C del " + path) if err != nil { return err } return syscall.CreateProcess(nil, argv, nil, nil, true, 0, nil, nil, &sI, &pI) } caddy-2.6.2/context.go000066400000000000000000000431261435007237400146330ustar00rootroot00000000000000// 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 ( "context" "encoding/json" "fmt" "log" "reflect" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) // Context is a type which defines the lifetime of modules that // are loaded and provides access to the parent configuration // that spawned the modules which are loaded. It should be used // with care and wrapped with derivation functions from the // standard context package only if you don't need the Caddy // specific features. These contexts are canceled when the // lifetime of the modules loaded from it is over. // // Use NewContext() to get a valid value (but most modules will // not actually need to do this). type Context struct { context.Context moduleInstances map[string][]Module cfg *Config cleanupFuncs []func() ancestry []Module } // NewContext provides a new context derived from the given // context ctx. Normally, you will not need to call this // function unless you are loading modules which have a // different lifespan than the ones for the context the // module was provisioned with. Be sure to call the cancel // func when the context is to be cleaned up so that // modules which are loaded will be properly unloaded. // See standard library context package's documentation. func NewContext(ctx Context) (Context, context.CancelFunc) { newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg} c, cancel := context.WithCancel(ctx.Context) wrappedCancel := func() { cancel() for _, f := range ctx.cleanupFuncs { f() } for modName, modInstances := range newCtx.moduleInstances { for _, inst := range modInstances { if cu, ok := inst.(CleanerUpper); ok { err := cu.Cleanup() if err != nil { log.Printf("[ERROR] %s (%p): cleanup: %v", modName, inst, err) } } } } } newCtx.Context = c return newCtx, wrappedCancel } // OnCancel executes f when ctx is canceled. func (ctx *Context) OnCancel(f func()) { ctx.cleanupFuncs = append(ctx.cleanupFuncs, f) } // LoadModule loads the Caddy module(s) from the specified field of the parent struct // pointer and returns the loaded module(s). The struct pointer and its field name as // a string are necessary so that reflection can be used to read the struct tag on the // field to get the module namespace and inline module name key (if specified). // // The field can be any one of the supported raw module types: json.RawMessage, // []json.RawMessage, map[string]json.RawMessage, or []map[string]json.RawMessage. // ModuleMap may be used in place of map[string]json.RawMessage. The return value's // underlying type mirrors the input field's type: // // json.RawMessage => any // []json.RawMessage => []any // [][]json.RawMessage => [][]any // map[string]json.RawMessage => map[string]any // []map[string]json.RawMessage => []map[string]any // // The field must have a "caddy" struct tag in this format: // // caddy:"key1=val1 key2=val2" // // To load modules, a "namespace" key is required. For example, to load modules // in the "http.handlers" namespace, you'd put: `namespace=http.handlers` in the // Caddy struct tag. // // The module name must also be available. If the field type is a map or slice of maps, // then key is assumed to be the module name if an "inline_key" is NOT specified in the // caddy struct tag. In this case, the module name does NOT need to be specified in-line // with the module itself. // // If not a map, or if inline_key is non-empty, then the module name must be embedded // into the values, which must be objects; then there must be a key in those objects // where its associated value is the module name. This is called the "inline key", // meaning the key containing the module's name that is defined inline with the module // itself. You must specify the inline key in a struct tag, along with the namespace: // // caddy:"namespace=http.handlers inline_key=handler" // // This will look for a key/value pair like `"handler": "..."` in the json.RawMessage // in order to know the module name. // // To make use of the loaded module(s) (the return value), you will probably want // to type-assert each 'any' value(s) to the types that are useful to you // and store them on the same struct. Storing them on the same struct makes for // easy garbage collection when your host module is no longer needed. // // Loaded modules have already been provisioned and validated. Upon returning // successfully, this method clears the json.RawMessage(s) in the field since // the raw JSON is no longer needed, and this allows the GC to free up memory. func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error) { val := reflect.ValueOf(structPointer).Elem().FieldByName(fieldName) typ := val.Type() field, ok := reflect.TypeOf(structPointer).Elem().FieldByName(fieldName) if !ok { panic(fmt.Sprintf("field %s does not exist in %#v", fieldName, structPointer)) } opts, err := ParseStructTag(field.Tag.Get("caddy")) if err != nil { panic(fmt.Sprintf("malformed tag on field %s: %v", fieldName, err)) } moduleNamespace, ok := opts["namespace"] if !ok { panic(fmt.Sprintf("missing 'namespace' key in struct tag on field %s", fieldName)) } inlineModuleKey := opts["inline_key"] var result any switch val.Kind() { case reflect.Slice: if isJSONRawMessage(typ) { // val is `json.RawMessage` ([]uint8 under the hood) if inlineModuleKey == "" { panic("unable to determine module name without inline_key when type is not a ModuleMap") } val, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, val.Interface().(json.RawMessage)) if err != nil { return nil, err } result = val } else if isJSONRawMessage(typ.Elem()) { // val is `[]json.RawMessage` if inlineModuleKey == "" { panic("unable to determine module name without inline_key because type is not a ModuleMap") } var all []any for i := 0; i < val.Len(); i++ { val, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, val.Index(i).Interface().(json.RawMessage)) if err != nil { return nil, fmt.Errorf("position %d: %v", i, err) } all = append(all, val) } result = all } else if typ.Elem().Kind() == reflect.Slice && isJSONRawMessage(typ.Elem().Elem()) { // val is `[][]json.RawMessage` if inlineModuleKey == "" { panic("unable to determine module name without inline_key because type is not a ModuleMap") } var all [][]any for i := 0; i < val.Len(); i++ { innerVal := val.Index(i) var allInner []any for j := 0; j < innerVal.Len(); j++ { innerInnerVal, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, innerVal.Index(j).Interface().(json.RawMessage)) if err != nil { return nil, fmt.Errorf("position %d: %v", j, err) } allInner = append(allInner, innerInnerVal) } all = append(all, allInner) } result = all } else if isModuleMapType(typ.Elem()) { // val is `[]map[string]json.RawMessage` var all []map[string]any for i := 0; i < val.Len(); i++ { thisSet, err := ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val.Index(i)) if err != nil { return nil, err } all = append(all, thisSet) } result = all } case reflect.Map: // val is a ModuleMap or some other kind of map result, err = ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val) if err != nil { return nil, err } default: return nil, fmt.Errorf("unrecognized type for module: %s", typ) } // we're done with the raw bytes; allow GC to deallocate val.Set(reflect.Zero(typ)) return result, nil } // loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any. // Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module // name) or as a regular map (key is not the module name, and module name is defined inline). func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]any, error) { // if no inline_key is specified, then val must be a ModuleMap, // where the key is the module name if inlineModuleKey == "" { if !isModuleMapType(val.Type()) { panic(fmt.Sprintf("expected ModuleMap because inline_key is empty; but we do not recognize this type: %s", val.Type())) } return ctx.loadModuleMap(namespace, val) } // otherwise, val is a map with modules, but the module name is // inline with each value (the key means something else) return ctx.loadModulesFromRegularMap(namespace, inlineModuleKey, val) } // loadModulesFromRegularMap loads modules from val, where val is a map[string]json.RawMessage. // Map keys are NOT interpreted as module names, so module names are still expected to appear // inline with the objects. func (ctx Context) loadModulesFromRegularMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]any, error) { mods := make(map[string]any) iter := val.MapRange() for iter.Next() { k := iter.Key() v := iter.Value() mod, err := ctx.loadModuleInline(inlineModuleKey, namespace, v.Interface().(json.RawMessage)) if err != nil { return nil, fmt.Errorf("key %s: %v", k, err) } mods[k.String()] = mod } return mods, nil } // loadModuleMap loads modules from a ModuleMap, i.e. map[string]any, where the key is the // module name. With a module map, module names do not need to be defined inline with their values. func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[string]any, error) { all := make(map[string]any) iter := val.MapRange() for iter.Next() { k := iter.Key().Interface().(string) v := iter.Value().Interface().(json.RawMessage) moduleName := namespace + "." + k if namespace == "" { moduleName = k } val, err := ctx.LoadModuleByID(moduleName, v) if err != nil { return nil, fmt.Errorf("module name '%s': %v", k, err) } all[k] = val } return all, nil } // LoadModuleByID decodes rawMsg into a new instance of mod and // returns the value. If mod.New is nil, an error is returned. // If the module implements Validator or Provisioner interfaces, // those methods are invoked to ensure the module is fully // configured and valid before being used. // // This is a lower-level method and will usually not be called // directly by most modules. However, this method is useful when // dynamically loading/unloading modules in their own context, // like from embedded scripts, etc. func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error) { modulesMu.RLock() modInfo, ok := modules[id] modulesMu.RUnlock() if !ok { return nil, fmt.Errorf("unknown module: %s", id) } if modInfo.New == nil { return nil, fmt.Errorf("module '%s' has no constructor", modInfo.ID) } val := modInfo.New() // value must be a pointer for unmarshaling into concrete type, even if // the module's concrete type is a slice or map; New() *should* return // a pointer, otherwise unmarshaling errors or panics will occur if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr { log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+ " so we are using reflection to make a pointer instead; please fix this by"+ " using new(Type) or &Type notation in your module's New() function.", id) val = reflect.New(rv.Type()).Elem().Addr().Interface().(Module) } // fill in its config only if there is a config to fill in if len(rawMsg) > 0 { err := strictUnmarshalJSON(rawMsg, &val) if err != nil { return nil, fmt.Errorf("decoding module config: %s: %v", modInfo, err) } } if val == nil { // returned module values are almost always type-asserted // before being used, so a nil value would panic; and there // is no good reason to explicitly declare null modules in // a config; it might be because the user is trying to achieve // a result the developer isn't expecting, which is a smell return nil, fmt.Errorf("module value cannot be null") } ctx.ancestry = append(ctx.ancestry, val) if prov, ok := val.(Provisioner); ok { err := prov.Provision(ctx) if err != nil { // incomplete provisioning could have left state // dangling, so make sure it gets cleaned up if cleanerUpper, ok := val.(CleanerUpper); ok { err2 := cleanerUpper.Cleanup() if err2 != nil { err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) } } return nil, fmt.Errorf("provision %s: %v", modInfo, err) } } if validator, ok := val.(Validator); ok { err := validator.Validate() if err != nil { // since the module was already provisioned, make sure we clean up if cleanerUpper, ok := val.(CleanerUpper); ok { err2 := cleanerUpper.Cleanup() if err2 != nil { err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) } } return nil, fmt.Errorf("%s: invalid configuration: %v", modInfo, err) } } ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val) return val, nil } // loadModuleInline loads a module from a JSON raw message which decodes to // a map[string]any, where one of the object keys is moduleNameKey // and the corresponding value is the module name (as a string) which can // be found in the given scope. In other words, the module name is declared // in-line with the module itself. // // This allows modules to be decoded into their concrete types and used when // their names cannot be the unique key in a map, such as when there are // multiple instances in the map or it appears in an array (where there are // no custom keys). In other words, the key containing the module name is // treated special/separate from all the other keys in the object. func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (any, error) { moduleName, raw, err := getModuleNameInline(moduleNameKey, raw) if err != nil { return nil, err } val, err := ctx.LoadModuleByID(moduleScope+"."+moduleName, raw) if err != nil { return nil, fmt.Errorf("loading module '%s': %v", moduleName, err) } return val, nil } // App returns the configured app named name. If that app has // not yet been loaded and provisioned, it will be immediately // loaded and provisioned. If no app with that name is // configured, a new empty one will be instantiated instead. // (The app module must still be registered.) This must not be // called during the Provision/Validate phase to reference a // module's own host app (since the parent app module is still // in the process of being provisioned, it is not yet ready). func (ctx Context) App(name string) (any, error) { if app, ok := ctx.cfg.apps[name]; ok { return app, nil } appRaw := ctx.cfg.AppsRaw[name] modVal, err := ctx.LoadModuleByID(name, appRaw) if err != nil { return nil, fmt.Errorf("loading %s app module: %v", name, err) } if appRaw != nil { ctx.cfg.AppsRaw[name] = nil // allow GC to deallocate } ctx.cfg.apps[name] = modVal.(App) return modVal, nil } // AppIsConfigured returns whether an app named name has been // configured. Can be called before calling App() to avoid // instantiating an empty app when that's not desirable. func (ctx Context) AppIsConfigured(name string) bool { if _, ok := ctx.cfg.apps[name]; ok { return true } appRaw := ctx.cfg.AppsRaw[name] return appRaw != nil } // Storage returns the configured Caddy storage implementation. func (ctx Context) Storage() certmagic.Storage { return ctx.cfg.storage } // Logger returns a logger that is intended for use by the most // recent module associated with the context. Callers should not // pass in any arguments unless they want to associate with a // different module; it panics if more than 1 value is passed in. // // Originally, this method's signature was `Logger(mod Module)`, // requiring that an instance of a Caddy module be passed in. // However, that is no longer necessary, as the closest module // most recently associated with the context will be automatically // assumed. To prevent a sudden breaking change, this method's // signature has been changed to be variadic, but we may remove // the parameter altogether in the future. Callers should not // pass in any argument. If there is valid need to specify a // different module, please open an issue to discuss. // // PARTIALLY DEPRECATED: The Logger(module) form is deprecated and // may be removed in the future. Do not pass in any arguments. func (ctx Context) Logger(module ...Module) *zap.Logger { if len(module) > 1 { panic("more than 1 module passed in") } if ctx.cfg == nil { // often the case in tests; just use a dev logger l, err := zap.NewDevelopment() if err != nil { panic("config missing, unable to create dev logger: " + err.Error()) } return l } mod := ctx.Module() if len(module) > 0 { mod = module[0] } return ctx.cfg.Logging.Logger(mod) } // Modules returns the lineage of modules that this context provisioned, // with the most recent/current module being last in the list. func (ctx Context) Modules() []Module { mods := make([]Module, len(ctx.ancestry)) copy(mods, ctx.ancestry) return mods } // Module returns the current module, or the most recent one // provisioned by the context. func (ctx Context) Module() Module { if len(ctx.ancestry) == 0 { return nil } return ctx.ancestry[len(ctx.ancestry)-1] } caddy-2.6.2/context_test.go000066400000000000000000000101661435007237400156700ustar00rootroot00000000000000// 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" "io" ) func ExampleContext_LoadModule() { // this whole first part is just setting up for the example; // note the struct tags - very important; we specify inline_key // because that is the only way to know the module name var ctx Context myStruct := &struct { // This godoc comment will appear in module documentation. GuestModuleRaw json.RawMessage `json:"guest_module,omitempty" caddy:"namespace=example inline_key=name"` // this is where the decoded module will be stored; in this // example, we pretend we need an io.Writer but it can be // any interface type that is useful to you guestModule io.Writer }{ GuestModuleRaw: json.RawMessage(`{"name":"module_name","foo":"bar"}`), } // if a guest module is provided, we can load it easily if myStruct.GuestModuleRaw != nil { mod, err := ctx.LoadModule(myStruct, "GuestModuleRaw") if err != nil { // you'd want to actually handle the error here // return fmt.Errorf("loading guest module: %v", err) } // mod contains the loaded and provisioned module, // it is now ready for us to use myStruct.guestModule = mod.(io.Writer) } // use myStruct.guestModule from now on } func ExampleContext_LoadModule_array() { // this whole first part is just setting up for the example; // note the struct tags - very important; we specify inline_key // because that is the only way to know the module name var ctx Context myStruct := &struct { // This godoc comment will appear in module documentation. GuestModulesRaw []json.RawMessage `json:"guest_modules,omitempty" caddy:"namespace=example inline_key=name"` // this is where the decoded module will be stored; in this // example, we pretend we need an io.Writer but it can be // any interface type that is useful to you guestModules []io.Writer }{ GuestModulesRaw: []json.RawMessage{ json.RawMessage(`{"name":"module1_name","foo":"bar1"}`), json.RawMessage(`{"name":"module2_name","foo":"bar2"}`), }, } // since our input is []json.RawMessage, the output will be []any mods, err := ctx.LoadModule(myStruct, "GuestModulesRaw") if err != nil { // you'd want to actually handle the error here // return fmt.Errorf("loading guest modules: %v", err) } for _, mod := range mods.([]any) { myStruct.guestModules = append(myStruct.guestModules, mod.(io.Writer)) } // use myStruct.guestModules from now on } func ExampleContext_LoadModule_map() { // this whole first part is just setting up for the example; // note the struct tags - very important; we don't specify // inline_key because the map key is the module name var ctx Context myStruct := &struct { // This godoc comment will appear in module documentation. GuestModulesRaw ModuleMap `json:"guest_modules,omitempty" caddy:"namespace=example"` // this is where the decoded module will be stored; in this // example, we pretend we need an io.Writer but it can be // any interface type that is useful to you guestModules map[string]io.Writer }{ GuestModulesRaw: ModuleMap{ "module1_name": json.RawMessage(`{"foo":"bar1"}`), "module2_name": json.RawMessage(`{"foo":"bar2"}`), }, } // since our input is map[string]json.RawMessage, the output will be map[string]any mods, err := ctx.LoadModule(myStruct, "GuestModulesRaw") if err != nil { // you'd want to actually handle the error here // return fmt.Errorf("loading guest modules: %v", err) } for modName, mod := range mods.(map[string]any) { myStruct.guestModules[modName] = mod.(io.Writer) } // use myStruct.guestModules from now on } caddy-2.6.2/duration_fuzz.go000066400000000000000000000014021435007237400160410ustar00rootroot00000000000000// 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. //go:build gofuzz package caddy func FuzzParseDuration(data []byte) int { _, err := ParseDuration(string(data)) if err != nil { return 0 } return 1 } caddy-2.6.2/go.mod000066400000000000000000000144061435007237400137250ustar00rootroot00000000000000module github.com/caddyserver/caddy/v2 go 1.18 require ( github.com/BurntSushi/toml v1.2.0 github.com/Masterminds/sprig/v3 v3.2.2 github.com/alecthomas/chroma v0.10.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/caddyserver/certmagic v0.17.2 github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac github.com/go-chi/chi v4.1.2+incompatible github.com/google/cel-go v0.12.5 github.com/google/uuid v1.3.0 github.com/klauspost/compress v1.15.11 github.com/klauspost/cpuid/v2 v2.1.1 github.com/lucas-clemente/quic-go v0.29.2 github.com/mholt/acmez v1.0.4 github.com/prometheus/client_golang v1.12.2 github.com/smallstep/certificates v0.22.1 github.com/smallstep/cli v0.22.0 github.com/smallstep/nosql v0.4.0 github.com/smallstep/truststore v0.12.0 github.com/spf13/cobra v1.5.0 github.com/spf13/pflag v1.0.5 github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 github.com/yuin/goldmark v1.5.2 github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 go.opentelemetry.io/otel v1.9.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0 go.opentelemetry.io/otel/sdk v1.4.0 go.uber.org/zap v1.23.0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa golang.org/x/net v0.0.0-20220812165438-1d4ff48094d1 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/golang/mock v1.6.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.1.2 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/dgraph-io/badger v1.6.2 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-logfmt/logfmt v0.5.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.10.1 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.2.0 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jackc/pgtype v1.9.0 // indirect github.com/jackc/pgx/v4 v4.14.0 // indirect github.com/libdns/libdns v0.2.1 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/marten-seemann/qpack v0.2.1 // indirect github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/micromdm/scep/v2 v2.1.0 // indirect github.com/miekg/dns v1.1.50 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/onsi/ginkgo v1.16.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/rs/xid v1.2.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/slackhq/nebula v1.5.2 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/urfave/cli v1.22.5 // indirect go.etcd.io/bbolt v1.3.6 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0 // indirect go.opentelemetry.io/otel/metric v0.31.0 // indirect go.opentelemetry.io/otel/trace v1.9.0 // indirect go.opentelemetry.io/proto/otlp v0.12.0 // indirect go.step.sm/cli-utils v0.7.4 // indirect go.step.sm/crypto v0.18.0 // indirect go.step.sm/linkedca v0.18.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect golang.org/x/tools v0.1.10 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect google.golang.org/grpc v1.47.0 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect howett.net/plist v1.0.0 // indirect ) caddy-2.6.2/go.sum000066400000000000000000003326231435007237400137560ustar00rootroot00000000000000cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/iam v0.1.0 h1:W2vbGCrE3Z7J/x3WXLxxGl9LMSB2uhsAA7Ss/6u/qRY= cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA= github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed h1:ue9pVfIcP+QMEjfgo/Ez4ZjNZfonGgR6NgjMaJMu1Cg= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.44.37 h1:KvDxCX6dfJeEDC77U5GPGSP0ErecmNnhDHFxw+NIvlI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE= github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd h1:KoJOtZf+6wpQaDTuOWGuo61GxcPBIfhwRxRTaTWGCTc= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac h1:opbrjaN/L8gg6Xh5D04Tem+8xVcz6ajZlGCs49mQgyg= github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/cel-go v0.12.5 h1:DmzaiSgoaqGCjtpPQWl26/gND+yRpim56H1jCVev6d8= github.com/google/cel-go v0.12.5/go.mod h1:Jk7ljRzLBhkmiAwBoUxB1sZSCVBAzkqPF25olK/iRDw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa h1:7MYGT2XEMam7Mtzv1yDUYXANedWvwk3HKkR3MyGowy8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8= github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.9.0 h1:/SH1RxEtltvJgsDqp3TbiTFApD3mey3iygpuEGeuBXk= github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.14.0 h1:TgdrmgnM7VY72EuSQzBbBd4JA1RLqJolrw9nQVZABVc= github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0= github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lucas-clemente/quic-go v0.29.2 h1:O8Mt0O6LpvEW+wfC40vZdcw0DngwYzoxq5xULZNzSI8= github.com/lucas-clemente/quic-go v0.29.2/go.mod h1:g6/h9YMmLuU54tL1gW25uIi3VlBp3uv+sBihplIuskE= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= github.com/marten-seemann/qtls-go1-18 v0.1.3 h1:R4H2Ks8P6pAtUagjFty2p7BVHn3XiwDAl7TTQf5h7TI= github.com/marten-seemann/qtls-go1-18 v0.1.3/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= github.com/marten-seemann/qtls-go1-19 v0.1.1 h1:mnbxeq3oEyQxQXwI4ReCgW9DPoPR94sNlqWoDZnjRIE= github.com/marten-seemann/qtls-go1-19 v0.1.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80= github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY= github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXmaU= github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvvGoOExBcc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/slackhq/nebula v1.5.2 h1:wuIOHsOnrNw3rQx8yPxXiGu8wAtAxxtUI/K8W7Vj7EI= github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIpR52WlE= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/certificates v0.22.1 h1:oAwb9tj+M3sKnlKLqPpJUOsn1HbxKOJ4MVCuvDHHjy8= github.com/smallstep/certificates v0.22.1/go.mod h1:3R1PxdLOEqbNaWp88WiUByQAHGL+b9NefaQ5q6oTIZo= github.com/smallstep/cli v0.22.0 h1:Mbb2CkunxAVig7Cr1NymS2NhjeOvkZqLPsfe9ZKMEfk= github.com/smallstep/cli v0.22.0/go.mod h1:147IhymdB7JJYz1vN7XP2JS1YE+7soYX1yB/1gpZWh8= github.com/smallstep/nosql v0.4.0 h1:Go3WYwttUuvwqMtFiiU4g7kBIlY+hR0bIZAqVdakQ3M= github.com/smallstep/nosql v0.4.0/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo= github.com/smallstep/truststore v0.12.0 h1:973Aa6fA7Ob/GCxqziosDzkQq6tV0Le6IUe4sikyW+U= github.com/smallstep/truststore v0.12.0/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 h1:xwMw7LFhV9dbvot9A7NLClP9udqbjrQlIwWMH8e7uiQ= github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2/go.mod h1:hL4gB6APAasMR2NNi/JHzqKkxW3EPQlFgLEq9PMi2t0= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.5/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 h1:yHfZyN55+5dp1wG7wDKv8HQ044moxkyGq12KFFMFDxg= github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594/go.mod h1:U9ihbh+1ZN7fR5Se3daSPoz1CGF9IYtSvWwVQtnzGHU= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.mozilla.org/pkcs7 v0.0.0-20210730143726-725912489c62/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 h1:9NkMW03wwEzPtP/KciZ4Ozu/Uz5ZA7kfqXJIObnrjGU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0/go.mod h1:548ZsYzmT4PL4zWKRd8q/N4z0Wxzn/ZxUE+lkEpwWQA= go.opentelemetry.io/otel v1.4.0/go.mod h1:jeAqMFKy2uLIxCtKxoFj0FAL5zAPKQagc3+GtBWakzk= go.opentelemetry.io/otel v1.9.0 h1:8WZNQFIB2a71LnANS9JeyidJKKGOOremcUtb/OtHISw= go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 h1:j7AwzDdAQBJjcqayAaYbvpYeZzII7cEe5qJTu+De6UY= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0 h1:lRpP10E8oTGVmY1nVXcwelCT1Z8ca41/l5ce7AqLAss= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0/go.mod h1:3oS+j2WUoJVyj6/BzQN/52G17lNJDulngsOxDm1w2PY= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0 h1:buSx4AMC/0Z232slPhicN/fU5KIlj0bMngct5pcZhkI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0/go.mod h1:ew1NcwkHo0QFT3uTm3m2IVZMkZdVIpbOYNPasgWwpdk= go.opentelemetry.io/otel/metric v0.31.0 h1:6SiklT+gfWAwWUR0meEMxQBtihpiEs4c+vL9spDTqUs= go.opentelemetry.io/otel/metric v0.31.0/go.mod h1:ohmwj9KTSIeBnDBm/ZwH2PSZxZzoOaG2xZeekTRzL5A= go.opentelemetry.io/otel/sdk v1.4.0 h1:LJE4SW3jd4lQTESnlpQZcBhQ3oci0U2MLR5uhicfTHQ= go.opentelemetry.io/otel/sdk v1.4.0/go.mod h1:71GJPNJh4Qju6zJuYl1CrYtXbrgfau/M9UAggqiy1UE= go.opentelemetry.io/otel/trace v1.4.0/go.mod h1:uc3eRsqDfWs9R7b92xbQbU42/eTNz4N+gLP8qJCi4aE= go.opentelemetry.io/otel/trace v1.9.0 h1:oZaCNJUjWcg60VXWee8lJKlqhPbXAPB51URuR47pQYc= go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.12.0 h1:CMJ/3Wp7iOWES+CYLfnBv+DVmPbB+kmy9PJ92XvlR6c= go.opentelemetry.io/proto/otlp v0.12.0/go.mod h1:TsIjwGWIx5VFYv9KGVlOpxoBl5Dy+63SUguV7GGvlSQ= go.step.sm/cli-utils v0.7.4 h1:oI7PStZqlvjPZ0u2EB4lN7yZ4R3ShTotdGL/L84Oorg= go.step.sm/cli-utils v0.7.4/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71I= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.18.0 h1:saD/tMG7uKJmUIPyOyudidVTHPnozTU02CDd+oqwKn0= go.step.sm/crypto v0.18.0/go.mod h1:qZ+pNU1nV+THwP7TPTNCRMRr9xrRURhETTAK7U5psfw= go.step.sm/linkedca v0.18.0 h1:uxRBd2WDvJNZ2i0nJm/QmG4lkRxWoebYKJinchX7T7o= go.step.sm/linkedca v0.18.0/go.mod h1:qSuYlIIhvPmA2+DSSS03E2IXhbXWTLW61Xh9zDQJ3VM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220812165438-1d4ff48094d1 h1:mx1QvUwXKGgh+3SB51PH4G1TouzL84rLG0CtpdX+TTg= golang.org/x/net v0.0.0-20220812165438-1d4ff48094d1/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb h1:8tDJ3aechhddbdPAxpycgXHJRMLpk/Ab+aa4OgdN5/g= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b h1:NXqSWXSRUSCaFuvitrWtU169I3876zRTalMRbfd6LL0= golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.84.0 h1:NMB9J4cCxs9xEm+1Z9QiO3eFvn7EnQj3Eo3hN6ugVlg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad h1:kqrS+lhvaMHCxul6sKQvKJ8nAAhlVItmZV822hYFH/U= google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= caddy-2.6.2/internal/000077500000000000000000000000001435007237400144265ustar00rootroot00000000000000caddy-2.6.2/internal/metrics/000077500000000000000000000000001435007237400160745ustar00rootroot00000000000000caddy-2.6.2/internal/metrics/metrics.go000066400000000000000000000020631435007237400200720ustar00rootroot00000000000000package metrics import ( "net/http" "strconv" ) func SanitizeCode(s int) string { switch s { case 0, 200: return "200" default: return strconv.Itoa(s) } } // Only support the list of "regular" HTTP methods, see // https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods var methodMap = map[string]string{ "GET": http.MethodGet, "get": http.MethodGet, "HEAD": http.MethodHead, "head": http.MethodHead, "PUT": http.MethodPut, "put": http.MethodPut, "POST": http.MethodPost, "post": http.MethodPost, "DELETE": http.MethodDelete, "delete": http.MethodDelete, "CONNECT": http.MethodConnect, "connect": http.MethodConnect, "OPTIONS": http.MethodOptions, "options": http.MethodOptions, "TRACE": http.MethodTrace, "trace": http.MethodTrace, "PATCH": http.MethodPatch, "patch": http.MethodPatch, } // SanitizeMethod sanitizes the method for use as a metric label. This helps // prevent high cardinality on the method label. The name is always upper case. func SanitizeMethod(m string) string { if m, ok := methodMap[m]; ok { return m } return "OTHER" } caddy-2.6.2/internal/metrics/metrics_test.go000066400000000000000000000011711435007237400211300ustar00rootroot00000000000000package metrics import ( "strings" "testing" ) func TestSanitizeMethod(t *testing.T) { tests := []struct { method string expected string }{ {method: "get", expected: "GET"}, {method: "POST", expected: "POST"}, {method: "OPTIONS", expected: "OPTIONS"}, {method: "connect", expected: "CONNECT"}, {method: "trace", expected: "TRACE"}, {method: "UNKNOWN", expected: "OTHER"}, {method: strings.Repeat("ohno", 9999), expected: "OTHER"}, } for _, d := range tests { actual := SanitizeMethod(d.method) if actual != d.expected { t.Errorf("Not same: expected %#v, but got %#v", d.expected, actual) } } } caddy-2.6.2/listen.go000066400000000000000000000141621435007237400144430ustar00rootroot00000000000000// 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. // TODO: Go 1.19 introduced the "unix" build tag. We have to support Go 1.18 until Go 1.20 is released. // When Go 1.19 is our minimum, change this build tag to simply "!unix". // (see similar change needed in listen_unix.go) //go:build !(aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris) package caddy import ( "context" "net" "sync" "sync/atomic" "time" "go.uber.org/zap" ) func reuseUnixSocket(network, addr string) (any, error) { return nil, nil } func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) { sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { ln, err := config.Listen(ctx, network, address) if err != nil { return nil, err } return &sharedListener{Listener: ln, key: lnKey}, nil }) if err != nil { return nil, err } return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil } // fakeCloseListener is a private wrapper over a listener that // is shared. The state of fakeCloseListener is not shared. // This allows one user of a socket to "close" the listener // while in reality the socket stays open for other users of // the listener. In this way, servers become hot-swappable // while the listener remains running. Listeners should be // re-wrapped in a new fakeCloseListener each time the listener // is reused. This type is atomic and values must not be copied. type fakeCloseListener struct { closed int32 // accessed atomically; belongs to this struct only *sharedListener // embedded, so we also become a net.Listener keepAlivePeriod time.Duration } type canSetKeepAlive interface { SetKeepAlivePeriod(d time.Duration) error SetKeepAlive(bool) error } func (fcl *fakeCloseListener) Accept() (net.Conn, error) { // if the listener is already "closed", return error if atomic.LoadInt32(&fcl.closed) == 1 { return nil, fakeClosedErr(fcl) } // call underlying accept conn, err := fcl.sharedListener.Accept() if err == nil { // if 0, do nothing, Go's default is already set // and if the connection allows setting KeepAlive, set it if tconn, ok := conn.(canSetKeepAlive); ok && fcl.keepAlivePeriod != 0 { if fcl.keepAlivePeriod > 0 { err = tconn.SetKeepAlivePeriod(fcl.keepAlivePeriod) } else { // negative err = tconn.SetKeepAlive(false) } if err != nil { Log().With(zap.String("server", fcl.sharedListener.key)).Warn("unable to set keepalive for new connection:", zap.Error(err)) } } return conn, nil } // since Accept() returned an error, it may be because our reference to // the listener (this fakeCloseListener) may have been closed, i.e. the // server is shutting down; in that case, we need to clear the deadline // that we set when Close() was called, and return a non-temporary and // non-timeout error value to the caller, masking the "true" error, so // that server loops / goroutines won't retry, linger, and leak if atomic.LoadInt32(&fcl.closed) == 1 { // we dereference the sharedListener explicitly even though it's embedded // so that it's clear in the code that side-effects are shared with other // users of this listener, not just our own reference to it; we also don't // do anything with the error because all we could do is log it, but we // expliclty assign it to nothing so we don't forget it's there if needed _ = fcl.sharedListener.clearDeadline() if netErr, ok := err.(net.Error); ok && netErr.Timeout() { return nil, fakeClosedErr(fcl) } } return nil, err } // Close stops accepting new connections without closing the // underlying listener. The underlying listener is only closed // if the caller is the last known user of the socket. func (fcl *fakeCloseListener) Close() error { if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) { // There are two ways I know of to get an Accept() // function to return to the server loop that called // it: close the listener, or set a deadline in the // past. Obviously, we can't close the socket yet // since others may be using it (hence this whole // file). But we can set the deadline in the past, // and this is kind of cheating, but it works, and // it apparently even works on Windows. _ = fcl.sharedListener.setDeadline() _, _ = listenerPool.Delete(fcl.sharedListener.key) } return nil } // sharedListener is a wrapper over an underlying listener. The listener // and the other fields on the struct are shared state that is synchronized, // so sharedListener structs must never be copied (always use a pointer). type sharedListener struct { net.Listener key string // uniquely identifies this listener deadline bool // whether a deadline is currently set deadlineMu sync.Mutex } func (sl *sharedListener) clearDeadline() error { var err error sl.deadlineMu.Lock() if sl.deadline { switch ln := sl.Listener.(type) { case *net.TCPListener: err = ln.SetDeadline(time.Time{}) } sl.deadline = false } sl.deadlineMu.Unlock() return err } func (sl *sharedListener) setDeadline() error { timeInPast := time.Now().Add(-1 * time.Minute) var err error sl.deadlineMu.Lock() if !sl.deadline { switch ln := sl.Listener.(type) { case *net.TCPListener: err = ln.SetDeadline(timeInPast) } sl.deadline = true } sl.deadlineMu.Unlock() return err } // Destruct is called by the UsagePool when the listener is // finally not being used anymore. It closes the socket. func (sl *sharedListener) Destruct() error { return sl.Listener.Close() } caddy-2.6.2/listen_unix.go000066400000000000000000000101341435007237400155010ustar00rootroot00000000000000// 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. // TODO: Go 1.19 introduced the "unix" build tag. We have to support Go 1.18 until Go 1.20 is released. // When Go 1.19 is our minimum, remove this build tag, since "_unix" in the filename will do this. // (see also change needed in listen.go) //go:build aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris package caddy import ( "context" "errors" "io/fs" "net" "sync/atomic" "syscall" "go.uber.org/zap" "golang.org/x/sys/unix" ) // reuseUnixSocket copies and reuses the unix domain socket (UDS) if we already // have it open; if not, unlink it so we can have it. No-op if not a unix network. func reuseUnixSocket(network, addr string) (any, error) { if !isUnixNetwork(network) { return nil, nil } socketKey := listenerKey(network, addr) socket, exists := unixSockets[socketKey] if exists { // make copy of file descriptor socketFile, err := socket.File() // does dup() deep down if err != nil { return nil, err } // use copied fd to make new Listener or PacketConn, then replace // it in the map so that future copies always come from the most // recent fd (as the previous ones will be closed, and we'd get // "use of closed network connection" errors) -- note that we // preserve the *pointer* to the counter (not just the value) so // that all socket wrappers will refer to the same value switch unixSocket := socket.(type) { case *unixListener: ln, err := net.FileListener(socketFile) if err != nil { return nil, err } atomic.AddInt32(unixSocket.count, 1) unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count} case *unixConn: pc, err := net.FilePacketConn(socketFile) if err != nil { return nil, err } atomic.AddInt32(unixSocket.count, 1) unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), addr, socketKey, unixSocket.count} } return unixSockets[socketKey], nil } // from what I can tell after some quick research, it's quite common for programs to // leave their socket file behind after they close, so the typical pattern is to // unlink it before you bind to it -- this is often crucial if the last program using // it was killed forcefully without a chance to clean up the socket, but there is a // race, as the comment in net.UnixListener.close() explains... oh well, I guess? if err := syscall.Unlink(addr); err != nil && !errors.Is(err, fs.ErrNotExist) { return nil, err } return nil, nil } func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) { // wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs oldControl := config.Control config.Control = func(network, address string, c syscall.RawConn) error { if oldControl != nil { if err := oldControl(network, address, c); err != nil { return err } } return reusePort(network, address, c) } return config.Listen(ctx, network, address) } // reusePort sets SO_REUSEPORT. Ineffective for unix sockets. func reusePort(network, address string, conn syscall.RawConn) error { if isUnixNetwork(network) { return nil } return conn.Control(func(descriptor uintptr) { if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { Log().Error("setting SO_REUSEPORT", zap.String("network", network), zap.String("address", address), zap.Uintptr("descriptor", descriptor), zap.Error(err)) } }) } caddy-2.6.2/listeners.go000066400000000000000000000561121435007237400151560ustar00rootroot00000000000000// 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 ( "context" "crypto/tls" "errors" "fmt" "io" "net" "net/netip" "os" "strconv" "strings" "sync" "sync/atomic" "syscall" "time" "github.com/lucas-clemente/quic-go" "github.com/lucas-clemente/quic-go/http3" "go.uber.org/zap" ) // NetworkAddress represents one or more network addresses. // It contains the individual components for a parsed network // address of the form accepted by ParseNetworkAddress(). type NetworkAddress struct { // Should be a network value accepted by Go's net package or // by a plugin providing a listener for that network type. Network string // The "main" part of the network address is the host, which // often takes the form of a hostname, DNS name, IP address, // or socket path. Host string // For addresses that contain a port, ranges are given by // [StartPort, EndPort]; i.e. for a single port, StartPort // and EndPort are the same. For no port, they are 0. StartPort uint EndPort uint } // ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range. // (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.) // It returns an error if any listener failed to bind, and closes any listeners opened up to that point. // // TODO: Experimental API: subject to change or removal. func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) { var listeners []any var err error // if one of the addresses has a failure, we need to close // any that did open a socket to avoid leaking resources defer func() { if err == nil { return } for _, ln := range listeners { if cl, ok := ln.(io.Closer); ok { cl.Close() } } }() // an address can contain a port range, which represents multiple addresses; // some addresses don't use ports at all and have a port range size of 1; // whatever the case, iterate each address represented and bind a socket for portOffset := uint(0); portOffset < na.PortRangeSize(); portOffset++ { select { case <-ctx.Done(): return nil, ctx.Err() default: } // create (or reuse) the listener ourselves var ln any ln, err = na.Listen(ctx, portOffset, config) if err != nil { return nil, err } listeners = append(listeners, ln) } return listeners, nil } // Listen is similar to net.Listen, with a few differences: // // Listen announces on the network address using the port calculated by adding // portOffset to the start port. (For network types that do not use ports, the // portOffset is ignored.) // // The provided ListenConfig is used to create the listener. Its Control function, // if set, may be wrapped by an internally-used Control function. The provided // context may be used to cancel long operations early. The context is not used // to close the listener after it has been created. // // Caddy's listeners can overlap each other: multiple listeners may be created on // the same socket at the same time. This is useful because during config changes, // the new config is started while the old config is still running. How this is // accomplished varies by platform and network type. For example, on Unix, SO_REUSEPORT // is set except on Unix sockets, for which the file descriptor is duplicated and // reused; on Windows, the close logic is virtualized using timeouts. Like normal // listeners, be sure to Close() them when you are done. // // This method returns any type, as the implementations of listeners for various // network types are not interchangeable. The type of listener returned is switched // on the network type. Stream-based networks ("tcp", "unix", "unixpacket", etc.) // return a net.Listener; datagram-based networks ("udp", "unixgram", etc.) return // a net.PacketConn; and so forth. The actual concrete types are not guaranteed to // be standard, exported types (wrapping is necessary to provide graceful reloads). // // Unix sockets will be unlinked before being created, to ensure we can bind to // it even if the previous program using it exited uncleanly; it will also be // unlinked upon a graceful exit (or when a new config does not use that socket). // // TODO: Experimental API: subject to change or removal. func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) { if na.IsUnixNetwork() { unixSocketsMu.Lock() defer unixSocketsMu.Unlock() } // check to see if plugin provides listener if ln, err := getListenerFromPlugin(ctx, na.Network, na.JoinHostPort(portOffset), config); ln != nil || err != nil { return ln, err } // create (or reuse) the listener ourselves return na.listen(ctx, portOffset, config) } func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) { var ln any var err error address := na.JoinHostPort(portOffset) // if this is a unix socket, see if we already have it open if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil { return socket, err } lnKey := listenerKey(na.Network, address) switch na.Network { case "tcp", "tcp4", "tcp6", "unix", "unixpacket": ln, err = listenTCPOrUnix(ctx, lnKey, na.Network, address, config) case "unixgram": ln, err = config.ListenPacket(ctx, na.Network, address) case "udp", "udp4", "udp6": sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { pc, err := config.ListenPacket(ctx, na.Network, address) if err != nil { return nil, err } return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil }) if err != nil { return nil, err } ln = &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)} } if strings.HasPrefix(na.Network, "ip") { ln, err = config.ListenPacket(ctx, na.Network, address) } if err != nil { return nil, err } if ln == nil { return nil, fmt.Errorf("unsupported network type: %s", na.Network) } // if new listener is a unix socket, make sure we can reuse it later // (we do our own "unlink on close" -- not required, but more tidy) one := int32(1) switch unix := ln.(type) { case *net.UnixListener: unix.SetUnlinkOnClose(false) ln = &unixListener{unix, lnKey, &one} unixSockets[lnKey] = ln.(*unixListener) case *net.UnixConn: ln = &unixConn{unix, address, lnKey, &one} unixSockets[lnKey] = ln.(*unixConn) } return ln, nil } // IsUnixNetwork returns true if na.Network is // unix, unixgram, or unixpacket. func (na NetworkAddress) IsUnixNetwork() bool { return isUnixNetwork(na.Network) } // JoinHostPort is like net.JoinHostPort, but where the port // is StartPort + offset. func (na NetworkAddress) JoinHostPort(offset uint) string { if na.IsUnixNetwork() { return na.Host } return net.JoinHostPort(na.Host, strconv.Itoa(int(na.StartPort+offset))) } // Expand returns one NetworkAddress for each port in the port range. // // This is EXPERIMENTAL and subject to change or removal. func (na NetworkAddress) Expand() []NetworkAddress { size := na.PortRangeSize() addrs := make([]NetworkAddress, size) for portOffset := uint(0); portOffset < size; portOffset++ { addrs[portOffset] = na.At(portOffset) } return addrs } // At returns a NetworkAddress with a port range of just 1 // at the given port offset; i.e. a NetworkAddress that // represents precisely 1 address only. func (na NetworkAddress) At(portOffset uint) NetworkAddress { na2 := na na2.StartPort, na2.EndPort = na.StartPort+portOffset, na.StartPort+portOffset return na2 } // PortRangeSize returns how many ports are in // pa's port range. Port ranges are inclusive, // so the size is the difference of start and // end ports plus one. func (na NetworkAddress) PortRangeSize() uint { if na.EndPort < na.StartPort { return 0 } return (na.EndPort - na.StartPort) + 1 } func (na NetworkAddress) isLoopback() bool { if na.IsUnixNetwork() { return true } if na.Host == "localhost" { return true } if ip, err := netip.ParseAddr(na.Host); err == nil { return ip.IsLoopback() } return false } func (na NetworkAddress) isWildcardInterface() bool { if na.Host == "" { return true } if ip, err := netip.ParseAddr(na.Host); err == nil { return ip.IsUnspecified() } return false } func (na NetworkAddress) port() string { if na.StartPort == na.EndPort { return strconv.FormatUint(uint64(na.StartPort), 10) } return fmt.Sprintf("%d-%d", na.StartPort, na.EndPort) } // String reconstructs the address string for human display. // The output can be parsed by ParseNetworkAddress(). If the // address is a unix socket, any non-zero port will be dropped. func (na NetworkAddress) String() string { if na.Network == "tcp" && (na.Host != "" || na.port() != "") { na.Network = "" // omit default network value for brevity } return JoinNetworkAddress(na.Network, na.Host, na.port()) } func isUnixNetwork(netw string) bool { return netw == "unix" || netw == "unixgram" || netw == "unixpacket" } // ParseNetworkAddress parses addr into its individual // components. The input string is expected to be of // the form "network/host:port-range" where any part is // optional. The default network, if unspecified, is tcp. // Port ranges are inclusive. // // Network addresses are distinct from URLs and do not // use URL syntax. func ParseNetworkAddress(addr string) (NetworkAddress, error) { var host, port string network, host, port, err := SplitNetworkAddress(addr) if err != nil { return NetworkAddress{}, err } if network == "" { network = "tcp" } if isUnixNetwork(network) { return NetworkAddress{ Network: network, Host: host, }, nil } var start, end uint64 if port != "" { before, after, found := strings.Cut(port, "-") if !found { after = before } start, err = strconv.ParseUint(before, 10, 16) if err != nil { return NetworkAddress{}, fmt.Errorf("invalid start port: %v", err) } end, err = strconv.ParseUint(after, 10, 16) if err != nil { return NetworkAddress{}, fmt.Errorf("invalid end port: %v", err) } if end < start { return NetworkAddress{}, fmt.Errorf("end port must not be less than start port") } if (end - start) > maxPortSpan { return NetworkAddress{}, fmt.Errorf("port range exceeds %d ports", maxPortSpan) } } return NetworkAddress{ Network: network, Host: host, StartPort: uint(start), EndPort: uint(end), }, nil } // SplitNetworkAddress splits a into its network, host, and port components. // Note that port may be a port range (:X-Y), or omitted for unix sockets. func SplitNetworkAddress(a string) (network, host, port string, err error) { beforeSlash, afterSlash, slashFound := strings.Cut(a, "/") if slashFound { network = strings.ToLower(strings.TrimSpace(beforeSlash)) a = afterSlash } if isUnixNetwork(network) { host = a return } host, port, err = net.SplitHostPort(a) if err == nil || a == "" { return } // in general, if there was an error, it was likely "missing port", // so try adding a bogus port to take advantage of standard library's // robust parser, then strip the artificial port before returning // (don't overwrite original error though; might still be relevant) var err2 error host, port, err2 = net.SplitHostPort(a + ":0") if err2 == nil { err = nil port = "" } return } // JoinNetworkAddress combines network, host, and port into a single // address string of the form accepted by ParseNetworkAddress(). For // unix sockets, the network should be "unix" (or "unixgram" or // "unixpacket") and the path to the socket should be given as the // host parameter. func JoinNetworkAddress(network, host, port string) string { var a string if network != "" { a = network + "/" } if (host != "" && port == "") || isUnixNetwork(network) { a += host } else if port != "" { a += net.JoinHostPort(host, port) } return a } // DEPRECATED: Use NetworkAddress.Listen instead. This function will likely be changed or removed in the future. func Listen(network, addr string) (net.Listener, error) { // a 0 timeout means Go uses its default return ListenTimeout(network, addr, 0) } // DEPRECATED: Use NetworkAddress.Listen instead. This function will likely be changed or removed in the future. func ListenTimeout(network, addr string, keepalivePeriod time.Duration) (net.Listener, error) { netAddr, err := ParseNetworkAddress(JoinNetworkAddress(network, addr, "")) if err != nil { return nil, err } ln, err := netAddr.Listen(context.TODO(), 0, net.ListenConfig{KeepAlive: keepalivePeriod}) if err != nil { return nil, err } return ln.(net.Listener), nil } // DEPRECATED: Use NetworkAddress.Listen instead. This function will likely be changed or removed in the future. func ListenPacket(network, addr string) (net.PacketConn, error) { netAddr, err := ParseNetworkAddress(JoinNetworkAddress(network, addr, "")) if err != nil { return nil, err } ln, err := netAddr.Listen(context.TODO(), 0, net.ListenConfig{}) if err != nil { return nil, err } return ln.(net.PacketConn), nil } // ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module. // The network will be transformed into a QUIC-compatible type (if unix, then // unixgram will be used; otherwise, udp will be used). // // NOTE: This API is EXPERIMENTAL and may be changed or removed. // // TODO: See if we can find a more elegant solution closer to the new NetworkAddress.Listen API. func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) { lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String()) sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(tlsConf), &quic.Config{ RequireAddressValidation: func(clientAddr net.Addr) bool { var highLoad bool if activeRequests != nil { highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable? } return highLoad }, }) if err != nil { return nil, err } return &sharedQuicListener{EarlyListener: earlyLn, key: lnKey}, nil }) if err != nil { return nil, err } // TODO: to serve QUIC over a unix socket, currently we need to hold onto // the underlying net.PacketConn (which we wrap as unixConn to keep count // of closes) because closing the quic.EarlyListener doesn't actually close // the underlying PacketConn, but we need to for unix sockets since we dup // the file descriptor and thus need to close the original; track issue: // https://github.com/lucas-clemente/quic-go/issues/3560#issuecomment-1258959608 var unix *unixConn if uc, ok := ln.(*unixConn); ok { unix = uc } ctx, cancel := context.WithCancel(context.Background()) return &fakeCloseQuicListener{ sharedQuicListener: sharedEarlyListener.(*sharedQuicListener), uc: unix, context: ctx, contextCancel: cancel, }, nil } // ListenerUsage returns the current usage count of the given listener address. func ListenerUsage(network, addr string) int { count, _ := listenerPool.References(listenerKey(network, addr)) return count } // sharedQuicListener is like sharedListener, but for quic.EarlyListeners. type sharedQuicListener struct { quic.EarlyListener key string } // Destruct closes the underlying QUIC listener. func (sql *sharedQuicListener) Destruct() error { return sql.EarlyListener.Close() } // sharedPacketConn is like sharedListener, but for net.PacketConns. type sharedPacketConn struct { net.PacketConn key string } // Destruct closes the underlying socket. func (spc *sharedPacketConn) Destruct() error { return spc.PacketConn.Close() } // fakeClosedErr returns an error value that is not temporary // nor a timeout, suitable for making the caller think the // listener is actually closed func fakeClosedErr(l interface{ Addr() net.Addr }) error { return &net.OpError{ Op: "accept", Net: l.Addr().Network(), Addr: l.Addr(), Err: errFakeClosed, } } // errFakeClosed is the underlying error value returned by // fakeCloseListener.Accept() after Close() has been called, // indicating that it is pretending to be closed so that the // server using it can terminate, while the underlying // socket is actually left open. var errFakeClosed = fmt.Errorf("listener 'closed' 😉") // fakeClosePacketConn is like fakeCloseListener, but for PacketConns. type fakeClosePacketConn struct { closed int32 // accessed atomically; belongs to this struct only *sharedPacketConn // embedded, so we also become a net.PacketConn } func (fcpc *fakeClosePacketConn) Close() error { if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) { _, _ = listenerPool.Delete(fcpc.sharedPacketConn.key) } return nil } // Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998 func (fcpc fakeClosePacketConn) SetReadBuffer(bytes int) error { if conn, ok := fcpc.PacketConn.(interface{ SetReadBuffer(int) error }); ok { return conn.SetReadBuffer(bytes) } return fmt.Errorf("SetReadBuffer() not implemented for %T", fcpc.PacketConn) } // Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998 func (fcpc fakeClosePacketConn) SyscallConn() (syscall.RawConn, error) { if conn, ok := fcpc.PacketConn.(interface { SyscallConn() (syscall.RawConn, error) }); ok { return conn.SyscallConn() } return nil, fmt.Errorf("SyscallConn() not implemented for %T", fcpc.PacketConn) } type fakeCloseQuicListener struct { closed int32 // accessed atomically; belongs to this struct only *sharedQuicListener // embedded, so we also become a quic.EarlyListener uc *unixConn // underlying unix socket, if UDS context context.Context contextCancel context.CancelFunc } // Currently Accept ignores the passed context, however a situation where // someone would need a hotswappable QUIC-only (not http3, since it uses context.Background here) // server on which Accept would be called with non-empty contexts // (mind that the default net listeners' Accept doesn't take a context argument) // sounds way too rare for us to sacrifice efficiency here. func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlyConnection, error) { conn, err := fcql.sharedQuicListener.Accept(fcql.context) if err == nil { return conn, nil } // if the listener is "closed", return a fake closed error instead if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) { return nil, fakeClosedErr(fcql) } return nil, err } func (fcql *fakeCloseQuicListener) Close() error { if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) { fcql.contextCancel() _, _ = listenerPool.Delete(fcql.sharedQuicListener.key) if fcql.uc != nil { // unix sockets need to be closed ourselves because we dup() the file // descriptor when we reuse them, so this avoids a resource leak fcql.uc.Close() } } return nil } // RegisterNetwork registers a network type with Caddy so that if a listener is // created for that network type, getListener will be invoked to get the listener. // This should be called during init() and will panic if the network type is standard // or reserved, or if it is already registered. EXPERIMENTAL and subject to change. func RegisterNetwork(network string, getListener ListenerFunc) { network = strings.TrimSpace(strings.ToLower(network)) if network == "tcp" || network == "tcp4" || network == "tcp6" || network == "udp" || network == "udp4" || network == "udp6" || network == "unix" || network == "unixpacket" || network == "unixgram" || strings.HasPrefix("ip:", network) || strings.HasPrefix("ip4:", network) || strings.HasPrefix("ip6:", network) { panic("network type " + network + " is reserved") } if _, ok := networkTypes[strings.ToLower(network)]; ok { panic("network type " + network + " is already registered") } networkTypes[network] = getListener } type unixListener struct { *net.UnixListener mapKey string count *int32 // accessed atomically } func (uln *unixListener) Close() error { newCount := atomic.AddInt32(uln.count, -1) if newCount == 0 { defer func() { addr := uln.Addr().String() unixSocketsMu.Lock() delete(unixSockets, uln.mapKey) unixSocketsMu.Unlock() _ = syscall.Unlink(addr) }() } return uln.UnixListener.Close() } type unixConn struct { *net.UnixConn filename string mapKey string count *int32 // accessed atomically } func (uc *unixConn) Close() error { newCount := atomic.AddInt32(uc.count, -1) if newCount == 0 { defer func() { unixSocketsMu.Lock() delete(unixSockets, uc.mapKey) unixSocketsMu.Unlock() _ = syscall.Unlink(uc.filename) }() } return uc.UnixConn.Close() } // unixSockets keeps track of the currently-active unix sockets // so we can transfer their FDs gracefully during reloads. var ( unixSockets = make(map[string]interface { File() (*os.File, error) }) unixSocketsMu sync.Mutex ) // getListenerFromPlugin returns a listener on the given network and address // if a plugin has registered the network name. It may return (nil, nil) if // no plugin can provide a listener. func getListenerFromPlugin(ctx context.Context, network, addr string, config net.ListenConfig) (any, error) { // get listener from plugin if network type is registered if getListener, ok := networkTypes[network]; ok { Log().Debug("getting listener from plugin", zap.String("network", network)) return getListener(ctx, network, addr, config) } return nil, nil } func listenerKey(network, addr string) string { return network + "/" + addr } // ListenerFunc is a function that can return a listener given a network and address. // The listeners must be capable of overlapping: with Caddy, new configs are loaded // before old ones are unloaded, so listeners may overlap briefly if the configs // both need the same listener. EXPERIMENTAL and subject to change. type ListenerFunc func(ctx context.Context, network, addr string, cfg net.ListenConfig) (any, error) var networkTypes = map[string]ListenerFunc{} // ListenerWrapper is a type that wraps a listener // so it can modify the input listener's methods. // Modules that implement this interface are found // in the caddy.listeners namespace. Usually, to // wrap a listener, you will define your own struct // type that embeds the input listener, then // implement your own methods that you want to wrap, // calling the underlying listener's methods where // appropriate. type ListenerWrapper interface { WrapListener(net.Listener) net.Listener } // listenerPool stores and allows reuse of active listeners. var listenerPool = NewUsagePool() const maxPortSpan = 65535 // Interface guards (see https://github.com/caddyserver/caddy/issues/3998) var ( _ (interface{ SetReadBuffer(int) error }) = (*fakeClosePacketConn)(nil) _ (interface { SyscallConn() (syscall.RawConn, error) }) = (*fakeClosePacketConn)(nil) ) caddy-2.6.2/listeners_fuzz.go000066400000000000000000000014161435007237400162310ustar00rootroot00000000000000// 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. //go:build gofuzz package caddy func FuzzParseNetworkAddress(data []byte) int { _, err := ParseNetworkAddress(string(data)) if err != nil { return 0 } return 1 } caddy-2.6.2/listeners_test.go000066400000000000000000000203321435007237400162100ustar00rootroot00000000000000// 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 ( "reflect" "testing" ) func TestSplitNetworkAddress(t *testing.T) { for i, tc := range []struct { input string expectNetwork string expectHost string expectPort string expectErr bool }{ { input: "", expectErr: true, }, { input: "foo", expectHost: "foo", }, { input: ":", // empty host & empty port }, { input: "::", expectErr: true, }, { input: "[::]", expectHost: "::", }, { input: ":1234", expectPort: "1234", }, { input: "foo:1234", expectHost: "foo", expectPort: "1234", }, { input: "foo:1234-5678", expectHost: "foo", expectPort: "1234-5678", }, { input: "udp/foo:1234", expectNetwork: "udp", expectHost: "foo", expectPort: "1234", }, { input: "tcp6/foo:1234-5678", expectNetwork: "tcp6", expectHost: "foo", expectPort: "1234-5678", }, { input: "udp/", expectNetwork: "udp", expectErr: true, }, { input: "unix//foo/bar", expectNetwork: "unix", expectHost: "/foo/bar", }, { input: "unixgram//foo/bar", expectNetwork: "unixgram", expectHost: "/foo/bar", }, { input: "unixpacket//foo/bar", expectNetwork: "unixpacket", expectHost: "/foo/bar", }, } { actualNetwork, actualHost, actualPort, err := SplitNetworkAddress(tc.input) if tc.expectErr && err == nil { t.Errorf("Test %d: Expected error but got %v", i, err) } if !tc.expectErr && err != nil { t.Errorf("Test %d: Expected no error but got %v", i, err) } if actualNetwork != tc.expectNetwork { t.Errorf("Test %d: Expected network '%s' but got '%s'", i, tc.expectNetwork, actualNetwork) } if actualHost != tc.expectHost { t.Errorf("Test %d: Expected host '%s' but got '%s'", i, tc.expectHost, actualHost) } if actualPort != tc.expectPort { t.Errorf("Test %d: Expected port '%s' but got '%s'", i, tc.expectPort, actualPort) } } } func TestJoinNetworkAddress(t *testing.T) { for i, tc := range []struct { network, host, port string expect string }{ { network: "", host: "", port: "", expect: "", }, { network: "tcp", host: "", port: "", expect: "tcp/", }, { network: "", host: "foo", port: "", expect: "foo", }, { network: "", host: "", port: "1234", expect: ":1234", }, { network: "", host: "", port: "1234-5678", expect: ":1234-5678", }, { network: "", host: "foo", port: "1234", expect: "foo:1234", }, { network: "udp", host: "foo", port: "1234", expect: "udp/foo:1234", }, { network: "udp", host: "", port: "1234", expect: "udp/:1234", }, { network: "unix", host: "/foo/bar", port: "", expect: "unix//foo/bar", }, { network: "unix", host: "/foo/bar", port: "0", expect: "unix//foo/bar", }, { network: "unix", host: "/foo/bar", port: "1234", expect: "unix//foo/bar", }, { network: "", host: "::1", port: "1234", expect: "[::1]:1234", }, } { actual := JoinNetworkAddress(tc.network, tc.host, tc.port) if actual != tc.expect { t.Errorf("Test %d: Expected '%s' but got '%s'", i, tc.expect, actual) } } } func TestParseNetworkAddress(t *testing.T) { for i, tc := range []struct { input string expectAddr NetworkAddress expectErr bool }{ { input: "", expectErr: true, }, { input: ":", expectAddr: NetworkAddress{ Network: "tcp", }, }, { input: "[::]", expectAddr: NetworkAddress{ Network: "tcp", Host: "::", }, }, { input: ":1234", expectAddr: NetworkAddress{ Network: "tcp", Host: "", StartPort: 1234, EndPort: 1234, }, }, { input: "tcp/:1234", expectAddr: NetworkAddress{ Network: "tcp", Host: "", StartPort: 1234, EndPort: 1234, }, }, { input: "tcp6/:1234", expectAddr: NetworkAddress{ Network: "tcp6", Host: "", StartPort: 1234, EndPort: 1234, }, }, { input: "tcp4/localhost:1234", expectAddr: NetworkAddress{ Network: "tcp4", Host: "localhost", StartPort: 1234, EndPort: 1234, }, }, { input: "unix//foo/bar", expectAddr: NetworkAddress{ Network: "unix", Host: "/foo/bar", }, }, { input: "localhost:1234-1234", expectAddr: NetworkAddress{ Network: "tcp", Host: "localhost", StartPort: 1234, EndPort: 1234, }, }, { input: "localhost:2-1", expectErr: true, }, { input: "localhost:0", expectAddr: NetworkAddress{ Network: "tcp", Host: "localhost", StartPort: 0, EndPort: 0, }, }, { input: "localhost:1-999999999999", expectErr: true, }, } { actualAddr, err := ParseNetworkAddress(tc.input) if tc.expectErr && err == nil { t.Errorf("Test %d: Expected error but got: %v", i, err) } if !tc.expectErr && err != nil { t.Errorf("Test %d: Expected no error but got: %v", i, err) } if actualAddr.Network != tc.expectAddr.Network { t.Errorf("Test %d: Expected network '%v' but got '%v'", i, tc.expectAddr, actualAddr) } if !reflect.DeepEqual(tc.expectAddr, actualAddr) { t.Errorf("Test %d: Expected addresses %v but got %v", i, tc.expectAddr, actualAddr) } } } func TestJoinHostPort(t *testing.T) { for i, tc := range []struct { pa NetworkAddress offset uint expect string }{ { pa: NetworkAddress{ Network: "tcp", Host: "localhost", StartPort: 1234, EndPort: 1234, }, expect: "localhost:1234", }, { pa: NetworkAddress{ Network: "tcp", Host: "localhost", StartPort: 1234, EndPort: 1235, }, expect: "localhost:1234", }, { pa: NetworkAddress{ Network: "tcp", Host: "localhost", StartPort: 1234, EndPort: 1235, }, offset: 1, expect: "localhost:1235", }, { pa: NetworkAddress{ Network: "unix", Host: "/run/php/php7.3-fpm.sock", }, expect: "/run/php/php7.3-fpm.sock", }, } { actual := tc.pa.JoinHostPort(tc.offset) if actual != tc.expect { t.Errorf("Test %d: Expected '%s' but got '%s'", i, tc.expect, actual) } } } func TestExpand(t *testing.T) { for i, tc := range []struct { input NetworkAddress expect []NetworkAddress }{ { input: NetworkAddress{ Network: "tcp", Host: "localhost", StartPort: 2000, EndPort: 2000, }, expect: []NetworkAddress{ { Network: "tcp", Host: "localhost", StartPort: 2000, EndPort: 2000, }, }, }, { input: NetworkAddress{ Network: "tcp", Host: "localhost", StartPort: 2000, EndPort: 2002, }, expect: []NetworkAddress{ { Network: "tcp", Host: "localhost", StartPort: 2000, EndPort: 2000, }, { Network: "tcp", Host: "localhost", StartPort: 2001, EndPort: 2001, }, { Network: "tcp", Host: "localhost", StartPort: 2002, EndPort: 2002, }, }, }, { input: NetworkAddress{ Network: "tcp", Host: "localhost", StartPort: 2000, EndPort: 1999, }, expect: []NetworkAddress{}, }, { input: NetworkAddress{ Network: "unix", Host: "/foo/bar", StartPort: 0, EndPort: 0, }, expect: []NetworkAddress{ { Network: "unix", Host: "/foo/bar", StartPort: 0, EndPort: 0, }, }, }, } { actual := tc.input.Expand() if !reflect.DeepEqual(actual, tc.expect) { t.Errorf("Test %d: Expected %+v but got %+v", i, tc.expect, actual) } } } caddy-2.6.2/logging.go000066400000000000000000000522161435007237400145750ustar00rootroot00000000000000// 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" "io" "log" "os" "strings" "sync" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" "golang.org/x/term" ) func init() { RegisterModule(StdoutWriter{}) RegisterModule(StderrWriter{}) RegisterModule(DiscardWriter{}) } // Logging facilitates logging within Caddy. The default log is // called "default" and you can customize it. You can also define // additional logs. // // By default, all logs at INFO level and higher are written to // standard error ("stderr" writer) in a human-readable format // ("console" encoder if stdout is an interactive terminal, "json" // encoder otherwise). // // All defined logs accept all log entries by default, but you // can filter by level and module/logger names. A logger's name // is the same as the module's name, but a module may append to // logger names for more specificity. For example, you can // filter logs emitted only by HTTP handlers using the name // "http.handlers", because all HTTP handler module names have // that prefix. // // Caddy logs (except the sink) are zero-allocation, so they are // very high-performing in terms of memory and CPU time. Enabling // sampling can further increase throughput on extremely high-load // servers. type Logging struct { // Sink is the destination for all unstructured logs emitted // from Go's standard library logger. These logs are common // in dependencies that are not designed specifically for use // in Caddy. Because it is global and unstructured, the sink // lacks most advanced features and customizations. Sink *StandardLibLog `json:"sink,omitempty"` // Logs are your logs, keyed by an arbitrary name of your // choosing. The default log can be customized by defining // a log called "default". You can further define other logs // and filter what kinds of entries they accept. Logs map[string]*CustomLog `json:"logs,omitempty"` // a list of all keys for open writers; all writers // that are opened to provision this logging config // must have their keys added to this list so they // can be closed when cleaning up writerKeys []string } // openLogs sets up the config and opens all the configured writers. // It closes its logs when ctx is canceled, so it should clean up // after itself. func (logging *Logging) openLogs(ctx Context) error { // make sure to deallocate resources when context is done ctx.OnCancel(func() { err := logging.closeLogs() if err != nil { Log().Error("closing logs", zap.Error(err)) } }) // set up the "sink" log first (std lib's default global logger) if logging.Sink != nil { err := logging.Sink.provision(ctx, logging) if err != nil { return fmt.Errorf("setting up sink log: %v", err) } } // as a special case, set up the default structured Caddy log next if err := logging.setupNewDefault(ctx); err != nil { return err } // then set up any other custom logs for name, l := range logging.Logs { // the default log is already set up if name == DefaultLoggerName { continue } err := l.provision(ctx, logging) if err != nil { return fmt.Errorf("setting up custom log '%s': %v", name, err) } // Any other logs that use the discard writer can be deleted // entirely. This avoids encoding and processing of each // log entry that would just be thrown away anyway. Notably, // we do not reach this point for the default log, which MUST // exist, otherwise core log emissions would panic because // they use the Log() function directly which expects a non-nil // logger. Even if we keep logs with a discard writer, they // have a nop core, and keeping them at all seems unnecessary. if _, ok := l.writerOpener.(*DiscardWriter); ok { delete(logging.Logs, name) continue } } return nil } func (logging *Logging) setupNewDefault(ctx Context) error { if logging.Logs == nil { logging.Logs = make(map[string]*CustomLog) } // extract the user-defined default log, if any newDefault := new(defaultCustomLog) if userDefault, ok := logging.Logs[DefaultLoggerName]; ok { newDefault.CustomLog = userDefault } else { // if none, make one with our own default settings var err error newDefault, err = newDefaultProductionLog() if err != nil { return fmt.Errorf("setting up default Caddy log: %v", err) } logging.Logs[DefaultLoggerName] = newDefault.CustomLog } // set up this new log err := newDefault.CustomLog.provision(ctx, logging) if err != nil { return fmt.Errorf("setting up default log: %v", err) } newDefault.logger = zap.New(newDefault.CustomLog.core) // redirect the default caddy logs defaultLoggerMu.Lock() oldDefault := defaultLogger defaultLogger = newDefault defaultLoggerMu.Unlock() // if the new writer is different, indicate it in the logs for convenience var newDefaultLogWriterKey, currentDefaultLogWriterKey string var newDefaultLogWriterStr, currentDefaultLogWriterStr string if newDefault.writerOpener != nil { newDefaultLogWriterKey = newDefault.writerOpener.WriterKey() newDefaultLogWriterStr = newDefault.writerOpener.String() } if oldDefault.writerOpener != nil { currentDefaultLogWriterKey = oldDefault.writerOpener.WriterKey() currentDefaultLogWriterStr = oldDefault.writerOpener.String() } if newDefaultLogWriterKey != currentDefaultLogWriterKey { oldDefault.logger.Info("redirected default logger", zap.String("from", currentDefaultLogWriterStr), zap.String("to", newDefaultLogWriterStr), ) } return nil } // closeLogs cleans up resources allocated during openLogs. // A successful call to openLogs calls this automatically // when the context is canceled. func (logging *Logging) closeLogs() error { for _, key := range logging.writerKeys { _, err := writers.Delete(key) if err != nil { log.Printf("[ERROR] Closing log writer %v: %v", key, err) } } return nil } // Logger returns a logger that is ready for the module to use. func (logging *Logging) Logger(mod Module) *zap.Logger { modID := string(mod.CaddyModule().ID) var cores []zapcore.Core if logging != nil { for _, l := range logging.Logs { if l.matchesModule(modID) { if len(l.Include) == 0 && len(l.Exclude) == 0 { cores = append(cores, l.core) continue } cores = append(cores, &filteringCore{Core: l.core, cl: l}) } } } multiCore := zapcore.NewTee(cores...) return zap.New(multiCore).Named(modID) } // openWriter opens a writer using opener, and returns true if // the writer is new, or false if the writer already exists. func (logging *Logging) openWriter(opener WriterOpener) (io.WriteCloser, bool, error) { key := opener.WriterKey() writer, loaded, err := writers.LoadOrNew(key, func() (Destructor, error) { w, err := opener.OpenWriter() return writerDestructor{w}, err }) if err != nil { return nil, false, err } logging.writerKeys = append(logging.writerKeys, key) return writer.(io.WriteCloser), !loaded, nil } // WriterOpener is a module that can open a log writer. // It can return a human-readable string representation // of itself so that operators can understand where // the logs are going. type WriterOpener interface { fmt.Stringer // WriterKey is a string that uniquely identifies this // writer configuration. It is not shown to humans. WriterKey() string // OpenWriter opens a log for writing. The writer // should be safe for concurrent use but need not // be synchronous. OpenWriter() (io.WriteCloser, error) } type writerDestructor struct { io.WriteCloser } func (wdest writerDestructor) Destruct() error { return wdest.Close() } // StandardLibLog configures the default Go standard library // global logger in the log package. This is necessary because // module dependencies which are not built specifically for // Caddy will use the standard logger. This is also known as // the "sink" logger. type StandardLibLog struct { // The module that writes out log entries for the sink. WriterRaw json.RawMessage `json:"writer,omitempty" caddy:"namespace=caddy.logging.writers inline_key=output"` writer io.WriteCloser } func (sll *StandardLibLog) provision(ctx Context, logging *Logging) error { if sll.WriterRaw != nil { mod, err := ctx.LoadModule(sll, "WriterRaw") if err != nil { return fmt.Errorf("loading sink log writer module: %v", err) } wo := mod.(WriterOpener) var isNew bool sll.writer, isNew, err = logging.openWriter(wo) if err != nil { return fmt.Errorf("opening sink log writer %#v: %v", mod, err) } if isNew { log.Printf("[INFO] Redirecting sink to: %s", wo) log.SetOutput(sll.writer) log.Printf("[INFO] Redirected sink to here (%s)", wo) } } return nil } // CustomLog represents a custom logger configuration. // // By default, a log will emit all log entries. Some entries // will be skipped if sampling is enabled. Further, the Include // and Exclude parameters define which loggers (by name) are // allowed or rejected from emitting in this log. If both Include // and Exclude are populated, their values must be mutually // exclusive, and longer namespaces have priority. If neither // are populated, all logs are emitted. type CustomLog struct { // The writer defines where log entries are emitted. WriterRaw json.RawMessage `json:"writer,omitempty" caddy:"namespace=caddy.logging.writers inline_key=output"` // The encoder is how the log entries are formatted or encoded. EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"` // Level is the minimum level to emit, and is inclusive. // Possible levels: DEBUG, INFO, WARN, ERROR, PANIC, and FATAL Level string `json:"level,omitempty"` // Sampling configures log entry sampling. If enabled, // only some log entries will be emitted. This is useful // for improving performance on extremely high-pressure // servers. Sampling *LogSampling `json:"sampling,omitempty"` // Include defines the names of loggers to emit in this // log. For example, to include only logs emitted by the // admin API, you would include "admin.api". Include []string `json:"include,omitempty"` // Exclude defines the names of loggers that should be // skipped by this log. For example, to exclude only // HTTP access logs, you would exclude "http.log.access". Exclude []string `json:"exclude,omitempty"` writerOpener WriterOpener writer io.WriteCloser encoder zapcore.Encoder levelEnabler zapcore.LevelEnabler core zapcore.Core } func (cl *CustomLog) provision(ctx Context, logging *Logging) error { // Replace placeholder for log level repl := NewReplacer() level, err := repl.ReplaceOrErr(cl.Level, true, true) if err != nil { return fmt.Errorf("invalid log level: %v", err) } level = strings.ToLower(level) // set up the log level switch level { case "debug": cl.levelEnabler = zapcore.DebugLevel case "", "info": cl.levelEnabler = zapcore.InfoLevel case "warn": cl.levelEnabler = zapcore.WarnLevel case "error": cl.levelEnabler = zapcore.ErrorLevel case "panic": cl.levelEnabler = zapcore.PanicLevel case "fatal": cl.levelEnabler = zapcore.FatalLevel default: return fmt.Errorf("unrecognized log level: %s", cl.Level) } // If both Include and Exclude lists are populated, then each item must // be a superspace or subspace of an item in the other list, because // populating both lists means that any given item is either a rule // or an exception to another rule. But if the item is not a super- // or sub-space of any item in the other list, it is neither a rule // nor an exception, and is a contradiction. Ensure, too, that the // sets do not intersect, which is also a contradiction. if len(cl.Include) > 0 && len(cl.Exclude) > 0 { // prevent intersections for _, allow := range cl.Include { for _, deny := range cl.Exclude { if allow == deny { return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow) } } } // ensure namespaces are nested outer: for _, allow := range cl.Include { for _, deny := range cl.Exclude { if strings.HasPrefix(allow+".", deny+".") || strings.HasPrefix(deny+".", allow+".") { continue outer } } return fmt.Errorf("when both include and exclude are populated, each element must be a superspace or subspace of one in the other list; check '%s' in include", allow) } } if cl.WriterRaw != nil { mod, err := ctx.LoadModule(cl, "WriterRaw") if err != nil { return fmt.Errorf("loading log writer module: %v", err) } cl.writerOpener = mod.(WriterOpener) } if cl.writerOpener == nil { cl.writerOpener = StderrWriter{} } cl.writer, _, err = logging.openWriter(cl.writerOpener) if err != nil { return fmt.Errorf("opening log writer using %#v: %v", cl.writerOpener, err) } if cl.EncoderRaw != nil { mod, err := ctx.LoadModule(cl, "EncoderRaw") if err != nil { return fmt.Errorf("loading log encoder module: %v", err) } cl.encoder = mod.(zapcore.Encoder) } if cl.encoder == nil { // only allow colorized output if this log is going to stdout or stderr var colorize bool switch cl.writerOpener.(type) { case StdoutWriter, StderrWriter, *StdoutWriter, *StderrWriter: colorize = true } cl.encoder = newDefaultProductionLogEncoder(colorize) } cl.buildCore() return nil } func (cl *CustomLog) buildCore() { // logs which only discard their output don't need // to perform encoding or any other processing steps // at all, so just shorcut to a nop core instead if _, ok := cl.writerOpener.(*DiscardWriter); ok { cl.core = zapcore.NewNopCore() return } c := zapcore.NewCore( cl.encoder, zapcore.AddSync(cl.writer), cl.levelEnabler, ) if cl.Sampling != nil { if cl.Sampling.Interval == 0 { cl.Sampling.Interval = 1 * time.Second } if cl.Sampling.First == 0 { cl.Sampling.First = 100 } if cl.Sampling.Thereafter == 0 { cl.Sampling.Thereafter = 100 } c = zapcore.NewSamplerWithOptions(c, cl.Sampling.Interval, cl.Sampling.First, cl.Sampling.Thereafter) } cl.core = c } func (cl *CustomLog) matchesModule(moduleID string) bool { return cl.loggerAllowed(moduleID, true) } // loggerAllowed returns true if name is allowed to emit // to cl. isModule should be true if name is the name of // a module and you want to see if ANY of that module's // logs would be permitted. func (cl *CustomLog) loggerAllowed(name string, isModule bool) bool { // accept all loggers by default if len(cl.Include) == 0 && len(cl.Exclude) == 0 { return true } // append a dot so that partial names don't match // (i.e. we don't want "foo.b" to match "foo.bar"); we // will also have to append a dot when we do HasPrefix // below to compensate for when when namespaces are equal if name != "" && name != "*" && name != "." { name += "." } var longestAccept, longestReject int if len(cl.Include) > 0 { for _, namespace := range cl.Include { var hasPrefix bool if isModule { hasPrefix = strings.HasPrefix(namespace+".", name) } else { hasPrefix = strings.HasPrefix(name, namespace+".") } if hasPrefix && len(namespace) > longestAccept { longestAccept = len(namespace) } } // the include list was populated, meaning that // a match in this list is absolutely required // if we are to accept the entry if longestAccept == 0 { return false } } if len(cl.Exclude) > 0 { for _, namespace := range cl.Exclude { // * == all logs emitted by modules // . == all logs emitted by core if (namespace == "*" && name != ".") || (namespace == "." && name == ".") { return false } if strings.HasPrefix(name, namespace+".") && len(namespace) > longestReject { longestReject = len(namespace) } } // the reject list is populated, so we have to // reject this entry if its match is better // than the best from the accept list if longestReject > longestAccept { return false } } return (longestAccept > longestReject) || (len(cl.Include) == 0 && longestReject == 0) } // filteringCore filters log entries based on logger name, // according to the rules of a CustomLog. type filteringCore struct { zapcore.Core cl *CustomLog } // With properly wraps With. func (fc *filteringCore) With(fields []zapcore.Field) zapcore.Core { return &filteringCore{ Core: fc.Core.With(fields), cl: fc.cl, } } // Check only allows the log entry if its logger name // is allowed from the include/exclude rules of fc.cl. func (fc *filteringCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { if fc.cl.loggerAllowed(e.LoggerName, false) { return fc.Core.Check(e, ce) } return ce } // LogSampling configures log entry sampling. type LogSampling struct { // The window over which to conduct sampling. Interval time.Duration `json:"interval,omitempty"` // Log this many entries within a given level and // message for each interval. First int `json:"first,omitempty"` // If more entries with the same level and message // are seen during the same interval, keep one in // this many entries until the end of the interval. Thereafter int `json:"thereafter,omitempty"` } type ( // StdoutWriter writes logs to standard out. StdoutWriter struct{} // StderrWriter writes logs to standard error. StderrWriter struct{} // DiscardWriter discards all writes. DiscardWriter struct{} ) // CaddyModule returns the Caddy module information. func (StdoutWriter) CaddyModule() ModuleInfo { return ModuleInfo{ ID: "caddy.logging.writers.stdout", New: func() Module { return new(StdoutWriter) }, } } // CaddyModule returns the Caddy module information. func (StderrWriter) CaddyModule() ModuleInfo { return ModuleInfo{ ID: "caddy.logging.writers.stderr", New: func() Module { return new(StderrWriter) }, } } // CaddyModule returns the Caddy module information. func (DiscardWriter) CaddyModule() ModuleInfo { return ModuleInfo{ ID: "caddy.logging.writers.discard", New: func() Module { return new(DiscardWriter) }, } } func (StdoutWriter) String() string { return "stdout" } func (StderrWriter) String() string { return "stderr" } func (DiscardWriter) String() string { return "discard" } // WriterKey returns a unique key representing stdout. func (StdoutWriter) WriterKey() string { return "std:out" } // WriterKey returns a unique key representing stderr. func (StderrWriter) WriterKey() string { return "std:err" } // WriterKey returns a unique key representing discard. func (DiscardWriter) WriterKey() string { return "discard" } // OpenWriter returns os.Stdout that can't be closed. func (StdoutWriter) OpenWriter() (io.WriteCloser, error) { return notClosable{os.Stdout}, nil } // OpenWriter returns os.Stderr that can't be closed. func (StderrWriter) OpenWriter() (io.WriteCloser, error) { return notClosable{os.Stderr}, nil } // OpenWriter returns io.Discard that can't be closed. func (DiscardWriter) OpenWriter() (io.WriteCloser, error) { return notClosable{io.Discard}, nil } // notClosable is an io.WriteCloser that can't be closed. type notClosable struct{ io.Writer } func (fc notClosable) Close() error { return nil } type defaultCustomLog struct { *CustomLog logger *zap.Logger } // newDefaultProductionLog configures a custom log that is // intended for use by default if no other log is specified // in a config. It writes to stderr, uses the console encoder, // and enables INFO-level logs and higher. func newDefaultProductionLog() (*defaultCustomLog, error) { cl := new(CustomLog) cl.writerOpener = StderrWriter{} var err error cl.writer, err = cl.writerOpener.OpenWriter() if err != nil { return nil, err } cl.encoder = newDefaultProductionLogEncoder(true) cl.levelEnabler = zapcore.InfoLevel cl.buildCore() logger := zap.New(cl.core) // capture logs from other libraries which // may not be using zap logging directly _ = zap.RedirectStdLog(logger) return &defaultCustomLog{ CustomLog: cl, logger: logger, }, nil } func newDefaultProductionLogEncoder(colorize bool) zapcore.Encoder { encCfg := zap.NewProductionEncoderConfig() if term.IsTerminal(int(os.Stdout.Fd())) { // if interactive terminal, make output more human-readable by default encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) { encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000")) } if colorize { encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder } return zapcore.NewConsoleEncoder(encCfg) } return zapcore.NewJSONEncoder(encCfg) } // Log returns the current default logger. func Log() *zap.Logger { defaultLoggerMu.RLock() defer defaultLoggerMu.RUnlock() return defaultLogger.logger } var ( defaultLogger, _ = newDefaultProductionLog() defaultLoggerMu sync.RWMutex ) var writers = NewUsagePool() const DefaultLoggerName = "default" // Interface guards var ( _ io.WriteCloser = (*notClosable)(nil) _ WriterOpener = (*StdoutWriter)(nil) _ WriterOpener = (*StderrWriter)(nil) ) caddy-2.6.2/metrics.go000066400000000000000000000037511435007237400146150ustar00rootroot00000000000000package caddy import ( "net/http" "github.com/caddyserver/caddy/v2/internal/metrics" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promauto" ) // define and register the metrics used in this package. func init() { prometheus.MustRegister(collectors.NewBuildInfoCollector()) const ns, sub = "caddy", "admin" adminMetrics.requestCount = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: ns, Subsystem: sub, Name: "http_requests_total", Help: "Counter of requests made to the Admin API's HTTP endpoints.", }, []string{"handler", "path", "code", "method"}) adminMetrics.requestErrors = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: ns, Subsystem: sub, Name: "http_request_errors_total", Help: "Number of requests resulting in middleware errors.", }, []string{"handler", "path", "method"}) } // adminMetrics is a collection of metrics that can be tracked for the admin API. var adminMetrics = struct { requestCount *prometheus.CounterVec requestErrors *prometheus.CounterVec }{} // Similar to promhttp.InstrumentHandlerCounter, but upper-cases method names // instead of lower-casing them. // // Unlike promhttp.InstrumentHandlerCounter, this assumes a "code" and "method" // label is present, and will panic otherwise. func instrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d := newDelegator(w) next.ServeHTTP(d, r) counter.With(prometheus.Labels{ "code": metrics.SanitizeCode(d.status), "method": metrics.SanitizeMethod(r.Method), }).Inc() }) } func newDelegator(w http.ResponseWriter) *delegator { return &delegator{ ResponseWriter: w, } } type delegator struct { http.ResponseWriter status int } func (d *delegator) WriteHeader(code int) { d.status = code d.ResponseWriter.WriteHeader(code) } caddy-2.6.2/modules.go000066400000000000000000000272411435007237400146170ustar00rootroot00000000000000// 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" "encoding/json" "fmt" "reflect" "sort" "strings" "sync" ) // Module is a type that is used as a Caddy module. In // addition to this interface, most modules will implement // some interface expected by their host module in order // to be useful. To learn which interface(s) to implement, // see the documentation for the host module. At a bare // minimum, this interface, when implemented, only provides // the module's ID and constructor function. // // Modules will often implement additional interfaces // including Provisioner, Validator, and CleanerUpper. // If a module implements these interfaces, their // methods are called during the module's lifespan. // // When a module is loaded by a host module, the following // happens: 1) ModuleInfo.New() is called to get a new // instance of the module. 2) The module's configuration is // unmarshaled into that instance. 3) If the module is a // Provisioner, the Provision() method is called. 4) If the // module is a Validator, the Validate() method is called. // 5) The module will probably be type-asserted from // 'any' to some other, more useful interface expected // by the host module. For example, HTTP handler modules are // type-asserted as caddyhttp.MiddlewareHandler values. // 6) When a module's containing Context is canceled, if it is // a CleanerUpper, its Cleanup() method is called. type Module interface { // This method indicates that the type is a Caddy // module. The returned ModuleInfo must have both // a name and a constructor function. This method // must not have any side-effects. CaddyModule() ModuleInfo } // ModuleInfo represents a registered Caddy module. type ModuleInfo struct { // ID is the "full name" of the module. It // must be unique and properly namespaced. ID ModuleID // New returns a pointer to a new, empty // instance of the module's type. This // method must not have any side-effects, // and no other initialization should // occur within it. Any initialization // of the returned value should be done // in a Provision() method (see the // Provisioner interface). New func() Module } // ModuleID is a string that uniquely identifies a Caddy module. A // module ID is lightly structured. It consists of dot-separated // labels which form a simple hierarchy from left to right. The last // label is the module name, and the labels before that constitute // the namespace (or scope). // // Thus, a module ID has the form: . // // An ID with no dot has the empty namespace, which is appropriate // for app modules (these are "top-level" modules that Caddy core // loads and runs). // // Module IDs should be lowercase and use underscores (_) instead of // spaces. // // Examples of valid IDs: // - http // - http.handlers.file_server // - caddy.logging.encoders.json type ModuleID string // Namespace returns the namespace (or scope) portion of a module ID, // which is all but the last label of the ID. If the ID has only one // label, then the namespace is empty. func (id ModuleID) Namespace() string { lastDot := strings.LastIndex(string(id), ".") if lastDot < 0 { return "" } return string(id)[:lastDot] } // Name returns the Name (last element) of a module ID. func (id ModuleID) Name() string { if id == "" { return "" } parts := strings.Split(string(id), ".") return parts[len(parts)-1] } func (mi ModuleInfo) String() string { return string(mi.ID) } // ModuleMap is a map that can contain multiple modules, // where the map key is the module's name. (The namespace // is usually read from an associated field's struct tag.) // Because the module's name is given as the key in a // module map, the name does not have to be given in the // json.RawMessage. type ModuleMap map[string]json.RawMessage // RegisterModule registers a module by receiving a // plain/empty value of the module. For registration to // be properly recorded, this should be called in the // init phase of runtime. Typically, the module package // will do this as a side-effect of being imported. // This function panics if the module's info is // incomplete or invalid, or if the module is already // registered. func RegisterModule(instance Module) { mod := instance.CaddyModule() if mod.ID == "" { panic("module ID missing") } if mod.ID == "caddy" || mod.ID == "admin" { panic(fmt.Sprintf("module ID '%s' is reserved", mod.ID)) } if mod.New == nil { panic("missing ModuleInfo.New") } if val := mod.New(); val == nil { panic("ModuleInfo.New must return a non-nil module instance") } modulesMu.Lock() defer modulesMu.Unlock() if _, ok := modules[string(mod.ID)]; ok { panic(fmt.Sprintf("module already registered: %s", mod.ID)) } modules[string(mod.ID)] = mod } // GetModule returns module information from its ID (full name). func GetModule(name string) (ModuleInfo, error) { modulesMu.RLock() defer modulesMu.RUnlock() m, ok := modules[name] if !ok { return ModuleInfo{}, fmt.Errorf("module not registered: %s", name) } return m, nil } // GetModuleName returns a module's name (the last label of its ID) // from an instance of its value. If the value is not a module, an // empty string will be returned. func GetModuleName(instance any) string { var name string if mod, ok := instance.(Module); ok { name = mod.CaddyModule().ID.Name() } return name } // GetModuleID returns a module's ID from an instance of its value. // If the value is not a module, an empty string will be returned. func GetModuleID(instance any) string { var id string if mod, ok := instance.(Module); ok { id = string(mod.CaddyModule().ID) } return id } // GetModules returns all modules in the given scope/namespace. // For example, a scope of "foo" returns modules named "foo.bar", // "foo.loo", but not "bar", "foo.bar.loo", etc. An empty scope // returns top-level modules, for example "foo" or "bar". Partial // scopes are not matched (i.e. scope "foo.ba" does not match // name "foo.bar"). // // Because modules are registered to a map under the hood, the // returned slice will be sorted to keep it deterministic. func GetModules(scope string) []ModuleInfo { modulesMu.RLock() defer modulesMu.RUnlock() scopeParts := strings.Split(scope, ".") // handle the special case of an empty scope, which // should match only the top-level modules if scope == "" { scopeParts = []string{} } var mods []ModuleInfo iterateModules: for id, m := range modules { modParts := strings.Split(id, ".") // match only the next level of nesting if len(modParts) != len(scopeParts)+1 { continue } // specified parts must be exact matches for i := range scopeParts { if modParts[i] != scopeParts[i] { continue iterateModules } } mods = append(mods, m) } // make return value deterministic sort.Slice(mods, func(i, j int) bool { return mods[i].ID < mods[j].ID }) return mods } // Modules returns the names of all registered modules // in ascending lexicographical order. func Modules() []string { modulesMu.RLock() defer modulesMu.RUnlock() names := make([]string, 0, len(modules)) for name := range modules { names = append(names, name) } sort.Strings(names) return names } // getModuleNameInline loads the string value from raw of moduleNameKey, // where raw must be a JSON encoding of a map. It returns that value, // along with the result of removing that key from raw. func getModuleNameInline(moduleNameKey string, raw json.RawMessage) (string, json.RawMessage, error) { var tmp map[string]any err := json.Unmarshal(raw, &tmp) if err != nil { return "", nil, err } moduleName, ok := tmp[moduleNameKey].(string) if !ok || moduleName == "" { return "", nil, fmt.Errorf("module name not specified with key '%s' in %+v", moduleNameKey, tmp) } // remove key from the object, otherwise decoding it later // will yield an error because the struct won't recognize it // (this is only needed because we strictly enforce that // all keys are recognized when loading modules) delete(tmp, moduleNameKey) result, err := json.Marshal(tmp) if err != nil { return "", nil, fmt.Errorf("re-encoding module configuration: %v", err) } return moduleName, result, nil } // Provisioner is implemented by modules which may need to perform // some additional "setup" steps immediately after being loaded. // Provisioning should be fast (imperceptible running time). If // any side-effects result in the execution of this function (e.g. // creating global state, any other allocations which require // garbage collection, opening files, starting goroutines etc.), // be sure to clean up properly by implementing the CleanerUpper // interface to avoid leaking resources. type Provisioner interface { Provision(Context) error } // Validator is implemented by modules which can verify that their // configurations are valid. This method will be called after // Provision() (if implemented). Validation should always be fast // (imperceptible running time) and an error must be returned if // the module's configuration is invalid. type Validator interface { Validate() error } // CleanerUpper is implemented by modules which may have side-effects // such as opened files, spawned goroutines, or allocated some sort // of non-stack state when they were provisioned. This method should // deallocate/cleanup those resources to prevent memory leaks. Cleanup // should be fast and efficient. Cleanup should work even if Provision // returns an error, to allow cleaning up from partial provisionings. type CleanerUpper interface { Cleanup() error } // ParseStructTag parses a caddy struct tag into its keys and values. // It is very simple. The expected syntax is: // `caddy:"key1=val1 key2=val2 ..."` func ParseStructTag(tag string) (map[string]string, error) { results := make(map[string]string) pairs := strings.Split(tag, " ") for i, pair := range pairs { if pair == "" { continue } before, after, isCut := strings.Cut(pair, "=") if !isCut { return nil, fmt.Errorf("missing key in '%s' (pair %d)", pair, i) } results[before] = after } return results, nil } // strictUnmarshalJSON is like json.Unmarshal but returns an error // if any of the fields are unrecognized. Useful when decoding // module configurations, where you want to be more sure they're // correct. func strictUnmarshalJSON(data []byte, v any) error { dec := json.NewDecoder(bytes.NewReader(data)) dec.DisallowUnknownFields() return dec.Decode(v) } // isJSONRawMessage returns true if the type is encoding/json.RawMessage. func isJSONRawMessage(typ reflect.Type) bool { return typ.PkgPath() == "encoding/json" && typ.Name() == "RawMessage" } // isModuleMapType returns true if the type is map[string]json.RawMessage. // It assumes that the string key is the module name, but this is not // always the case. To know for sure, this function must return true, but // also the struct tag where this type appears must NOT define an inline_key // attribute, which would mean that the module names appear inline with the // values, not in the key. func isModuleMapType(typ reflect.Type) bool { return typ.Kind() == reflect.Map && typ.Key().Kind() == reflect.String && isJSONRawMessage(typ.Elem()) } var ( modules = make(map[string]ModuleInfo) modulesMu sync.RWMutex ) caddy-2.6.2/modules/000077500000000000000000000000001435007237400142625ustar00rootroot00000000000000caddy-2.6.2/modules/caddyevents/000077500000000000000000000000001435007237400165735ustar00rootroot00000000000000caddy-2.6.2/modules/caddyevents/app.go000066400000000000000000000270051435007237400177060ustar00rootroot00000000000000// 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 caddyevents import ( "context" "encoding/json" "errors" "fmt" "strings" "time" "github.com/caddyserver/caddy/v2" "github.com/google/uuid" "go.uber.org/zap" ) func init() { caddy.RegisterModule(App{}) } // App implements a global eventing system within Caddy. // Modules can emit and subscribe to events, providing // hooks into deep parts of the code base that aren't // otherwise accessible. Events provide information about // what and when things are happening, and this facility // allows handlers to take action when events occur, // add information to the event's metadata, and even // control program flow in some cases. // // Events are propagated in a DOM-like fashion. An event // emitted from module `a.b.c` (the "origin") will first // invoke handlers listening to `a.b.c`, then `a.b`, // then `a`, then those listening regardless of origin. // If a handler returns the special error Aborted, then // propagation immediately stops and the event is marked // as aborted. Emitters may optionally choose to adjust // program flow based on an abort. // // Modules can subscribe to events by origin and/or name. // A handler is invoked only if it is subscribed to the // event by name and origin. Subscriptions should be // registered during the provisioning phase, before apps // are started. // // Event handlers are fired synchronously as part of the // regular flow of the program. This allows event handlers // to control the flow of the program if the origin permits // it and also allows handlers to convey new information // back into the origin module before it continues. // In essence, event handlers are similar to HTTP // middleware handlers. // // Event bindings/subscribers are unordered; i.e. // event handlers are invoked in an arbitrary order. // Event handlers should not rely on the logic of other // handlers to succeed. // // The entirety of this app module is EXPERIMENTAL and // subject to change. Pay attention to release notes. type App struct { // Subscriptions bind handlers to one or more events // either globally or scoped to specific modules or module // namespaces. Subscriptions []*Subscription `json:"subscriptions,omitempty"` // Map of event name to map of module ID/namespace to handlers subscriptions map[string]map[caddy.ModuleID][]Handler logger *zap.Logger started bool } // Subscription represents binding of one or more handlers to // one or more events. type Subscription struct { // The name(s) of the event(s) to bind to. Default: all events. Events []string `json:"events,omitempty"` // The ID or namespace of the module(s) from which events // originate to listen to for events. Default: all modules. // // Events propagate up, so events emitted by module "a.b.c" // will also trigger the event for "a.b" and "a". Thus, to // receive all events from "a.b.c" and "a.b.d", for example, // one can subscribe to either "a.b" or all of "a" entirely. Modules []caddy.ModuleID `json:"modules,omitempty"` // The event handler modules. These implement the actual // behavior to invoke when an event occurs. At least one // handler is required. HandlersRaw []json.RawMessage `json:"handlers,omitempty" caddy:"namespace=events.handlers inline_key=handler"` // The decoded handlers; Go code that is subscribing to // an event should set this field directly; HandlersRaw // is meant for JSON configuration to fill out this field. Handlers []Handler `json:"-"` } // CaddyModule returns the Caddy module information. func (App) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "events", New: func() caddy.Module { return new(App) }, } } // Provision sets up the app. func (app *App) Provision(ctx caddy.Context) error { app.logger = ctx.Logger() app.subscriptions = make(map[string]map[caddy.ModuleID][]Handler) for _, sub := range app.Subscriptions { if sub.HandlersRaw != nil { handlersIface, err := ctx.LoadModule(sub, "HandlersRaw") if err != nil { return fmt.Errorf("loading event subscriber modules: %v", err) } for _, h := range handlersIface.([]any) { sub.Handlers = append(sub.Handlers, h.(Handler)) } if len(sub.Handlers) == 0 { // pointless to bind without any handlers return fmt.Errorf("no handlers defined") } } } return nil } // Start runs the app. func (app *App) Start() error { for _, sub := range app.Subscriptions { if err := app.Subscribe(sub); err != nil { return err } } app.started = true return nil } // Stop gracefully shuts down the app. func (app *App) Stop() error { return nil } // Subscribe binds one or more event handlers to one or more events // according to the subscription s. For now, subscriptions can only // be created during the provision phase; new bindings cannot be // created after the events app has started. func (app *App) Subscribe(s *Subscription) error { if app.started { return fmt.Errorf("events already started; new subscriptions closed") } // handle special case of catch-alls (omission of event name or module space implies all) if len(s.Events) == 0 { s.Events = []string{""} } if len(s.Modules) == 0 { s.Modules = []caddy.ModuleID{""} } for _, eventName := range s.Events { if app.subscriptions[eventName] == nil { app.subscriptions[eventName] = make(map[caddy.ModuleID][]Handler) } for _, originModule := range s.Modules { app.subscriptions[eventName][originModule] = append(app.subscriptions[eventName][originModule], s.Handlers...) } } return nil } // On is syntactic sugar for Subscribe() that binds a single handler // to a single event from any module. If the eventName is empty string, // it counts for all events. func (app *App) On(eventName string, handler Handler) error { return app.Subscribe(&Subscription{ Events: []string{eventName}, Handlers: []Handler{handler}, }) } // Emit creates and dispatches an event named eventName to all relevant handlers with // the metadata data. Events are emitted and propagated synchronously. The returned Event // value will have any additional information from the invoked handlers. // // Note that the data map is not copied, for efficiency. After Emit() is called, the // data passed in should not be changed in other goroutines. func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event { logger := app.logger.With(zap.String("name", eventName)) id, err := uuid.NewRandom() if err != nil { logger.Error("failed generating new event ID", zap.Error(err)) } eventName = strings.ToLower(eventName) e := Event{ Data: data, id: id, ts: time.Now(), name: eventName, origin: ctx.Module(), } logger = logger.With( zap.String("id", e.id.String()), zap.String("origin", e.origin.CaddyModule().String())) // add event info to replacer, make sure it's in the context repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) if !ok { repl = caddy.NewReplacer() ctx.Context = context.WithValue(ctx.Context, caddy.ReplacerCtxKey, repl) } repl.Map(func(key string) (any, bool) { switch key { case "event": return e, true case "event.id": return e.id, true case "event.name": return e.name, true case "event.time": return e.ts, true case "event.time_unix": return e.ts.UnixMilli(), true case "event.module": return e.origin.CaddyModule().ID, true case "event.data": return e.Data, true } if strings.HasPrefix(key, "event.data.") { key = strings.TrimPrefix(key, "event.data.") if val, ok := e.Data[key]; ok { return val, true } } return nil, false }) logger.Debug("event", zap.Any("data", e.Data)) // invoke handlers bound to the event by name and also all events; this for loop // iterates twice at most: once for the event name, once for "" (all events) for { moduleID := e.origin.CaddyModule().ID // implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "") for { if app.subscriptions[eventName] == nil { break // shortcut if event not bound at all } for _, handler := range app.subscriptions[eventName][moduleID] { select { case <-ctx.Done(): logger.Error("context canceled; event handling stopped") return e default: } if err := handler.Handle(ctx, e); err != nil { aborted := errors.Is(err, ErrAborted) logger.Error("handler error", zap.Error(err), zap.Bool("aborted", aborted)) if aborted { e.Aborted = err return e } } } if moduleID == "" { break } lastDot := strings.LastIndex(string(moduleID), ".") if lastDot < 0 { moduleID = "" // include handlers bound to events regardless of module } else { moduleID = moduleID[:lastDot] } } // include handlers listening to all events if eventName == "" { break } eventName = "" } return e } // Event represents something that has happened or is happening. // An Event value is not synchronized, so it should be copied if // being used in goroutines. // // EXPERIMENTAL: As with the rest of this package, events are // subject to change. type Event struct { // If non-nil, the event has been aborted, meaning // propagation has stopped to other handlers and // the code should stop what it was doing. Emitters // may choose to use this as a signal to adjust their // code path appropriately. Aborted error // The data associated with the event. Usually the // original emitter will be the only one to set or // change these values, but the field is exported // so handlers can have full access if needed. // However, this map is not synchronized, so // handlers must not use this map directly in new // goroutines; instead, copy the map to use it in a // goroutine. Data map[string]any id uuid.UUID ts time.Time name string origin caddy.Module } // CloudEvent exports event e as a structure that, when // serialized as JSON, is compatible with the // CloudEvents spec. func (e Event) CloudEvent() CloudEvent { dataJSON, _ := json.Marshal(e.Data) return CloudEvent{ ID: e.id.String(), Source: e.origin.CaddyModule().String(), SpecVersion: "1.0", Type: e.name, Time: e.ts, DataContentType: "application/json", Data: dataJSON, } } // CloudEvent is a JSON-serializable structure that // is compatible with the CloudEvents specification. // See https://cloudevents.io. type CloudEvent struct { ID string `json:"id"` Source string `json:"source"` SpecVersion string `json:"specversion"` Type string `json:"type"` Time time.Time `json:"time"` DataContentType string `json:"datacontenttype,omitempty"` Data json.RawMessage `json:"data,omitempty"` } // ErrAborted cancels an event. var ErrAborted = errors.New("event aborted") // Handler is a type that can handle events. type Handler interface { Handle(context.Context, Event) error } // Interface guards var ( _ caddy.App = (*App)(nil) _ caddy.Provisioner = (*App)(nil) ) caddy-2.6.2/modules/caddyevents/eventsconfig/000077500000000000000000000000001435007237400212655ustar00rootroot00000000000000caddy-2.6.2/modules/caddyevents/eventsconfig/caddyfile.go000066400000000000000000000045021435007237400235410ustar00rootroot00000000000000// 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 eventsconfig is for configuring caddyevents.App with the // Caddyfile. This code can't be in the caddyevents package because // the httpcaddyfile package imports caddyhttp, which imports // caddyevents: hence, it creates an import cycle. package eventsconfig import ( "encoding/json" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyevents" ) func init() { httpcaddyfile.RegisterGlobalOption("events", parseApp) } // parseApp configures the "events" global option from Caddyfile to set up the events app. // Syntax: // // events { // on // } // // If is *, then it will bind to all events. func parseApp(d *caddyfile.Dispenser, _ any) (any, error) { app := new(caddyevents.App) // consume the option name if !d.Next() { return nil, d.ArgErr() } // handle the block for d.NextBlock(0) { switch d.Val() { case "on": if !d.NextArg() { return nil, d.ArgErr() } eventName := d.Val() if eventName == "*" { eventName = "" } if !d.NextArg() { return nil, d.ArgErr() } handlerName := d.Val() modID := "events.handlers." + handlerName unm, err := caddyfile.UnmarshalModule(d, modID) if err != nil { return nil, err } app.Subscriptions = append(app.Subscriptions, &caddyevents.Subscription{ Events: []string{eventName}, HandlersRaw: []json.RawMessage{ caddyconfig.JSONModuleObject(unm, "handler", handlerName, nil), }, }) default: return nil, d.ArgErr() } } return httpcaddyfile.App{ Name: "events", Value: caddyconfig.JSON(app, nil), }, nil } caddy-2.6.2/modules/caddyhttp/000077500000000000000000000000001435007237400162465ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/app.go000066400000000000000000000573521435007237400173710ustar00rootroot00000000000000// 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 caddyhttp import ( "context" "crypto/tls" "fmt" "net" "net/http" "strconv" "sync" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddytls" "go.uber.org/zap" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) func init() { caddy.RegisterModule(App{}) } // App is a robust, production-ready HTTP server. // // HTTPS is enabled by default if host matchers with qualifying names are used // in any of routes; certificates are automatically provisioned and renewed. // Additionally, automatic HTTPS will also enable HTTPS for servers that listen // only on the HTTPS port but which do not have any TLS connection policies // defined by adding a good, default TLS connection policy. // // In HTTP routes, additional placeholders are available (replace any `*`): // // Placeholder | Description // ------------|--------------- // `{http.request.body}` | The request body (⚠️ inefficient; use only for debugging) // `{http.request.cookie.*}` | HTTP request cookie // `{http.request.duration}` | Time up to now spent handling the request (after decoding headers from client) // `{http.request.duration_ms}` | Same as 'duration', but in milliseconds. // `{http.request.uuid}` | The request unique identifier // `{http.request.header.*}` | Specific request header field // `{http.request.host.labels.*}` | Request host labels (0-based from right); e.g. for foo.example.com: 0=com, 1=example, 2=foo // `{http.request.host}` | The host part of the request's Host header // `{http.request.hostport}` | The host and port from the request's Host header // `{http.request.method}` | The request method // `{http.request.orig_method}` | The request's original method // `{http.request.orig_uri.path.dir}` | The request's original directory // `{http.request.orig_uri.path.file}` | The request's original filename // `{http.request.orig_uri.path}` | The request's original path // `{http.request.orig_uri.query}` | The request's original query string (without `?`) // `{http.request.orig_uri}` | The request's original URI // `{http.request.port}` | The port part of the request's Host header // `{http.request.proto}` | The protocol of the request // `{http.request.remote.host}` | The host (IP) part of the remote client's address // `{http.request.remote.port}` | The port part of the remote client's address // `{http.request.remote}` | The address of the remote client // `{http.request.scheme}` | The request scheme // `{http.request.tls.version}` | The TLS version name // `{http.request.tls.cipher_suite}` | The TLS cipher suite // `{http.request.tls.resumed}` | The TLS connection resumed a previous connection // `{http.request.tls.proto}` | The negotiated next protocol // `{http.request.tls.proto_mutual}` | The negotiated next protocol was advertised by the server // `{http.request.tls.server_name}` | The server name requested by the client, if any // `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate // `{http.request.tls.client.public_key}` | The public key of the client certificate. // `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key. // `{http.request.tls.client.certificate_pem}` | The PEM-encoded value of the certificate. // `{http.request.tls.client.certificate_der_base64}` | The base64-encoded value of the certificate. // `{http.request.tls.client.issuer}` | The issuer DN of the client certificate // `{http.request.tls.client.serial}` | The serial number of the client certificate // `{http.request.tls.client.subject}` | The subject DN of the client certificate // `{http.request.tls.client.san.dns_names.*}` | SAN DNS names(index optional) // `{http.request.tls.client.san.emails.*}` | SAN email addresses (index optional) // `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional) // `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional) // `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left) // `{http.request.uri.path.dir}` | The directory, excluding leaf filename // `{http.request.uri.path.file}` | The filename of the path, excluding directory // `{http.request.uri.path}` | The path component of the request URI // `{http.request.uri.query.*}` | Individual query string value // `{http.request.uri.query}` | The query string (without `?`) // `{http.request.uri}` | The full request URI // `{http.response.header.*}` | Specific response header field // `{http.vars.*}` | Custom variables in the HTTP handler chain // `{http.shutting_down}` | True if the HTTP app is shutting down // `{http.time_until_shutdown}` | Time until HTTP server shutdown, if scheduled type App struct { // HTTPPort specifies the port to use for HTTP (as opposed to HTTPS), // which is used when setting up HTTP->HTTPS redirects or ACME HTTP // challenge solvers. Default: 80. HTTPPort int `json:"http_port,omitempty"` // HTTPSPort specifies the port to use for HTTPS, which is used when // solving the ACME TLS-ALPN challenges, or whenever HTTPS is needed // but no specific port number is given. Default: 443. HTTPSPort int `json:"https_port,omitempty"` // GracePeriod is how long to wait for active connections when shutting // down the servers. During the grace period, no new connections are // accepted, idle connections are closed, and active connections will // be given the full length of time to become idle and close. // Once the grace period is over, connections will be forcefully closed. // If zero, the grace period is eternal. Default: 0. GracePeriod caddy.Duration `json:"grace_period,omitempty"` // ShutdownDelay is how long to wait before initiating the grace // period. When this app is stopping (e.g. during a config reload or // process exit), all servers will be shut down. Normally this immediately // initiates the grace period. However, if this delay is configured, servers // will not be shut down until the delay is over. During this time, servers // continue to function normally and allow new connections. At the end, the // grace period will begin. This can be useful to allow downstream load // balancers time to move this instance out of the rotation without hiccups. // // When shutdown has been scheduled, placeholders {http.shutting_down} (bool) // and {http.time_until_shutdown} (duration) may be useful for health checks. ShutdownDelay caddy.Duration `json:"shutdown_delay,omitempty"` // Servers is the list of servers, keyed by arbitrary names chosen // at your discretion for your own convenience; the keys do not // affect functionality. Servers map[string]*Server `json:"servers,omitempty"` ctx caddy.Context logger *zap.Logger tlsApp *caddytls.TLS // used temporarily between phases 1 and 2 of auto HTTPS allCertDomains []string } // CaddyModule returns the Caddy module information. func (App) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http", New: func() caddy.Module { return new(App) }, } } // Provision sets up the app. func (app *App) Provision(ctx caddy.Context) error { // store some references tlsAppIface, err := ctx.App("tls") if err != nil { return fmt.Errorf("getting tls app: %v", err) } app.tlsApp = tlsAppIface.(*caddytls.TLS) app.ctx = ctx app.logger = ctx.Logger() eventsAppIface, err := ctx.App("events") if err != nil { return fmt.Errorf("getting events app: %v", err) } repl := caddy.NewReplacer() // this provisions the matchers for each route, // and prepares auto HTTP->HTTPS redirects, and // is required before we provision each server err = app.automaticHTTPSPhase1(ctx, repl) if err != nil { return err } // prepare each server oldContext := ctx.Context for srvName, srv := range app.Servers { ctx.Context = context.WithValue(oldContext, ServerCtxKey, srv) srv.name = srvName srv.tlsApp = app.tlsApp srv.events = eventsAppIface.(*caddyevents.App) srv.ctx = ctx srv.logger = app.logger.Named("log") srv.errorLogger = app.logger.Named("log.error") srv.shutdownAtMu = new(sync.RWMutex) // only enable access logs if configured if srv.Logs != nil { srv.accessLogger = app.logger.Named("log.access") } // the Go standard library does not let us serve only HTTP/2 using // http.Server; we would probably need to write our own server if !srv.protocol("h1") && (srv.protocol("h2") || srv.protocol("h2c")) { return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName) } // if no protocols configured explicitly, enable all except h2c if len(srv.Protocols) == 0 { srv.Protocols = []string{"h1", "h2", "h3"} } // if not explicitly configured by the user, disallow TLS // client auth bypass (domain fronting) which could // otherwise be exploited by sending an unprotected SNI // value during a TLS handshake, then putting a protected // domain in the Host header after establishing connection; // this is a safe default, but we allow users to override // it for example in the case of running a proxy where // domain fronting is desired and access is not restricted // based on hostname if srv.StrictSNIHost == nil && srv.hasTLSClientAuth() { app.logger.Warn("enabling strict SNI-Host enforcement because TLS client auth is configured", zap.String("server_id", srvName)) trueBool := true srv.StrictSNIHost = &trueBool } // process each listener address for i := range srv.Listen { lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true) if err != nil { return fmt.Errorf("server %s, listener %d: %v", srvName, i, err) } srv.Listen[i] = lnOut } // set up each listener modifier if srv.ListenerWrappersRaw != nil { vals, err := ctx.LoadModule(srv, "ListenerWrappersRaw") if err != nil { return fmt.Errorf("loading listener wrapper modules: %v", err) } var hasTLSPlaceholder bool for i, val := range vals.([]any) { if _, ok := val.(*tlsPlaceholderWrapper); ok { if i == 0 { // putting the tls placeholder wrapper first is nonsensical because // that is the default, implicit setting: without it, all wrappers // will go after the TLS listener anyway return fmt.Errorf("it is unnecessary to specify the TLS listener wrapper in the first position because that is the default") } if hasTLSPlaceholder { return fmt.Errorf("TLS listener wrapper can only be specified once") } hasTLSPlaceholder = true } srv.listenerWrappers = append(srv.listenerWrappers, val.(caddy.ListenerWrapper)) } // if any wrappers were configured but the TLS placeholder wrapper is // absent, prepend it so all defined wrappers come after the TLS // handshake; this simplifies logic when starting the server, since we // can simply assume the TLS placeholder will always be there if !hasTLSPlaceholder && len(srv.listenerWrappers) > 0 { srv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...) } } // pre-compile the primary handler chain, and be sure to wrap it in our // route handler so that important security checks are done, etc. primaryRoute := emptyHandler if srv.Routes != nil { err := srv.Routes.ProvisionHandlers(ctx, srv.Metrics) if err != nil { return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err) } primaryRoute = srv.Routes.Compile(emptyHandler) } srv.primaryHandlerChain = srv.wrapPrimaryRoute(primaryRoute) // pre-compile the error handler chain if srv.Errors != nil { err := srv.Errors.Routes.Provision(ctx) if err != nil { return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err) } srv.errorHandlerChain = srv.Errors.Routes.Compile(errorEmptyHandler) } // prepare the TLS connection policies err = srv.TLSConnPolicies.Provision(ctx) if err != nil { return fmt.Errorf("server %s: setting up TLS connection policies: %v", srvName, err) } // if there is no idle timeout, set a sane default; users have complained // before that aggressive CDNs leave connections open until the server // closes them, so if we don't close them it leads to resource exhaustion if srv.IdleTimeout == 0 { srv.IdleTimeout = defaultIdleTimeout } } ctx.Context = oldContext return nil } // Validate ensures the app's configuration is valid. func (app *App) Validate() error { // each server must use distinct listener addresses lnAddrs := make(map[string]string) for srvName, srv := range app.Servers { for _, addr := range srv.Listen { listenAddr, err := caddy.ParseNetworkAddress(addr) if err != nil { return fmt.Errorf("invalid listener address '%s': %v", addr, err) } // check that every address in the port range is unique to this server; // we do not use <= here because PortRangeSize() adds 1 to EndPort for us for i := uint(0); i < listenAddr.PortRangeSize(); i++ { addr := caddy.JoinNetworkAddress(listenAddr.Network, listenAddr.Host, strconv.Itoa(int(listenAddr.StartPort+i))) if sn, ok := lnAddrs[addr]; ok { return fmt.Errorf("server %s: listener address repeated: %s (already claimed by server '%s')", srvName, addr, sn) } lnAddrs[addr] = srvName } } } return nil } // Start runs the app. It finishes automatic HTTPS if enabled, // including management of certificates. func (app *App) Start() error { // get a logger compatible with http.Server serverLogger, err := zap.NewStdLogAt(app.logger.Named("stdlib"), zap.DebugLevel) if err != nil { return fmt.Errorf("failed to set up server logger: %v", err) } for srvName, srv := range app.Servers { srv.server = &http.Server{ ReadTimeout: time.Duration(srv.ReadTimeout), ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout), WriteTimeout: time.Duration(srv.WriteTimeout), IdleTimeout: time.Duration(srv.IdleTimeout), MaxHeaderBytes: srv.MaxHeaderBytes, Handler: srv, ErrorLog: serverLogger, } // disable HTTP/2, which we enabled by default during provisioning if !srv.protocol("h2") { srv.server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) for _, cp := range srv.TLSConnPolicies { // the TLSConfig was already provisioned, so... manually remove it for i, np := range cp.TLSConfig.NextProtos { if np == "h2" { cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...) break } } // remove it from the parent connection policy too, just to keep things tidy for i, alpn := range cp.ALPN { if alpn == "h2" { cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...) break } } } } // this TLS config is used by the std lib to choose the actual TLS config for connections // by looking through the connection policies to find the first one that matches tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx) srv.configureServer(srv.server) // enable H2C if configured if srv.protocol("h2c") { h2server := &http2.Server{ IdleTimeout: time.Duration(srv.IdleTimeout), } srv.server.Handler = h2c.NewHandler(srv, h2server) } for _, lnAddr := range srv.Listen { listenAddr, err := caddy.ParseNetworkAddress(lnAddr) if err != nil { return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err) } srv.addresses = append(srv.addresses, listenAddr) for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ { // create the listener for this socket hostport := listenAddr.JoinHostPort(portOffset) lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)}) if err != nil { return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err) } ln := lnAny.(net.Listener) // wrap listener before TLS (up to the TLS placeholder wrapper) var lnWrapperIdx int for i, lnWrapper := range srv.listenerWrappers { if _, ok := lnWrapper.(*tlsPlaceholderWrapper); ok { lnWrapperIdx = i + 1 // mark the next wrapper's spot break } ln = lnWrapper.WrapListener(ln) } // enable TLS if there is a policy and if this is not the HTTP port useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort() if useTLS { // create TLS listener - this enables and terminates TLS ln = tls.NewListener(ln, tlsCfg) // enable HTTP/3 if configured if srv.protocol("h3") { // Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses // a different transport mechanism... which is fine, but the OS doesn't // differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they // are still one file on the system. So even though "unixpacket" and // "unixgram" are different network types just as "tcp" and "udp" are, // the OS will not let us use the same file as both STREAM and DGRAM. if len(srv.Protocols) > 1 && listenAddr.IsUnixNetwork() { app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket", zap.String("file", hostport)) for i := range srv.Protocols { if srv.Protocols[i] == "h3" { srv.Protocols = append(srv.Protocols[:i], srv.Protocols[i+1:]...) break } } } else { app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport)) if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil { return err } } } } // finish wrapping listener where we left off before TLS for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ { ln = srv.listenerWrappers[i].WrapListener(ln) } // if binding to port 0, the OS chooses a port for us; // but the user won't know the port unless we print it if !listenAddr.IsUnixNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 { app.logger.Info("port 0 listener", zap.String("input_address", lnAddr), zap.String("actual_address", ln.Addr().String())) } app.logger.Debug("starting server loop", zap.String("address", ln.Addr().String()), zap.Bool("tls", useTLS), zap.Bool("http3", srv.h3server != nil)) srv.listeners = append(srv.listeners, ln) // enable HTTP/1 if configured if srv.protocol("h1") { //nolint:errcheck go srv.server.Serve(ln) } } } srv.logger.Info("server running", zap.String("name", srvName), zap.Strings("protocols", srv.Protocols)) } // finish automatic HTTPS by finally beginning // certificate management err = app.automaticHTTPSPhase2() if err != nil { return fmt.Errorf("finalizing automatic HTTPS: %v", err) } return nil } // Stop gracefully shuts down the HTTP server. func (app *App) Stop() error { ctx := context.Background() // see if any listeners in our config will be closing or if they are continuing // hrough a reload; because if any are closing, we will enforce shutdown delay var delay bool scheduledTime := time.Now().Add(time.Duration(app.ShutdownDelay)) if app.ShutdownDelay > 0 { for _, server := range app.Servers { for _, na := range server.addresses { for _, addr := range na.Expand() { if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 { app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String())) server.shutdownAtMu.Lock() server.shutdownAt = scheduledTime server.shutdownAtMu.Unlock() delay = true } else { app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String())) } } } } } // honor scheduled/delayed shutdown time if delay { app.logger.Debug("shutdown scheduled", zap.Duration("delay_duration", time.Duration(app.ShutdownDelay)), zap.Time("time", scheduledTime)) time.Sleep(time.Duration(app.ShutdownDelay)) } // enforce grace period if configured if app.GracePeriod > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod)) defer cancel() app.logger.Debug("servers shutting down; grace period initiated", zap.Duration("duration", time.Duration(app.GracePeriod))) } else { app.logger.Debug("servers shutting down with eternal grace period") } // goroutines aren't guaranteed to be scheduled right away, // so we'll use one WaitGroup to wait for all the goroutines // to start their server shutdowns, and another to wait for // them to finish; we'll always block for them to start so // that when we return the caller can be confident* that the // old servers are no longer accepting new connections // (* the scheduler might still pause them right before // calling Shutdown(), but it's unlikely) var startedShutdown, finishedShutdown sync.WaitGroup // these will run in goroutines stopServer := func(server *Server) { defer finishedShutdown.Done() startedShutdown.Done() if err := server.server.Shutdown(ctx); err != nil { app.logger.Error("server shutdown", zap.Error(err), zap.Strings("addresses", server.Listen)) } } stopH3Server := func(server *Server) { defer finishedShutdown.Done() startedShutdown.Done() if server.h3server == nil { return } // TODO: we have to manually close our listeners because quic-go won't // close listeners it didn't create along with the server itself... // see https://github.com/lucas-clemente/quic-go/issues/3560 for _, el := range server.h3listeners { if err := el.Close(); err != nil { app.logger.Error("HTTP/3 listener close", zap.Error(err), zap.String("address", el.LocalAddr().String())) } } // TODO: CloseGracefully, once implemented upstream (see https://github.com/lucas-clemente/quic-go/issues/2103) if err := server.h3server.Close(); err != nil { app.logger.Error("HTTP/3 server shutdown", zap.Error(err), zap.Strings("addresses", server.Listen)) } } for _, server := range app.Servers { startedShutdown.Add(2) finishedShutdown.Add(2) go stopServer(server) go stopH3Server(server) } // block until all the goroutines have been run by the scheduler; // this means that they have likely called Shutdown() by now startedShutdown.Wait() // if the process is exiting, we need to block here and wait // for the grace periods to complete, otherwise the process will // terminate before the servers are finished shutting down; but // we don't really need to wait for the grace period to finish // if the process isn't exiting (but note that frequent config // reloads with long grace periods for a sustained length of time // may deplete resources) if caddy.Exiting() { finishedShutdown.Wait() } return nil } func (app *App) httpPort() int { if app.HTTPPort == 0 { return DefaultHTTPPort } return app.HTTPPort } func (app *App) httpsPort() int { if app.HTTPSPort == 0 { return DefaultHTTPSPort } return app.HTTPSPort } // defaultIdleTimeout is the default HTTP server timeout // for closing idle connections; useful to avoid resource // exhaustion behind hungry CDNs, for example (we've had // several complaints without this). const defaultIdleTimeout = caddy.Duration(5 * time.Minute) // Interface guards var ( _ caddy.App = (*App)(nil) _ caddy.Provisioner = (*App)(nil) _ caddy.Validator = (*App)(nil) ) caddy-2.6.2/modules/caddyhttp/autohttps.go000066400000000000000000000653171435007237400206440ustar00rootroot00000000000000// 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 caddyhttp import ( "fmt" "net/http" "strconv" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) // AutoHTTPSConfig is used to disable automatic HTTPS // or certain aspects of it for a specific server. // HTTPS is enabled automatically and by default when // qualifying hostnames are available from the config. type AutoHTTPSConfig struct { // If true, automatic HTTPS will be entirely disabled, // including certificate management and redirects. Disabled bool `json:"disable,omitempty"` // If true, only automatic HTTP->HTTPS redirects will // be disabled, but other auto-HTTPS features will // remain enabled. DisableRedir bool `json:"disable_redirects,omitempty"` // If true, automatic certificate management will be // disabled, but other auto-HTTPS features will // remain enabled. DisableCerts bool `json:"disable_certificates,omitempty"` // Hosts/domain names listed here will not be included // in automatic HTTPS (they will not have certificates // loaded nor redirects applied). Skip []string `json:"skip,omitempty"` // Hosts/domain names listed here will still be enabled // for automatic HTTPS (unless in the Skip list), except // that certificates will not be provisioned and managed // for these names. SkipCerts []string `json:"skip_certificates,omitempty"` // By default, automatic HTTPS will obtain and renew // certificates for qualifying hostnames. However, if // a certificate with a matching SAN is already loaded // into the cache, certificate management will not be // enabled. To force automated certificate management // regardless of loaded certificates, set this to true. IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"` } // Skipped returns true if name is in skipSlice, which // should be either the Skip or SkipCerts field on ahc. func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool { for _, n := range skipSlice { if name == n { return true } } return false } // automaticHTTPSPhase1 provisions all route matchers, determines // which domain names found in the routes qualify for automatic // HTTPS, and sets up HTTP->HTTPS redirects. This phase must occur // at the beginning of provisioning, because it may add routes and // even servers to the app, which still need to be set up with the // rest of them during provisioning. func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) error { // this map acts as a set to store the domain names // for which we will manage certificates automatically uniqueDomainsForCerts := make(map[string]struct{}) // this maps domain names for automatic HTTP->HTTPS // redirects to their destination server addresses // (there might be more than 1 if bind is used; see // https://github.com/caddyserver/caddy/issues/3443) redirDomains := make(map[string][]caddy.NetworkAddress) // the log configuration for an HTTPS enabled server var logCfg *ServerLogConfig for srvName, srv := range app.Servers { // as a prerequisite, provision route matchers; this is // required for all routes on all servers, and must be // done before we attempt to do phase 1 of auto HTTPS, // since we have to access the decoded host matchers the // handlers will be provisioned later if srv.Routes != nil { err := srv.Routes.ProvisionMatchers(ctx) if err != nil { return fmt.Errorf("server %s: setting up route matchers: %v", srvName, err) } } // prepare for automatic HTTPS if srv.AutoHTTPS == nil { srv.AutoHTTPS = new(AutoHTTPSConfig) } if srv.AutoHTTPS.Disabled { app.logger.Warn("automatic HTTPS is completely disabled for server", zap.String("server_name", srvName)) continue } // skip if all listeners use the HTTP port if !srv.listenersUseAnyPortOtherThan(app.httpPort()) { app.logger.Warn("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server", zap.String("server_name", srvName), zap.Int("http_port", app.httpPort()), ) srv.AutoHTTPS.Disabled = true continue } // if all listeners are on the HTTPS port, make sure // there is at least one TLS connection policy; it // should be obvious that they want to use TLS without // needing to specify one empty policy to enable it if srv.TLSConnPolicies == nil && !srv.listenersUseAnyPortOtherThan(app.httpsPort()) { app.logger.Info("server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS", zap.String("server_name", srvName), zap.Int("https_port", app.httpsPort()), ) srv.TLSConnPolicies = caddytls.ConnectionPolicies{new(caddytls.ConnectionPolicy)} } // find all qualifying domain names (deduplicated) in this server // (this is where we need the provisioned, decoded request matchers) serverDomainSet := make(map[string]struct{}) for routeIdx, route := range srv.Routes { for matcherSetIdx, matcherSet := range route.MatcherSets { for matcherIdx, m := range matcherSet { if hm, ok := m.(*MatchHost); ok { for hostMatcherIdx, d := range *hm { var err error d, err = repl.ReplaceOrErr(d, true, false) if err != nil { return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v", srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err) } if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) { serverDomainSet[d] = struct{}{} } } } } } } // nothing more to do here if there are no domains that qualify for // automatic HTTPS and there are no explicit TLS connection policies: // if there is at least one domain but no TLS conn policy (F&&T), we'll // add one below; if there are no domains but at least one TLS conn // policy (meaning TLS is enabled) (T&&F), it could be a catch-all with // on-demand TLS -- and in that case we would still need HTTP->HTTPS // redirects, which we set up below; hence these two conditions if len(serverDomainSet) == 0 && len(srv.TLSConnPolicies) == 0 { continue } // clone the logger so we can apply it to the HTTP server // (not sure if necessary to clone it; but probably safer) // (we choose one log cfg arbitrarily; not sure which is best) if srv.Logs != nil { logCfg = srv.Logs.clone() } // for all the hostnames we found, filter them so we have // a deduplicated list of names for which to obtain certs // (only if cert management not disabled for this server) if srv.AutoHTTPS.DisableCerts { app.logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName)) } else { for d := range serverDomainSet { // the implicit Tailscale manager module will get its own certs at run-time if isTailscaleDomain(d) { continue } if certmagic.SubjectQualifiesForCert(d) && !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) { // if a certificate for this name is already loaded, // don't obtain another one for it, unless we are // supposed to ignore loaded certificates if !srv.AutoHTTPS.IgnoreLoadedCerts && len(app.tlsApp.AllMatchingCertificates(d)) > 0 { app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded", zap.String("domain", d), zap.String("server_name", srvName), ) continue } // most clients don't accept wildcards like *.tld... we // can handle that, but as a courtesy, warn the user if strings.Contains(d, "*") && strings.Count(strings.Trim(d, "."), ".") == 1 { app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)", zap.String("domain", d)) } uniqueDomainsForCerts[d] = struct{}{} } } } // tell the server to use TLS if it is not already doing so if srv.TLSConnPolicies == nil { srv.TLSConnPolicies = caddytls.ConnectionPolicies{new(caddytls.ConnectionPolicy)} } // nothing left to do if auto redirects are disabled if srv.AutoHTTPS.DisableRedir { app.logger.Warn("automatic HTTP->HTTPS redirects are disabled", zap.String("server_name", srvName)) continue } app.logger.Info("enabling automatic HTTP->HTTPS redirects", zap.String("server_name", srvName)) // create HTTP->HTTPS redirects for _, listenAddr := range srv.Listen { // figure out the address we will redirect to... addr, err := caddy.ParseNetworkAddress(listenAddr) if err != nil { msg := "%s: invalid listener address: %v" if strings.Count(listenAddr, ":") > 1 { msg = msg + ", there are too many colons, so the port is ambiguous. Did you mean to wrap the IPv6 address with [] brackets?" } return fmt.Errorf(msg, srvName, listenAddr) } // this address might not have a hostname, i.e. might be a // catch-all address for a particular port; we need to keep // track if it is, so we can set up redirects for it anyway // (e.g. the user might have enabled on-demand TLS); we use // an empty string to indicate a catch-all, which we have to // treat special later if len(serverDomainSet) == 0 { redirDomains[""] = append(redirDomains[""], addr) continue } // ...and associate it with each domain in this server for d := range serverDomainSet { // if this domain is used on more than one HTTPS-enabled // port, we'll have to choose one, so prefer the HTTPS port if _, ok := redirDomains[d]; !ok || addr.StartPort == uint(app.httpsPort()) { redirDomains[d] = []caddy.NetworkAddress{addr} } } } } // we now have a list of all the unique names for which we need certs; // turn the set into a slice so that phase 2 can use it app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) var internal []string uniqueDomainsLoop: for d := range uniqueDomainsForCerts { // whether or not there is already an automation policy for this // name, we should add it to the list to manage a cert for it app.allCertDomains = append(app.allCertDomains, d) // some names we've found might already have automation policies // explicitly specified for them; we should exclude those from // our hidden/implicit policy, since applying a name to more than // one automation policy would be confusing and an error if app.tlsApp.Automation != nil { for _, ap := range app.tlsApp.Automation.Policies { for _, apHost := range ap.Subjects { if apHost == d { continue uniqueDomainsLoop } } } } // if no automation policy exists for the name yet, we // will associate it with an implicit one if !certmagic.SubjectQualifiesForPublicCert(d) { internal = append(internal, d) } } // ensure there is an automation policy to handle these certs err := app.createAutomationPolicies(ctx, internal) if err != nil { return err } // we need to reduce the mapping, i.e. group domains by address // since new routes are appended to servers by their address domainsByAddr := make(map[string][]string) for domain, addrs := range redirDomains { for _, addr := range addrs { addrStr := addr.String() domainsByAddr[addrStr] = append(domainsByAddr[addrStr], domain) } } // these keep track of the redirect server address(es) // and the routes for those servers which actually // respond with the redirects redirServerAddrs := make(map[string]struct{}) redirServers := make(map[string][]Route) var redirRoutes RouteList for addrStr, domains := range domainsByAddr { // build the matcher set for this redirect route; (note that we happen // to bypass Provision and Validate steps for these matcher modules) matcherSet := MatcherSet{MatchProtocol("http")} // match on known domain names, unless it's our special case of a // catch-all which is an empty string (common among catch-all sites // that enable on-demand TLS for yet-unknown domain names) if !(len(domains) == 1 && domains[0] == "") { matcherSet = append(matcherSet, MatchHost(domains)) } addr, err := caddy.ParseNetworkAddress(addrStr) if err != nil { return err } redirRoute := app.makeRedirRoute(addr.StartPort, matcherSet) // use the network/host information from the address, // but change the port to the HTTP port then rebuild redirAddr := addr redirAddr.StartPort = uint(app.httpPort()) redirAddr.EndPort = redirAddr.StartPort redirAddrStr := redirAddr.String() redirServers[redirAddrStr] = append(redirServers[redirAddrStr], redirRoute) } // on-demand TLS means that hostnames may be used which are not // explicitly defined in the config, and we still need to redirect // those; so we can append a single catch-all route (notice there // is no Host matcher) after the other redirect routes which will // allow us to handle unexpected/new hostnames... however, it's // not entirely clear what the redirect destination should be, // so I'm going to just hard-code the app's HTTPS port and call // it good for now... // TODO: This implies that all plaintext requests will be blindly // redirected to their HTTPS equivalent, even if this server // doesn't handle that hostname at all; I don't think this is a // bad thing, and it also obscures the actual hostnames that this // server is configured to match on, which may be desirable, but // it's not something that should be relied on. We can change this // if we want to. appendCatchAll := func(routes []Route) []Route { return append(routes, app.makeRedirRoute(uint(app.httpsPort()), MatcherSet{MatchProtocol("http")})) } redirServersLoop: for redirServerAddr, routes := range redirServers { // for each redirect listener, see if there's already a // server configured to listen on that exact address; if so, // insert the redirect route to the end of its route list // after any other routes with host matchers; otherwise, // we'll create a new server for all the listener addresses // that are unused and serve the remaining redirects from it for _, srv := range app.Servers { // only look at servers which listen on an address which // we want to add redirects to if !srv.hasListenerAddress(redirServerAddr) { continue } // find the index of the route after the last route with a host // matcher, then insert the redirects there, but before any // user-defined catch-all routes // see https://github.com/caddyserver/caddy/issues/3212 insertIndex := srv.findLastRouteWithHostMatcher() // add the redirects at the insert index, except for when // we have a catch-all for HTTPS, in which case the user's // defined catch-all should take precedence. See #4829 if len(uniqueDomainsForCerts) != 0 { srv.Routes = append(srv.Routes[:insertIndex], append(routes, srv.Routes[insertIndex:]...)...) } // append our catch-all route in case the user didn't define their own srv.Routes = appendCatchAll(srv.Routes) continue redirServersLoop } // no server with this listener address exists; // save this address and route for custom server redirServerAddrs[redirServerAddr] = struct{}{} redirRoutes = append(redirRoutes, routes...) } // if there are routes remaining which do not belong // in any existing server, make our own to serve the // rest of the redirects if len(redirServerAddrs) > 0 { redirServerAddrsList := make([]string, 0, len(redirServerAddrs)) for a := range redirServerAddrs { redirServerAddrsList = append(redirServerAddrsList, a) } app.Servers["remaining_auto_https_redirects"] = &Server{ Listen: redirServerAddrsList, Routes: appendCatchAll(redirRoutes), Logs: logCfg, } } return nil } func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route { redirTo := "https://{http.request.host}" // since this is an external redirect, we should only append an explicit // port if we know it is not the officially standardized HTTPS port, and, // notably, also not the port that Caddy thinks is the HTTPS port (the // configurable HTTPSPort parameter) - we can't change the standard HTTPS // port externally, so that config parameter is for internal use only; // we also do not append the port if it happens to be the HTTP port as // well, obviously (for example, user defines the HTTP port explicitly // in the list of listen addresses for a server) if redirToPort != uint(app.httpPort()) && redirToPort != uint(app.httpsPort()) && redirToPort != DefaultHTTPPort && redirToPort != DefaultHTTPSPort { redirTo += ":" + strconv.Itoa(int(redirToPort)) } redirTo += "{http.request.uri}" return Route{ MatcherSets: []MatcherSet{matcherSet}, Handlers: []MiddlewareHandler{ StaticResponse{ StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)), Headers: http.Header{ "Location": []string{redirTo}, }, Close: true, }, }, } } // createAutomationPolicies ensures that automated certificates for this // app are managed properly. This adds up to two automation policies: // one for the public names, and one for the internal names. If a catch-all // automation policy exists, it will be shallow-copied and used as the // base for the new ones (this is important for preserving behavior the // user intends to be "defaults"). func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []string) error { // before we begin, loop through the existing automation policies // and, for any ACMEIssuers we find, make sure they're filled in // with default values that might be specified in our HTTP app; also // look for a base (or "catch-all" / default) automation policy, // which we're going to essentially require, to make sure it has // those defaults, too var basePolicy *caddytls.AutomationPolicy var foundBasePolicy bool if app.tlsApp.Automation == nil { // we will expect this to not be nil from now on app.tlsApp.Automation = new(caddytls.AutomationConfig) } for _, ap := range app.tlsApp.Automation.Policies { // set up default issuer -- honestly, this is only // really necessary because the HTTP app is opinionated // and has settings which could be inferred as new // defaults for the ACMEIssuer in the TLS app (such as // what the HTTP and HTTPS ports are) if ap.Issuers == nil { var err error ap.Issuers, err = caddytls.DefaultIssuersProvisioned(ctx) if err != nil { return err } } for _, iss := range ap.Issuers { if acmeIssuer, ok := iss.(acmeCapable); ok { err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer()) if err != nil { return err } } } // if no external managers were configured, enable // implicit Tailscale support for convenience if ap.Managers == nil { ts, err := implicitTailscale(ctx) if err != nil { return err } ap.Managers = []certmagic.Manager{ts} // must reprovision the automation policy so that the underlying // CertMagic config knows about the updated Managers if err := ap.Provision(app.tlsApp); err != nil { return fmt.Errorf("re-provisioning automation policy: %v", err) } } // while we're here, is this the catch-all/base policy? if !foundBasePolicy && len(ap.Subjects) == 0 { basePolicy = ap foundBasePolicy = true } } if basePolicy == nil { // no base policy found; we will make one basePolicy = new(caddytls.AutomationPolicy) } if basePolicy.Managers == nil { // add implicit Tailscale integration, for harmless convenience ts, err := implicitTailscale(ctx) if err != nil { return err } basePolicy.Managers = []certmagic.Manager{ts} } // if the basePolicy has an existing ACMEIssuer (particularly to // include any type that embeds/wraps an ACMEIssuer), let's use it // (I guess we just use the first one?), otherwise we'll make one var baseACMEIssuer *caddytls.ACMEIssuer for _, iss := range basePolicy.Issuers { if acmeWrapper, ok := iss.(acmeCapable); ok { baseACMEIssuer = acmeWrapper.GetACMEIssuer() break } } if baseACMEIssuer == nil { // note that this happens if basePolicy.Issuers is empty // OR if it is not empty but does not have not an ACMEIssuer baseACMEIssuer = new(caddytls.ACMEIssuer) } // if there was a base policy to begin with, we already // filled in its issuer's defaults; if there wasn't, we // still need to do that if !foundBasePolicy { err := app.fillInACMEIssuer(baseACMEIssuer) if err != nil { return err } } // never overwrite any other issuer that might already be configured if basePolicy.Issuers == nil { var err error basePolicy.Issuers, err = caddytls.DefaultIssuersProvisioned(ctx) if err != nil { return err } for _, iss := range basePolicy.Issuers { if acmeIssuer, ok := iss.(acmeCapable); ok { err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer()) if err != nil { return err } } } } if !foundBasePolicy { // there was no base policy to begin with, so add // our base/catch-all policy - this will serve the // public-looking names as well as any other names // that don't match any other policy err := app.tlsApp.AddAutomationPolicy(basePolicy) if err != nil { return err } } else { // a base policy already existed; we might have // changed it, so re-provision it err := basePolicy.Provision(app.tlsApp) if err != nil { return err } } // public names will be taken care of by the base (catch-all) // policy, which we've ensured exists if not already specified; // internal names, however, need to be handled by an internal // issuer, which we need to make a new policy for, scoped to // just those names (yes, this logic is a bit asymmetric, but // it works, because our assumed/natural default issuer is an // ACME issuer) if len(internalNames) > 0 { internalIssuer := new(caddytls.InternalIssuer) // shallow-copy the base policy; we want to inherit // from it, not replace it... this takes two lines to // overrule compiler optimizations policyCopy := *basePolicy newPolicy := &policyCopy // very important to provision the issuer, since we // are bypassing the JSON-unmarshaling step if err := internalIssuer.Provision(ctx); err != nil { return err } // this policy should apply only to the given names // and should use our issuer -- yes, this overrides // any issuer that may have been set in the base // policy, but we do this because these names do not // already have a policy associated with them, which // is easy to do; consider the case of a Caddyfile // that has only "localhost" as a name, but sets the // default/global ACME CA to the Let's Encrypt staging // endpoint... they probably don't intend to change the // fundamental set of names that setting applies to, // rather they just want to change the CA for the set // of names that would normally use the production API; // anyway, that gets into the weeds a bit... newPolicy.Subjects = internalNames newPolicy.Issuers = []certmagic.Issuer{internalIssuer} err := app.tlsApp.AddAutomationPolicy(newPolicy) if err != nil { return err } } // we just changed a lot of stuff, so double-check that it's all good err := app.tlsApp.Validate() if err != nil { return err } return nil } // fillInACMEIssuer fills in default values into acmeIssuer that // are defined in app; these values at time of writing are just // app.HTTPPort and app.HTTPSPort, which are used by ACMEIssuer. // Sure, we could just use the global/CertMagic defaults, but if // a user has configured those ports in the HTTP app, it makes // sense to use them in the TLS app too, even if they forgot (or // were too lazy, like me) to set it in each automation policy // that uses it -- this just makes things a little less tedious // for the user, so they don't have to repeat those ports in // potentially many places. This function never steps on existing // config values. If any changes are made, acmeIssuer is // reprovisioned. acmeIssuer must not be nil. func (app *App) fillInACMEIssuer(acmeIssuer *caddytls.ACMEIssuer) error { if app.HTTPPort > 0 || app.HTTPSPort > 0 { if acmeIssuer.Challenges == nil { acmeIssuer.Challenges = new(caddytls.ChallengesConfig) } } if app.HTTPPort > 0 { if acmeIssuer.Challenges.HTTP == nil { acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig) } // don't overwrite existing explicit config if acmeIssuer.Challenges.HTTP.AlternatePort == 0 { acmeIssuer.Challenges.HTTP.AlternatePort = app.HTTPPort } } if app.HTTPSPort > 0 { if acmeIssuer.Challenges.TLSALPN == nil { acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig) } // don't overwrite existing explicit config if acmeIssuer.Challenges.TLSALPN.AlternatePort == 0 { acmeIssuer.Challenges.TLSALPN.AlternatePort = app.HTTPSPort } } // we must provision all ACME issuers, even if nothing // was changed, because we don't know if they are new // and haven't been provisioned yet; if an ACME issuer // never gets provisioned, its Agree field stays false, // which leads to, um, problems later on return acmeIssuer.Provision(app.ctx) } // automaticHTTPSPhase2 begins certificate management for // all names in the qualifying domain set for each server. // This phase must occur after provisioning and at the end // of app start, after all the servers have been started. // Doing this last ensures that there won't be any race // for listeners on the HTTP or HTTPS ports when management // is async (if CertMagic's solvers bind to those ports // first, then our servers would fail to bind to them, // which would be bad, since CertMagic's bindings are // temporary and don't serve the user's sites!). func (app *App) automaticHTTPSPhase2() error { if len(app.allCertDomains) == 0 { return nil } app.logger.Info("enabling automatic TLS certificate management", zap.Strings("domains", app.allCertDomains), ) err := app.tlsApp.Manage(app.allCertDomains) if err != nil { return fmt.Errorf("managing certificates for %v: %s", app.allCertDomains, err) } app.allCertDomains = nil // no longer needed; allow GC to deallocate return nil } // implicitTailscale returns a new and provisioned Tailscale module configured to be optional. func implicitTailscale(ctx caddy.Context) (caddytls.Tailscale, error) { ts := caddytls.Tailscale{Optional: true} err := ts.Provision(ctx) return ts, err } func isTailscaleDomain(name string) bool { return strings.HasSuffix(strings.ToLower(name), ".ts.net") } type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer } caddy-2.6.2/modules/caddyhttp/caddyauth/000077500000000000000000000000001435007237400202145ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/caddyauth/basicauth.go000066400000000000000000000230501435007237400225060ustar00rootroot00000000000000// 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 caddyauth import ( "encoding/base64" "encoding/hex" "encoding/json" "fmt" weakrand "math/rand" "net/http" "strings" "sync" "time" "github.com/caddyserver/caddy/v2" ) func init() { caddy.RegisterModule(HTTPBasicAuth{}) weakrand.Seed(time.Now().UnixNano()) } // HTTPBasicAuth facilitates HTTP basic authentication. type HTTPBasicAuth struct { // The algorithm with which the passwords are hashed. Default: bcrypt HashRaw json.RawMessage `json:"hash,omitempty" caddy:"namespace=http.authentication.hashes inline_key=algorithm"` // The list of accounts to authenticate. AccountList []Account `json:"accounts,omitempty"` // The name of the realm. Default: restricted Realm string `json:"realm,omitempty"` // If non-nil, a mapping of plaintext passwords to their // hashes will be cached in memory (with random eviction). // This can greatly improve the performance of traffic-heavy // servers that use secure password hashing algorithms, with // the downside that plaintext passwords will be stored in // memory for a longer time (this should not be a problem // as long as your machine is not compromised, at which point // all bets are off, since basicauth necessitates plaintext // passwords being received over the wire anyway). Note that // a cache hit does not mean it is a valid password. HashCache *Cache `json:"hash_cache,omitempty"` Accounts map[string]Account `json:"-"` Hash Comparer `json:"-"` // fakePassword is used when a given user is not found, // so that timing side-channels can be mitigated: it gives // us something to hash and compare even if the user does // not exist, which should have similar timing as a user // account that does exist. fakePassword []byte } // CaddyModule returns the Caddy module information. func (HTTPBasicAuth) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.authentication.providers.http_basic", New: func() caddy.Module { return new(HTTPBasicAuth) }, } } // Provision provisions the HTTP basic auth provider. func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error { if hba.HashRaw == nil { hba.HashRaw = json.RawMessage(`{"algorithm": "bcrypt"}`) } // load password hasher hasherIface, err := ctx.LoadModule(hba, "HashRaw") if err != nil { return fmt.Errorf("loading password hasher module: %v", err) } hba.Hash = hasherIface.(Comparer) if hba.Hash == nil { return fmt.Errorf("hash is required") } // if supported, generate a fake password we can compare against if needed if hasher, ok := hba.Hash.(Hasher); ok { hba.fakePassword = hasher.FakeHash() } repl := caddy.NewReplacer() // load account list hba.Accounts = make(map[string]Account) for i, acct := range hba.AccountList { if _, ok := hba.Accounts[acct.Username]; ok { return fmt.Errorf("account %d: username is not unique: %s", i, acct.Username) } acct.Username = repl.ReplaceAll(acct.Username, "") acct.Password = repl.ReplaceAll(acct.Password, "") acct.Salt = repl.ReplaceAll(acct.Salt, "") if acct.Username == "" || acct.Password == "" { return fmt.Errorf("account %d: username and password are required", i) } // TODO: Remove support for redundantly-encoded b64-encoded hashes // Passwords starting with '$' are likely in Modular Crypt Format, // so we don't need to base64 decode them. But historically, we // required redundant base64, so we try to decode it otherwise. if strings.HasPrefix(acct.Password, "$") { acct.password = []byte(acct.Password) } else { acct.password, err = base64.StdEncoding.DecodeString(acct.Password) if err != nil { return fmt.Errorf("base64-decoding password: %v", err) } } if acct.Salt != "" { acct.salt, err = base64.StdEncoding.DecodeString(acct.Salt) if err != nil { return fmt.Errorf("base64-decoding salt: %v", err) } } hba.Accounts[acct.Username] = acct } hba.AccountList = nil // allow GC to deallocate if hba.HashCache != nil { hba.HashCache.cache = make(map[string]bool) hba.HashCache.mu = new(sync.RWMutex) } return nil } // Authenticate validates the user credentials in req and returns the user, if valid. func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request) (User, bool, error) { username, plaintextPasswordStr, ok := req.BasicAuth() if !ok { return hba.promptForCredentials(w, nil) } account, accountExists := hba.Accounts[username] if !accountExists { // don't return early if account does not exist; we want // to try to avoid side-channels that leak existence, so // we use a fake password to simulate realistic CPU cycles account.password = hba.fakePassword } same, err := hba.correctPassword(account, []byte(plaintextPasswordStr)) if err != nil || !same || !accountExists { return hba.promptForCredentials(w, err) } return User{ID: username}, true, nil } func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []byte) (bool, error) { compare := func() (bool, error) { return hba.Hash.Compare(account.password, plaintextPassword, account.salt) } // if no caching is enabled, simply return the result of hashing + comparing if hba.HashCache == nil { return compare() } // compute a cache key that is unique for these input parameters cacheKey := hex.EncodeToString(append(append(account.password, account.salt...), plaintextPassword...)) // fast track: if the result of the input is already cached, use it hba.HashCache.mu.RLock() same, ok := hba.HashCache.cache[cacheKey] hba.HashCache.mu.RUnlock() if ok { return same, nil } // slow track: do the expensive op, then add it to the cache same, err := compare() if err != nil { return false, err } hba.HashCache.mu.Lock() if len(hba.HashCache.cache) >= 1000 { hba.HashCache.makeRoom() // keep cache size under control } hba.HashCache.cache[cacheKey] = same hba.HashCache.mu.Unlock() return same, nil } func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error) (User, bool, error) { // browsers show a message that says something like: // "The website says: " // which is kinda dumb, but whatever. realm := hba.Realm if realm == "" { realm = "restricted" } w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm)) return User{}, false, err } // Cache enables caching of basic auth results. This is especially // helpful for secure password hashes which can be expensive to // compute on every HTTP request. type Cache struct { mu *sync.RWMutex // map of concatenated hashed password + plaintext password + salt, to result cache map[string]bool } // makeRoom deletes about 1/10 of the items in the cache // in order to keep its size under control. It must not be // called without a lock on c.mu. func (c *Cache) makeRoom() { // we delete more than just 1 entry so that we don't have // to do this on every request; assuming the capacity of // the cache is on a long tail, we can save a lot of CPU // time by doing a whole bunch of deletions now and then // we won't have to do them again for a while numToDelete := len(c.cache) / 10 if numToDelete < 1 { numToDelete = 1 } for deleted := 0; deleted <= numToDelete; deleted++ { // Go maps are "nondeterministic" not actually random, // so although we could just chop off the "front" of the // map with less code, this is a heavily skewed eviction // strategy; generating random numbers is cheap and // ensures a much better distribution. //nolint:gosec rnd := weakrand.Intn(len(c.cache)) i := 0 for key := range c.cache { if i == rnd { delete(c.cache, key) break } i++ } } } // Comparer is a type that can securely compare // a plaintext password with a hashed password // in constant-time. Comparers should hash the // plaintext password and then use constant-time // comparison. type Comparer interface { // Compare returns true if the result of hashing // plaintextPassword with salt is hashedPassword, // false otherwise. An error is returned only if // there is a technical/configuration error. Compare(hashedPassword, plaintextPassword, salt []byte) (bool, error) } // Hasher is a type that can generate a secure hash // given a plaintext and optional salt (for algorithms // that require a salt). Hashing modules which implement // this interface can be used with the hash-password // subcommand as well as benefitting from anti-timing // features. A hasher also returns a fake hash which // can be used for timing side-channel mitigation. type Hasher interface { Hash(plaintext, salt []byte) ([]byte, error) FakeHash() []byte } // Account contains a username, password, and salt (if applicable). type Account struct { // A user's username. Username string `json:"username"` // The user's hashed password, base64-encoded. Password string `json:"password"` // The user's password salt, base64-encoded; for // algorithms where external salt is needed. Salt string `json:"salt,omitempty"` password, salt []byte } // Interface guards var ( _ caddy.Provisioner = (*HTTPBasicAuth)(nil) _ Authenticator = (*HTTPBasicAuth)(nil) ) caddy-2.6.2/modules/caddyhttp/caddyauth/caddyauth.go000066400000000000000000000072651435007237400225230ustar00rootroot00000000000000// 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 caddyauth import ( "fmt" "net/http" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap" ) func init() { caddy.RegisterModule(Authentication{}) } // Authentication is a middleware which provides user authentication. // Rejects requests with HTTP 401 if the request is not authenticated. // // After a successful authentication, the placeholder // `{http.auth.user.id}` will be set to the username, and also // `{http.auth.user.*}` placeholders may be set for any authentication // modules that provide user metadata. // // Its API is still experimental and may be subject to change. type Authentication struct { // A set of authentication providers. If none are specified, // all requests will always be unauthenticated. ProvidersRaw caddy.ModuleMap `json:"providers,omitempty" caddy:"namespace=http.authentication.providers"` Providers map[string]Authenticator `json:"-"` logger *zap.Logger } // CaddyModule returns the Caddy module information. func (Authentication) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.authentication", New: func() caddy.Module { return new(Authentication) }, } } // Provision sets up a. func (a *Authentication) Provision(ctx caddy.Context) error { a.logger = ctx.Logger() a.Providers = make(map[string]Authenticator) mods, err := ctx.LoadModule(a, "ProvidersRaw") if err != nil { return fmt.Errorf("loading authentication providers: %v", err) } for modName, modIface := range mods.(map[string]any) { a.Providers[modName] = modIface.(Authenticator) } return nil } func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { var user User var authed bool var err error for provName, prov := range a.Providers { user, authed, err = prov.Authenticate(w, r) if err != nil { a.logger.Error("auth provider returned error", zap.String("provider", provName), zap.Error(err)) continue } if authed { break } } if !authed { return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")) } repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl.Set("http.auth.user.id", user.ID) for k, v := range user.Metadata { repl.Set("http.auth.user."+k, v) } return next.ServeHTTP(w, r) } // Authenticator is a type which can authenticate a request. // If a request was not authenticated, it returns false. An // error is only returned if authenticating the request fails // for a technical reason (not for bad/missing credentials). type Authenticator interface { Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) } // User represents an authenticated user. type User struct { // The ID of the authenticated user. ID string // Any other relevant data about this // user. Keys should be adhere to Caddy // conventions (snake_casing), as all // keys will be made available as // placeholders. Metadata map[string]string } // Interface guards var ( _ caddy.Provisioner = (*Authentication)(nil) _ caddyhttp.MiddlewareHandler = (*Authentication)(nil) ) caddy-2.6.2/modules/caddyhttp/caddyauth/caddyfile.go000066400000000000000000000045351435007237400224760ustar00rootroot00000000000000// 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 caddyauth import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { httpcaddyfile.RegisterHandlerDirective("basicauth", parseCaddyfile) } // parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // basicauth [] [ []] { // [] // ... // } // // If no hash algorithm is supplied, bcrypt will be assumed. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { var ba HTTPBasicAuth ba.HashCache = new(Cache) for h.Next() { var cmp Comparer args := h.RemainingArgs() var hashName string switch len(args) { case 0: hashName = "bcrypt" case 1: hashName = args[0] case 2: hashName = args[0] ba.Realm = args[1] default: return nil, h.ArgErr() } switch hashName { case "bcrypt": cmp = BcryptHash{} case "scrypt": cmp = ScryptHash{} default: return nil, h.Errf("unrecognized hash algorithm: %s", hashName) } ba.HashRaw = caddyconfig.JSONModuleObject(cmp, "algorithm", hashName, nil) for h.NextBlock(0) { username := h.Val() var b64Pwd, b64Salt string h.Args(&b64Pwd, &b64Salt) if h.NextArg() { return nil, h.ArgErr() } if username == "" || b64Pwd == "" { return nil, h.Err("username and password cannot be empty or missing") } ba.AccountList = append(ba.AccountList, Account{ Username: username, Password: b64Pwd, Salt: b64Salt, }) } } return Authentication{ ProvidersRaw: caddy.ModuleMap{ "http_basic": caddyconfig.JSON(ba, nil), }, }, nil } caddy-2.6.2/modules/caddyhttp/caddyauth/command.go000066400000000000000000000072051435007237400221650ustar00rootroot00000000000000// 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 caddyauth import ( "bufio" "bytes" "encoding/base64" "flag" "fmt" "os" "os/signal" "github.com/caddyserver/caddy/v2" caddycmd "github.com/caddyserver/caddy/v2/cmd" "golang.org/x/term" ) func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "hash-password", Func: cmdHashPassword, Usage: "[--algorithm ] [--salt ] [--plaintext ]", Short: "Hashes a password and writes base64", Long: ` Convenient way to hash a plaintext password. The resulting hash is written to stdout as a base64 string. --plaintext, when omitted, will be read from stdin. If Caddy is attached to a controlling tty, the plaintext will not be echoed. --algorithm may be bcrypt or scrypt. If scrypt, the default parameters are used. Use the --salt flag for algorithms which require a salt to be provided (scrypt). Note that scrypt is deprecated. Please use 'bcrypt' instead. `, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("hash-password", flag.ExitOnError) fs.String("algorithm", "bcrypt", "Name of the hash algorithm") fs.String("plaintext", "", "The plaintext password") fs.String("salt", "", "The password salt") return fs }(), }) } func cmdHashPassword(fs caddycmd.Flags) (int, error) { var err error algorithm := fs.String("algorithm") plaintext := []byte(fs.String("plaintext")) salt := []byte(fs.String("salt")) if len(plaintext) == 0 { fd := int(os.Stdin.Fd()) if term.IsTerminal(fd) { // ensure the terminal state is restored on SIGINT state, _ := term.GetState(fd) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) go func() { <-c _ = term.Restore(fd, state) os.Exit(caddy.ExitCodeFailedStartup) }() defer signal.Stop(c) fmt.Fprint(os.Stderr, "Enter password: ") plaintext, err = term.ReadPassword(fd) fmt.Fprintln(os.Stderr) if err != nil { return caddy.ExitCodeFailedStartup, err } fmt.Fprint(os.Stderr, "Confirm password: ") confirmation, err := term.ReadPassword(fd) fmt.Fprintln(os.Stderr) if err != nil { return caddy.ExitCodeFailedStartup, err } if !bytes.Equal(plaintext, confirmation) { return caddy.ExitCodeFailedStartup, fmt.Errorf("password does not match") } } else { rd := bufio.NewReader(os.Stdin) plaintext, err = rd.ReadBytes('\n') if err != nil { return caddy.ExitCodeFailedStartup, err } plaintext = plaintext[:len(plaintext)-1] // Trailing newline } if len(plaintext) == 0 { return caddy.ExitCodeFailedStartup, fmt.Errorf("plaintext is required") } } var hash []byte var hashString string switch algorithm { case "bcrypt": hash, err = BcryptHash{}.Hash(plaintext, nil) hashString = string(hash) case "scrypt": def := ScryptHash{} def.SetDefaults() hash, err = def.Hash(plaintext, salt) hashString = base64.StdEncoding.EncodeToString(hash) default: return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm) } if err != nil { return caddy.ExitCodeFailedStartup, err } fmt.Println(hashString) return 0, nil } caddy-2.6.2/modules/caddyhttp/caddyauth/hashes.go000066400000000000000000000105541435007237400220230ustar00rootroot00000000000000// 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 caddyauth import ( "crypto/subtle" "encoding/base64" "github.com/caddyserver/caddy/v2" "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/scrypt" ) func init() { caddy.RegisterModule(BcryptHash{}) caddy.RegisterModule(ScryptHash{}) } // BcryptHash implements the bcrypt hash. type BcryptHash struct{} // CaddyModule returns the Caddy module information. func (BcryptHash) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.authentication.hashes.bcrypt", New: func() caddy.Module { return new(BcryptHash) }, } } // Compare compares passwords. func (BcryptHash) Compare(hashed, plaintext, _ []byte) (bool, error) { err := bcrypt.CompareHashAndPassword(hashed, plaintext) if err == bcrypt.ErrMismatchedHashAndPassword { return false, nil } if err != nil { return false, err } return true, nil } // Hash hashes plaintext using a random salt. func (BcryptHash) Hash(plaintext, _ []byte) ([]byte, error) { return bcrypt.GenerateFromPassword(plaintext, 14) } // FakeHash returns a fake hash. func (BcryptHash) FakeHash() []byte { // hashed with the following command: // caddy hash-password --plaintext "antitiming" --algorithm "bcrypt" return []byte("$2a$14$X3ulqf/iGxnf1k6oMZ.RZeJUoqI9PX2PM4rS5lkIKJXduLGXGPrt6") } // ScryptHash implements the scrypt KDF as a hash. // // DEPRECATED, please use 'bcrypt' instead. type ScryptHash struct { // scrypt's N parameter. If unset or 0, a safe default is used. N int `json:"N,omitempty"` // scrypt's r parameter. If unset or 0, a safe default is used. R int `json:"r,omitempty"` // scrypt's p parameter. If unset or 0, a safe default is used. P int `json:"p,omitempty"` // scrypt's key length parameter (in bytes). If unset or 0, a // safe default is used. KeyLength int `json:"key_length,omitempty"` } // CaddyModule returns the Caddy module information. func (ScryptHash) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.authentication.hashes.scrypt", New: func() caddy.Module { return new(ScryptHash) }, } } // Provision sets up s. func (s *ScryptHash) Provision(ctx caddy.Context) error { s.SetDefaults() ctx.Logger().Warn("use of 'scrypt' is deprecated, please use 'bcrypt' instead") return nil } // SetDefaults sets safe default parameters, but does // not overwrite existing values. Each default parameter // is set independently; it does not check to ensure // that r*p < 2^30. The defaults chosen are those as // recommended in 2019 by // https://godoc.org/golang.org/x/crypto/scrypt. func (s *ScryptHash) SetDefaults() { if s.N == 0 { s.N = 32768 } if s.R == 0 { s.R = 8 } if s.P == 0 { s.P = 1 } if s.KeyLength == 0 { s.KeyLength = 32 } } // Compare compares passwords. func (s ScryptHash) Compare(hashed, plaintext, salt []byte) (bool, error) { ourHash, err := scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength) if err != nil { return false, err } if hashesMatch(hashed, ourHash) { return true, nil } return false, nil } // Hash hashes plaintext using the given salt. func (s ScryptHash) Hash(plaintext, salt []byte) ([]byte, error) { return scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength) } // FakeHash returns a fake hash. func (ScryptHash) FakeHash() []byte { // hashed with the following command: // caddy hash-password --plaintext "antitiming" --salt "fakesalt" --algorithm "scrypt" bytes, _ := base64.StdEncoding.DecodeString("kFbjiVemlwK/ZS0tS6/UQqEDeaNMigyCs48KEsGUse8=") return bytes } func hashesMatch(pwdHash1, pwdHash2 []byte) bool { return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1 } // Interface guards var ( _ Comparer = (*BcryptHash)(nil) _ Comparer = (*ScryptHash)(nil) _ Hasher = (*BcryptHash)(nil) _ Hasher = (*ScryptHash)(nil) _ caddy.Provisioner = (*ScryptHash)(nil) ) caddy-2.6.2/modules/caddyhttp/caddyhttp.go000066400000000000000000000233031435007237400205620ustar00rootroot00000000000000// 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 caddyhttp import ( "bytes" "encoding/json" "io" "net" "net/http" "path" "path/filepath" "strconv" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { caddy.RegisterModule(tlsPlaceholderWrapper{}) } // RequestMatcher is a type that can match to a request. // A route matcher MUST NOT modify the request, with the // only exception being its context. type RequestMatcher interface { Match(*http.Request) bool } // Handler is like http.Handler except ServeHTTP may return an error. // // If any handler encounters an error, it should be returned for proper // handling. Return values should be propagated down the middleware chain // by returning it unchanged. Returned errors should not be re-wrapped // if they are already HandlerError values. type Handler interface { ServeHTTP(http.ResponseWriter, *http.Request) error } // HandlerFunc is a convenience type like http.HandlerFunc. type HandlerFunc func(http.ResponseWriter, *http.Request) error // ServeHTTP implements the Handler interface. func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error { return f(w, r) } // Middleware chains one Handler to the next by being passed // the next Handler in the chain. type Middleware func(Handler) Handler // MiddlewareHandler is like Handler except it takes as a third // argument the next handler in the chain. The next handler will // never be nil, but may be a no-op handler if this is the last // handler in the chain. Handlers which act as middleware should // call the next handler's ServeHTTP method so as to propagate // the request down the chain properly. Handlers which act as // responders (content origins) need not invoke the next handler, // since the last handler in the chain should be the first to // write the response. type MiddlewareHandler interface { ServeHTTP(http.ResponseWriter, *http.Request, Handler) error } // emptyHandler is used as a no-op handler. var emptyHandler Handler = HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil }) // An implicit suffix middleware that, if reached, sets the StatusCode to the // error stored in the ErrorCtxKey. This is to prevent situations where the // Error chain does not actually handle the error (for instance, it matches only // on some errors). See #3053 var errorEmptyHandler Handler = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { httpError := r.Context().Value(ErrorCtxKey) if handlerError, ok := httpError.(HandlerError); ok { w.WriteHeader(handlerError.StatusCode) } else { w.WriteHeader(http.StatusInternalServerError) } return nil }) // ResponseHandler pairs a response matcher with custom handling // logic. Either the status code can be changed to something else // while using the original response body, or, if a status code // is not set, it can execute a custom route list; this is useful // for executing handler routes based on the properties of an HTTP // response that has not been written out to the client yet. // // To use this type, provision it at module load time, then when // ready to use, match the response against its matcher; if it // matches (or doesn't have a matcher), change the status code on // the response if configured; otherwise invoke the routes by // calling `rh.Routes.Compile(next).ServeHTTP(rw, req)` (or similar). type ResponseHandler struct { // The response matcher for this handler. If empty/nil, // it always matches. Match *ResponseMatcher `json:"match,omitempty"` // To write the original response body but with a different // status code, set this field to the desired status code. // If set, this takes priority over routes. StatusCode WeakString `json:"status_code,omitempty"` // The list of HTTP routes to execute if no status code is // specified. If evaluated, the original response body // will not be written. Routes RouteList `json:"routes,omitempty"` } // Provision sets up the routse in rh. func (rh *ResponseHandler) Provision(ctx caddy.Context) error { if rh.Routes != nil { err := rh.Routes.Provision(ctx) if err != nil { return err } } return nil } // WeakString is a type that unmarshals any JSON value // as a string literal, with the following exceptions: // // 1. actual string values are decoded as strings; and // 2. null is decoded as empty string; // // and provides methods for getting the value as various // primitive types. However, using this type removes any // type safety as far as deserializing JSON is concerned. type WeakString string // UnmarshalJSON satisfies json.Unmarshaler according to // this type's documentation. func (ws *WeakString) UnmarshalJSON(b []byte) error { if len(b) == 0 { return io.EOF } if b[0] == byte('"') && b[len(b)-1] == byte('"') { var s string err := json.Unmarshal(b, &s) if err != nil { return err } *ws = WeakString(s) return nil } if bytes.Equal(b, []byte("null")) { return nil } *ws = WeakString(b) return nil } // MarshalJSON marshals was a boolean if true or false, // a number if an integer, or a string otherwise. func (ws WeakString) MarshalJSON() ([]byte, error) { if ws == "true" { return []byte("true"), nil } if ws == "false" { return []byte("false"), nil } if num, err := strconv.Atoi(string(ws)); err == nil { return json.Marshal(num) } return json.Marshal(string(ws)) } // Int returns ws as an integer. If ws is not an // integer, 0 is returned. func (ws WeakString) Int() int { num, _ := strconv.Atoi(string(ws)) return num } // Float64 returns ws as a float64. If ws is not a // float value, the zero value is returned. func (ws WeakString) Float64() float64 { num, _ := strconv.ParseFloat(string(ws), 64) return num } // Bool returns ws as a boolean. If ws is not a // boolean, false is returned. func (ws WeakString) Bool() bool { return string(ws) == "true" } // String returns ws as a string. func (ws WeakString) String() string { return string(ws) } // StatusCodeMatches returns true if a real HTTP status code matches // the configured status code, which may be either a real HTTP status // code or an integer representing a class of codes (e.g. 4 for all // 4xx statuses). func StatusCodeMatches(actual, configured int) bool { if actual == configured { return true } if configured < 100 && actual >= configured*100 && actual < (configured+1)*100 { return true } return false } // SanitizedPathJoin performs filepath.Join(root, reqPath) that // is safe against directory traversal attacks. It uses logic // similar to that in the Go standard library, specifically // in the implementation of http.Dir. The root is assumed to // be a trusted path, but reqPath is not; and the output will // never be outside of root. The resulting path can be used // with the local file system. func SanitizedPathJoin(root, reqPath string) string { if root == "" { root = "." } path := filepath.Join(root, filepath.Clean("/"+reqPath)) // filepath.Join also cleans the path, and cleaning strips // the trailing slash, so we need to re-add it afterwards. // if the length is 1, then it's a path to the root, // and that should return ".", so we don't append the separator. if strings.HasSuffix(reqPath, "/") && len(reqPath) > 1 { path += separator } return path } // CleanPath cleans path p according to path.Clean(), but only // merges repeated slashes if collapseSlashes is true, and always // preserves trailing slashes. func CleanPath(p string, collapseSlashes bool) string { if collapseSlashes { return cleanPath(p) } // insert an invalid/impossible URI character into each two consecutive // slashes to expand empty path segments; then clean the path as usual, // and then remove the remaining temporary characters. const tmpCh = 0xff var sb strings.Builder for i, ch := range p { if ch == '/' && i > 0 && p[i-1] == '/' { sb.WriteByte(tmpCh) } sb.WriteRune(ch) } halfCleaned := cleanPath(sb.String()) halfCleaned = strings.ReplaceAll(halfCleaned, string([]byte{tmpCh}), "") return halfCleaned } // cleanPath does path.Clean(p) but preserves any trailing slash. func cleanPath(p string) string { cleaned := path.Clean(p) if cleaned != "/" && strings.HasSuffix(p, "/") { cleaned = cleaned + "/" } return cleaned } // tlsPlaceholderWrapper is a no-op listener wrapper that marks // where the TLS listener should be in a chain of listener wrappers. // It should only be used if another listener wrapper must be placed // in front of the TLS handshake. type tlsPlaceholderWrapper struct{} func (tlsPlaceholderWrapper) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.listeners.tls", New: func() caddy.Module { return new(tlsPlaceholderWrapper) }, } } func (tlsPlaceholderWrapper) WrapListener(ln net.Listener) net.Listener { return ln } func (tlsPlaceholderWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } const ( // DefaultHTTPPort is the default port for HTTP. DefaultHTTPPort = 80 // DefaultHTTPSPort is the default port for HTTPS. DefaultHTTPSPort = 443 ) const separator = string(filepath.Separator) // Interface guard var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil) var _ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil) caddy-2.6.2/modules/caddyhttp/caddyhttp_test.go000066400000000000000000000057201435007237400216240ustar00rootroot00000000000000package caddyhttp import ( "net/url" "path/filepath" "testing" ) func TestSanitizedPathJoin(t *testing.T) { // For reference: // %2e = . // %2f = / // %5c = \ for i, tc := range []struct { inputRoot string inputPath string expect string }{ { inputPath: "", expect: ".", }, { inputPath: "/", expect: ".", }, { inputPath: "/foo", expect: "foo", }, { inputPath: "/foo/", expect: "foo" + separator, }, { inputPath: "/foo/bar", expect: filepath.Join("foo", "bar"), }, { inputRoot: "/a", inputPath: "/foo/bar", expect: filepath.Join("/", "a", "foo", "bar"), }, { inputPath: "/foo/../bar", expect: "bar", }, { inputRoot: "/a/b", inputPath: "/foo/../bar", expect: filepath.Join("/", "a", "b", "bar"), }, { inputRoot: "/a/b", inputPath: "/..%2fbar", expect: filepath.Join("/", "a", "b", "bar"), }, { inputRoot: "/a/b", inputPath: "/%2e%2e%2fbar", expect: filepath.Join("/", "a", "b", "bar"), }, { inputRoot: "/a/b", inputPath: "/%2e%2e%2f%2e%2e%2f", expect: filepath.Join("/", "a", "b") + separator, }, { inputRoot: "C:\\www", inputPath: "/foo/bar", expect: filepath.Join("C:\\www", "foo", "bar"), }, { inputRoot: "C:\\www", inputPath: "/D:\\foo\\bar", expect: filepath.Join("C:\\www", "D:\\foo\\bar"), }, } { // we don't *need* to use an actual parsed URL, but it // adds some authenticity to the tests since real-world // values will be coming in from URLs; thus, the test // corpus can contain paths as encoded by clients, which // more closely emulates the actual attack vector u, err := url.Parse("http://test:9999" + tc.inputPath) if err != nil { t.Fatalf("Test %d: invalid URL: %v", i, err) } actual := SanitizedPathJoin(tc.inputRoot, u.Path) if actual != tc.expect { t.Errorf("Test %d: SanitizedPathJoin('%s', '%s') => %s (expected '%s')", i, tc.inputRoot, tc.inputPath, actual, tc.expect) } } } func TestCleanPath(t *testing.T) { for i, tc := range []struct { input string mergeSlashes bool expect string }{ { input: "/foo", expect: "/foo", }, { input: "/foo/", expect: "/foo/", }, { input: "//foo", expect: "//foo", }, { input: "//foo", mergeSlashes: true, expect: "/foo", }, { input: "/foo//bar/", mergeSlashes: true, expect: "/foo/bar/", }, { input: "/foo/./.././bar", expect: "/bar", }, { input: "/foo//./..//./bar", expect: "/foo//bar", }, { input: "/foo///./..//./bar", expect: "/foo///bar", }, { input: "/foo///./..//.", expect: "/foo//", }, { input: "/foo//./bar", expect: "/foo//bar", }, } { actual := CleanPath(tc.input, tc.mergeSlashes) if actual != tc.expect { t.Errorf("Test %d [input='%s' mergeSlashes=%t]: Got '%s', expected '%s'", i, tc.input, tc.mergeSlashes, actual, tc.expect) } } } caddy-2.6.2/modules/caddyhttp/celmatcher.go000066400000000000000000000543301435007237400207110ustar00rootroot00000000000000// 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 caddyhttp import ( "crypto/x509/pkix" "encoding/json" "errors" "fmt" "net/http" "reflect" "regexp" "strings" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/google/cel-go/cel" "github.com/google/cel-go/common" "github.com/google/cel-go/common/operators" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/traits" "github.com/google/cel-go/ext" "github.com/google/cel-go/interpreter" "github.com/google/cel-go/interpreter/functions" "github.com/google/cel-go/parser" "go.uber.org/zap" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) func init() { caddy.RegisterModule(MatchExpression{}) } // MatchExpression matches requests by evaluating a // [CEL](https://github.com/google/cel-spec) expression. // This enables complex logic to be expressed using a comfortable, // familiar syntax. Please refer to // [the standard definitions of CEL functions and operators](https://github.com/google/cel-spec/blob/master/doc/langdef.md#standard-definitions). // // This matcher's JSON interface is actually a string, not a struct. // The generated docs are not correct because this type has custom // marshaling logic. // // COMPATIBILITY NOTE: This module is still experimental and is not // subject to Caddy's compatibility guarantee. type MatchExpression struct { // The CEL expression to evaluate. Any Caddy placeholders // will be expanded and situated into proper CEL function // calls before evaluating. Expr string expandedExpr string prg cel.Program ta ref.TypeAdapter log *zap.Logger } // CaddyModule returns the Caddy module information. func (MatchExpression) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.expression", New: func() caddy.Module { return new(MatchExpression) }, } } // MarshalJSON marshals m's expression. func (m MatchExpression) MarshalJSON() ([]byte, error) { return json.Marshal(m.Expr) } // UnmarshalJSON unmarshals m's expression. func (m *MatchExpression) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &m.Expr) } // Provision sets ups m. func (m *MatchExpression) Provision(ctx caddy.Context) error { m.log = ctx.Logger() // replace placeholders with a function call - this is just some // light (and possibly naïve) syntactic sugar m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion) // our type adapter expands CEL's standard type support m.ta = celTypeAdapter{} // initialize the CEL libraries from the Matcher implementations which // have been configured to support CEL. matcherLibProducers := []CELLibraryProducer{} for _, info := range caddy.GetModules("http.matchers") { p, ok := info.New().(CELLibraryProducer) if ok { matcherLibProducers = append(matcherLibProducers, p) } } // Assemble the compilation and program options from the different library // producers into a single cel.Library implementation. matcherEnvOpts := []cel.EnvOption{} matcherProgramOpts := []cel.ProgramOption{} for _, producer := range matcherLibProducers { l, err := producer.CELLibrary(ctx) if err != nil { return fmt.Errorf("error initializing CEL library for %T: %v", producer, err) } matcherEnvOpts = append(matcherEnvOpts, l.CompileOptions()...) matcherProgramOpts = append(matcherProgramOpts, l.ProgramOptions()...) } matcherLib := cel.Lib(NewMatcherCELLibrary(matcherEnvOpts, matcherProgramOpts)) // create the CEL environment env, err := cel.NewEnv( cel.Function(placeholderFuncName, cel.SingletonBinaryImpl(m.caddyPlaceholderFunc), cel.Overload( placeholderFuncName+"_httpRequest_string", []*cel.Type{httpRequestObjectType, cel.StringType}, cel.AnyType, )), cel.Variable("request", httpRequestObjectType), cel.CustomTypeAdapter(m.ta), ext.Strings(), matcherLib, ) if err != nil { return fmt.Errorf("setting up CEL environment: %v", err) } // parse and type-check the expression checked, issues := env.Compile(m.expandedExpr) if issues.Err() != nil { return fmt.Errorf("compiling CEL program: %s", issues.Err()) } // request matching is a boolean operation, so we don't really know // what to do if the expression returns a non-boolean type if checked.OutputType() != cel.BoolType { return fmt.Errorf("CEL request matcher expects return type of bool, not %s", checked.OutputType()) } // compile the "program" m.prg, err = env.Program(checked, cel.EvalOptions(cel.OptOptimize)) if err != nil { return fmt.Errorf("compiling CEL program: %s", err) } return nil } // Match returns true if r matches m. func (m MatchExpression) Match(r *http.Request) bool { celReq := celHTTPRequest{r} out, _, err := m.prg.Eval(celReq) if err != nil { m.log.Error("evaluating expression", zap.Error(err)) SetVar(r.Context(), MatcherErrorVarKey, err) return false } if outBool, ok := out.Value().(bool); ok { return outBool } return false } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchExpression) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.CountRemainingArgs() > 1 { m.Expr = strings.Join(d.RemainingArgsRaw(), " ") } else { m.Expr = d.Val() } } return nil } // caddyPlaceholderFunc implements the custom CEL function that accesses the // Replacer on a request and gets values from it. func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val { celReq, ok := lhs.(celHTTPRequest) if !ok { return types.NewErr( "invalid request of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)", lhs.Type(), ) } phStr, ok := rhs.(types.String) if !ok { return types.NewErr( "invalid placeholder variable name of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)", rhs.Type(), ) } repl := celReq.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) val, _ := repl.Get(string(phStr)) return m.ta.NativeToValue(val) } // httpRequestCELType is the type representation of a native HTTP request. var httpRequestCELType = types.NewTypeValue("http.Request", traits.ReceiverType) // celHTTPRequest wraps an http.Request with ref.Val interface methods. // // This type also implements the interpreter.Activation interface which // drops allocation costs for CEL expression evaluations by roughly half. type celHTTPRequest struct{ *http.Request } func (cr celHTTPRequest) ResolveName(name string) (any, bool) { if name == "request" { return cr, true } return nil, false } func (cr celHTTPRequest) Parent() interpreter.Activation { return nil } func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (any, error) { return cr.Request, nil } func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val { panic("not implemented") } func (cr celHTTPRequest) Equal(other ref.Val) ref.Val { if o, ok := other.Value().(celHTTPRequest); ok { return types.Bool(o.Request == cr.Request) } return types.ValOrErr(other, "%v is not comparable type", other) } func (celHTTPRequest) Type() ref.Type { return httpRequestCELType } func (cr celHTTPRequest) Value() any { return cr } var pkixNameCELType = types.NewTypeValue("pkix.Name", traits.ReceiverType) // celPkixName wraps an pkix.Name with // methods to satisfy the ref.Val interface. type celPkixName struct{ *pkix.Name } func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (any, error) { return pn.Name, nil } func (celPkixName) ConvertToType(typeVal ref.Type) ref.Val { panic("not implemented") } func (pn celPkixName) Equal(other ref.Val) ref.Val { if o, ok := other.Value().(string); ok { return types.Bool(pn.Name.String() == o) } return types.ValOrErr(other, "%v is not comparable type", other) } func (celPkixName) Type() ref.Type { return pkixNameCELType } func (pn celPkixName) Value() any { return pn } // celTypeAdapter can adapt our custom types to a CEL value. type celTypeAdapter struct{} func (celTypeAdapter) NativeToValue(value any) ref.Val { switch v := value.(type) { case celHTTPRequest: return v case pkix.Name: return celPkixName{&v} case time.Time: return types.Timestamp{Time: v} case error: types.NewErr(v.Error()) } return types.DefaultTypeAdapter.NativeToValue(value) } // CELLibraryProducer provide CEL libraries that expose a Matcher // implementation as a first class function within the CEL expression // matcher. type CELLibraryProducer interface { // CELLibrary creates a cel.Library which makes it possible to use the // target object within CEL expression matchers. CELLibrary(caddy.Context) (cel.Library, error) } // CELMatcherImpl creates a new cel.Library based on the following pieces of // data: // // - macroName: the function name to be used within CEL. This will be a macro // and not a function proper. // - funcName: the function overload name generated by the CEL macro used to // represent the matcher. // - matcherDataTypes: the argument types to the macro. // - fac: a matcherFactory implementation which converts from CEL constant // values to a Matcher instance. // // Note, macro names and function names must not collide with other macros or // functions exposed within CEL expressions, or an error will be produced // during the expression matcher plan time. // // The existing CELMatcherImpl support methods are configured to support a // limited set of function signatures. For strong type validation you may need // to provide a custom macro which does a more detailed analysis of the CEL // literal provided to the macro as an argument. func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fac CELMatcherFactory) (cel.Library, error) { requestType := cel.ObjectType("http.Request") var macro parser.Macro switch len(matcherDataTypes) { case 1: matcherDataType := matcherDataTypes[0] switch matcherDataType.String() { case "list(string)": macro = parser.NewGlobalVarArgMacro(macroName, celMatcherStringListMacroExpander(funcName)) case cel.StringType.String(): macro = parser.NewGlobalMacro(macroName, 1, celMatcherStringMacroExpander(funcName)) case CELTypeJSON.String(): macro = parser.NewGlobalMacro(macroName, 1, celMatcherJSONMacroExpander(funcName)) default: return nil, fmt.Errorf("unsupported matcher data type: %s", matcherDataType) } case 2: if matcherDataTypes[0] == cel.StringType && matcherDataTypes[1] == cel.StringType { macro = parser.NewGlobalMacro(macroName, 2, celMatcherStringListMacroExpander(funcName)) matcherDataTypes = []*cel.Type{cel.ListType(cel.StringType)} } else { return nil, fmt.Errorf("unsupported matcher data type: %s, %s", matcherDataTypes[0], matcherDataTypes[1]) } case 3: if matcherDataTypes[0] == cel.StringType && matcherDataTypes[1] == cel.StringType && matcherDataTypes[2] == cel.StringType { macro = parser.NewGlobalMacro(macroName, 3, celMatcherStringListMacroExpander(funcName)) matcherDataTypes = []*cel.Type{cel.ListType(cel.StringType)} } else { return nil, fmt.Errorf("unsupported matcher data type: %s, %s, %s", matcherDataTypes[0], matcherDataTypes[1], matcherDataTypes[2]) } } envOptions := []cel.EnvOption{ cel.Macros(macro), cel.Function(funcName, cel.Overload(funcName, append([]*cel.Type{requestType}, matcherDataTypes...), cel.BoolType), cel.SingletonBinaryImpl(CELMatcherRuntimeFunction(funcName, fac))), } programOptions := []cel.ProgramOption{ cel.CustomDecorator(CELMatcherDecorator(funcName, fac)), } return NewMatcherCELLibrary(envOptions, programOptions), nil } // CELMatcherFactory converts a constant CEL value into a RequestMatcher. type CELMatcherFactory func(data ref.Val) (RequestMatcher, error) // matcherCELLibrary is a simplistic configurable cel.Library implementation. type matcherCELLibary struct { envOptions []cel.EnvOption programOptions []cel.ProgramOption } // NewMatcherCELLibrary creates a matcherLibrary from option setes. func NewMatcherCELLibrary(envOptions []cel.EnvOption, programOptions []cel.ProgramOption) cel.Library { return &matcherCELLibary{ envOptions: envOptions, programOptions: programOptions, } } func (lib *matcherCELLibary) CompileOptions() []cel.EnvOption { return lib.envOptions } func (lib *matcherCELLibary) ProgramOptions() []cel.ProgramOption { return lib.programOptions } // CELMatcherDecorator matches a call overload generated by a CEL macro // that takes a single argument, and optimizes the implementation to precompile // the matcher and return a function that references the precompiled and // provisioned matcher. func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.InterpretableDecorator { return func(i interpreter.Interpretable) (interpreter.Interpretable, error) { call, ok := i.(interpreter.InterpretableCall) if !ok { return i, nil } if call.OverloadID() != funcName { return i, nil } callArgs := call.Args() reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute) if !ok { return nil, errors.New("missing 'request' argument") } nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute) if !ok { return nil, errors.New("missing 'request' argument") } varNames := nsAttr.CandidateVariableNames() if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != "request" { return nil, errors.New("missing 'request' argument") } matcherData, ok := callArgs[1].(interpreter.InterpretableConst) if !ok { // If the matcher arguments are not constant, then this means // they contain a Caddy placeholder reference and the evaluation // and matcher provisioning should be handled at dynamically. return i, nil } matcher, err := fac(matcherData.Value()) if err != nil { return nil, err } return interpreter.NewCall( i.ID(), funcName, funcName+"_opt", []interpreter.Interpretable{reqAttr}, func(args ...ref.Val) ref.Val { // The request value, guaranteed to be of type celHTTPRequest celReq := args[0] // If needed this call could be changed to convert the value // to a *http.Request using CEL's ConvertToNative method. httpReq := celReq.Value().(celHTTPRequest) return types.Bool(matcher.Match(httpReq.Request)) }, ), nil } } // CELMatcherRuntimeFunction creates a function binding for when the input to the matcher // is dynamically resolved rather than a set of static constant values. func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions.BinaryOp { return func(celReq, matcherData ref.Val) ref.Val { matcher, err := fac(matcherData) if err != nil { return types.NewErr(err.Error()) } httpReq := celReq.Value().(celHTTPRequest) return types.Bool(matcher.Match(httpReq.Request)) } } // celMatcherStringListMacroExpander validates that the macro is called // with a variable number of string arguments (at least one). // // The arguments are collected into a single list argument the following // function call returned: (request, [args]) func celMatcherStringListMacroExpander(funcName string) parser.MacroExpander { return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) { matchArgs := []*exprpb.Expr{} if len(args) == 0 { return nil, &common.Error{ Message: "matcher requires at least one argument", } } for _, arg := range args { if isCELStringExpr(arg) { matchArgs = append(matchArgs, arg) } else { return nil, &common.Error{ Location: eh.OffsetLocation(arg.GetId()), Message: "matcher arguments must be string constants", } } } return eh.GlobalCall(funcName, eh.Ident("request"), eh.NewList(matchArgs...)), nil } } // celMatcherStringMacroExpander validates that the macro is called a single // string argument. // // The following function call is returned: (request, arg) func celMatcherStringMacroExpander(funcName string) parser.MacroExpander { return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) { if len(args) != 1 { return nil, &common.Error{ Message: "matcher requires one argument", } } if isCELStringExpr(args[0]) { return eh.GlobalCall(funcName, eh.Ident("request"), args[0]), nil } return nil, &common.Error{ Location: eh.OffsetLocation(args[0].GetId()), Message: "matcher argument must be a string literal", } } } // celMatcherStringMacroExpander validates that the macro is called a single // map literal argument. // // The following function call is returned: (request, arg) func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander { return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) { if len(args) != 1 { return nil, &common.Error{ Message: "matcher requires a map literal argument", } } arg := args[0] switch arg.GetExprKind().(type) { case *exprpb.Expr_StructExpr: structExpr := arg.GetStructExpr() if structExpr.GetMessageName() != "" { return nil, &common.Error{ Location: eh.OffsetLocation(arg.GetId()), Message: fmt.Sprintf( "matcher input must be a map literal, not a %s", structExpr.GetMessageName(), ), } } for _, entry := range structExpr.GetEntries() { isStringPlaceholder := isCELStringExpr(entry.GetMapKey()) if !isStringPlaceholder { return nil, &common.Error{ Location: eh.OffsetLocation(entry.GetId()), Message: "matcher map keys must be string literals", } } isStringListPlaceholder := isCELStringExpr(entry.GetValue()) || isCELStringListLiteral(entry.GetValue()) if !isStringListPlaceholder { return nil, &common.Error{ Location: eh.OffsetLocation(entry.GetValue().GetId()), Message: "matcher map values must be string or list literals", } } } return eh.GlobalCall(funcName, eh.Ident("request"), arg), nil } return nil, &common.Error{ Location: eh.OffsetLocation(arg.GetId()), Message: "matcher requires a map literal argument", } } } // CELValueToMapStrList converts a CEL value to a map[string][]string // // Earlier validation stages should guarantee that the value has this type // at compile time, and that the runtime value type is map[string]any. // The reason for the slight difference in value type is that CEL allows for // map literals containing heterogeneous values, in this case string and list // of string. func CELValueToMapStrList(data ref.Val) (map[string][]string, error) { mapStrType := reflect.TypeOf(map[string]any{}) mapStrRaw, err := data.ConvertToNative(mapStrType) if err != nil { return nil, err } mapStrIface := mapStrRaw.(map[string]any) mapStrListStr := make(map[string][]string, len(mapStrIface)) for k, v := range mapStrIface { switch val := v.(type) { case string: mapStrListStr[k] = []string{val} case types.String: mapStrListStr[k] = []string{string(val)} case []string: mapStrListStr[k] = val case []ref.Val: convVals := make([]string, len(val)) for i, elem := range val { strVal, ok := elem.(types.String) if !ok { return nil, fmt.Errorf("unsupported value type in header match: %T", val) } convVals[i] = string(strVal) } mapStrListStr[k] = convVals default: return nil, fmt.Errorf("unsupported value type in header match: %T", val) } } return mapStrListStr, nil } // isCELStringExpr indicates whether the expression is a supported string expression func isCELStringExpr(e *exprpb.Expr) bool { return isCELStringLiteral(e) || isCELCaddyPlaceholderCall(e) || isCELConcatCall(e) } // isCELStringLiteral returns whether the expression is a CEL string literal. func isCELStringLiteral(e *exprpb.Expr) bool { switch e.GetExprKind().(type) { case *exprpb.Expr_ConstExpr: constant := e.GetConstExpr() switch constant.GetConstantKind().(type) { case *exprpb.Constant_StringValue: return true } } return false } // isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call. func isCELCaddyPlaceholderCall(e *exprpb.Expr) bool { switch e.GetExprKind().(type) { case *exprpb.Expr_CallExpr: call := e.GetCallExpr() if call.GetFunction() == "caddyPlaceholder" { return true } } return false } // isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or // other concat call arguments. func isCELConcatCall(e *exprpb.Expr) bool { switch e.GetExprKind().(type) { case *exprpb.Expr_CallExpr: call := e.GetCallExpr() if call.GetTarget() != nil { return false } if call.GetFunction() != operators.Add { return false } for _, arg := range call.GetArgs() { if !isCELStringExpr(arg) { return false } } return true } return false } // isCELStringListLiteral returns whether the expression resolves to a list literal // containing only string constants or a placeholder call. func isCELStringListLiteral(e *exprpb.Expr) bool { switch e.GetExprKind().(type) { case *exprpb.Expr_ListExpr: list := e.GetListExpr() for _, elem := range list.GetElements() { if !isCELStringExpr(elem) { return false } } return true } return false } // Variables used for replacing Caddy placeholders in CEL // expressions with a proper CEL function call; this is // just for syntactic sugar. var ( placeholderRegexp = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`) placeholderExpansion = `caddyPlaceholder(request, "${1}")` CELTypeJSON = cel.MapType(cel.StringType, cel.DynType) ) var httpRequestObjectType = cel.ObjectType("http.Request") // The name of the CEL function which accesses Replacer values. const placeholderFuncName = "caddyPlaceholder" // Interface guards var ( _ caddy.Provisioner = (*MatchExpression)(nil) _ RequestMatcher = (*MatchExpression)(nil) _ caddyfile.Unmarshaler = (*MatchExpression)(nil) _ json.Marshaler = (*MatchExpression)(nil) _ json.Unmarshaler = (*MatchExpression)(nil) ) caddy-2.6.2/modules/caddyhttp/celmatcher_test.go000066400000000000000000000326051435007237400217510ustar00rootroot00000000000000// 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 caddyhttp import ( "context" "crypto/tls" "crypto/x509" "encoding/pem" "net/http" "net/http/httptest" "testing" "github.com/caddyserver/caddy/v2" ) var ( clientCert = []byte(`-----BEGIN CERTIFICATE----- MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+ fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV 3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH 9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g= -----END CERTIFICATE-----`) matcherTests = []struct { name string expression *MatchExpression urlTarget string httpMethod string httpHeader *http.Header wantErr bool wantResult bool clientCertificate []byte }{ { name: "boolean matches succeed for placeholder http.request.tls.client.subject", expression: &MatchExpression{ Expr: "{http.request.tls.client.subject} == 'CN=client.localdomain'", }, clientCertificate: clientCert, urlTarget: "https://example.com/foo", wantResult: true, }, { name: "header matches (MatchHeader)", expression: &MatchExpression{ Expr: `header({'Field': 'foo'})`, }, urlTarget: "https://example.com/foo", httpHeader: &http.Header{"Field": []string{"foo", "bar"}}, wantResult: true, }, { name: "header error (MatchHeader)", expression: &MatchExpression{ Expr: `header('foo')`, }, urlTarget: "https://example.com/foo", httpHeader: &http.Header{"Field": []string{"foo", "bar"}}, wantErr: true, }, { name: "header_regexp matches (MatchHeaderRE)", expression: &MatchExpression{ Expr: `header_regexp('Field', 'fo{2}')`, }, urlTarget: "https://example.com/foo", httpHeader: &http.Header{"Field": []string{"foo", "bar"}}, wantResult: true, }, { name: "header_regexp matches with name (MatchHeaderRE)", expression: &MatchExpression{ Expr: `header_regexp('foo', 'Field', 'fo{2}')`, }, urlTarget: "https://example.com/foo", httpHeader: &http.Header{"Field": []string{"foo", "bar"}}, wantResult: true, }, { name: "header_regexp does not match (MatchHeaderRE)", expression: &MatchExpression{ Expr: `header_regexp('foo', 'Nope', 'fo{2}')`, }, urlTarget: "https://example.com/foo", httpHeader: &http.Header{"Field": []string{"foo", "bar"}}, wantResult: false, }, { name: "header_regexp error (MatchHeaderRE)", expression: &MatchExpression{ Expr: `header_regexp('foo')`, }, urlTarget: "https://example.com/foo", httpHeader: &http.Header{"Field": []string{"foo", "bar"}}, wantErr: true, }, { name: "host matches localhost (MatchHost)", expression: &MatchExpression{ Expr: `host('localhost')`, }, urlTarget: "http://localhost", wantResult: true, }, { name: "host matches (MatchHost)", expression: &MatchExpression{ Expr: `host('*.example.com')`, }, urlTarget: "https://foo.example.com", wantResult: true, }, { name: "host does not match (MatchHost)", expression: &MatchExpression{ Expr: `host('example.net', '*.example.com')`, }, urlTarget: "https://foo.example.org", wantResult: false, }, { name: "host error (MatchHost)", expression: &MatchExpression{ Expr: `host(80)`, }, urlTarget: "http://localhost:80", wantErr: true, }, { name: "method does not match (MatchMethod)", expression: &MatchExpression{ Expr: `method('PUT')`, }, urlTarget: "https://foo.example.com", httpMethod: "GET", wantResult: false, }, { name: "method matches (MatchMethod)", expression: &MatchExpression{ Expr: `method('DELETE', 'PUT', 'POST')`, }, urlTarget: "https://foo.example.com", httpMethod: "PUT", wantResult: true, }, { name: "method error not enough arguments (MatchMethod)", expression: &MatchExpression{ Expr: `method()`, }, urlTarget: "https://foo.example.com", httpMethod: "PUT", wantErr: true, }, { name: "path matches substring (MatchPath)", expression: &MatchExpression{ Expr: `path('*substring*')`, }, urlTarget: "https://example.com/foo/substring/bar.txt", wantResult: true, }, { name: "path does not match (MatchPath)", expression: &MatchExpression{ Expr: `path('/foo')`, }, urlTarget: "https://example.com/foo/bar", wantResult: false, }, { name: "path matches end url fragment (MatchPath)", expression: &MatchExpression{ Expr: `path('/foo')`, }, urlTarget: "https://example.com/FOO", wantResult: true, }, { name: "path matches end fragment with substring prefix (MatchPath)", expression: &MatchExpression{ Expr: `path('/foo*')`, }, urlTarget: "https://example.com/FOOOOO", wantResult: true, }, { name: "path matches one of multiple (MatchPath)", expression: &MatchExpression{ Expr: `path('/foo', '/foo/*', '/bar', '/bar/*', '/baz', '/baz*')`, }, urlTarget: "https://example.com/foo", wantResult: true, }, { name: "path_regexp with empty regex matches empty path (MatchPathRE)", expression: &MatchExpression{ Expr: `path_regexp('')`, }, urlTarget: "https://example.com/", wantResult: true, }, { name: "path_regexp with slash regex matches empty path (MatchPathRE)", expression: &MatchExpression{ Expr: `path_regexp('/')`, }, urlTarget: "https://example.com/", wantResult: true, }, { name: "path_regexp matches end url fragment (MatchPathRE)", expression: &MatchExpression{ Expr: `path_regexp('^/foo')`, }, urlTarget: "https://example.com/foo/", wantResult: true, }, { name: "path_regexp does not match fragment at end (MatchPathRE)", expression: &MatchExpression{ Expr: `path_regexp('bar_at_start', '^/bar')`, }, urlTarget: "https://example.com/foo/bar", wantResult: false, }, { name: "protocol matches (MatchProtocol)", expression: &MatchExpression{ Expr: `protocol('HTTPs')`, }, urlTarget: "https://example.com", wantResult: true, }, { name: "protocol does not match (MatchProtocol)", expression: &MatchExpression{ Expr: `protocol('grpc')`, }, urlTarget: "https://example.com", wantResult: false, }, { name: "protocol invocation error no args (MatchProtocol)", expression: &MatchExpression{ Expr: `protocol()`, }, urlTarget: "https://example.com", wantErr: true, }, { name: "protocol invocation error too many args (MatchProtocol)", expression: &MatchExpression{ Expr: `protocol('grpc', 'https')`, }, urlTarget: "https://example.com", wantErr: true, }, { name: "protocol invocation error wrong arg type (MatchProtocol)", expression: &MatchExpression{ Expr: `protocol(true)`, }, urlTarget: "https://example.com", wantErr: true, }, { name: "query does not match against a specific value (MatchQuery)", expression: &MatchExpression{ Expr: `query({"debug": "1"})`, }, urlTarget: "https://example.com/foo", wantResult: false, }, { name: "query matches against a specific value (MatchQuery)", expression: &MatchExpression{ Expr: `query({"debug": "1"})`, }, urlTarget: "https://example.com/foo/?debug=1", wantResult: true, }, { name: "query matches against multiple values (MatchQuery)", expression: &MatchExpression{ Expr: `query({"debug": ["0", "1", {http.request.uri.query.debug}+"1"]})`, }, urlTarget: "https://example.com/foo/?debug=1", wantResult: true, }, { name: "query matches against a wildcard (MatchQuery)", expression: &MatchExpression{ Expr: `query({"debug": ["*"]})`, }, urlTarget: "https://example.com/foo/?debug=something", wantResult: true, }, { name: "query matches against a placeholder value (MatchQuery)", expression: &MatchExpression{ Expr: `query({"debug": {http.request.uri.query.debug}})`, }, urlTarget: "https://example.com/foo/?debug=1", wantResult: true, }, { name: "query error bad map key type (MatchQuery)", expression: &MatchExpression{ Expr: `query({1: "1"})`, }, urlTarget: "https://example.com/foo", wantErr: true, }, { name: "query error typed struct instead of map (MatchQuery)", expression: &MatchExpression{ Expr: `query(Message{field: "1"})`, }, urlTarget: "https://example.com/foo", wantErr: true, }, { name: "query error bad map value type (MatchQuery)", expression: &MatchExpression{ Expr: `query({"debug": 1})`, }, urlTarget: "https://example.com/foo/?debug=1", wantErr: true, }, { name: "query error no args (MatchQuery)", expression: &MatchExpression{ Expr: `query()`, }, urlTarget: "https://example.com/foo/?debug=1", wantErr: true, }, { name: "remote_ip error no args (MatchRemoteIP)", expression: &MatchExpression{ Expr: `remote_ip()`, }, urlTarget: "https://example.com/foo", wantErr: true, }, { name: "remote_ip single IP match (MatchRemoteIP)", expression: &MatchExpression{ Expr: `remote_ip('192.0.2.1')`, }, urlTarget: "https://example.com/foo", wantResult: true, }, { name: "remote_ip forwarded (MatchRemoteIP)", expression: &MatchExpression{ Expr: `remote_ip('forwarded', '192.0.2.1')`, }, urlTarget: "https://example.com/foo", wantResult: true, }, { name: "remote_ip forwarded not first (MatchRemoteIP)", expression: &MatchExpression{ Expr: `remote_ip('192.0.2.1', 'forwarded')`, }, urlTarget: "https://example.com/foo", wantErr: true, }, } ) func TestMatchExpressionMatch(t *testing.T) { for _, tst := range matcherTests { tc := tst t.Run(tc.name, func(t *testing.T) { err := tc.expression.Provision(caddy.Context{}) if err != nil { if !tc.wantErr { t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr) } return } req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil) if tc.httpHeader != nil { req.Header = *tc.httpHeader } repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) if tc.clientCertificate != nil { block, _ := pem.Decode(clientCert) if block == nil { t.Fatalf("failed to decode PEM certificate") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("failed to decode PEM certificate: %v", err) } req.TLS = &tls.ConnectionState{ PeerCertificates: []*x509.Certificate{cert}, } } if tc.expression.Match(req) != tc.wantResult { t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr) } }) } } func BenchmarkMatchExpressionMatch(b *testing.B) { for _, tst := range matcherTests { tc := tst if tc.wantErr { continue } b.Run(tst.name, func(b *testing.B) { tc.expression.Provision(caddy.Context{}) req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil) if tc.httpHeader != nil { req.Header = *tc.httpHeader } repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) if tc.clientCertificate != nil { block, _ := pem.Decode(clientCert) if block == nil { b.Fatalf("failed to decode PEM certificate") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { b.Fatalf("failed to decode PEM certificate: %v", err) } req.TLS = &tls.ConnectionState{ PeerCertificates: []*x509.Certificate{cert}, } } b.ResetTimer() for i := 0; i < b.N; i++ { tc.expression.Match(req) } }) } } func TestMatchExpressionProvision(t *testing.T) { tests := []struct { name string expression *MatchExpression wantErr bool }{ { name: "boolean matches succeed", expression: &MatchExpression{ Expr: "{http.request.uri.query} != ''", }, wantErr: false, }, { name: "reject expressions with non-boolean results", expression: &MatchExpression{ Expr: "{http.request.uri.query}", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := tt.expression.Provision(caddy.Context{}); (err != nil) != tt.wantErr { t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr) } }) } } caddy-2.6.2/modules/caddyhttp/encode/000077500000000000000000000000001435007237400175035ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/encode/brotli/000077500000000000000000000000001435007237400207765ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/encode/brotli/brotli_precompressed.go000066400000000000000000000016451435007237400255610ustar00rootroot00000000000000package caddybrotli import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" ) func init() { caddy.RegisterModule(BrotliPrecompressed{}) } // BrotliPrecompressed provides the file extension for files precompressed with brotli encoding. type BrotliPrecompressed struct{} // CaddyModule returns the Caddy module information. func (BrotliPrecompressed) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.precompressed.br", New: func() caddy.Module { return new(BrotliPrecompressed) }, } } // AcceptEncoding returns the name of the encoding as // used in the Accept-Encoding request headers. func (BrotliPrecompressed) AcceptEncoding() string { return "br" } // Suffix returns the filename suffix of precompressed files. func (BrotliPrecompressed) Suffix() string { return ".br" } // Interface guards var _ encode.Precompressed = (*BrotliPrecompressed)(nil) caddy-2.6.2/modules/caddyhttp/encode/caddyfile.go000066400000000000000000000066401435007237400217640ustar00rootroot00000000000000// 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 encode 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/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { httpcaddyfile.RegisterHandlerDirective("encode", parseCaddyfile) } func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { enc := new(Encode) err := enc.UnmarshalCaddyfile(h.Dispenser) if err != nil { return nil, err } return enc, nil } // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // encode [] { // gzip [] // zstd // minimum_length // # response matcher block // match { // status // header [] // } // # or response matcher single line syntax // match [header []] | [status ] // } // // Specifying the formats on the first line will use those formats' defaults. func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { var prefer []string responseMatchers := make(map[string]caddyhttp.ResponseMatcher) for d.Next() { for _, arg := range d.RemainingArgs() { mod, err := caddy.GetModule("http.encoders." + arg) if err != nil { return d.Errf("finding encoder module '%s': %v", mod, err) } encoding, ok := mod.New().(Encoding) if !ok { return d.Errf("module %s is not an HTTP encoding", mod) } if enc.EncodingsRaw == nil { enc.EncodingsRaw = make(caddy.ModuleMap) } enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil) prefer = append(prefer, arg) } for d.NextBlock(0) { switch d.Val() { case "minimum_length": if !d.NextArg() { return d.ArgErr() } minLength, err := strconv.Atoi(d.Val()) if err != nil { return err } enc.MinLength = minLength case "match": err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), responseMatchers) if err != nil { return err } matcher := responseMatchers["match"] enc.Matcher = &matcher default: name := d.Val() modID := "http.encoders." + name unm, err := caddyfile.UnmarshalModule(d, modID) if err != nil { return err } encoding, ok := unm.(Encoding) if !ok { return d.Errf("module %s is not an HTTP encoding; is %T", modID, unm) } if enc.EncodingsRaw == nil { enc.EncodingsRaw = make(caddy.ModuleMap) } enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil) prefer = append(prefer, name) } } } // use the order in which the encoders were defined. enc.Prefer = prefer return nil } // Interface guard var _ caddyfile.Unmarshaler = (*Encode)(nil) caddy-2.6.2/modules/caddyhttp/encode/encode.go000066400000000000000000000273411435007237400212760ustar00rootroot00000000000000// 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 encode implements an encoder middleware for Caddy. The initial // enhancements related to Accept-Encoding, minimum content length, and // buffer/writer pools were adapted from https://github.com/xi2/httpgzip // then modified heavily to accommodate modular encoders and fix bugs. // Code borrowed from that repository is Copyright (c) 2015 The Httpgzip Authors. package encode import ( "fmt" "io" "math" "net/http" "sort" "strconv" "strings" "sync" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { caddy.RegisterModule(Encode{}) } // Encode is a middleware which can encode responses. type Encode struct { // Selection of compression algorithms to choose from. The best one // will be chosen based on the client's Accept-Encoding header. EncodingsRaw caddy.ModuleMap `json:"encodings,omitempty" caddy:"namespace=http.encoders"` // If the client has no strong preference, choose these encodings in order. Prefer []string `json:"prefer,omitempty"` // Only encode responses that are at least this many bytes long. MinLength int `json:"minimum_length,omitempty"` // Only encode responses that match against this ResponseMmatcher. // The default is a collection of text-based Content-Type headers. Matcher *caddyhttp.ResponseMatcher `json:"match,omitempty"` writerPools map[string]*sync.Pool // TODO: these pools do not get reused through config reloads... } // CaddyModule returns the Caddy module information. func (Encode) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.encode", New: func() caddy.Module { return new(Encode) }, } } // Provision provisions enc. func (enc *Encode) Provision(ctx caddy.Context) error { mods, err := ctx.LoadModule(enc, "EncodingsRaw") if err != nil { return fmt.Errorf("loading encoder modules: %v", err) } for modName, modIface := range mods.(map[string]any) { err = enc.addEncoding(modIface.(Encoding)) if err != nil { return fmt.Errorf("adding encoding %s: %v", modName, err) } } if enc.MinLength == 0 { enc.MinLength = defaultMinLength } if enc.Matcher == nil { // common text-based content types enc.Matcher = &caddyhttp.ResponseMatcher{ Headers: http.Header{ "Content-Type": []string{ "text/*", "application/json*", "application/javascript*", "application/xhtml+xml*", "application/atom+xml*", "application/rss+xml*", "image/svg+xml*", }, }, } } return nil } // Validate ensures that enc's configuration is valid. func (enc *Encode) Validate() error { check := make(map[string]bool) for _, encName := range enc.Prefer { if _, ok := enc.writerPools[encName]; !ok { return fmt.Errorf("encoding %s not enabled", encName) } if _, ok := check[encName]; ok { return fmt.Errorf("encoding %s is duplicated in prefer", encName) } check[encName] = true } return nil } func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { for _, encName := range AcceptedEncodings(r, enc.Prefer) { if _, ok := enc.writerPools[encName]; !ok { continue // encoding not offered } w = enc.openResponseWriter(encName, w) defer w.(*responseWriter).Close() break } return next.ServeHTTP(w, r) } func (enc *Encode) addEncoding(e Encoding) error { ae := e.AcceptEncoding() if ae == "" { return fmt.Errorf("encoder does not specify an Accept-Encoding value") } if _, ok := enc.writerPools[ae]; ok { return fmt.Errorf("encoder already added: %s", ae) } if enc.writerPools == nil { enc.writerPools = make(map[string]*sync.Pool) } enc.writerPools[ae] = &sync.Pool{ New: func() any { return e.NewEncoder() }, } return nil } // openResponseWriter creates a new response writer that may (or may not) // encode the response with encodingName. The returned response writer MUST // be closed after the handler completes. func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter) *responseWriter { var rw responseWriter return enc.initResponseWriter(&rw, encodingName, w) } // initResponseWriter initializes the responseWriter instance // allocated in openResponseWriter, enabling mid-stack inlining. func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter { if httpInterfaces, ok := wrappedRW.(caddyhttp.HTTPInterfaces); ok { rw.HTTPInterfaces = httpInterfaces } else { rw.HTTPInterfaces = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW} } rw.encodingName = encodingName rw.config = enc return rw } // responseWriter writes to an underlying response writer // using the encoding represented by encodingName and // configured by config. type responseWriter struct { caddyhttp.HTTPInterfaces encodingName string w Encoder config *Encode statusCode int wroteHeader bool } // WriteHeader stores the status to write when the time comes // to actually write the header. func (rw *responseWriter) WriteHeader(status int) { rw.statusCode = status } // Match determines, if encoding should be done based on the ResponseMatcher. func (enc *Encode) Match(rw *responseWriter) bool { return enc.Matcher.Match(rw.statusCode, rw.Header()) } // Flush implements http.Flusher. It delays the actual Flush of the underlying ResponseWriterWrapper // until headers were written. func (rw *responseWriter) Flush() { if !rw.wroteHeader { // flushing the underlying ResponseWriter will write header and status code, // but we need to delay that until we can determine if we must encode and // therefore add the Content-Encoding header; this happens in the first call // to rw.Write (see bug in #4314) return } rw.HTTPInterfaces.Flush() } // Write writes to the response. If the response qualifies, // it is encoded using the encoder, which is initialized // if not done so already. func (rw *responseWriter) Write(p []byte) (int, error) { // ignore zero data writes, probably head request if len(p) == 0 { return 0, nil } // sniff content-type and determine content-length if !rw.wroteHeader && rw.config.MinLength > 0 { var gtMinLength bool if len(p) > rw.config.MinLength { gtMinLength = true } else if cl, err := strconv.Atoi(rw.Header().Get("Content-Length")); err == nil && cl > rw.config.MinLength { gtMinLength = true } if gtMinLength { if rw.Header().Get("Content-Type") == "" { rw.Header().Set("Content-Type", http.DetectContentType(p)) } rw.init() } } // before we write to the response, we need to make // sure the header is written exactly once; we do // that by checking if a status code has been set, // and if so, that means we haven't written the // header OR the default status code will be written // by the standard library if !rw.wroteHeader { if rw.statusCode != 0 { rw.HTTPInterfaces.WriteHeader(rw.statusCode) } rw.wroteHeader = true } if rw.w != nil { return rw.w.Write(p) } else { return rw.HTTPInterfaces.Write(p) } } // Close writes any remaining buffered response and // deallocates any active resources. func (rw *responseWriter) Close() error { // didn't write, probably head request if !rw.wroteHeader { cl, err := strconv.Atoi(rw.Header().Get("Content-Length")) if err == nil && cl > rw.config.MinLength { rw.init() } // issue #5059, don't write status code if not set explicitly. if rw.statusCode != 0 { rw.HTTPInterfaces.WriteHeader(rw.statusCode) } rw.wroteHeader = true } var err error if rw.w != nil { err = rw.w.Close() rw.w.Reset(nil) rw.config.writerPools[rw.encodingName].Put(rw.w) rw.w = nil } return err } // init should be called before we write a response, if rw.buf has contents. func (rw *responseWriter) init() { if rw.Header().Get("Content-Encoding") == "" && rw.config.Match(rw) { rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder) rw.w.Reset(rw.HTTPInterfaces) rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975 rw.Header().Set("Content-Encoding", rw.encodingName) rw.Header().Add("Vary", "Accept-Encoding") rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content } } // AcceptedEncodings returns the list of encodings that the // client supports, in descending order of preference. // The client preference via q-factor and the server // preference via Prefer setting are taken into account. If // the Sec-WebSocket-Key header is present then non-identity // encodings are not considered. See // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html. func AcceptedEncodings(r *http.Request, preferredOrder []string) []string { acceptEncHeader := r.Header.Get("Accept-Encoding") websocketKey := r.Header.Get("Sec-WebSocket-Key") if acceptEncHeader == "" { return []string{} } prefs := []encodingPreference{} for _, accepted := range strings.Split(acceptEncHeader, ",") { parts := strings.Split(accepted, ";") encName := strings.ToLower(strings.TrimSpace(parts[0])) // determine q-factor qFactor := 1.0 if len(parts) > 1 { qFactorStr := strings.ToLower(strings.TrimSpace(parts[1])) if strings.HasPrefix(qFactorStr, "q=") { if qFactorFloat, err := strconv.ParseFloat(qFactorStr[2:], 32); err == nil { if qFactorFloat >= 0 && qFactorFloat <= 1 { qFactor = qFactorFloat } } } } // encodings with q-factor of 0 are not accepted; // use a small threshold to account for float precision if qFactor < 0.00001 { continue } // don't encode WebSocket handshakes if websocketKey != "" && encName != "identity" { continue } // set server preference prefOrder := -1 for i, p := range preferredOrder { if encName == p { prefOrder = len(preferredOrder) - i break } } prefs = append(prefs, encodingPreference{ encoding: encName, q: qFactor, preferOrder: prefOrder, }) } // sort preferences by descending q-factor first, then by preferOrder sort.Slice(prefs, func(i, j int) bool { if math.Abs(prefs[i].q-prefs[j].q) < 0.00001 { return prefs[i].preferOrder > prefs[j].preferOrder } return prefs[i].q > prefs[j].q }) prefEncNames := make([]string, len(prefs)) for i := range prefs { prefEncNames[i] = prefs[i].encoding } return prefEncNames } // encodingPreference pairs an encoding with its q-factor. type encodingPreference struct { encoding string q float64 preferOrder int } // Encoder is a type which can encode a stream of data. type Encoder interface { io.WriteCloser Reset(io.Writer) } // Encoding is a type which can create encoders of its kind // and return the name used in the Accept-Encoding header. type Encoding interface { AcceptEncoding() string NewEncoder() Encoder } // Precompressed is a type which returns filename suffix of precompressed // file and Accept-Encoding header to use when serving this file. type Precompressed interface { AcceptEncoding() string Suffix() string } // defaultMinLength is the minimum length at which to compress content. const defaultMinLength = 512 // Interface guards var ( _ caddy.Provisioner = (*Encode)(nil) _ caddy.Validator = (*Encode)(nil) _ caddyhttp.MiddlewareHandler = (*Encode)(nil) _ caddyhttp.HTTPInterfaces = (*responseWriter)(nil) ) caddy-2.6.2/modules/caddyhttp/encode/encode_test.go000066400000000000000000000153241435007237400223330ustar00rootroot00000000000000package encode import ( "net/http" "sync" "testing" ) func BenchmarkOpenResponseWriter(b *testing.B) { enc := new(Encode) for n := 0; n < b.N; n++ { enc.openResponseWriter("test", nil) } } func TestPreferOrder(t *testing.T) { testCases := []struct { name string accept string prefer []string expected []string }{ { name: "PreferOrder(): 4 accept, 3 prefer", accept: "deflate, gzip, br, zstd", prefer: []string{"zstd", "br", "gzip"}, expected: []string{"zstd", "br", "gzip", "deflate"}, }, { name: "PreferOrder(): 2 accept, 3 prefer", accept: "deflate, zstd", prefer: []string{"zstd", "br", "gzip"}, expected: []string{"zstd", "deflate"}, }, { name: "PreferOrder(): 2 accept (1 empty), 3 prefer", accept: "gzip,,zstd", prefer: []string{"zstd", "br", "gzip"}, expected: []string{"zstd", "gzip", ""}, }, { name: "PreferOrder(): 1 accept, 2 prefer", accept: "gzip", prefer: []string{"zstd", "gzip"}, expected: []string{"gzip"}, }, { name: "PreferOrder(): 4 accept (1 duplicate), 1 prefer", accept: "deflate, gzip, br, br", prefer: []string{"br"}, expected: []string{"br", "br", "deflate", "gzip"}, }, { name: "PreferOrder(): empty accept, 0 prefer", accept: "", prefer: []string{}, expected: []string{}, }, { name: "PreferOrder(): empty accept, 1 prefer", accept: "", prefer: []string{"gzip"}, expected: []string{}, }, { name: "PreferOrder(): with q-factor", accept: "deflate;q=0.8, gzip;q=0.4, br;q=0.2, zstd", prefer: []string{"gzip"}, expected: []string{"zstd", "deflate", "gzip", "br"}, }, { name: "PreferOrder(): with q-factor, no prefer", accept: "deflate;q=0.8, gzip;q=0.4, br;q=0.2, zstd", prefer: []string{}, expected: []string{"zstd", "deflate", "gzip", "br"}, }, { name: "PreferOrder(): q-factor=0 filtered out", accept: "deflate;q=0.1, gzip;q=0.4, br;q=0.5, zstd;q=0", prefer: []string{"gzip"}, expected: []string{"br", "gzip", "deflate"}, }, { name: "PreferOrder(): q-factor=0 filtered out, no prefer", accept: "deflate;q=0.1, gzip;q=0.4, br;q=0.5, zstd;q=0", prefer: []string{}, expected: []string{"br", "gzip", "deflate"}, }, { name: "PreferOrder(): with invalid q-factor", accept: "br, deflate, gzip;q=2, zstd;q=0.1", prefer: []string{"zstd", "gzip"}, expected: []string{"gzip", "br", "deflate", "zstd"}, }, { name: "PreferOrder(): with invalid q-factor, no prefer", accept: "br, deflate, gzip;q=2, zstd;q=0.1", prefer: []string{}, expected: []string{"br", "deflate", "gzip", "zstd"}, }, } enc := new(Encode) r, _ := http.NewRequest("", "", nil) for _, test := range testCases { t.Run(test.name, func(t *testing.T) { if test.accept == "" { r.Header.Del("Accept-Encoding") } else { r.Header.Set("Accept-Encoding", test.accept) } enc.Prefer = test.prefer result := AcceptedEncodings(r, enc.Prefer) if !sliceEqual(result, test.expected) { t.Errorf("AcceptedEncodings() actual: %s expected: %s", result, test.expected) } }) } } func sliceEqual(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func TestValidate(t *testing.T) { type testCase struct { name string prefer []string wantErr bool } var err error var testCases []testCase enc := new(Encode) enc.writerPools = map[string]*sync.Pool{ "zstd": nil, "gzip": nil, "br": nil, } testCases = []testCase{ { name: "ValidatePrefer (zstd, gzip & br enabled): valid order with all encoder", prefer: []string{"zstd", "br", "gzip"}, wantErr: false, }, { name: "ValidatePrefer (zstd, gzip & br enabled): valid order with 2 out of 3 encoders", prefer: []string{"br", "gzip"}, wantErr: false, }, { name: "ValidatePrefer (zstd, gzip & br enabled): valid order with 1 out of 3 encoders", prefer: []string{"gzip"}, wantErr: false, }, { name: "ValidatePrefer (zstd, gzip & br enabled): 1 duplicated (once) encoder", prefer: []string{"gzip", "zstd", "gzip"}, wantErr: true, }, { name: "ValidatePrefer (zstd, gzip & br enabled): 1 not enabled encoder in prefer list", prefer: []string{"br", "zstd", "gzip", "deflate"}, wantErr: true, }, { name: "ValidatePrefer (zstd, gzip & br enabled): no prefer list", prefer: []string{}, wantErr: false, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { enc.Prefer = test.prefer err = enc.Validate() if (err != nil) != test.wantErr { t.Errorf("Validate() error = %v, wantErr = %v", err, test.wantErr) } }) } enc.writerPools = map[string]*sync.Pool{ "zstd": nil, "gzip": nil, } testCases = []testCase{ { name: "ValidatePrefer (zstd & gzip enabled): 1 not enabled encoder in prefer list", prefer: []string{"zstd", "br", "gzip"}, wantErr: true, }, { name: "ValidatePrefer (zstd & gzip enabled): 2 not enabled encoder in prefer list", prefer: []string{"br", "zstd", "gzip", "deflate"}, wantErr: true, }, { name: "ValidatePrefer (zstd & gzip enabled): only not enabled encoder in prefer list", prefer: []string{"deflate", "br", "gzip"}, wantErr: true, }, { name: "ValidatePrefer (zstd & gzip enabled): 1 duplicated (once) encoder in prefer list", prefer: []string{"gzip", "zstd", "gzip"}, wantErr: true, }, { name: "ValidatePrefer (zstd & gzip enabled): 1 duplicated (twice) encoder in prefer list", prefer: []string{"gzip", "zstd", "gzip", "gzip"}, wantErr: true, }, { name: "ValidatePrefer (zstd & gzip enabled): 1 duplicated encoder in prefer list", prefer: []string{"zstd", "zstd", "gzip", "gzip"}, wantErr: true, }, { name: "ValidatePrefer (zstd & gzip enabled): 1 duplicated not enabled encoder in prefer list", prefer: []string{"br", "br", "gzip"}, wantErr: true, }, { name: "ValidatePrefer (zstd & gzip enabled): 2 duplicated not enabled encoder in prefer list", prefer: []string{"br", "deflate", "br", "deflate"}, wantErr: true, }, { name: "ValidatePrefer (zstd & gzip enabled): valid order zstd first", prefer: []string{"zstd", "gzip"}, wantErr: false, }, { name: "ValidatePrefer (zstd & gzip enabled): valid order gzip first", prefer: []string{"gzip", "zstd"}, wantErr: false, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { enc.Prefer = test.prefer err = enc.Validate() if (err != nil) != test.wantErr { t.Errorf("Validate() error = %v, wantErr = %v", err, test.wantErr) } }) } } caddy-2.6.2/modules/caddyhttp/encode/gzip/000077500000000000000000000000001435007237400204545ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/encode/gzip/gzip.go000066400000000000000000000050611435007237400217560ustar00rootroot00000000000000// 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 caddygzip import ( "fmt" "strconv" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" "github.com/klauspost/compress/gzip" ) func init() { caddy.RegisterModule(Gzip{}) } // Gzip can create gzip encoders. type Gzip struct { Level int `json:"level,omitempty"` } // CaddyModule returns the Caddy module information. func (Gzip) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.encoders.gzip", New: func() caddy.Module { return new(Gzip) }, } } // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. func (g *Gzip) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if !d.NextArg() { continue } levelStr := d.Val() level, err := strconv.Atoi(levelStr) if err != nil { return err } g.Level = level } return nil } // Provision provisions g's configuration. func (g *Gzip) Provision(ctx caddy.Context) error { if g.Level == 0 { g.Level = defaultGzipLevel } return nil } // Validate validates g's configuration. func (g Gzip) Validate() error { if g.Level < gzip.StatelessCompression { return fmt.Errorf("quality too low; must be >= %d", gzip.StatelessCompression) } if g.Level > gzip.BestCompression { return fmt.Errorf("quality too high; must be <= %d", gzip.BestCompression) } return nil } // AcceptEncoding returns the name of the encoding as // used in the Accept-Encoding request headers. func (Gzip) AcceptEncoding() string { return "gzip" } // NewEncoder returns a new gzip writer. func (g Gzip) NewEncoder() encode.Encoder { writer, _ := gzip.NewWriterLevel(nil, g.Level) return writer } // Informed from http://blog.klauspost.com/gzip-performance-for-go-webservers/ var defaultGzipLevel = 5 // Interface guards var ( _ encode.Encoding = (*Gzip)(nil) _ caddy.Provisioner = (*Gzip)(nil) _ caddy.Validator = (*Gzip)(nil) _ caddyfile.Unmarshaler = (*Gzip)(nil) ) caddy-2.6.2/modules/caddyhttp/encode/gzip/gzip_precompressed.go000066400000000000000000000013371435007237400247130ustar00rootroot00000000000000package caddygzip import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" ) func init() { caddy.RegisterModule(GzipPrecompressed{}) } // GzipPrecompressed provides the file extension for files precompressed with gzip encoding. type GzipPrecompressed struct { Gzip } // CaddyModule returns the Caddy module information. func (GzipPrecompressed) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.precompressed.gzip", New: func() caddy.Module { return new(GzipPrecompressed) }, } } // Suffix returns the filename suffix of precompressed files. func (GzipPrecompressed) Suffix() string { return ".gz" } var _ encode.Precompressed = (*GzipPrecompressed)(nil) caddy-2.6.2/modules/caddyhttp/encode/zstd/000077500000000000000000000000001435007237400204675ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/encode/zstd/zstd.go000066400000000000000000000035701435007237400220070ustar00rootroot00000000000000// 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 caddyzstd import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" "github.com/klauspost/compress/zstd" ) func init() { caddy.RegisterModule(Zstd{}) } // Zstd can create Zstandard encoders. type Zstd struct{} // CaddyModule returns the Caddy module information. func (Zstd) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.encoders.zstd", New: func() caddy.Module { return new(Zstd) }, } } // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } // AcceptEncoding returns the name of the encoding as // used in the Accept-Encoding request headers. func (Zstd) AcceptEncoding() string { return "zstd" } // NewEncoder returns a new Zstandard writer. func (z Zstd) NewEncoder() encode.Encoder { // The default of 8MB for the window is // too large for many clients, so we limit // it to 128K to lighten their load. writer, _ := zstd.NewWriter(nil, zstd.WithWindowSize(128<<10), zstd.WithEncoderConcurrency(1), zstd.WithZeroFrames(true)) return writer } // Interface guards var ( _ encode.Encoding = (*Zstd)(nil) _ caddyfile.Unmarshaler = (*Zstd)(nil) ) caddy-2.6.2/modules/caddyhttp/encode/zstd/zstd_precompressed.go000066400000000000000000000013451435007237400247400ustar00rootroot00000000000000package caddyzstd import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" ) func init() { caddy.RegisterModule(ZstdPrecompressed{}) } // ZstdPrecompressed provides the file extension for files precompressed with zstandard encoding. type ZstdPrecompressed struct { Zstd } // CaddyModule returns the Caddy module information. func (ZstdPrecompressed) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.precompressed.zstd", New: func() caddy.Module { return new(ZstdPrecompressed) }, } } // Suffix returns the filename suffix of precompressed files. func (ZstdPrecompressed) Suffix() string { return ".zst" } var _ encode.Precompressed = (*ZstdPrecompressed)(nil) caddy-2.6.2/modules/caddyhttp/errors.go000066400000000000000000000063031435007237400201130ustar00rootroot00000000000000// 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 caddyhttp import ( "fmt" weakrand "math/rand" "path" "runtime" "strings" "time" "github.com/caddyserver/caddy/v2" ) func init() { weakrand.Seed(time.Now().UnixNano()) } // Error is a convenient way for a Handler to populate the // essential fields of a HandlerError. If err is itself a // HandlerError, then any essential fields that are not // set will be populated. func Error(statusCode int, err error) HandlerError { const idLen = 9 if he, ok := err.(HandlerError); ok { if he.ID == "" { he.ID = randString(idLen, true) } if he.Trace == "" { he.Trace = trace() } if he.StatusCode == 0 { he.StatusCode = statusCode } return he } return HandlerError{ ID: randString(idLen, true), StatusCode: statusCode, Err: err, Trace: trace(), } } // HandlerError is a serializable representation of // an error from within an HTTP handler. type HandlerError struct { Err error // the original error value and message StatusCode int // the HTTP status code to associate with this error ID string // generated; for identifying this error in logs Trace string // produced from call stack } func (e HandlerError) Error() string { var s string if e.ID != "" { s += fmt.Sprintf("{id=%s}", e.ID) } if e.Trace != "" { s += " " + e.Trace } if e.StatusCode != 0 { s += fmt.Sprintf(": HTTP %d", e.StatusCode) } if e.Err != nil { s += ": " + e.Err.Error() } return strings.TrimSpace(s) } // Unwrap returns the underlying error value. See the `errors` package for info. func (e HandlerError) Unwrap() error { return e.Err } // randString returns a string of n random characters. // It is not even remotely secure OR a proper distribution. // But it's good enough for some things. It excludes certain // confusing characters like I, l, 1, 0, O, etc. If sameCase // is true, then uppercase letters are excluded. func randString(n int, sameCase bool) string { if n <= 0 { return "" } dict := []byte("abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY23456789") if sameCase { dict = []byte("abcdefghijkmnpqrstuvwxyz0123456789") } b := make([]byte, n) for i := range b { //nolint:gosec b[i] = dict[weakrand.Int63()%int64(len(dict))] } return string(b) } func trace() string { if pc, file, line, ok := runtime.Caller(2); ok { filename := path.Base(file) pkgAndFuncName := path.Base(runtime.FuncForPC(pc).Name()) return fmt.Sprintf("%s (%s:%d)", pkgAndFuncName, filename, line) } return "" } // ErrorCtxKey is the context key to use when storing // an error (for use with context.Context). const ErrorCtxKey = caddy.CtxKey("handler_chain_error") caddy-2.6.2/modules/caddyhttp/fileserver/000077500000000000000000000000001435007237400204145ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/fileserver/browse.go000066400000000000000000000173561435007237400222600ustar00rootroot00000000000000// 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 fileserver import ( "bytes" "context" _ "embed" "encoding/json" "fmt" "io" "io/fs" "net/http" "os" "path" "strings" "sync" "text/template" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates" "go.uber.org/zap" ) //go:embed browse.html var defaultBrowseTemplate string // Browse configures directory browsing. type Browse struct { // Use this template file instead of the default browse template. TemplateFile string `json:"template_file,omitempty"` } func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { fsrv.logger.Debug("browse enabled; listing directory contents", zap.String("path", dirPath), zap.String("root", root)) // Navigation on the client-side gets messed up if the // URL doesn't end in a trailing slash because hrefs to // "b/c" at path "/a" end up going to "/b/c" instead // of "/a/b/c" - so we have to redirect in this case // so that the path is "/a/" and the client constructs // relative hrefs "b/c" to be "/a/b/c". // // Only redirect if the last element of the path (the filename) was not // rewritten; if the admin wanted to rewrite to the canonical path, they // would have, and we have to be very careful not to introduce unwanted // redirects and especially redirect loops! (Redirecting using the // original URI is necessary because that's the URI the browser knows, // we don't want to redirect from internally-rewritten URIs.) // See https://github.com/caddyserver/caddy/issues/4205. // We also redirect if the path is empty, because this implies the path // prefix was fully stripped away by a `handle_path` handler for example. // See https://github.com/caddyserver/caddy/issues/4466. origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request) if r.URL.Path == "" || path.Base(origReq.URL.Path) == path.Base(r.URL.Path) { if !strings.HasSuffix(origReq.URL.Path, "/") { fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path)) return redirect(w, r, origReq.URL.Path+"/") } } dir, err := fsrv.openFile(dirPath, w) if err != nil { return err } defer dir.Close() repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) // calling path.Clean here prevents weird breadcrumbs when URL paths are sketchy like /%2e%2e%2f listing, err := fsrv.loadDirectoryContents(r.Context(), dir.(fs.ReadDirFile), root, path.Clean(r.URL.Path), repl) switch { case os.IsPermission(err): return caddyhttp.Error(http.StatusForbidden, err) case os.IsNotExist(err): return fsrv.notFound(w, r, next) case err != nil: return caddyhttp.Error(http.StatusInternalServerError, err) } fsrv.browseApplyQueryParams(w, r, &listing) buf := bufPool.Get().(*bytes.Buffer) buf.Reset() defer bufPool.Put(buf) acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) // write response as either JSON or HTML if strings.Contains(acceptHeader, "application/json") { if err := json.NewEncoder(buf).Encode(listing.Items); err != nil { return caddyhttp.Error(http.StatusInternalServerError, err) } w.Header().Set("Content-Type", "application/json; charset=utf-8") } else { var fs http.FileSystem if fsrv.Root != "" { fs = http.Dir(repl.ReplaceAll(fsrv.Root, ".")) } var tplCtx = &templateContext{ TemplateContext: templates.TemplateContext{ Root: fs, Req: r, RespHeader: templates.WrappedHeader{Header: w.Header()}, }, browseTemplateContext: listing, } tpl, err := fsrv.makeBrowseTemplate(tplCtx) if err != nil { return fmt.Errorf("parsing browse template: %v", err) } if err := tpl.Execute(buf, tplCtx); err != nil { return caddyhttp.Error(http.StatusInternalServerError, err) } w.Header().Set("Content-Type", "text/html; charset=utf-8") } _, _ = buf.WriteTo(w) return nil } func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (browseTemplateContext, error) { files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable if err != nil && err != io.EOF { return browseTemplateContext{}, err } // user can presumably browse "up" to parent folder if path is longer than "/" canGoUp := len(urlPath) > 1 return fsrv.directoryListing(ctx, files, canGoUp, root, urlPath, repl), nil } // browseApplyQueryParams applies query parameters to the listing. // It mutates the listing and may set cookies. func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseTemplateContext) { sortParam := r.URL.Query().Get("sort") orderParam := r.URL.Query().Get("order") limitParam := r.URL.Query().Get("limit") offsetParam := r.URL.Query().Get("offset") // first figure out what to sort by switch sortParam { case "": sortParam = sortByNameDirFirst if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { sortParam = sortCookie.Value } case sortByName, sortByNameDirFirst, sortBySize, sortByTime: http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil}) } // then figure out the order switch orderParam { case "": orderParam = "asc" if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { orderParam = orderCookie.Value } case "asc", "desc": http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil}) } // finally, apply the sorting and limiting listing.applySortAndLimit(sortParam, orderParam, limitParam, offsetParam) } // makeBrowseTemplate creates the template to be used for directory listings. func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.Template, error) { var tpl *template.Template var err error if fsrv.Browse.TemplateFile != "" { tpl = tplCtx.NewTemplate(path.Base(fsrv.Browse.TemplateFile)) tpl, err = tpl.ParseFiles(fsrv.Browse.TemplateFile) if err != nil { return nil, fmt.Errorf("parsing browse template file: %v", err) } } else { tpl = tplCtx.NewTemplate("default_listing") tpl, err = tpl.Parse(defaultBrowseTemplate) if err != nil { return nil, fmt.Errorf("parsing default browse template: %v", err) } } return tpl, nil } // isSymlinkTargetDir returns true if f's symbolic link target // is a directory. func (fsrv *FileServer) isSymlinkTargetDir(f fs.FileInfo, root, urlPath string) bool { if !isSymlink(f) { return false } target := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name())) targetInfo, err := fs.Stat(fsrv.fileSystem, target) if err != nil { return false } return targetInfo.IsDir() } // isSymlink return true if f is a symbolic link. func isSymlink(f fs.FileInfo) bool { return f.Mode()&os.ModeSymlink != 0 } // templateContext powers the context used when evaluating the browse template. // It combines browse-specific features with the standard templates handler // features. type templateContext struct { templates.TemplateContext browseTemplateContext } // bufPool is used to increase the efficiency of file listings. var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } caddy-2.6.2/modules/caddyhttp/fileserver/browse.html000066400000000000000000000425101435007237400226050ustar00rootroot00000000000000 {{html .Name}}

{{range $i, $crumb := .Breadcrumbs}}{{html $crumb.Text}}{{if ne $i 0}}/{{end}}{{end}}

{{.NumDirs}} director{{if eq 1 .NumDirs}}y{{else}}ies{{end}} {{.NumFiles}} file{{if ne 1 .NumFiles}}s{{end}} {{- if ne 0 .Limit}} (of which only {{.Limit}} are displayed) {{- end}}
{{- if .CanGoUp}} {{- end}} {{- range .Items}} {{- if .IsDir}} {{- else}} {{- end}} {{- end}}
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}} {{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}} {{- else}} {{- end}} {{- if and (eq .Sort "name") (ne .Order "desc")}} Name {{- else if and (eq .Sort "name") (ne .Order "asc")}} Name {{- else}} Name {{- end}} {{- if and (eq .Sort "size") (ne .Order "desc")}} Size {{- else if and (eq .Sort "size") (ne .Order "asc")}} Size {{- else}} Size {{- end}} {{- if and (eq .Sort "time") (ne .Order "desc")}} Modified {{- else if and (eq .Sort "time") (ne .Order "asc")}} Modified {{- else}} Modified {{- end}}
Go up
{{- if .IsDir}} {{- else}} {{- end}} {{html .Name}} {{.HumanSize}}
caddy-2.6.2/modules/caddyhttp/fileserver/browsetplcontext.go000066400000000000000000000200661435007237400243750ustar00rootroot00000000000000// 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 fileserver import ( "context" "io/fs" "net/url" "os" "path" "sort" "strconv" "strings" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/dustin/go-humanize" "go.uber.org/zap" ) func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) browseTemplateContext { filesToHide := fsrv.transformHidePaths(repl) var dirCount, fileCount int fileInfos := []fileInfo{} for _, entry := range entries { if err := ctx.Err(); err != nil { break } name := entry.Name() if fileHidden(name, filesToHide) { continue } info, err := entry.Info() if err != nil { fsrv.logger.Error("could not get info about directory entry", zap.String("name", entry.Name()), zap.String("root", root)) continue } isDir := entry.IsDir() || fsrv.isSymlinkTargetDir(info, root, urlPath) // add the slash after the escape of path to avoid escaping the slash as well if isDir { name += "/" dirCount++ } else { fileCount++ } size := info.Size() fileIsSymlink := isSymlink(info) if fileIsSymlink { path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, info.Name())) fileInfo, err := fs.Stat(fsrv.fileSystem, path) if err == nil { size = fileInfo.Size() } // An error most likely means the symlink target doesn't exist, // which isn't entirely unusual and shouldn't fail the listing. // In this case, just use the size of the symlink itself, which // was already set above. } u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name fileInfos = append(fileInfos, fileInfo{ IsDir: isDir, IsSymlink: fileIsSymlink, Name: name, Size: size, URL: u.String(), ModTime: info.ModTime().UTC(), Mode: info.Mode(), }) } name, _ := url.PathUnescape(urlPath) return browseTemplateContext{ Name: path.Base(name), Path: urlPath, CanGoUp: canGoUp, Items: fileInfos, NumDirs: dirCount, NumFiles: fileCount, } } // browseTemplateContext provides the template context for directory listings. type browseTemplateContext struct { // The name of the directory (the last element of the path). Name string `json:"name"` // The full path of the request. Path string `json:"path"` // Whether the parent directory is browseable. CanGoUp bool `json:"can_go_up"` // The items (files and folders) in the path. Items []fileInfo `json:"items,omitempty"` // If ≠0 then Items starting from that many elements. Offset int `json:"offset,omitempty"` // If ≠0 then Items have been limited to that many elements. Limit int `json:"limit,omitempty"` // The number of directories in the listing. NumDirs int `json:"num_dirs"` // The number of files (items that aren't directories) in the listing. NumFiles int `json:"num_files"` // Sort column used Sort string `json:"sort,omitempty"` // Sorting order Order string `json:"order,omitempty"` } // Breadcrumbs returns l.Path where every element maps // the link to the text to display. func (l browseTemplateContext) Breadcrumbs() []crumb { if len(l.Path) == 0 { return []crumb{} } // skip trailing slash lpath := l.Path if lpath[len(lpath)-1] == '/' { lpath = lpath[:len(lpath)-1] } parts := strings.Split(lpath, "/") result := make([]crumb, len(parts)) for i, p := range parts { if i == 0 && p == "" { p = "/" } // the directory name could include an encoded slash in its path, // so the item name should be unescaped in the loop rather than unescaping the // entire path outside the loop. p, _ = url.PathUnescape(p) lnk := strings.Repeat("../", len(parts)-i-1) result[i] = crumb{Link: lnk, Text: p} } return result } func (l *browseTemplateContext) applySortAndLimit(sortParam, orderParam, limitParam string, offsetParam string) { l.Sort = sortParam l.Order = orderParam if l.Order == "desc" { switch l.Sort { case sortByName: sort.Sort(sort.Reverse(byName(*l))) case sortByNameDirFirst: sort.Sort(sort.Reverse(byNameDirFirst(*l))) case sortBySize: sort.Sort(sort.Reverse(bySize(*l))) case sortByTime: sort.Sort(sort.Reverse(byTime(*l))) } } else { switch l.Sort { case sortByName: sort.Sort(byName(*l)) case sortByNameDirFirst: sort.Sort(byNameDirFirst(*l)) case sortBySize: sort.Sort(bySize(*l)) case sortByTime: sort.Sort(byTime(*l)) } } if offsetParam != "" { offset, _ := strconv.Atoi(offsetParam) if offset > 0 && offset <= len(l.Items) { l.Items = l.Items[offset:] l.Offset = offset } } if limitParam != "" { limit, _ := strconv.Atoi(limitParam) if limit > 0 && limit <= len(l.Items) { l.Items = l.Items[:limit] l.Limit = limit } } } // crumb represents part of a breadcrumb menu, // pairing a link with the text to display. type crumb struct { Link, Text string } // fileInfo contains serializable information // about a file or directory. type fileInfo struct { Name string `json:"name"` Size int64 `json:"size"` URL string `json:"url"` ModTime time.Time `json:"mod_time"` Mode os.FileMode `json:"mode"` IsDir bool `json:"is_dir"` IsSymlink bool `json:"is_symlink"` } // HumanSize returns the size of the file as a // human-readable string in IEC format (i.e. // power of 2 or base 1024). func (fi fileInfo) HumanSize() string { return humanize.IBytes(uint64(fi.Size)) } // HumanModTime returns the modified time of the file // as a human-readable string given by format. func (fi fileInfo) HumanModTime(format string) string { return fi.ModTime.Format(format) } type ( byName browseTemplateContext byNameDirFirst browseTemplateContext bySize browseTemplateContext byTime browseTemplateContext ) func (l byName) Len() int { return len(l.Items) } func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } func (l byName) Less(i, j int) bool { return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) } func (l byNameDirFirst) Len() int { return len(l.Items) } func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } func (l byNameDirFirst) Less(i, j int) bool { // sort by name if both are dir or file if l.Items[i].IsDir == l.Items[j].IsDir { return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) } // sort dir ahead of file return l.Items[i].IsDir } func (l bySize) Len() int { return len(l.Items) } func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } func (l bySize) Less(i, j int) bool { const directoryOffset = -1 << 31 // = -math.MinInt32 iSize, jSize := l.Items[i].Size, l.Items[j].Size // directory sizes depend on the file system; to // provide a consistent experience, put them up front // and sort them by name if l.Items[i].IsDir { iSize = directoryOffset } if l.Items[j].IsDir { jSize = directoryOffset } if l.Items[i].IsDir && l.Items[j].IsDir { return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) } return iSize < jSize } func (l byTime) Len() int { return len(l.Items) } func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) } const ( sortByName = "name" sortByNameDirFirst = "namedirfirst" sortBySize = "size" sortByTime = "time" ) caddy-2.6.2/modules/caddyhttp/fileserver/browsetplcontext_test.go000066400000000000000000000034631435007237400254360ustar00rootroot00000000000000// 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 fileserver import ( "testing" ) func TestBreadcrumbs(t *testing.T) { testdata := []struct { path string expected []crumb }{ {"", []crumb{}}, {"/", []crumb{{Text: "/"}}}, {"foo/bar/baz", []crumb{ {Link: "../../", Text: "foo"}, {Link: "../", Text: "bar"}, {Link: "", Text: "baz"}, }}, {"/qux/quux/corge/", []crumb{ {Link: "../../../", Text: "/"}, {Link: "../../", Text: "qux"}, {Link: "../", Text: "quux"}, {Link: "", Text: "corge"}, }}, {"/مجلد/", []crumb{ {Link: "../", Text: "/"}, {Link: "", Text: "مجلد"}, }}, {"/مجلد-1/مجلد-2", []crumb{ {Link: "../../", Text: "/"}, {Link: "../", Text: "مجلد-1"}, {Link: "", Text: "مجلد-2"}, }}, {"/مجلد%2F1", []crumb{ {Link: "../", Text: "/"}, {Link: "", Text: "مجلد/1"}, }}, } for _, d := range testdata { l := browseTemplateContext{Path: d.path} actual := l.Breadcrumbs() if len(actual) != len(d.expected) { t.Errorf("wrong size output, got %d elements but expected %d", len(actual), len(d.expected)) continue } for i, c := range actual { if c != d.expected[i] { t.Errorf("got %#v but expected %#v at index %d", c, d.expected[i], i) } } } } caddy-2.6.2/modules/caddyhttp/fileserver/caddyfile.go000066400000000000000000000174431435007237400227000ustar00rootroot00000000000000// 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 fileserver import ( "io/fs" "path/filepath" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" ) func init() { httpcaddyfile.RegisterHandlerDirective("file_server", parseCaddyfile) httpcaddyfile.RegisterDirective("try_files", parseTryFiles) } // parseCaddyfile parses the file_server directive. It enables the static file // server and configures it with this syntax: // // file_server [] [browse] { // fs // root // hide // index // browse [] // precompressed // status // disable_canonical_uris // } func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { var fsrv FileServer for h.Next() { args := h.RemainingArgs() switch len(args) { case 0: case 1: if args[0] != "browse" { return nil, h.ArgErr() } fsrv.Browse = new(Browse) default: return nil, h.ArgErr() } for h.NextBlock(0) { switch h.Val() { case "fs": if !h.NextArg() { return nil, h.ArgErr() } if fsrv.FileSystemRaw != nil { return nil, h.Err("file system module already specified") } name := h.Val() modID := "caddy.fs." + name unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) if err != nil { return nil, err } fsys, ok := unm.(fs.FS) if !ok { return nil, h.Errf("module %s (%T) is not a supported file system implementation (requires fs.FS)", modID, unm) } fsrv.FileSystemRaw = caddyconfig.JSONModuleObject(fsys, "backend", name, nil) case "hide": fsrv.Hide = h.RemainingArgs() if len(fsrv.Hide) == 0 { return nil, h.ArgErr() } case "index": fsrv.IndexNames = h.RemainingArgs() if len(fsrv.IndexNames) == 0 { return nil, h.ArgErr() } case "root": if !h.Args(&fsrv.Root) { return nil, h.ArgErr() } case "browse": if fsrv.Browse != nil { return nil, h.Err("browsing is already configured") } fsrv.Browse = new(Browse) h.Args(&fsrv.Browse.TemplateFile) case "precompressed": var order []string for h.NextArg() { modID := "http.precompressed." + h.Val() mod, err := caddy.GetModule(modID) if err != nil { return nil, h.Errf("getting module named '%s': %v", modID, err) } inst := mod.New() precompress, ok := inst.(encode.Precompressed) if !ok { return nil, h.Errf("module %s is not a precompressor; is %T", modID, inst) } if fsrv.PrecompressedRaw == nil { fsrv.PrecompressedRaw = make(caddy.ModuleMap) } fsrv.PrecompressedRaw[h.Val()] = caddyconfig.JSON(precompress, nil) order = append(order, h.Val()) } fsrv.PrecompressedOrder = order case "status": if !h.NextArg() { return nil, h.ArgErr() } fsrv.StatusCode = caddyhttp.WeakString(h.Val()) case "disable_canonical_uris": if h.NextArg() { return nil, h.ArgErr() } falseBool := false fsrv.CanonicalURIs = &falseBool case "pass_thru": if h.NextArg() { return nil, h.ArgErr() } fsrv.PassThru = true default: return nil, h.Errf("unknown subdirective '%s'", h.Val()) } } } // hide the Caddyfile (and any imported Caddyfiles) if configFiles := h.Caddyfiles(); len(configFiles) > 0 { for _, file := range configFiles { file = filepath.Clean(file) if !fileHidden(file, fsrv.Hide) { // if there's no path separator, the file server module will hide all // files by that name, rather than a specific one; but we want to hide // only this specific file, so ensure there's always a path separator if !strings.Contains(file, separator) { file = "." + separator + file } fsrv.Hide = append(fsrv.Hide, file) } } } return &fsrv, nil } // parseTryFiles parses the try_files directive. It combines a file matcher // with a rewrite directive, so this is not a standard handler directive. // A try_files directive has this syntax (notice no matcher tokens accepted): // // try_files { // policy first_exist|smallest_size|largest_size|most_recently_modified // } // // and is basically shorthand for: // // @try_files file { // try_files // policy first_exist|smallest_size|largest_size|most_recently_modified // } // rewrite @try_files {http.matchers.file.relative} // // This directive rewrites request paths only, preserving any other part // of the URI, unless the part is explicitly given in the file list. For // example, if any of the files in the list have a query string: // // try_files {path} index.php?{query}&p={path} // // then the query string will not be treated as part of the file name; and // if that file matches, the given query string will replace any query string // that already exists on the request URI. func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { if !h.Next() { return nil, h.ArgErr() } tryFiles := h.RemainingArgs() if len(tryFiles) == 0 { return nil, h.ArgErr() } // parse out the optional try policy var tryPolicy string for nesting := h.Nesting(); h.NextBlock(nesting); { switch h.Val() { case "policy": if tryPolicy != "" { return nil, h.Err("try policy already configured") } if !h.NextArg() { return nil, h.ArgErr() } tryPolicy = h.Val() switch tryPolicy { case tryPolicyFirstExist, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod: default: return nil, h.Errf("unrecognized try policy: %s", tryPolicy) } } } // makeRoute returns a route that tries the files listed in try // and then rewrites to the matched file; userQueryString is // appended to the rewrite rule. makeRoute := func(try []string, userQueryString string) []httpcaddyfile.ConfigValue { handler := rewrite.Rewrite{ URI: "{http.matchers.file.relative}" + userQueryString, } matcherSet := caddy.ModuleMap{ "file": h.JSON(MatchFile{TryFiles: try, TryPolicy: tryPolicy}), } return h.NewRoute(matcherSet, handler) } var result []httpcaddyfile.ConfigValue // if there are query strings in the list, we have to split into // a separate route for each item with a query string, because // the rewrite is different for that item try := make([]string, 0, len(tryFiles)) for _, item := range tryFiles { if idx := strings.Index(item, "?"); idx >= 0 { if len(try) > 0 { result = append(result, makeRoute(try, "")...) try = []string{} } result = append(result, makeRoute([]string{item[:idx]}, item[idx:])...) continue } // accumulate consecutive non-query-string parameters try = append(try, item) } if len(try) > 0 { result = append(result, makeRoute(try, "")...) } // ensure that multiple routes (possible if rewrite targets // have query strings, for example) are grouped together // so only the first matching rewrite is performed (#2891) h.GroupRoutes(result) return result, nil } caddy-2.6.2/modules/caddyhttp/fileserver/command.go000066400000000000000000000102741435007237400223650ustar00rootroot00000000000000// 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 fileserver import ( "encoding/json" "flag" "log" "strconv" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/modules/caddyhttp" caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "file-server", Func: cmdFileServer, Usage: "[--domain ] [--root ] [--listen ] [--browse] [--access-log]", Short: "Spins up a production-ready file server", Long: ` A simple but production-ready file server. Useful for quick deployments, demos, and development. The listener's socket address can be customized with the --listen flag. If a domain name is specified with --domain, the default listener address will be changed to the HTTPS port and the server will use HTTPS. If using a public domain, ensure A/AAAA records are properly configured before using this option. If --browse is enabled, requests for folders without an index file will respond with a file listing.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("file-server", flag.ExitOnError) fs.String("domain", "", "Domain name at which to serve the files") fs.String("root", "", "The path to the root of the site") fs.String("listen", "", "The address to which to bind the listener") fs.Bool("browse", false, "Enable directory browsing") fs.Bool("templates", false, "Enable template rendering") fs.Bool("access-log", false, "Enable the access log") fs.Bool("debug", false, "Enable verbose debug logs") return fs }(), }) } func cmdFileServer(fs caddycmd.Flags) (int, error) { caddy.TrapSignals() domain := fs.String("domain") root := fs.String("root") listen := fs.String("listen") browse := fs.Bool("browse") templates := fs.Bool("templates") accessLog := fs.Bool("access-log") debug := fs.Bool("debug") var handlers []json.RawMessage if templates { handler := caddytpl.Templates{FileRoot: root} handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "templates", nil)) } handler := FileServer{Root: root} if browse { handler.Browse = new(Browse) } handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "file_server", nil)) route := caddyhttp.Route{HandlersRaw: handlers} if domain != "" { route.MatcherSetsRaw = []caddy.ModuleMap{ { "host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil), }, } } server := &caddyhttp.Server{ ReadHeaderTimeout: caddy.Duration(10 * time.Second), IdleTimeout: caddy.Duration(30 * time.Second), MaxHeaderBytes: 1024 * 10, Routes: caddyhttp.RouteList{route}, } if listen == "" { if domain == "" { listen = ":80" } else { listen = ":" + strconv.Itoa(certmagic.HTTPSPort) } } server.Listen = []string{listen} if accessLog { server.Logs = &caddyhttp.ServerLogConfig{} } httpApp := caddyhttp.App{ Servers: map[string]*caddyhttp.Server{"static": server}, } var false bool cfg := &caddy.Config{ Admin: &caddy.AdminConfig{ Disabled: true, Config: &caddy.ConfigSettings{ Persist: &false, }, }, AppsRaw: caddy.ModuleMap{ "http": caddyconfig.JSON(httpApp, nil), }, } if debug { cfg.Logging = &caddy.Logging{ Logs: map[string]*caddy.CustomLog{ "default": {Level: zap.DebugLevel.CapitalString()}, }, } } err := caddy.Run(cfg) if err != nil { return caddy.ExitCodeFailedStartup, err } log.Printf("Caddy serving static files on %s", listen) select {} } caddy-2.6.2/modules/caddyhttp/fileserver/matcher.go000066400000000000000000000500061435007237400223670ustar00rootroot00000000000000// 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 fileserver import ( "encoding/json" "fmt" "io/fs" "net/http" "os" "path" "path/filepath" "runtime" "strconv" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/google/cel-go/cel" "github.com/google/cel-go/common" "github.com/google/cel-go/common/operators" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/parser" "go.uber.org/zap" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) func init() { caddy.RegisterModule(MatchFile{}) } // MatchFile is an HTTP request matcher that can match // requests based upon file existence. // // Upon matching, three new placeholders will be made // available: // // - `{http.matchers.file.relative}` The root-relative // path of the file. This is often useful when rewriting // requests. // - `{http.matchers.file.absolute}` The absolute path // of the matched file. // - `{http.matchers.file.type}` Set to "directory" if // the matched file is a directory, "file" otherwise. // - `{http.matchers.file.remainder}` Set to the remainder // of the path if the path was split by `split_path`. // // Even though file matching may depend on the OS path // separator, the placeholder values always use /. type MatchFile struct { // The file system implementation to use. By default, the // local disk file system will be used. FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"` fileSystem fs.FS // The root directory, used for creating absolute // file paths, and required when working with // relative paths; if not specified, `{http.vars.root}` // will be used, if set; otherwise, the current // directory is assumed. Accepts placeholders. Root string `json:"root,omitempty"` // The list of files to try. Each path here is // considered related to Root. If nil, the request // URL's path will be assumed. Files and // directories are treated distinctly, so to match // a directory, the filepath MUST end in a forward // slash `/`. To match a regular file, there must // be no trailing slash. Accepts placeholders. If // the policy is "first_exist", then an error may // be triggered as a fallback by configuring "=" // followed by a status code number, // for example "=404". TryFiles []string `json:"try_files,omitempty"` // How to choose a file in TryFiles. Can be: // // - first_exist // - smallest_size // - largest_size // - most_recently_modified // // Default is first_exist. TryPolicy string `json:"try_policy,omitempty"` // A list of delimiters to use to split the path in two // when trying files. If empty, no splitting will // occur, and the path will be tried as-is. For each // split value, the left-hand side of the split, // including the split value, will be the path tried. // For example, the path `/remote.php/dav/` using the // split value `.php` would try the file `/remote.php`. // Each delimiter must appear at the end of a URI path // component in order to be used as a split delimiter. SplitPath []string `json:"split_path,omitempty"` logger *zap.Logger } // CaddyModule returns the Caddy module information. func (MatchFile) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.file", New: func() caddy.Module { return new(MatchFile) }, } } // UnmarshalCaddyfile sets up the matcher from Caddyfile tokens. Syntax: // // file { // root // try_files // try_policy first_exist|smallest_size|largest_size|most_recently_modified // } func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { m.TryFiles = append(m.TryFiles, d.RemainingArgs()...) for d.NextBlock(0) { switch d.Val() { case "root": if !d.NextArg() { return d.ArgErr() } m.Root = d.Val() case "try_files": m.TryFiles = append(m.TryFiles, d.RemainingArgs()...) if len(m.TryFiles) == 0 { return d.ArgErr() } case "try_policy": if !d.NextArg() { return d.ArgErr() } m.TryPolicy = d.Val() case "split_path": m.SplitPath = d.RemainingArgs() if len(m.SplitPath) == 0 { return d.ArgErr() } default: return d.Errf("unrecognized subdirective: %s", d.Val()) } } } return nil } // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // // Example: // // expression file({'root': '/srv', 'try_files': [{http.request.uri.path}, '/index.php'], 'try_policy': 'first_exist', 'split_path': ['.php']}) func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) { requestType := cel.ObjectType("http.Request") matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) { values, err := caddyhttp.CELValueToMapStrList(data) if err != nil { return nil, err } var root string if len(values["root"]) > 0 { root = values["root"][0] } var try_policy string if len(values["try_policy"]) > 0 { root = values["try_policy"][0] } m := MatchFile{ Root: root, TryFiles: values["try_files"], TryPolicy: try_policy, SplitPath: values["split_path"], } err = m.Provision(ctx) return m, err } envOptions := []cel.EnvOption{ cel.Macros(parser.NewGlobalVarArgMacro("file", celFileMatcherMacroExpander())), cel.Function("file", cel.Overload("file_request_map", []*cel.Type{requestType, caddyhttp.CELTypeJSON}, cel.BoolType)), cel.Function("file_request_map", cel.Overload("file_request_map", []*cel.Type{requestType, caddyhttp.CELTypeJSON}, cel.BoolType), cel.SingletonBinaryImpl(caddyhttp.CELMatcherRuntimeFunction("file_request_map", matcherFactory))), } programOptions := []cel.ProgramOption{ cel.CustomDecorator(caddyhttp.CELMatcherDecorator("file_request_map", matcherFactory)), } return caddyhttp.NewMatcherCELLibrary(envOptions, programOptions), nil } func celFileMatcherMacroExpander() parser.MacroExpander { return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) { if len(args) == 0 { return nil, &common.Error{ Message: "matcher requires at least one argument", } } if len(args) == 1 { arg := args[0] if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) { return eh.GlobalCall("file", eh.Ident("request"), eh.NewMap( eh.NewMapEntry(eh.LiteralString("try_files"), eh.NewList(arg)), ), ), nil } if isCELTryFilesLiteral(arg) { return eh.GlobalCall("file", eh.Ident("request"), arg), nil } return nil, &common.Error{ Location: eh.OffsetLocation(arg.GetId()), Message: "matcher requires either a map or string literal argument", } } for _, arg := range args { if !(isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg)) { return nil, &common.Error{ Location: eh.OffsetLocation(arg.GetId()), Message: "matcher only supports repeated string literal arguments", } } } return eh.GlobalCall("file", eh.Ident("request"), eh.NewMap( eh.NewMapEntry( eh.LiteralString("try_files"), eh.NewList(args...), ), ), ), nil } } // Provision sets up m's defaults. func (m *MatchFile) Provision(ctx caddy.Context) error { m.logger = ctx.Logger() // establish the file system to use if len(m.FileSystemRaw) > 0 { mod, err := ctx.LoadModule(m, "FileSystemRaw") if err != nil { return fmt.Errorf("loading file system module: %v", err) } m.fileSystem = mod.(fs.FS) } if m.fileSystem == nil { m.fileSystem = osFS{} } if m.Root == "" { m.Root = "{http.vars.root}" } // if list of files to try was omitted entirely, assume URL path // (use placeholder instead of r.URL.Path; see issue #4146) if m.TryFiles == nil { m.TryFiles = []string{"{http.request.uri.path}"} } return nil } // Validate ensures m has a valid configuration. func (m MatchFile) Validate() error { switch m.TryPolicy { case "", tryPolicyFirstExist, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod: default: return fmt.Errorf("unknown try policy %s", m.TryPolicy) } return nil } // Match returns true if r matches m. Returns true // if a file was matched. If so, four placeholders // will be available: // - http.matchers.file.relative: Path to file relative to site root // - http.matchers.file.absolute: Path to file including site root // - http.matchers.file.type: file or directory // - http.matchers.file.remainder: Portion remaining after splitting file path (if configured) func (m MatchFile) Match(r *http.Request) bool { return m.selectFile(r) } // selectFile chooses a file according to m.TryPolicy by appending // the paths in m.TryFiles to m.Root, with placeholder replacements. func (m MatchFile) selectFile(r *http.Request) (matched bool) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) root := filepath.Clean(repl.ReplaceAll(m.Root, ".")) type matchCandidate struct { fullpath, relative, splitRemainder string } // makeCandidates evaluates placeholders in file and expands any glob expressions // to build a list of file candidates. Special glob characters are escaped in // placeholder replacements so globs cannot be expanded from placeholders, and // globs are not evaluated on Windows because of its path separator character: // escaping is not supported so we can't safely glob on Windows, or we can't // support placeholders on Windows (pick one). (Actually, evaluating untrusted // globs is not the end of the world since the file server will still hide any // hidden files, it just might lead to unexpected behavior.) makeCandidates := func(file string) []matchCandidate { // first, evaluate placeholders in the file pattern expandedFile, err := repl.ReplaceFunc(file, func(variable string, val any) (any, error) { if runtime.GOOS == "windows" { return val, nil } switch v := val.(type) { case string: return globSafeRepl.Replace(v), nil case fmt.Stringer: return globSafeRepl.Replace(v.String()), nil } return val, nil }) if err != nil { m.logger.Error("evaluating placeholders", zap.Error(err)) expandedFile = file // "oh well," I guess? } // clean the path and split, if configured -- we must split before // globbing so that the file system doesn't include the remainder // ("afterSplit") in the filename; be sure to restore trailing slash beforeSplit, afterSplit := m.firstSplit(path.Clean(expandedFile)) if strings.HasSuffix(file, "/") { beforeSplit += "/" } // create the full path to the file by prepending the site root fullPattern := caddyhttp.SanitizedPathJoin(root, beforeSplit) // expand glob expressions, but not on Windows because Glob() doesn't // support escaping on Windows due to path separator) var globResults []string if runtime.GOOS == "windows" { globResults = []string{fullPattern} // precious Windows } else { globResults, err = fs.Glob(m.fileSystem, fullPattern) if err != nil { m.logger.Error("expanding glob", zap.Error(err)) } } // for each glob result, combine all the forms of the path var candidates []matchCandidate for _, result := range globResults { candidates = append(candidates, matchCandidate{ fullpath: result, relative: strings.TrimPrefix(result, root), splitRemainder: afterSplit, }) } return candidates } // setPlaceholders creates the placeholders for the matched file setPlaceholders := func(candidate matchCandidate, info fs.FileInfo) { repl.Set("http.matchers.file.relative", filepath.ToSlash(candidate.relative)) repl.Set("http.matchers.file.absolute", filepath.ToSlash(candidate.fullpath)) repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder)) fileType := "file" if info.IsDir() { fileType = "directory" } repl.Set("http.matchers.file.type", fileType) } // match file according to the configured policy switch m.TryPolicy { case "", tryPolicyFirstExist: for _, pattern := range m.TryFiles { if err := parseErrorCode(pattern); err != nil { caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err) return } candidates := makeCandidates(pattern) for _, c := range candidates { if info, exists := m.strictFileExists(c.fullpath); exists { setPlaceholders(c, info) return true } } } case tryPolicyLargestSize: var largestSize int64 var largest matchCandidate var largestInfo os.FileInfo for _, pattern := range m.TryFiles { candidates := makeCandidates(pattern) for _, c := range candidates { info, err := fs.Stat(m.fileSystem, c.fullpath) if err == nil && info.Size() > largestSize { largestSize = info.Size() largest = c largestInfo = info } } } if largestInfo == nil { return false } setPlaceholders(largest, largestInfo) return true case tryPolicySmallestSize: var smallestSize int64 var smallest matchCandidate var smallestInfo os.FileInfo for _, pattern := range m.TryFiles { candidates := makeCandidates(pattern) for _, c := range candidates { info, err := fs.Stat(m.fileSystem, c.fullpath) if err == nil && (smallestSize == 0 || info.Size() < smallestSize) { smallestSize = info.Size() smallest = c smallestInfo = info } } } if smallestInfo == nil { return false } setPlaceholders(smallest, smallestInfo) return true case tryPolicyMostRecentlyMod: var recent matchCandidate var recentInfo os.FileInfo for _, pattern := range m.TryFiles { candidates := makeCandidates(pattern) for _, c := range candidates { info, err := fs.Stat(m.fileSystem, c.fullpath) if err == nil && (recentInfo == nil || info.ModTime().After(recentInfo.ModTime())) { recent = c recentInfo = info } } } if recentInfo == nil { return false } setPlaceholders(recent, recentInfo) return true } return } // parseErrorCode checks if the input is a status // code number, prefixed by "=", and returns an // error if so. func parseErrorCode(input string) error { if len(input) > 1 && input[0] == '=' { code, err := strconv.Atoi(input[1:]) if err != nil || code < 100 || code > 999 { return nil } return caddyhttp.Error(code, fmt.Errorf("%s", input[1:])) } return nil } // strictFileExists returns true if file exists // and matches the convention of the given file // path. If the path ends in a forward slash, // the file must also be a directory; if it does // NOT end in a forward slash, the file must NOT // be a directory. func (m MatchFile) strictFileExists(file string) (os.FileInfo, bool) { info, err := fs.Stat(m.fileSystem, file) if err != nil { // in reality, this can be any error // such as permission or even obscure // ones like "is not a directory" (when // trying to stat a file within a file); // in those cases we can't be sure if // the file exists, so we just treat any // error as if it does not exist; see // https://stackoverflow.com/a/12518877/1048862 return nil, false } if strings.HasSuffix(file, separator) { // by convention, file paths ending // in a path separator must be a directory return info, info.IsDir() } // by convention, file paths NOT ending // in a path separator must NOT be a directory return info, !info.IsDir() } // firstSplit returns the first result where the path // can be split in two by a value in m.SplitPath. The // return values are the first piece of the path that // ends with the split substring and the remainder. // If the path cannot be split, the path is returned // as-is (with no remainder). func (m MatchFile) firstSplit(path string) (splitPart, remainder string) { for _, split := range m.SplitPath { if idx := indexFold(path, split); idx > -1 { pos := idx + len(split) // skip the split if it's not the final part of the filename if pos != len(path) && !strings.HasPrefix(path[pos:], "/") { continue } return path[:pos], path[pos:] } } return path, "" } // There is no strings.IndexFold() function like there is strings.EqualFold(), // but we can use strings.EqualFold() to build our own case-insensitive // substring search (as of Go 1.14). func indexFold(haystack, needle string) int { nlen := len(needle) for i := 0; i+nlen < len(haystack); i++ { if strings.EqualFold(haystack[i:i+nlen], needle) { return i } } return -1 } // isCELMapLiteral returns whether the expression resolves to a map literal containing // only string keys with or a placeholder call. func isCELTryFilesLiteral(e *exprpb.Expr) bool { switch e.GetExprKind().(type) { case *exprpb.Expr_StructExpr: structExpr := e.GetStructExpr() if structExpr.GetMessageName() != "" { return false } for _, entry := range structExpr.GetEntries() { mapKey := entry.GetMapKey() mapVal := entry.GetValue() if !isCELStringLiteral(mapKey) { return false } mapKeyStr := mapKey.GetConstExpr().GetStringValue() if mapKeyStr == "try_files" || mapKeyStr == "split_path" { if !isCELStringListLiteral(mapVal) { return false } } else if mapKeyStr == "try_policy" || mapKeyStr == "root" { if !(isCELStringExpr(mapVal)) { return false } } else { return false } } return true } return false } // isCELStringExpr indicates whether the expression is a supported string expression func isCELStringExpr(e *exprpb.Expr) bool { return isCELStringLiteral(e) || isCELCaddyPlaceholderCall(e) || isCELConcatCall(e) } // isCELStringLiteral returns whether the expression is a CEL string literal. func isCELStringLiteral(e *exprpb.Expr) bool { switch e.GetExprKind().(type) { case *exprpb.Expr_ConstExpr: constant := e.GetConstExpr() switch constant.GetConstantKind().(type) { case *exprpb.Constant_StringValue: return true } } return false } // isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call. func isCELCaddyPlaceholderCall(e *exprpb.Expr) bool { switch e.GetExprKind().(type) { case *exprpb.Expr_CallExpr: call := e.GetCallExpr() if call.GetFunction() == "caddyPlaceholder" { return true } } return false } // isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or // other concat call arguments. func isCELConcatCall(e *exprpb.Expr) bool { switch e.GetExprKind().(type) { case *exprpb.Expr_CallExpr: call := e.GetCallExpr() if call.GetTarget() != nil { return false } if call.GetFunction() != operators.Add { return false } for _, arg := range call.GetArgs() { if !isCELStringExpr(arg) { return false } } return true } return false } // isCELStringListLiteral returns whether the expression resolves to a list literal // containing only string constants or a placeholder call. func isCELStringListLiteral(e *exprpb.Expr) bool { switch e.GetExprKind().(type) { case *exprpb.Expr_ListExpr: list := e.GetListExpr() for _, elem := range list.GetElements() { if !isCELStringExpr(elem) { return false } } return true } return false } // globSafeRepl replaces special glob characters with escaped // equivalents. Note that the filepath godoc states that // escaping is not done on Windows because of the separator. var globSafeRepl = strings.NewReplacer( "*", "\\*", "[", "\\[", "?", "\\?", ) const ( tryPolicyFirstExist = "first_exist" tryPolicyLargestSize = "largest_size" tryPolicySmallestSize = "smallest_size" tryPolicyMostRecentlyMod = "most_recently_modified" ) // Interface guards var ( _ caddy.Validator = (*MatchFile)(nil) _ caddyhttp.RequestMatcher = (*MatchFile)(nil) _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil) ) caddy-2.6.2/modules/caddyhttp/fileserver/matcher_test.go000066400000000000000000000222311435007237400234250ustar00rootroot00000000000000// 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 fileserver import ( "context" "net/http" "net/http/httptest" "net/url" "os" "runtime" "testing" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func TestFileMatcher(t *testing.T) { // Windows doesn't like colons in files names isWindows := runtime.GOOS == "windows" if !isWindows { filename := "with:in-name.txt" f, err := os.Create("./testdata/" + filename) if err != nil { t.Fail() return } t.Cleanup(func() { os.Remove("./testdata/" + filename) }) f.WriteString(filename) f.Close() } for i, tc := range []struct { path string expectedPath string expectedType string matched bool }{ { path: "/foo.txt", expectedPath: "/foo.txt", expectedType: "file", matched: true, }, { path: "/foo.txt/", expectedPath: "/foo.txt", expectedType: "file", matched: true, }, { path: "/foodir", expectedPath: "/foodir/", expectedType: "directory", matched: true, }, { path: "/foodir/", expectedPath: "/foodir/", expectedType: "directory", matched: true, }, { path: "/foodir/foo.txt", expectedPath: "/foodir/foo.txt", expectedType: "file", matched: true, }, { path: "/missingfile.php", matched: false, }, { path: "ملف.txt", // the path file name is not escaped expectedPath: "/ملف.txt", expectedType: "file", matched: true, }, { path: url.PathEscape("ملف.txt"), // singly-escaped path expectedPath: "/ملف.txt", expectedType: "file", matched: true, }, { path: url.PathEscape(url.PathEscape("ملف.txt")), // doubly-escaped path expectedPath: "/%D9%85%D9%84%D9%81.txt", expectedType: "file", matched: true, }, { path: "./with:in-name.txt", // browsers send the request with the path as such expectedPath: "/with:in-name.txt", expectedType: "file", matched: !isWindows, }, } { m := &MatchFile{ fileSystem: osFS{}, Root: "./testdata", TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, } u, err := url.Parse(tc.path) if err != nil { t.Errorf("Test %d: parsing path: %v", i, err) } req := &http.Request{URL: u} repl := caddyhttp.NewTestReplacer(req) result := m.Match(req) if result != tc.matched { t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result) } rel, ok := repl.Get("http.matchers.file.relative") if !ok && result { t.Errorf("Test %d: expected replacer value", i) } if !result { continue } if rel != tc.expectedPath { t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath) } fileType, _ := repl.Get("http.matchers.file.type") if fileType != tc.expectedType { t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) } } } func TestPHPFileMatcher(t *testing.T) { for i, tc := range []struct { path string expectedPath string expectedType string matched bool }{ { path: "/index.php", expectedPath: "/index.php", expectedType: "file", matched: true, }, { path: "/index.php/somewhere", expectedPath: "/index.php", expectedType: "file", matched: true, }, { path: "/remote.php", expectedPath: "/remote.php", expectedType: "file", matched: true, }, { path: "/remote.php/somewhere", expectedPath: "/remote.php", expectedType: "file", matched: true, }, { path: "/missingfile.php", matched: false, }, { path: "/notphp.php.txt", expectedPath: "/notphp.php.txt", expectedType: "file", matched: true, }, { path: "/notphp.php.txt/", expectedPath: "/notphp.php.txt", expectedType: "file", matched: true, }, { path: "/notphp.php.txt.suffixed", matched: false, }, { path: "/foo.php.php/index.php", expectedPath: "/foo.php.php/index.php", expectedType: "file", matched: true, }, { // See https://github.com/caddyserver/caddy/issues/3623 path: "/%E2%C3", expectedPath: "/%E2%C3", expectedType: "file", matched: false, }, } { m := &MatchFile{ fileSystem: osFS{}, Root: "./testdata", TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"}, SplitPath: []string{".php"}, } u, err := url.Parse(tc.path) if err != nil { t.Errorf("Test %d: parsing path: %v", i, err) } req := &http.Request{URL: u} repl := caddyhttp.NewTestReplacer(req) result := m.Match(req) if result != tc.matched { t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result) } rel, ok := repl.Get("http.matchers.file.relative") if !ok && result { t.Errorf("Test %d: expected replacer value", i) } if !result { continue } if rel != tc.expectedPath { t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath) } fileType, _ := repl.Get("http.matchers.file.type") if fileType != tc.expectedType { t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) } } } func TestFirstSplit(t *testing.T) { m := MatchFile{SplitPath: []string{".php"}} actual, remainder := m.firstSplit("index.PHP/somewhere") expected := "index.PHP" expectedRemainder := "/somewhere" if actual != expected { t.Errorf("Expected split %s but got %s", expected, actual) } if remainder != expectedRemainder { t.Errorf("Expected remainder %s but got %s", expectedRemainder, remainder) } } var ( expressionTests = []struct { name string expression *caddyhttp.MatchExpression urlTarget string httpMethod string httpHeader *http.Header wantErr bool wantResult bool clientCertificate []byte }{ { name: "file error no args (MatchFile)", expression: &caddyhttp.MatchExpression{ Expr: `file()`, }, urlTarget: "https://example.com/foo", wantErr: true, }, { name: "file error bad try files (MatchFile)", expression: &caddyhttp.MatchExpression{ Expr: `file({"try_file": ["bad_arg"]})`, }, urlTarget: "https://example.com/foo", wantErr: true, }, { name: "file match short pattern index.php (MatchFile)", expression: &caddyhttp.MatchExpression{ Expr: `file("index.php")`, }, urlTarget: "https://example.com/foo", wantResult: true, }, { name: "file match short pattern foo.txt (MatchFile)", expression: &caddyhttp.MatchExpression{ Expr: `file({http.request.uri.path})`, }, urlTarget: "https://example.com/foo.txt", wantResult: true, }, { name: "file match index.php (MatchFile)", expression: &caddyhttp.MatchExpression{ Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`, }, urlTarget: "https://example.com/foo", wantResult: true, }, { name: "file match long pattern foo.txt (MatchFile)", expression: &caddyhttp.MatchExpression{ Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`, }, urlTarget: "https://example.com/foo.txt", wantResult: true, }, { name: "file match long pattern foo.txt with concatenation (MatchFile)", expression: &caddyhttp.MatchExpression{ Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`, }, urlTarget: "https://example.com/foo.txt", wantResult: true, }, { name: "file not match long pattern (MatchFile)", expression: &caddyhttp.MatchExpression{ Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`, }, urlTarget: "https://example.com/nopenope.txt", wantResult: false, }, } ) func TestMatchExpressionMatch(t *testing.T) { for _, tst := range expressionTests { tc := tst t.Run(tc.name, func(t *testing.T) { err := tc.expression.Provision(caddy.Context{}) if err != nil { if !tc.wantErr { t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr) } return } req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil) if tc.httpHeader != nil { req.Header = *tc.httpHeader } repl := caddyhttp.NewTestReplacer(req) repl.Set("http.vars.root", "./testdata") ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) if tc.expression.Match(req) != tc.wantResult { t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr) } }) } } caddy-2.6.2/modules/caddyhttp/fileserver/staticfiles.go000066400000000000000000000570501435007237400232640ustar00rootroot00000000000000// 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 fileserver import ( "encoding/json" "errors" "fmt" "io" "io/fs" weakrand "math/rand" "mime" "net/http" "os" "path" "path/filepath" "strconv" "strings" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" "go.uber.org/zap" ) func init() { weakrand.Seed(time.Now().UnixNano()) caddy.RegisterModule(FileServer{}) } // FileServer implements a handler that serves static files. // // The path of the file to serve is constructed by joining the site root // and the sanitized request path. Any and all files within the root and // links with targets outside the site root may therefore be accessed. // For example, with a site root of `/www`, requests to `/foo/bar.txt` // will serve the file at `/www/foo/bar.txt`. // // The request path is sanitized using the Go standard library's // path.Clean() function (https://pkg.go.dev/path#Clean) before being // joined to the root. Request paths must be valid and well-formed. // // For requests that access directories instead of regular files, // Caddy will attempt to serve an index file if present. For example, // a request to `/dir/` will attempt to serve `/dir/index.html` if // it exists. The index file names to try are configurable. If a // requested directory does not have an index file, Caddy writes a // 404 response. Alternatively, file browsing can be enabled with // the "browse" parameter which shows a list of files when directories // are requested if no index file is present. // // By default, this handler will canonicalize URIs so that requests to // directories end with a slash, but requests to regular files do not. // This is enforced with HTTP redirects automatically and can be disabled. // Canonicalization redirects are not issued, however, if a URI rewrite // modified the last component of the path (the filename). // // This handler sets the Etag and Last-Modified headers for static files. // It does not perform MIME sniffing to determine Content-Type based on // contents, but does use the extension (if known); see the Go docs for // details: https://pkg.go.dev/mime#TypeByExtension // // The file server properly handles requests with If-Match, // If-Unmodified-Since, If-Modified-Since, If-None-Match, Range, and // If-Range headers. It includes the file's modification time in the // Last-Modified header of the response. type FileServer struct { // The file system implementation to use. By default, Caddy uses the local // disk file system. // // File system modules used here must adhere to the following requirements: // - Implement fs.FS interface. // - Support seeking on opened files; i.e.returned fs.File values must // implement the io.Seeker interface. This is required for determining // Content-Length and satisfying Range requests. // - fs.File values that represent directories must implement the // fs.ReadDirFile interface so that directory listings can be procured. FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"` fileSystem fs.FS // The path to the root of the site. Default is `{http.vars.root}` if set, // or current working directory otherwise. This should be a trusted value. // // Note that a site root is not a sandbox. Although the file server does // sanitize the request URI to prevent directory traversal, files (including // links) within the site root may be directly accessed based on the request // path. Files and folders within the root should be secure and trustworthy. Root string `json:"root,omitempty"` // A list of files or folders to hide; the file server will pretend as if // they don't exist. Accepts globular patterns like `*.ext` or `/foo/*/bar` // as well as placeholders. Because site roots can be dynamic, this list // uses file system paths, not request paths. To clarify, the base of // relative paths is the current working directory, NOT the site root. // // Entries without a path separator (`/` or `\` depending on OS) will match // any file or directory of that name regardless of its path. To hide only a // specific file with a name that may not be unique, always use a path // separator. For example, to hide all files or folder trees named "hidden", // put "hidden" in the list. To hide only ./hidden, put "./hidden" in the list. // // When possible, all paths are resolved to their absolute form before // comparisons are made. For maximum clarity and explictness, use complete, // absolute paths; or, for greater portability, use relative paths instead. Hide []string `json:"hide,omitempty"` // The names of files to try as index files if a folder is requested. // Default: index.html, index.txt. IndexNames []string `json:"index_names,omitempty"` // Enables file listings if a directory was requested and no index // file is present. Browse *Browse `json:"browse,omitempty"` // Use redirects to enforce trailing slashes for directories, or to // remove trailing slash from URIs for files. Default is true. // // Canonicalization will not happen if the last element of the request's // path (the filename) is changed in an internal rewrite, to avoid // clobbering the explicit rewrite with implicit behavior. CanonicalURIs *bool `json:"canonical_uris,omitempty"` // Override the status code written when successfully serving a file. // Particularly useful when explicitly serving a file as display for // an error, like a 404 page. A placeholder may be used. By default, // the status code will typically be 200, or 206 for partial content. StatusCode caddyhttp.WeakString `json:"status_code,omitempty"` // If pass-thru mode is enabled and a requested file is not found, // it will invoke the next handler in the chain instead of returning // a 404 error. By default, this is false (disabled). PassThru bool `json:"pass_thru,omitempty"` // Selection of encoders to use to check for precompressed files. PrecompressedRaw caddy.ModuleMap `json:"precompressed,omitempty" caddy:"namespace=http.precompressed"` // If the client has no strong preference (q-factor), choose these encodings in order. // If no order specified here, the first encoding from the Accept-Encoding header // that both client and server support is used PrecompressedOrder []string `json:"precompressed_order,omitempty"` precompressors map[string]encode.Precompressed logger *zap.Logger } // CaddyModule returns the Caddy module information. func (FileServer) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.file_server", New: func() caddy.Module { return new(FileServer) }, } } // Provision sets up the static files responder. func (fsrv *FileServer) Provision(ctx caddy.Context) error { fsrv.logger = ctx.Logger() // establish which file system (possibly a virtual one) we'll be using if len(fsrv.FileSystemRaw) > 0 { mod, err := ctx.LoadModule(fsrv, "FileSystemRaw") if err != nil { return fmt.Errorf("loading file system module: %v", err) } fsrv.fileSystem = mod.(fs.FS) } if fsrv.fileSystem == nil { fsrv.fileSystem = osFS{} } if fsrv.Root == "" { fsrv.Root = "{http.vars.root}" } if fsrv.IndexNames == nil { fsrv.IndexNames = defaultIndexNames } // for hide paths that are static (i.e. no placeholders), we can transform them into // absolute paths before the server starts for very slight performance improvement for i, h := range fsrv.Hide { if !strings.Contains(h, "{") && strings.Contains(h, separator) { if abs, err := filepath.Abs(h); err == nil { fsrv.Hide[i] = abs } } } // support precompressed sidecar files mods, err := ctx.LoadModule(fsrv, "PrecompressedRaw") if err != nil { return fmt.Errorf("loading encoder modules: %v", err) } for modName, modIface := range mods.(map[string]any) { p, ok := modIface.(encode.Precompressed) if !ok { return fmt.Errorf("module %s is not precompressor", modName) } ae := p.AcceptEncoding() if ae == "" { return fmt.Errorf("precompressor does not specify an Accept-Encoding value") } suffix := p.Suffix() if suffix == "" { return fmt.Errorf("precompressor does not specify a Suffix value") } if _, ok := fsrv.precompressors[ae]; ok { return fmt.Errorf("precompressor already added: %s", ae) } if fsrv.precompressors == nil { fsrv.precompressors = make(map[string]encode.Precompressed) } fsrv.precompressors[ae] = p } return nil } func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) filesToHide := fsrv.transformHidePaths(repl) root := repl.ReplaceAll(fsrv.Root, ".") filename := caddyhttp.SanitizedPathJoin(root, r.URL.Path) fsrv.logger.Debug("sanitized path join", zap.String("site_root", root), zap.String("request_path", r.URL.Path), zap.String("result", filename)) // get information about the file info, err := fs.Stat(fsrv.fileSystem, filename) if err != nil { err = fsrv.mapDirOpenError(err, filename) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) { return fsrv.notFound(w, r, next) } else if errors.Is(err, fs.ErrPermission) { return caddyhttp.Error(http.StatusForbidden, err) } return caddyhttp.Error(http.StatusInternalServerError, err) } // if the request mapped to a directory, see if // there is an index file we can serve var implicitIndexFile bool if info.IsDir() && len(fsrv.IndexNames) > 0 { for _, indexPage := range fsrv.IndexNames { indexPage := repl.ReplaceAll(indexPage, "") indexPath := caddyhttp.SanitizedPathJoin(filename, indexPage) if fileHidden(indexPath, filesToHide) { // pretend this file doesn't exist fsrv.logger.Debug("hiding index file", zap.String("filename", indexPath), zap.Strings("files_to_hide", filesToHide)) continue } indexInfo, err := fs.Stat(fsrv.fileSystem, indexPath) if err != nil { continue } // don't rewrite the request path to append // the index file, because we might need to // do a canonical-URL redirect below based // on the URL as-is // we've chosen to use this index file, // so replace the last file info and path // with that of the index file info = indexInfo filename = indexPath implicitIndexFile = true fsrv.logger.Debug("located index file", zap.String("filename", filename)) break } } // if still referencing a directory, delegate // to browse or return an error if info.IsDir() { fsrv.logger.Debug("no index file in directory", zap.String("path", filename), zap.Strings("index_filenames", fsrv.IndexNames)) if fsrv.Browse != nil && !fileHidden(filename, filesToHide) { return fsrv.serveBrowse(root, filename, w, r, next) } return fsrv.notFound(w, r, next) } // one last check to ensure the file isn't hidden (we might // have changed the filename from when we last checked) if fileHidden(filename, filesToHide) { fsrv.logger.Debug("hiding file", zap.String("filename", filename), zap.Strings("files_to_hide", filesToHide)) return fsrv.notFound(w, r, next) } // if URL canonicalization is enabled, we need to enforce trailing // slash convention: if a directory, trailing slash; if a file, no // trailing slash - not enforcing this can break relative hrefs // in HTML (see https://github.com/caddyserver/caddy/issues/2741) if fsrv.CanonicalURIs == nil || *fsrv.CanonicalURIs { // Only redirect if the last element of the path (the filename) was not // rewritten; if the admin wanted to rewrite to the canonical path, they // would have, and we have to be very careful not to introduce unwanted // redirects and especially redirect loops! // See https://github.com/caddyserver/caddy/issues/4205. origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request) if path.Base(origReq.URL.Path) == path.Base(r.URL.Path) { if implicitIndexFile && !strings.HasSuffix(origReq.URL.Path, "/") { to := origReq.URL.Path + "/" fsrv.logger.Debug("redirecting to canonical URI (adding trailing slash for directory)", zap.String("from_path", origReq.URL.Path), zap.String("to_path", to)) return redirect(w, r, to) } else if !implicitIndexFile && strings.HasSuffix(origReq.URL.Path, "/") { to := origReq.URL.Path[:len(origReq.URL.Path)-1] fsrv.logger.Debug("redirecting to canonical URI (removing trailing slash for file)", zap.String("from_path", origReq.URL.Path), zap.String("to_path", to)) return redirect(w, r, to) } } } var file fs.File var etag string // check for precompressed files for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) { precompress, ok := fsrv.precompressors[ae] if !ok { continue } compressedFilename := filename + precompress.Suffix() compressedInfo, err := fs.Stat(fsrv.fileSystem, compressedFilename) if err != nil || compressedInfo.IsDir() { fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err)) continue } fsrv.logger.Debug("opening compressed sidecar file", zap.String("filename", compressedFilename), zap.Error(err)) file, err = fsrv.openFile(compressedFilename, w) if err != nil { fsrv.logger.Warn("opening precompressed file failed", zap.String("filename", compressedFilename), zap.Error(err)) if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable { return err } file = nil continue } defer file.Close() w.Header().Set("Content-Encoding", ae) w.Header().Del("Accept-Ranges") w.Header().Add("Vary", "Accept-Encoding") // don't assign info = compressedInfo because sidecars are kind // of transparent; however we do need to set the Etag: // https://caddy.community/t/gzipped-sidecar-file-wrong-same-etag/16793 etag = calculateEtag(compressedInfo) break } // no precompressed file found, use the actual file if file == nil { fsrv.logger.Debug("opening file", zap.String("filename", filename)) // open the file file, err = fsrv.openFile(filename, w) if err != nil { if herr, ok := err.(caddyhttp.HandlerError); ok && herr.StatusCode == http.StatusNotFound { return fsrv.notFound(w, r, next) } return err // error is already structured } defer file.Close() etag = calculateEtag(info) } // set the Etag - note that a conditional If-None-Match request is handled // by http.ServeContent below, which checks against this Etag value w.Header().Set("Etag", etag) if w.Header().Get("Content-Type") == "" { mtyp := mime.TypeByExtension(filepath.Ext(filename)) if mtyp == "" { // do not allow Go to sniff the content-type; see https://www.youtube.com/watch?v=8t8JYpt0egE w.Header()["Content-Type"] = nil } else { w.Header().Set("Content-Type", mtyp) } } var statusCodeOverride int // if this handler exists in an error context (i.e. is part of a // handler chain that is supposed to handle a previous error), // we should set status code to the one from the error instead // of letting http.ServeContent set the default (usually 200) if reqErr, ok := r.Context().Value(caddyhttp.ErrorCtxKey).(error); ok { statusCodeOverride = http.StatusInternalServerError if handlerErr, ok := reqErr.(caddyhttp.HandlerError); ok { if handlerErr.StatusCode > 0 { statusCodeOverride = handlerErr.StatusCode } } } // if a status code override is configured, run the replacer on it if codeStr := fsrv.StatusCode.String(); codeStr != "" { statusCodeOverride, err = strconv.Atoi(repl.ReplaceAll(codeStr, "")) if err != nil { return caddyhttp.Error(http.StatusInternalServerError, err) } } // if we do have an override from the previous two parts, then // we wrap the response writer to intercept the WriteHeader call if statusCodeOverride > 0 { w = statusOverrideResponseWriter{ResponseWriter: w, code: statusCodeOverride} } // let the standard library do what it does best; note, however, // that errors generated by ServeContent are written immediately // to the response, so we cannot handle them (but errors there // are rare) http.ServeContent(w, r, info.Name(), info.ModTime(), file.(io.ReadSeeker)) return nil } // openFile opens the file at the given filename. If there was an error, // the response is configured to inform the client how to best handle it // and a well-described handler error is returned (do not wrap the // returned error value). func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (fs.File, error) { file, err := fsrv.fileSystem.Open(filename) if err != nil { err = fsrv.mapDirOpenError(err, filename) if os.IsNotExist(err) { fsrv.logger.Debug("file not found", zap.String("filename", filename), zap.Error(err)) return nil, caddyhttp.Error(http.StatusNotFound, err) } else if os.IsPermission(err) { fsrv.logger.Debug("permission denied", zap.String("filename", filename), zap.Error(err)) return nil, caddyhttp.Error(http.StatusForbidden, err) } // maybe the server is under load and ran out of file descriptors? // have client wait arbitrary seconds to help prevent a stampede //nolint:gosec backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff w.Header().Set("Retry-After", strconv.Itoa(backoff)) fsrv.logger.Debug("retry after backoff", zap.String("filename", filename), zap.Int("backoff", backoff), zap.Error(err)) return nil, caddyhttp.Error(http.StatusServiceUnavailable, err) } return file, nil } // mapDirOpenError maps the provided non-nil error from opening name // to a possibly better non-nil error. In particular, it turns OS-specific errors // about opening files in non-directories into os.ErrNotExist. See golang/go#18984. // Adapted from the Go standard library; originally written by Nathaniel Caza. // https://go-review.googlesource.com/c/go/+/36635/ // https://go-review.googlesource.com/c/go/+/36804/ func (fsrv *FileServer) mapDirOpenError(originalErr error, name string) error { if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) { return originalErr } parts := strings.Split(name, separator) for i := range parts { if parts[i] == "" { continue } fi, err := fs.Stat(fsrv.fileSystem, strings.Join(parts[:i+1], separator)) if err != nil { return originalErr } if !fi.IsDir() { return fs.ErrNotExist } } return originalErr } // transformHidePaths performs replacements for all the elements of fsrv.Hide and // makes them absolute paths (if they contain a path separator), then returns a // new list of the transformed values. func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string { hide := make([]string, len(fsrv.Hide)) for i := range fsrv.Hide { hide[i] = repl.ReplaceAll(fsrv.Hide[i], "") if strings.Contains(hide[i], separator) { abs, err := filepath.Abs(hide[i]) if err == nil { hide[i] = abs } } } return hide } // fileHidden returns true if filename is hidden according to the hide list. // filename must be a relative or absolute file system path, not a request // URI path. It is expected that all the paths in the hide list are absolute // paths or are singular filenames (without a path separator). func fileHidden(filename string, hide []string) bool { if len(hide) == 0 { return false } // all path comparisons use the complete absolute path if possible filenameAbs, err := filepath.Abs(filename) if err == nil { filename = filenameAbs } var components []string for _, h := range hide { if !strings.Contains(h, separator) { // if there is no separator in h, then we assume the user // wants to hide any files or folders that match that // name; thus we have to compare against each component // of the filename, e.g. hiding "bar" would hide "/bar" // as well as "/foo/bar/baz" but not "/barstool". if len(components) == 0 { components = strings.Split(filename, separator) } for _, c := range components { if hidden, _ := filepath.Match(h, c); hidden { return true } } } else if strings.HasPrefix(filename, h) { // if there is a separator in h, and filename is exactly // prefixed with h, then we can do a prefix match so that // "/foo" matches "/foo/bar" but not "/foobar". withoutPrefix := strings.TrimPrefix(filename, h) if strings.HasPrefix(withoutPrefix, separator) { return true } } // in the general case, a glob match will suffice if hidden, _ := filepath.Match(h, filename); hidden { return true } } return false } // notFound returns a 404 error or, if pass-thru is enabled, // it calls the next handler in the chain. func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { if fsrv.PassThru { return next.ServeHTTP(w, r) } return caddyhttp.Error(http.StatusNotFound, nil) } // calculateEtag produces a strong etag by default, although, for // efficiency reasons, it does not actually consume the contents // of the file to make a hash of all the bytes. ¯\_(ツ)_/¯ // Prefix the etag with "W/" to convert it into a weak etag. // See: https://tools.ietf.org/html/rfc7232#section-2.3 func calculateEtag(d os.FileInfo) string { t := strconv.FormatInt(d.ModTime().Unix(), 36) s := strconv.FormatInt(d.Size(), 36) return `"` + t + s + `"` } func redirect(w http.ResponseWriter, r *http.Request, to string) error { for strings.HasPrefix(to, "//") { // prevent path-based open redirects to = strings.TrimPrefix(to, "/") } http.Redirect(w, r, to, http.StatusPermanentRedirect) return nil } // statusOverrideResponseWriter intercepts WriteHeader calls // to instead write the HTTP status code we want instead // of the one http.ServeContent will use by default (usually 200) type statusOverrideResponseWriter struct { http.ResponseWriter code int } // WriteHeader intercepts calls by the stdlib to WriteHeader // to instead write the HTTP status code we want. func (wr statusOverrideResponseWriter) WriteHeader(int) { wr.ResponseWriter.WriteHeader(wr.code) } // osFS is a simple fs.FS implementation that uses the local // file system. (We do not use os.DirFS because we do our own // rooting or path prefixing without being constrained to a single // root folder. The standard os.DirFS implementation is problematic // since roots can be dynamic in our application.) // // osFS also implements fs.StatFS, fs.GlobFS, fs.ReadDirFS, and fs.ReadFileFS. type osFS struct{} func (osFS) Open(name string) (fs.File, error) { return os.Open(name) } func (osFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } func (osFS) Glob(pattern string) ([]string, error) { return filepath.Glob(pattern) } func (osFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(name) } func (osFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) } var defaultIndexNames = []string{"index.html", "index.txt"} const ( minBackoff, maxBackoff = 2, 5 separator = string(filepath.Separator) ) // Interface guards var ( _ caddy.Provisioner = (*FileServer)(nil) _ caddyhttp.MiddlewareHandler = (*FileServer)(nil) _ fs.StatFS = (*osFS)(nil) _ fs.GlobFS = (*osFS)(nil) _ fs.ReadDirFS = (*osFS)(nil) _ fs.ReadFileFS = (*osFS)(nil) ) caddy-2.6.2/modules/caddyhttp/fileserver/staticfiles_test.go000066400000000000000000000055271435007237400243250ustar00rootroot00000000000000// 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 fileserver import ( "path/filepath" "runtime" "strings" "testing" ) func TestFileHidden(t *testing.T) { for i, tc := range []struct { inputHide []string inputPath string expect bool }{ { inputHide: nil, inputPath: "", expect: false, }, { inputHide: []string{".gitignore"}, inputPath: "/.gitignore", expect: true, }, { inputHide: []string{".git"}, inputPath: "/.gitignore", expect: false, }, { inputHide: []string{"/.git"}, inputPath: "/.gitignore", expect: false, }, { inputHide: []string{".git"}, inputPath: "/.git", expect: true, }, { inputHide: []string{".git"}, inputPath: "/.git/foo", expect: true, }, { inputHide: []string{".git"}, inputPath: "/foo/.git/bar", expect: true, }, { inputHide: []string{"/prefix"}, inputPath: "/prefix/foo", expect: true, }, { inputHide: []string{"/foo/*/bar"}, inputPath: "/foo/asdf/bar", expect: true, }, { inputHide: []string{"*.txt"}, inputPath: "/foo/bar.txt", expect: true, }, { inputHide: []string{"/foo/bar/*.txt"}, inputPath: "/foo/bar/baz.txt", expect: true, }, { inputHide: []string{"/foo/bar/*.txt"}, inputPath: "/foo/bar.txt", expect: false, }, { inputHide: []string{"/foo/bar/*.txt"}, inputPath: "/foo/bar/index.html", expect: false, }, { inputHide: []string{"/foo"}, inputPath: "/foo", expect: true, }, { inputHide: []string{"/foo"}, inputPath: "/foobar", expect: false, }, { inputHide: []string{"first", "second"}, inputPath: "/second", expect: true, }, } { if runtime.GOOS == "windows" { if strings.HasPrefix(tc.inputPath, "/") { tc.inputPath, _ = filepath.Abs(tc.inputPath) } tc.inputPath = filepath.FromSlash(tc.inputPath) for i := range tc.inputHide { if strings.HasPrefix(tc.inputHide[i], "/") { tc.inputHide[i], _ = filepath.Abs(tc.inputHide[i]) } tc.inputHide[i] = filepath.FromSlash(tc.inputHide[i]) } } actual := fileHidden(tc.inputPath, tc.inputHide) if actual != tc.expect { t.Errorf("Test %d: Does %v hide %s? Got %t but expected %t", i, tc.inputHide, tc.inputPath, actual, tc.expect) } } } caddy-2.6.2/modules/caddyhttp/fileserver/testdata/000077500000000000000000000000001435007237400222255ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/fileserver/testdata/%D9%85%D9%84%D9%81.txt000066400000000000000000000000261435007237400246720ustar00rootroot00000000000000%D9%85%D9%84%D9%81.txtcaddy-2.6.2/modules/caddyhttp/fileserver/testdata/foo.php.php/000077500000000000000000000000001435007237400243645ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/fileserver/testdata/foo.php.php/index.php000066400000000000000000000000261435007237400262020ustar00rootroot00000000000000foo.php.php/index.php caddy-2.6.2/modules/caddyhttp/fileserver/testdata/foo.txt000066400000000000000000000000071435007237400235460ustar00rootroot00000000000000foo.txtcaddy-2.6.2/modules/caddyhttp/fileserver/testdata/foodir/000077500000000000000000000000001435007237400235075ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/fileserver/testdata/foodir/bar.txt000066400000000000000000000000161435007237400250110ustar00rootroot00000000000000foodir/bar.txtcaddy-2.6.2/modules/caddyhttp/fileserver/testdata/foodir/foo.txt000066400000000000000000000000161435007237400250300ustar00rootroot00000000000000foodir/foo.txtcaddy-2.6.2/modules/caddyhttp/fileserver/testdata/index.php000066400000000000000000000000111435007237400240350ustar00rootroot00000000000000index.phpcaddy-2.6.2/modules/caddyhttp/fileserver/testdata/notphp.php.txt000066400000000000000000000000171435007237400250620ustar00rootroot00000000000000notphp.php.txt caddy-2.6.2/modules/caddyhttp/fileserver/testdata/remote.php000066400000000000000000000000121435007237400242220ustar00rootroot00000000000000remote.phpcaddy-2.6.2/modules/caddyhttp/fileserver/testdata/ملف.txt000066400000000000000000000000121435007237400250630ustar00rootroot00000000000000ملف.txtcaddy-2.6.2/modules/caddyhttp/headers/000077500000000000000000000000001435007237400176615ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/headers/caddyfile.go000066400000000000000000000163401435007237400221400ustar00rootroot00000000000000// 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 headers import ( "fmt" "net/http" "reflect" "strings" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { httpcaddyfile.RegisterDirective("header", parseCaddyfile) httpcaddyfile.RegisterDirective("request_header", parseReqHdrCaddyfile) } // parseCaddyfile sets up the handler for response headers from // Caddyfile tokens. Syntax: // // header [] [[+|-|?] [] []] { // [+] [ []] // ? // - // [defer] // } // // Either a block can be opened or a single header field can be configured // in the first line, but not both in the same directive. Header operations // are deferred to write-time if any headers are being deleted or if the // 'defer' subdirective is used. + appends a header value, - deletes a field, // and ? conditionally sets a value only if the header field is not already // set. func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { if !h.Next() { return nil, h.ArgErr() } matcherSet, err := h.ExtractMatcherSet() if err != nil { return nil, err } makeHandler := func() Handler { return Handler{ Response: &RespHeaderOps{ HeaderOps: &HeaderOps{}, }, } } handler, handlerWithRequire := makeHandler(), makeHandler() for h.Next() { // first see if headers are in the initial line var hasArgs bool if h.NextArg() { hasArgs = true field := h.Val() var value, replacement string if h.NextArg() { value = h.Val() } if h.NextArg() { replacement = h.Val() } err := applyHeaderOp( handler.Response.HeaderOps, handler.Response, field, value, replacement, ) if err != nil { return nil, h.Err(err.Error()) } if len(handler.Response.HeaderOps.Delete) > 0 { handler.Response.Deferred = true } } // if not, they should be in a block for h.NextBlock(0) { field := h.Val() if field == "defer" { handler.Response.Deferred = true continue } if hasArgs { return nil, h.Err("cannot specify headers in both arguments and block") // because it would be weird } // sometimes it is habitual for users to suffix a field name with a colon, // as if they were writing a curl command or something; see // https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349/19 field = strings.TrimSuffix(field, ":") var value, replacement string if h.NextArg() { value = h.Val() } if h.NextArg() { replacement = h.Val() } handlerToUse := handler if strings.HasPrefix(field, "?") { handlerToUse = handlerWithRequire } err := applyHeaderOp( handlerToUse.Response.HeaderOps, handlerToUse.Response, field, value, replacement, ) if err != nil { return nil, h.Err(err.Error()) } } } var configValues []httpcaddyfile.ConfigValue if !reflect.DeepEqual(handler, makeHandler()) { configValues = append(configValues, h.NewRoute(matcherSet, handler)...) } if !reflect.DeepEqual(handlerWithRequire, makeHandler()) { configValues = append(configValues, h.NewRoute(matcherSet, handlerWithRequire)...) } return configValues, nil } // parseReqHdrCaddyfile sets up the handler for request headers // from Caddyfile tokens. Syntax: // // request_header [] [[+|-] [] []] func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { if !h.Next() { return nil, h.ArgErr() } matcherSet, err := h.ExtractMatcherSet() if err != nil { return nil, err } configValues := []httpcaddyfile.ConfigValue{} for h.Next() { if !h.NextArg() { return nil, h.ArgErr() } field := h.Val() hdr := Handler{ Request: &HeaderOps{}, } // sometimes it is habitual for users to suffix a field name with a colon, // as if they were writing a curl command or something; see // https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349/19 field = strings.TrimSuffix(field, ":") var value, replacement string if h.NextArg() { value = h.Val() } if h.NextArg() { replacement = h.Val() if h.NextArg() { return nil, h.ArgErr() } } if hdr.Request == nil { hdr.Request = new(HeaderOps) } if err := CaddyfileHeaderOp(hdr.Request, field, value, replacement); err != nil { return nil, h.Err(err.Error()) } configValues = append(configValues, h.NewRoute(matcherSet, hdr)...) if h.NextArg() { return nil, h.ArgErr() } } return configValues, nil } // CaddyfileHeaderOp applies a new header operation according to // field, value, and replacement. The field can be prefixed with // "+" or "-" to specify adding or removing; otherwise, the value // will be set (overriding any previous value). If replacement is // non-empty, value will be treated as a regular expression which // will be used to search and then replacement will be used to // complete the substring replacement; in that case, any + or - // prefix to field will be ignored. func CaddyfileHeaderOp(ops *HeaderOps, field, value, replacement string) error { return applyHeaderOp(ops, nil, field, value, replacement) } func applyHeaderOp(ops *HeaderOps, respHeaderOps *RespHeaderOps, field, value, replacement string) error { switch { case strings.HasPrefix(field, "+"): // append if ops.Add == nil { ops.Add = make(http.Header) } ops.Add.Add(field[1:], value) case strings.HasPrefix(field, "-"): // delete ops.Delete = append(ops.Delete, field[1:]) if respHeaderOps != nil { respHeaderOps.Deferred = true } case strings.HasPrefix(field, "?"): // default (conditional on not existing) - response headers only if respHeaderOps == nil { return fmt.Errorf("%v: the default header modifier ('?') can only be used on response headers; for conditional manipulation of request headers, use matchers", field) } if respHeaderOps.Require == nil { respHeaderOps.Require = &caddyhttp.ResponseMatcher{ Headers: make(http.Header), } } field = strings.TrimPrefix(field, "?") respHeaderOps.Require.Headers[field] = nil if respHeaderOps.Set == nil { respHeaderOps.Set = make(http.Header) } respHeaderOps.Set.Set(field, value) case replacement != "": // replace if ops.Replace == nil { ops.Replace = make(map[string][]Replacement) } field = strings.TrimLeft(field, "+-?") ops.Replace[field] = append( ops.Replace[field], Replacement{ SearchRegexp: value, Replace: replacement, }, ) default: // set (overwrite) if ops.Set == nil { ops.Set = make(http.Header) } ops.Set.Set(field, value) } return nil } caddy-2.6.2/modules/caddyhttp/headers/headers.go000066400000000000000000000243671435007237400216370ustar00rootroot00000000000000// 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 headers import ( "fmt" "net/http" "regexp" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { caddy.RegisterModule(Handler{}) } // Handler is a middleware which modifies request and response headers. // // Changes to headers are applied immediately, except for the response // headers when Deferred is true or when Required is set. In those cases, // the changes are applied when the headers are written to the response. // Note that deferred changes do not take effect if an error occurs later // in the middleware chain. // // Properties in this module accept placeholders. // // Response header operations can be conditioned upon response status code // and/or other header values. type Handler struct { Request *HeaderOps `json:"request,omitempty"` Response *RespHeaderOps `json:"response,omitempty"` } // CaddyModule returns the Caddy module information. func (Handler) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.headers", New: func() caddy.Module { return new(Handler) }, } } // Provision sets up h's configuration. func (h *Handler) Provision(ctx caddy.Context) error { if h.Request != nil { err := h.Request.Provision(ctx) if err != nil { return err } } if h.Response != nil { err := h.Response.Provision(ctx) if err != nil { return err } } return nil } // Validate ensures h's configuration is valid. func (h Handler) Validate() error { if h.Request != nil { err := h.Request.validate() if err != nil { return err } } if h.Response != nil { err := h.Response.validate() if err != nil { return err } } return nil } func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) if h.Request != nil { h.Request.ApplyToRequest(r) } if h.Response != nil { if h.Response.Deferred || h.Response.Require != nil { w = &responseWriterWrapper{ ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w}, replacer: repl, require: h.Response.Require, headerOps: h.Response.HeaderOps, } } else { h.Response.ApplyTo(w.Header(), repl) } } return next.ServeHTTP(w, r) } // HeaderOps defines manipulations for HTTP headers. type HeaderOps struct { // Adds HTTP headers; does not replace any existing header fields. Add http.Header `json:"add,omitempty"` // Sets HTTP headers; replaces existing header fields. Set http.Header `json:"set,omitempty"` // Names of HTTP header fields to delete. Basic wildcards are supported: // // - Start with `*` for all field names with the given suffix; // - End with `*` for all field names with the given prefix; // - Start and end with `*` for all field names containing a substring. Delete []string `json:"delete,omitempty"` // Performs in-situ substring replacements of HTTP headers. // Keys are the field names on which to perform the associated replacements. // If the field name is `*`, the replacements are performed on all header fields. Replace map[string][]Replacement `json:"replace,omitempty"` } // Provision sets up the header operations. func (ops *HeaderOps) Provision(_ caddy.Context) error { for fieldName, replacements := range ops.Replace { for i, r := range replacements { if r.SearchRegexp != "" { re, err := regexp.Compile(r.SearchRegexp) if err != nil { return fmt.Errorf("replacement %d for header field '%s': %v", i, fieldName, err) } replacements[i].re = re } } } return nil } func (ops HeaderOps) validate() error { for fieldName, replacements := range ops.Replace { for _, r := range replacements { if r.Search != "" && r.SearchRegexp != "" { return fmt.Errorf("cannot specify both a substring search and a regular expression search for field '%s'", fieldName) } } } return nil } // Replacement describes a string replacement, // either a simple and fast substring search // or a slower but more powerful regex search. type Replacement struct { // The substring to search for. Search string `json:"search,omitempty"` // The regular expression to search with. SearchRegexp string `json:"search_regexp,omitempty"` // The string with which to replace matches. Replace string `json:"replace,omitempty"` re *regexp.Regexp } // RespHeaderOps defines manipulations for response headers. type RespHeaderOps struct { *HeaderOps // If set, header operations will be deferred until // they are written out and only performed if the // response matches these criteria. Require *caddyhttp.ResponseMatcher `json:"require,omitempty"` // If true, header operations will be deferred until // they are written out. Superceded if Require is set. // Usually you will need to set this to true if any // fields are being deleted. Deferred bool `json:"deferred,omitempty"` } // ApplyTo applies ops to hdr using repl. func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { // add for fieldName, vals := range ops.Add { fieldName = repl.ReplaceKnown(fieldName, "") for _, v := range vals { hdr.Add(fieldName, repl.ReplaceKnown(v, "")) } } // set for fieldName, vals := range ops.Set { fieldName = repl.ReplaceKnown(fieldName, "") var newVals []string for i := range vals { // append to new slice so we don't overwrite // the original values in ops.Set newVals = append(newVals, repl.ReplaceKnown(vals[i], "")) } hdr.Set(fieldName, strings.Join(newVals, ",")) } // delete for _, fieldName := range ops.Delete { fieldName = strings.ToLower(repl.ReplaceKnown(fieldName, "")) switch { case strings.HasPrefix(fieldName, "*") && strings.HasSuffix(fieldName, "*"): for existingField := range hdr { if strings.Contains(strings.ToLower(existingField), fieldName[1:len(fieldName)-1]) { delete(hdr, existingField) } } case strings.HasPrefix(fieldName, "*"): for existingField := range hdr { if strings.HasSuffix(strings.ToLower(existingField), fieldName[1:]) { delete(hdr, existingField) } } case strings.HasSuffix(fieldName, "*"): for existingField := range hdr { if strings.HasPrefix(strings.ToLower(existingField), fieldName[:len(fieldName)-1]) { delete(hdr, existingField) } } default: hdr.Del(fieldName) } } // replace for fieldName, replacements := range ops.Replace { fieldName = http.CanonicalHeaderKey(repl.ReplaceKnown(fieldName, "")) // all fields... if fieldName == "*" { for _, r := range replacements { search := repl.ReplaceKnown(r.Search, "") replace := repl.ReplaceKnown(r.Replace, "") for fieldName, vals := range hdr { for i := range vals { if r.re != nil { hdr[fieldName][i] = r.re.ReplaceAllString(hdr[fieldName][i], replace) } else { hdr[fieldName][i] = strings.ReplaceAll(hdr[fieldName][i], search, replace) } } } } continue } // ...or only with the named field for _, r := range replacements { search := repl.ReplaceKnown(r.Search, "") replace := repl.ReplaceKnown(r.Replace, "") for hdrFieldName, vals := range hdr { // see issue #4330 for why we don't simply use hdr[fieldName] if http.CanonicalHeaderKey(hdrFieldName) != fieldName { continue } for i := range vals { if r.re != nil { hdr[hdrFieldName][i] = r.re.ReplaceAllString(hdr[hdrFieldName][i], replace) } else { hdr[hdrFieldName][i] = strings.ReplaceAll(hdr[hdrFieldName][i], search, replace) } } } } } } // ApplyToRequest applies ops to r, specially handling the Host // header which the standard library does not include with the // header map with all the others. This method mutates r.Host. func (ops HeaderOps) ApplyToRequest(r *http.Request) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) // capture the current Host header so we can // reset to it when we're done origHost, hadHost := r.Header["Host"] // append r.Host; this way, we know that our value // was last in the list, and if an Add operation // appended something else after it, that's probably // fine because it's weird to have multiple Host // headers anyway and presumably the one they added // is the one they wanted r.Header["Host"] = append(r.Header["Host"], r.Host) // apply header operations ops.ApplyTo(r.Header, repl) // retrieve the last Host value (likely the one we appended) if len(r.Header["Host"]) > 0 { r.Host = r.Header["Host"][len(r.Header["Host"])-1] } else { r.Host = "" } // reset the Host header slice if hadHost { r.Header["Host"] = origHost } else { delete(r.Header, "Host") } } // responseWriterWrapper defers response header // operations until WriteHeader is called. type responseWriterWrapper struct { *caddyhttp.ResponseWriterWrapper replacer *caddy.Replacer require *caddyhttp.ResponseMatcher headerOps *HeaderOps wroteHeader bool } func (rww *responseWriterWrapper) WriteHeader(status int) { if rww.wroteHeader { return } // 1xx responses aren't final; just informational if status < 100 || status > 199 { rww.wroteHeader = true } if rww.require == nil || rww.require.Match(status, rww.ResponseWriterWrapper.Header()) { if rww.headerOps != nil { rww.headerOps.ApplyTo(rww.ResponseWriterWrapper.Header(), rww.replacer) } } rww.ResponseWriterWrapper.WriteHeader(status) } func (rww *responseWriterWrapper) Write(d []byte) (int, error) { if !rww.wroteHeader { rww.WriteHeader(http.StatusOK) } return rww.ResponseWriterWrapper.Write(d) } // Interface guards var ( _ caddy.Provisioner = (*Handler)(nil) _ caddyhttp.MiddlewareHandler = (*Handler)(nil) _ caddyhttp.HTTPInterfaces = (*responseWriterWrapper)(nil) ) caddy-2.6.2/modules/caddyhttp/headers/headers_test.go000066400000000000000000000135401435007237400226650ustar00rootroot00000000000000// 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 headers import ( "context" "fmt" "net/http" "net/http/httptest" "reflect" "testing" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func TestHandler(t *testing.T) { for i, tc := range []struct { handler Handler reqHeader http.Header respHeader http.Header respStatusCode int expectedReqHeader http.Header expectedRespHeader http.Header }{ { handler: Handler{ Request: &HeaderOps{ Add: http.Header{ "Expose-Secrets": []string{"always"}, }, }, }, reqHeader: http.Header{ "Expose-Secrets": []string{"i'm serious"}, }, expectedReqHeader: http.Header{ "Expose-Secrets": []string{"i'm serious", "always"}, }, }, { handler: Handler{ Request: &HeaderOps{ Set: http.Header{ "Who-Wins": []string{"batman"}, }, }, }, reqHeader: http.Header{ "Who-Wins": []string{"joker"}, }, expectedReqHeader: http.Header{ "Who-Wins": []string{"batman"}, }, }, { handler: Handler{ Request: &HeaderOps{ Delete: []string{"Kick-Me"}, }, }, reqHeader: http.Header{ "Kick-Me": []string{"if you can"}, "Keep-Me": []string{"i swear i'm innocent"}, }, expectedReqHeader: http.Header{ "Keep-Me": []string{"i swear i'm innocent"}, }, }, { handler: Handler{ Request: &HeaderOps{ Delete: []string{ "*-suffix", "prefix-*", "*_*", }, }, }, reqHeader: http.Header{ "Header-Suffix": []string{"lalala"}, "Prefix-Test": []string{"asdf"}, "Host_Header": []string{"silly django... sigh"}, // see issue #4830 "Keep-Me": []string{"foofoofoo"}, }, expectedReqHeader: http.Header{ "Keep-Me": []string{"foofoofoo"}, }, }, { handler: Handler{ Request: &HeaderOps{ Replace: map[string][]Replacement{ "Best-Server": { Replacement{ Search: "NGINX", Replace: "the Caddy web server", }, Replacement{ SearchRegexp: `Apache(\d+)`, Replace: "Caddy", }, }, }, }, }, reqHeader: http.Header{ "Best-Server": []string{"it's NGINX, undoubtedly", "I love Apache2"}, }, expectedReqHeader: http.Header{ "Best-Server": []string{"it's the Caddy web server, undoubtedly", "I love Caddy"}, }, }, { handler: Handler{ Response: &RespHeaderOps{ Require: &caddyhttp.ResponseMatcher{ Headers: http.Header{ "Cache-Control": nil, }, }, HeaderOps: &HeaderOps{ Add: http.Header{ "Cache-Control": []string{"no-cache"}, }, }, }, }, respHeader: http.Header{}, expectedRespHeader: http.Header{ "Cache-Control": []string{"no-cache"}, }, }, { handler: Handler{ Response: &RespHeaderOps{ Require: &caddyhttp.ResponseMatcher{ Headers: http.Header{ "Cache-Control": []string{"no-cache"}, }, }, HeaderOps: &HeaderOps{ Delete: []string{"Cache-Control"}, }, }, }, respHeader: http.Header{ "Cache-Control": []string{"no-cache"}, }, expectedRespHeader: http.Header{}, }, { handler: Handler{ Response: &RespHeaderOps{ Require: &caddyhttp.ResponseMatcher{ StatusCode: []int{5}, }, HeaderOps: &HeaderOps{ Add: http.Header{ "Fail-5xx": []string{"true"}, }, }, }, }, respStatusCode: 503, respHeader: http.Header{}, expectedRespHeader: http.Header{ "Fail-5xx": []string{"true"}, }, }, { handler: Handler{ Request: &HeaderOps{ Replace: map[string][]Replacement{ "Case-Insensitive": { Replacement{ Search: "issue4330", Replace: "issue #4330", }, }, }, }, }, reqHeader: http.Header{ "case-insensitive": []string{"issue4330"}, "Other-Header": []string{"issue4330"}, }, expectedReqHeader: http.Header{ "case-insensitive": []string{"issue #4330"}, "Other-Header": []string{"issue4330"}, }, }, } { rr := httptest.NewRecorder() req := &http.Request{Header: tc.reqHeader} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) tc.handler.Provision(caddy.Context{}) next := nextHandler(func(w http.ResponseWriter, r *http.Request) error { for k, hdrs := range tc.respHeader { for _, v := range hdrs { w.Header().Add(k, v) } } status := 200 if tc.respStatusCode != 0 { status = tc.respStatusCode } w.WriteHeader(status) if tc.expectedReqHeader != nil && !reflect.DeepEqual(r.Header, tc.expectedReqHeader) { return fmt.Errorf("expected request header %v, got %v", tc.expectedReqHeader, r.Header) } return nil }) if err := tc.handler.ServeHTTP(rr, req, next); err != nil { t.Errorf("Test %d: %v", i, err) continue } actual := rr.Header() if tc.expectedRespHeader != nil && !reflect.DeepEqual(actual, tc.expectedRespHeader) { t.Errorf("Test %d: expected response header %v, got %v", i, tc.expectedRespHeader, actual) continue } } } type nextHandler func(http.ResponseWriter, *http.Request) error func (f nextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error { return f(w, r) } caddy-2.6.2/modules/caddyhttp/httpredirectlistener.go000066400000000000000000000105421435007237400230460ustar00rootroot00000000000000// 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 caddyhttp import ( "bufio" "fmt" "net" "net/http" "sync" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { caddy.RegisterModule(HTTPRedirectListenerWrapper{}) } // HTTPRedirectListenerWrapper provides HTTP->HTTPS redirects for // connections that come on the TLS port as an HTTP request, // by detecting using the first few bytes that it's not a TLS // handshake, but instead an HTTP request. // // This is especially useful when using a non-standard HTTPS port. // A user may simply type the address in their browser without the // https:// scheme, which would cause the browser to attempt the // connection over HTTP, but this would cause a "Client sent an // HTTP request to an HTTPS server" error response. // // This listener wrapper must be placed BEFORE the "tls" listener // wrapper, for it to work properly. type HTTPRedirectListenerWrapper struct{} func (HTTPRedirectListenerWrapper) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.listeners.http_redirect", New: func() caddy.Module { return new(HTTPRedirectListenerWrapper) }, } } func (h *HTTPRedirectListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener { return &httpRedirectListener{l} } // httpRedirectListener is listener that checks the first few bytes // of the request when the server is intended to accept HTTPS requests, // to respond to an HTTP request with a redirect. type httpRedirectListener struct { net.Listener } // Accept waits for and returns the next connection to the listener, // wrapping it with a httpRedirectConn. func (l *httpRedirectListener) Accept() (net.Conn, error) { c, err := l.Listener.Accept() if err != nil { return nil, err } return &httpRedirectConn{ Conn: c, r: bufio.NewReader(c), }, nil } type httpRedirectConn struct { net.Conn once sync.Once r *bufio.Reader } // Read tries to peek at the first few bytes of the request, and if we get // an error reading the headers, and that error was due to the bytes looking // like an HTTP request, then we perform a HTTP->HTTPS redirect on the same // port as the original connection. func (c *httpRedirectConn) Read(p []byte) (int, error) { var errReturn error c.once.Do(func() { firstBytes, err := c.r.Peek(5) if err != nil { return } // If the request doesn't look like HTTP, then it's probably // TLS bytes and we don't need to do anything. if !firstBytesLookLikeHTTP(firstBytes) { return } // Parse the HTTP request, so we can get the Host and URL to redirect to. req, err := http.ReadRequest(c.r) if err != nil { return } // Build the redirect response, using the same Host and URL, // but replacing the scheme with https. headers := make(http.Header) headers.Add("Location", "https://"+req.Host+req.URL.String()) resp := &http.Response{ Proto: "HTTP/1.0", Status: "308 Permanent Redirect", StatusCode: 308, ProtoMajor: 1, ProtoMinor: 0, Header: headers, } err = resp.Write(c.Conn) if err != nil { errReturn = fmt.Errorf("couldn't write HTTP->HTTPS redirect") return } errReturn = fmt.Errorf("redirected HTTP request on HTTPS port") c.Conn.Close() }) if errReturn != nil { return 0, errReturn } return c.r.Read(p) } // firstBytesLookLikeHTTP reports whether a TLS record header // looks like it might've been a misdirected plaintext HTTP request. func firstBytesLookLikeHTTP(hdr []byte) bool { switch string(hdr[:5]) { case "GET /", "HEAD ", "POST ", "PUT /", "OPTIO": return true } return false } var ( _ caddy.ListenerWrapper = (*HTTPRedirectListenerWrapper)(nil) _ caddyfile.Unmarshaler = (*HTTPRedirectListenerWrapper)(nil) ) caddy-2.6.2/modules/caddyhttp/logging.go000066400000000000000000000112751435007237400202310ustar00rootroot00000000000000// 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 caddyhttp import ( "errors" "net" "net/http" "strings" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // ServerLogConfig describes a server's logging configuration. If // enabled without customization, all requests to this server are // logged to the default logger; logger destinations may be // customized per-request-host. type ServerLogConfig struct { // The default logger name for all logs emitted by this server for // hostnames that are not in the LoggerNames (logger_names) map. DefaultLoggerName string `json:"default_logger_name,omitempty"` // LoggerNames maps request hostnames to a custom logger name. // For example, a mapping of "example.com" to "example" would // cause access logs from requests with a Host of example.com // to be emitted by a logger named "http.log.access.example". LoggerNames map[string]string `json:"logger_names,omitempty"` // By default, all requests to this server will be logged if // access logging is enabled. This field lists the request // hosts for which access logging should be disabled. SkipHosts []string `json:"skip_hosts,omitempty"` // If true, requests to any host not appearing in the // LoggerNames (logger_names) map will not be logged. SkipUnmappedHosts bool `json:"skip_unmapped_hosts,omitempty"` // If true, credentials that are otherwise omitted, will be logged. // The definition of credentials is defined by https://fetch.spec.whatwg.org/#credentials, // and this includes some request and response headers, i.e `Cookie`, // `Set-Cookie`, `Authorization`, and `Proxy-Authorization`. ShouldLogCredentials bool `json:"should_log_credentials,omitempty"` } // wrapLogger wraps logger in a logger named according to user preferences for the given host. func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) *zap.Logger { if loggerName := slc.getLoggerName(host); loggerName != "" { return logger.Named(loggerName) } return logger } func (slc ServerLogConfig) getLoggerName(host string) string { tryHost := func(key string) (string, bool) { // first try exact match if loggerName, ok := slc.LoggerNames[key]; ok { return loggerName, ok } // strip port and try again (i.e. Host header of "example.com:1234" should // match "example.com" if there is no "example.com:1234" in the map) hostOnly, _, err := net.SplitHostPort(key) if err != nil { return "", false } loggerName, ok := slc.LoggerNames[hostOnly] return loggerName, ok } // try the exact hostname first if loggerName, ok := tryHost(host); ok { return loggerName } // try matching wildcard domains if other non-specific loggers exist labels := strings.Split(host, ".") for i := range labels { if labels[i] == "" { continue } labels[i] = "*" wildcardHost := strings.Join(labels, ".") if loggerName, ok := tryHost(wildcardHost); ok { return loggerName } } return slc.DefaultLoggerName } func (slc *ServerLogConfig) clone() *ServerLogConfig { clone := &ServerLogConfig{ DefaultLoggerName: slc.DefaultLoggerName, LoggerNames: make(map[string]string), SkipHosts: append([]string{}, slc.SkipHosts...), SkipUnmappedHosts: slc.SkipUnmappedHosts, ShouldLogCredentials: slc.ShouldLogCredentials, } for k, v := range slc.LoggerNames { clone.LoggerNames[k] = v } return clone } // errLogValues inspects err and returns the status code // to use, the error log message, and any extra fields. // If err is a HandlerError, the returned values will // have richer information. func errLogValues(err error) (status int, msg string, fields []zapcore.Field) { var handlerErr HandlerError if errors.As(err, &handlerErr) { status = handlerErr.StatusCode if handlerErr.Err == nil { msg = err.Error() } else { msg = handlerErr.Err.Error() } fields = []zapcore.Field{ zap.Int("status", handlerErr.StatusCode), zap.String("err_id", handlerErr.ID), zap.String("err_trace", handlerErr.Trace), } return } status = http.StatusInternalServerError msg = err.Error() return } // Variable name used to indicate that this request // should be omitted from the access logs const SkipLogVar = "skip_log" caddy-2.6.2/modules/caddyhttp/map/000077500000000000000000000000001435007237400170235ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/map/caddyfile.go000066400000000000000000000065301435007237400213020ustar00rootroot00000000000000// 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 maphandler import ( "strings" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { httpcaddyfile.RegisterHandlerDirective("map", parseCaddyfile) } // parseCaddyfile sets up the map handler from Caddyfile tokens. Syntax: // // map [] { // [~] // default // } // // If the input value is prefixed with a tilde (~), then the input will be parsed as a // regular expression. // // The Caddyfile adapter treats outputs that are a literal hyphen (-) as a null/nil // value. This is useful if you want to fall back to default for that particular output. // // The number of outputs for each mapping must not be more than the number of destinations. // However, for convenience, there may be fewer outputs than destinations and any missing // outputs will be filled in implicitly. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { var handler Handler for h.Next() { // source if !h.NextArg() { return nil, h.ArgErr() } handler.Source = h.Val() // destinations handler.Destinations = h.RemainingArgs() if len(handler.Destinations) == 0 { return nil, h.Err("missing destination argument(s)") } for _, dest := range handler.Destinations { if shorthand := httpcaddyfile.WasReplacedPlaceholderShorthand(dest); shorthand != "" { return nil, h.Errf("destination %s conflicts with a Caddyfile placeholder shorthand", shorthand) } } // mappings for h.NextBlock(0) { // defaults are a special case if h.Val() == "default" { if len(handler.Defaults) > 0 { return nil, h.Err("defaults already defined") } handler.Defaults = h.RemainingArgs() for len(handler.Defaults) < len(handler.Destinations) { handler.Defaults = append(handler.Defaults, "") } continue } // every line maps an input value to one or more outputs in := h.Val() var outs []any for h.NextArg() { val := h.ScalarVal() if val == "-" { outs = append(outs, nil) } else { outs = append(outs, val) } } // cannot have more outputs than destinations if len(outs) > len(handler.Destinations) { return nil, h.Err("too many outputs") } // for convenience, can have fewer outputs than destinations, but the // underlying handler won't accept that, so we fill in nil values for len(outs) < len(handler.Destinations) { outs = append(outs, nil) } // create the mapping mapping := Mapping{Outputs: outs} if strings.HasPrefix(in, "~") { mapping.InputRegexp = in[1:] } else { mapping.Input = in } handler.Mappings = append(handler.Mappings, mapping) } } return handler, nil } caddy-2.6.2/modules/caddyhttp/map/map.go000066400000000000000000000140741435007237400201350ustar00rootroot00000000000000// 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 maphandler import ( "fmt" "net/http" "regexp" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { caddy.RegisterModule(Handler{}) } // Handler implements a middleware that maps inputs to outputs. Specifically, it // compares a source value against the map inputs, and for one that matches, it // applies the output values to each destination. Destinations become placeholder // names. // // Mapped placeholders are not evaluated until they are used, so even for very // large mappings, this handler is quite efficient. type Handler struct { // Source is the placeholder from which to get the input value. Source string `json:"source,omitempty"` // Destinations are the names of placeholders in which to store the outputs. Destinations []string `json:"destinations,omitempty"` // Mappings from source values (inputs) to destination values (outputs). // The first matching, non-nil mapping will be applied. Mappings []Mapping `json:"mappings,omitempty"` // If no mappings match or if the mapped output is null/nil, the associated // default output will be applied (optional). Defaults []string `json:"defaults,omitempty"` } // CaddyModule returns the Caddy module information. func (Handler) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.map", New: func() caddy.Module { return new(Handler) }, } } // Provision sets up h. func (h *Handler) Provision(_ caddy.Context) error { for j, dest := range h.Destinations { if strings.Count(dest, "{") != 1 || !strings.HasPrefix(dest, "{") { return fmt.Errorf("destination must be a placeholder and only a placeholder") } h.Destinations[j] = strings.Trim(dest, "{}") } for i, m := range h.Mappings { if m.InputRegexp == "" { continue } var err error h.Mappings[i].re, err = regexp.Compile(m.InputRegexp) if err != nil { return fmt.Errorf("compiling regexp for mapping %d: %v", i, err) } } // TODO: improve efficiency even further by using an actual map type // for the non-regexp mappings, OR sort them and do a binary search return nil } // Validate ensures that h is configured properly. func (h *Handler) Validate() error { nDest, nDef := len(h.Destinations), len(h.Defaults) if nDef > 0 && nDef != nDest { return fmt.Errorf("%d destinations != %d defaults", nDest, nDef) } seen := make(map[string]int) for i, m := range h.Mappings { // prevent confusing/ambiguous mappings if m.Input != "" && m.InputRegexp != "" { return fmt.Errorf("mapping %d has both input and input_regexp fields specified, which is confusing", i) } // prevent duplicate mappings input := m.Input if m.InputRegexp != "" { input = m.InputRegexp } if prev, ok := seen[input]; ok { return fmt.Errorf("mapping %d has a duplicate input '%s' previously used with mapping %d", i, input, prev) } seen[input] = i // ensure mappings have 1:1 output-to-destination correspondence nOut := len(m.Outputs) if nOut != nDest { return fmt.Errorf("mapping %d has %d outputs but there are %d destinations defined", i, nOut, nDest) } } return nil } func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) // defer work until a variable is actually evaluated by using replacer's Map callback repl.Map(func(key string) (any, bool) { // return early if the variable is not even a configured destination destIdx := h.destinationIndex(key) if destIdx < 0 { return nil, false } input := repl.ReplaceAll(h.Source, "") // find the first mapping matching the input and return // the requested destination/output value for _, m := range h.Mappings { output := m.Outputs[destIdx] if output == nil { continue } outputStr := caddy.ToString(output) // evaluate regular expression if configured if m.re != nil { var result []byte matches := m.re.FindStringSubmatchIndex(input) if matches == nil { continue } result = m.re.ExpandString(result, outputStr, input, matches) return string(result), true } // otherwise simple string comparison if input == m.Input { return repl.ReplaceAll(outputStr, ""), true } } // fall back to default if no match or if matched nil value if len(h.Defaults) > destIdx { return repl.ReplaceAll(h.Defaults[destIdx], ""), true } return nil, true }) return next.ServeHTTP(w, r) } // destinationIndex returns the positional index of the destination // is name is a known destination; otherwise it returns -1. func (h Handler) destinationIndex(name string) int { for i, dest := range h.Destinations { if dest == name { return i } } return -1 } // Mapping describes a mapping from input to outputs. type Mapping struct { // The input value to match. Must be distinct from other mappings. // Mutually exclusive to input_regexp. Input string `json:"input,omitempty"` // The input regular expression to match. Mutually exclusive to input. InputRegexp string `json:"input_regexp,omitempty"` // Upon a match with the input, each output is positionally correlated // with each destination of the parent handler. An output that is null // (nil) will be treated as if it was not mapped at all. Outputs []any `json:"outputs,omitempty"` re *regexp.Regexp } // Interface guards var ( _ caddy.Provisioner = (*Handler)(nil) _ caddy.Validator = (*Handler)(nil) _ caddyhttp.MiddlewareHandler = (*Handler)(nil) ) caddy-2.6.2/modules/caddyhttp/map/map_test.go000066400000000000000000000067451435007237400212020ustar00rootroot00000000000000package maphandler import ( "context" "net/http" "net/http/httptest" "reflect" "testing" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func TestHandler(t *testing.T) { for i, tc := range []struct { handler Handler reqURI string expect map[string]any }{ { reqURI: "/foo", handler: Handler{ Source: "{http.request.uri.path}", Destinations: []string{"{output}"}, Mappings: []Mapping{ { Input: "/foo", Outputs: []any{"FOO"}, }, }, }, expect: map[string]any{ "output": "FOO", }, }, { reqURI: "/abcdef", handler: Handler{ Source: "{http.request.uri.path}", Destinations: []string{"{output}"}, Mappings: []Mapping{ { InputRegexp: "(/abc)", Outputs: []any{"ABC"}, }, }, }, expect: map[string]any{ "output": "ABC", }, }, { reqURI: "/ABCxyzDEF", handler: Handler{ Source: "{http.request.uri.path}", Destinations: []string{"{output}"}, Mappings: []Mapping{ { InputRegexp: "(xyz)", Outputs: []any{"...${1}..."}, }, }, }, expect: map[string]any{ "output": "...xyz...", }, }, { // Test case from https://caddy.community/t/map-directive-and-regular-expressions/13866/14?u=matt reqURI: "/?s=0%27+AND+%28SELECT+0+FROM+%28SELECT+count%28%2A%29%2C+CONCAT%28%28SELECT+%40%40version%29%2C+0x23%2C+FLOOR%28RAND%280%29%2A2%29%29+AS+x+FROM+information_schema.columns+GROUP+BY+x%29+y%29+-+-+%27", handler: Handler{ Source: "{http.request.uri}", Destinations: []string{"{output}"}, Mappings: []Mapping{ { InputRegexp: "(?i)(\\^|`|<|>|%|\\\\|\\{|\\}|\\|)", Outputs: []any{"3"}, }, }, }, expect: map[string]any{ "output": "3", }, }, { reqURI: "/foo", handler: Handler{ Source: "{http.request.uri.path}", Destinations: []string{"{output}"}, Mappings: []Mapping{ { Input: "/foo", Outputs: []any{"{testvar}"}, }, }, }, expect: map[string]any{ "output": "testing", }, }, { reqURI: "/foo", handler: Handler{ Source: "{http.request.uri.path}", Destinations: []string{"{output}"}, Defaults: []string{"default"}, }, expect: map[string]any{ "output": "default", }, }, { reqURI: "/foo", handler: Handler{ Source: "{http.request.uri.path}", Destinations: []string{"{output}"}, Defaults: []string{"{testvar}"}, }, expect: map[string]any{ "output": "testing", }, }, } { if err := tc.handler.Provision(caddy.Context{}); err != nil { t.Fatalf("Test %d: Provisioning handler: %v", i, err) } req, err := http.NewRequest(http.MethodGet, tc.reqURI, nil) if err != nil { t.Fatalf("Test %d: Creating request: %v", i, err) } repl := caddyhttp.NewTestReplacer(req) repl.Set("testvar", "testing") ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) rr := httptest.NewRecorder() noop := caddyhttp.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) error { return nil }) if err := tc.handler.ServeHTTP(rr, req, noop); err != nil { t.Errorf("Test %d: Handler returned error: %v", i, err) continue } for key, expected := range tc.expect { actual, _ := repl.Get(key) if !reflect.DeepEqual(actual, expected) { t.Errorf("Test %d: Expected %#v but got %#v for {%s}", i, expected, actual, key) } } } } caddy-2.6.2/modules/caddyhttp/marshalers.go000066400000000000000000000070131435007237400207370ustar00rootroot00000000000000// 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 caddyhttp import ( "crypto/tls" "net" "net/http" "strings" "go.uber.org/zap/zapcore" ) // LoggableHTTPRequest makes an HTTP request loggable with zap.Object(). type LoggableHTTPRequest struct { *http.Request ShouldLogCredentials bool } // MarshalLogObject satisfies the zapcore.ObjectMarshaler interface. func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error { ip, port, err := net.SplitHostPort(r.RemoteAddr) if err != nil { ip = r.RemoteAddr port = "" } enc.AddString("remote_ip", ip) enc.AddString("remote_port", port) enc.AddString("proto", r.Proto) enc.AddString("method", r.Method) enc.AddString("host", r.Host) enc.AddString("uri", r.RequestURI) enc.AddObject("headers", LoggableHTTPHeader{ Header: r.Header, ShouldLogCredentials: r.ShouldLogCredentials, }) if r.TLS != nil { enc.AddObject("tls", LoggableTLSConnState(*r.TLS)) } return nil } // LoggableHTTPHeader makes an HTTP header loggable with zap.Object(). // Headers with potentially sensitive information (Cookie, Set-Cookie, // Authorization, and Proxy-Authorization) are logged with empty values. type LoggableHTTPHeader struct { http.Header ShouldLogCredentials bool } // MarshalLogObject satisfies the zapcore.ObjectMarshaler interface. func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error { if h.Header == nil { return nil } for key, val := range h.Header { if !h.ShouldLogCredentials { switch strings.ToLower(key) { case "cookie", "set-cookie", "authorization", "proxy-authorization": val = []string{} } } enc.AddArray(key, LoggableStringArray(val)) } return nil } // LoggableStringArray makes a slice of strings marshalable for logging. type LoggableStringArray []string // MarshalLogArray satisfies the zapcore.ArrayMarshaler interface. func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error { if sa == nil { return nil } for _, s := range sa { enc.AppendString(s) } return nil } // LoggableTLSConnState makes a TLS connection state loggable with zap.Object(). type LoggableTLSConnState tls.ConnectionState // MarshalLogObject satisfies the zapcore.ObjectMarshaler interface. func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error { enc.AddBool("resumed", t.DidResume) enc.AddUint16("version", t.Version) enc.AddUint16("cipher_suite", t.CipherSuite) enc.AddString("proto", t.NegotiatedProtocol) enc.AddString("server_name", t.ServerName) if len(t.PeerCertificates) > 0 { enc.AddString("client_common_name", t.PeerCertificates[0].Subject.CommonName) enc.AddString("client_serial", t.PeerCertificates[0].SerialNumber.String()) } return nil } // Interface guards var ( _ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil) _ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil) _ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil) _ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil) ) caddy-2.6.2/modules/caddyhttp/matchers.go000066400000000000000000001435511435007237400204140ustar00rootroot00000000000000// 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 caddyhttp import ( "encoding/json" "errors" "fmt" "net" "net/http" "net/netip" "net/textproto" "net/url" "path" "reflect" "regexp" "sort" "strconv" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "go.uber.org/zap" ) type ( // MatchHost matches requests by the Host value (case-insensitive). // // When used in a top-level HTTP route, // [qualifying domain names](/docs/automatic-https#hostname-requirements) // may trigger [automatic HTTPS](/docs/automatic-https), which automatically // provisions and renews certificates for you. Before doing this, you // should ensure that DNS records for these domains are properly configured, // especially A/AAAA pointed at your server. // // Automatic HTTPS can be // [customized or disabled](/docs/modules/http#servers/automatic_https). // // Wildcards (`*`) may be used to represent exactly one label of the // hostname, in accordance with RFC 1034 (because host matchers are also // used for automatic HTTPS which influences TLS certificates). Thus, // a host of `*` matches hosts like `localhost` or `internal` but not // `example.com`. To catch all hosts, omit the host matcher entirely. // // The wildcard can be useful for matching all subdomains, for example: // `*.example.com` matches `foo.example.com` but not `foo.bar.example.com`. // // Duplicate entries will return an error. MatchHost []string // MatchPath case-insensitively matches requests by the URI's path. Path // matching is exact, not prefix-based, giving you more control and clarity // over matching. Wildcards (`*`) may be used: // // - At the end only, for a prefix match (`/prefix/*`) // - At the beginning only, for a suffix match (`*.suffix`) // - On both sides only, for a substring match (`*/contains/*`) // - In the middle, for a globular match (`/accounts/*/info`) // // Slashes are significant; i.e. `/foo*` matches `/foo`, `/foo/`, `/foo/bar`, // and `/foobar`; but `/foo/*` does not match `/foo` or `/foobar`. Valid // paths start with a slash `/`. // // Because there are, in general, multiple possible escaped forms of any // path, path matchers operate in unescaped space; that is, path matchers // should be written in their unescaped form to prevent ambiguities and // possible security issues, as all request paths will be normalized to // their unescaped forms before matcher evaluation. // // However, escape sequences in a match pattern are supported; they are // compared with the request's raw/escaped path for those bytes only. // In other words, a matcher of `/foo%2Fbar` will match a request path // of precisely `/foo%2Fbar`, but not `/foo/bar`. It follows that matching // the literal percent sign (%) in normalized space can be done using the // escaped form, `%25`. // // Even though wildcards (`*`) operate in the normalized space, the special // escaped wildcard (`%*`), which is not a valid escape sequence, may be // used in place of a span that should NOT be decoded; that is, `/bands/%*` // will match `/bands/AC%2fDC` whereas `/bands/*` will not. // // Even though path matching is done in normalized space, the special // wildcard `%*` may be used in place of a span that should NOT be decoded; // that is, `/bands/%*/` will match `/bands/AC%2fDC/` whereas `/bands/*/` // will not. // // This matcher is fast, so it does not support regular expressions or // capture groups. For slower but more powerful matching, use the // path_regexp matcher. (Note that due to the special treatment of // escape sequences in matcher patterns, they may perform slightly slower // in high-traffic environments.) MatchPath []string // MatchPathRE matches requests by a regular expression on the URI's path. // Path matching is performed in the unescaped (decoded) form of the path. // // Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}` // where `name` is the regular expression's name, and `capture_group` is either // the named or positional capture group from the expression itself. If no name // is given, then the placeholder omits the name: `{http.regexp.capture_group}` // (potentially leading to collisions). MatchPathRE struct{ MatchRegexp } // MatchMethod matches requests by the method. MatchMethod []string // MatchQuery matches requests by the URI's query string. It takes a JSON object // keyed by the query keys, with an array of string values to match for that key. // Query key matches are exact, but wildcards may be used for value matches. Both // keys and values may be placeholders. // An example of the structure to match `?key=value&topic=api&query=something` is: // // ```json // { // "key": ["value"], // "topic": ["api"], // "query": ["*"] // } // ``` // // Invalid query strings, including those with bad escapings or illegal characters // like semicolons, will fail to parse and thus fail to match. MatchQuery url.Values // MatchHeader matches requests by header fields. The key is the field // name and the array is the list of field values. It performs fast, // exact string comparisons of the field values. Fast prefix, suffix, // and substring matches can also be done by suffixing, prefixing, or // surrounding the value with the wildcard `*` character, respectively. // If a list is null, the header must not exist. If the list is empty, // the field must simply exist, regardless of its value. MatchHeader http.Header // MatchHeaderRE matches requests by a regular expression on header fields. // // Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}` // where `name` is the regular expression's name, and `capture_group` is either // the named or positional capture group from the expression itself. If no name // is given, then the placeholder omits the name: `{http.regexp.capture_group}` // (potentially leading to collisions). MatchHeaderRE map[string]*MatchRegexp // MatchProtocol matches requests by protocol. Recognized values are // "http", "https", and "grpc" for broad protocol matches, or specific // HTTP versions can be specified like so: "http/1", "http/1.1", // "http/2", "http/3", or minimum versions: "http/2+", etc. MatchProtocol string // MatchRemoteIP matches requests by client IP (or CIDR range). MatchRemoteIP struct { // The IPs or CIDR ranges to match. Ranges []string `json:"ranges,omitempty"` // If true, prefer the first IP in the request's X-Forwarded-For // header, if present, rather than the immediate peer's IP, as // the reference IP against which to match. Note that it is easy // to spoof request headers. Default: false Forwarded bool `json:"forwarded,omitempty"` // cidrs and zones vars should aligned always in the same // length and indexes for matching later cidrs []*netip.Prefix zones []string logger *zap.Logger } // MatchNot matches requests by negating the results of its matcher // sets. A single "not" matcher takes one or more matcher sets. Each // matcher set is OR'ed; in other words, if any matcher set returns // true, the final result of the "not" matcher is false. Individual // matchers within a set work the same (i.e. different matchers in // the same set are AND'ed). // // NOTE: The generated docs which describe the structure of this // module are wrong because of how this type unmarshals JSON in a // custom way. The correct structure is: // // ```json // [ // {}, // {} // ] // ``` // // where each of the array elements is a matcher set, i.e. an // object keyed by matcher name. MatchNot struct { MatcherSetsRaw []caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"` MatcherSets []MatcherSet `json:"-"` } ) func init() { caddy.RegisterModule(MatchHost{}) caddy.RegisterModule(MatchPath{}) caddy.RegisterModule(MatchPathRE{}) caddy.RegisterModule(MatchMethod{}) caddy.RegisterModule(MatchQuery{}) caddy.RegisterModule(MatchHeader{}) caddy.RegisterModule(MatchHeaderRE{}) caddy.RegisterModule(new(MatchProtocol)) caddy.RegisterModule(MatchRemoteIP{}) caddy.RegisterModule(MatchNot{}) } // CaddyModule returns the Caddy module information. func (MatchHost) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.host", New: func() caddy.Module { return new(MatchHost) }, } } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { *m = append(*m, d.RemainingArgs()...) if d.NextBlock(0) { return d.Err("malformed host matcher: blocks are not supported") } } return nil } // Provision sets up and validates m, including making it more efficient for large lists. func (m MatchHost) Provision(_ caddy.Context) error { // check for duplicates; they are nonsensical and reduce efficiency // (we could just remove them, but the user should know their config is erroneous) seen := make(map[string]int) for i, h := range m { h = strings.ToLower(h) if firstI, ok := seen[h]; ok { return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, h) } seen[h] = i } if m.large() { // sort the slice lexicographically, grouping "fuzzy" entries (wildcards and placeholders) // at the front of the list; this allows us to use binary search for exact matches, which // we have seen from experience is the most common kind of value in large lists; and any // other kinds of values (wildcards and placeholders) are grouped in front so the linear // search should find a match fairly quickly sort.Slice(m, func(i, j int) bool { iInexact, jInexact := m.fuzzy(m[i]), m.fuzzy(m[j]) if iInexact && !jInexact { return true } if !iInexact && jInexact { return false } return m[i] < m[j] }) } return nil } // Match returns true if r matches m. func (m MatchHost) Match(r *http.Request) bool { reqHost, _, err := net.SplitHostPort(r.Host) if err != nil { // OK; probably didn't have a port reqHost = r.Host // make sure we strip the brackets from IPv6 addresses reqHost = strings.TrimPrefix(reqHost, "[") reqHost = strings.TrimSuffix(reqHost, "]") } if m.large() { // fast path: locate exact match using binary search (about 100-1000x faster for large lists) pos := sort.Search(len(m), func(i int) bool { if m.fuzzy(m[i]) { return false } return m[i] >= reqHost }) if pos < len(m) && m[pos] == reqHost { return true } } repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) outer: for _, host := range m { // fast path: if matcher is large, we already know we don't have an exact // match, so we're only looking for fuzzy match now, which should be at the // front of the list; if we have reached a value that is not fuzzy, there // will be no match and we can short-circuit for efficiency if m.large() && !m.fuzzy(host) { break } host = repl.ReplaceAll(host, "") if strings.Contains(host, "*") { patternParts := strings.Split(host, ".") incomingParts := strings.Split(reqHost, ".") if len(patternParts) != len(incomingParts) { continue } for i := range patternParts { if patternParts[i] == "*" { continue } if !strings.EqualFold(patternParts[i], incomingParts[i]) { continue outer } } return true } else if strings.EqualFold(reqHost, host) { return true } } return false } // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // // Example: // // expression host('localhost') func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) { return CELMatcherImpl( "host", "host_match_request_list", []*cel.Type{cel.ListType(cel.StringType)}, func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err } matcher := MatchHost(strList.([]string)) err = matcher.Provision(ctx) return matcher, err }, ) } // fuzzy returns true if the given hostname h is not a specific // hostname, e.g. has placeholders or wildcards. func (MatchHost) fuzzy(h string) bool { return strings.ContainsAny(h, "{*") } // large returns true if m is considered to be large. Optimizing // the matcher for smaller lists has diminishing returns. // See related benchmark function in test file to conduct experiments. func (m MatchHost) large() bool { return len(m) > 100 } // CaddyModule returns the Caddy module information. func (MatchPath) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.path", New: func() caddy.Module { return new(MatchPath) }, } } // Provision lower-cases the paths in m to ensure case-insensitive matching. func (m MatchPath) Provision(_ caddy.Context) error { for i := range m { if m[i] == "*" && i > 0 { // will always match, so just put it first m[0] = m[i] break } m[i] = strings.ToLower(m[i]) } return nil } // Match returns true if r matches m. func (m MatchPath) Match(r *http.Request) bool { // Even though RFC 9110 says that path matching is case-sensitive // (https://www.rfc-editor.org/rfc/rfc9110.html#section-4.2.3), // we do case-insensitive matching to mitigate security issues // related to differences between operating systems, applications, // etc; if case-sensitive matching is needed, the regex matcher // can be used instead. reqPath := strings.ToLower(r.URL.Path) // See #2917; Windows ignores trailing dots and spaces // when accessing files (sigh), potentially causing a // security risk (cry) if PHP files end up being served // as static files, exposing the source code, instead of // being matched by *.php to be treated as PHP scripts. reqPath = strings.TrimRight(reqPath, ". ") repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) for _, matchPattern := range m { matchPattern = repl.ReplaceAll(matchPattern, "") // special case: whole path is wildcard; this is unnecessary // as it matches all requests, which is the same as no matcher if matchPattern == "*" { return true } // Clean the path, merge doubled slashes, etc. // This ensures maliciously crafted requests can't bypass // the path matcher. See #4407. Good security posture // requires that we should do all we can to reduce any // funny-looking paths into "normalized" forms such that // weird variants can't sneak by. // // How we clean the path depends on the kind of pattern: // we either merge slashes or we don't. If the pattern // has double slashes, we preserve them in the path. // // TODO: Despite the fact that the *vast* majority of path // matchers have only 1 pattern, a possible optimization is // to remember the cleaned form of the path for future // iterations; it's just that the way we clean depends on // the kind of pattern. mergeSlashes := !strings.Contains(matchPattern, "//") // if '%' appears in the match pattern, we interpret that to mean // the intent is to compare that part of the path in raw/escaped // space; i.e. "%40"=="%40", not "@", and "%2F"=="%2F", not "/" if strings.Contains(matchPattern, "%") { reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes) if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) { return true } // doing prefix/suffix/substring matches doesn't make sense continue } reqPathForPattern := CleanPath(reqPath, mergeSlashes) // for substring, prefix, and suffix matching, only perform those // special, fast matches if they are the only wildcards in the pattern; // otherwise we assume a globular match if any * appears in the middle // special case: first and last characters are wildcard, // treat it as a fast substring match if strings.Count(matchPattern, "*") == 2 && strings.HasPrefix(matchPattern, "*") && strings.HasSuffix(matchPattern, "*") && strings.Count(matchPattern, "*") == 2 { if strings.Contains(reqPathForPattern, matchPattern[1:len(matchPattern)-1]) { return true } continue } // only perform prefix/suffix match if it is the only wildcard... // I think that is more correct most of the time if strings.Count(matchPattern, "*") == 1 { // special case: first character is a wildcard, // treat it as a fast suffix match if strings.HasPrefix(matchPattern, "*") { if strings.HasSuffix(reqPathForPattern, matchPattern[1:]) { return true } continue } // special case: last character is a wildcard, // treat it as a fast prefix match if strings.HasSuffix(matchPattern, "*") { if strings.HasPrefix(reqPathForPattern, matchPattern[:len(matchPattern)-1]) { return true } continue } } // at last, use globular matching, which also is exact matching // if there are no glob/wildcard chars; we ignore the error here // because we can't handle it anyway matches, _ := path.Match(matchPattern, reqPathForPattern) if matches { return true } } return false } func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool { // We would just compare the pattern against r.URL.Path, // but the pattern contains %, indicating that we should // compare at least some part of the path in raw/escaped // space, not normalized space; so we build the string we // will compare against by adding the normalized parts // of the path, then switching to the escaped parts where // the pattern hints to us wherever % is present. var sb strings.Builder // iterate the pattern and escaped path in lock-step; // increment iPattern every time we consume a char from the pattern, // increment iPath every time we consume a char from the path; // iPattern and iPath are our cursors/iterator positions for each string var iPattern, iPath int for { if iPattern >= len(matchPath) || iPath >= len(escapedPath) { break } // get the next character from the request path pathCh := string(escapedPath[iPath]) var escapedPathCh string // normalize (decode) escape sequences if pathCh == "%" && len(escapedPath) >= iPath+3 { // hold onto this in case we find out the intent is to match in escaped space here; // we lowercase it even though technically the spec says: "For consistency, URI // producers and normalizers should use uppercase hexadecimal digits for all percent- // encodings" (RFC 3986 section 2.1) - we lowercased the matcher pattern earlier in // provisioning so we do the same here to gain case-insensitivity in equivalence; // besides, this string is never shown visibly escapedPathCh = strings.ToLower(escapedPath[iPath : iPath+3]) var err error pathCh, err = url.PathUnescape(escapedPathCh) if err != nil { // should be impossible unless EscapedPath() is giving us an invalid sequence! return false } iPath += 2 // escape sequence is 2 bytes longer than normal char } // now get the next character from the pattern normalize := true switch matchPath[iPattern] { case '%': // escape sequence // if not a wildcard ("%*"), compare literally; consume next two bytes of pattern if len(matchPath) >= iPattern+3 && matchPath[iPattern+1] != '*' { sb.WriteString(escapedPathCh) iPath++ iPattern += 2 break } // escaped wildcard sequence; consume next byte only ('*') iPattern++ normalize = false fallthrough case '*': // wildcard, so consume until next matching character remaining := escapedPath[iPath:] until := len(escapedPath) - iPath // go until end of string... if iPattern < len(matchPath)-1 { // ...unless the * is not at the end nextCh := matchPath[iPattern+1] until = strings.IndexByte(remaining, nextCh) if until == -1 { // terminating char of wildcard span not found, so definitely no match return false } } if until == 0 { // empty span; nothing to add on this iteration break } next := remaining[:until] if normalize { var err error next, err = url.PathUnescape(next) if err != nil { return false // should be impossible anyway } } sb.WriteString(next) iPath += until default: sb.WriteString(pathCh) iPath++ } iPattern++ } // we can now treat rawpath globs (%*) as regular globs (*) matchPath = strings.ReplaceAll(matchPath, "%*", "*") // ignore error here because we can't handle it anyway= matches, _ := path.Match(matchPath, sb.String()) return matches } // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // // Example: // // expression path('*substring*', '*suffix') func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) { return CELMatcherImpl( // name of the macro, this is the function name that users see when writing expressions. "path", // name of the function that the macro will be rewritten to call. "path_match_request_list", // internal data type of the MatchPath value. []*cel.Type{cel.ListType(cel.StringType)}, // function to convert a constant list of strings to a MatchPath instance. func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err } matcher := MatchPath(strList.([]string)) err = matcher.Provision(ctx) return matcher, err }, ) } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { *m = append(*m, d.RemainingArgs()...) if d.NextBlock(0) { return d.Err("malformed path matcher: blocks are not supported") } } return nil } // CaddyModule returns the Caddy module information. func (MatchPathRE) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.path_regexp", New: func() caddy.Module { return new(MatchPathRE) }, } } // Match returns true if r matches m. func (m MatchPathRE) Match(r *http.Request) bool { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) // Clean the path, merges doubled slashes, etc. // This ensures maliciously crafted requests can't bypass // the path matcher. See #4407 cleanedPath := cleanPath(r.URL.Path) return m.MatchRegexp.Match(cleanedPath, repl) } // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // // Example: // // expression path_regexp('^/bar') func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { unnamedPattern, err := CELMatcherImpl( "path_regexp", "path_regexp_request_string", []*cel.Type{cel.StringType}, func(data ref.Val) (RequestMatcher, error) { pattern := data.(types.String) matcher := MatchPathRE{MatchRegexp{Pattern: string(pattern)}} err := matcher.Provision(ctx) return matcher, err }, ) if err != nil { return nil, err } namedPattern, err := CELMatcherImpl( "path_regexp", "path_regexp_request_string_string", []*cel.Type{cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err } strParams := params.([]string) matcher := MatchPathRE{MatchRegexp{Name: strParams[0], Pattern: strParams[1]}} err = matcher.Provision(ctx) return matcher, err }, ) if err != nil { return nil, err } envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...) prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...) return NewMatcherCELLibrary(envOpts, prgOpts), nil } // CaddyModule returns the Caddy module information. func (MatchMethod) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.method", New: func() caddy.Module { return new(MatchMethod) }, } } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { *m = append(*m, d.RemainingArgs()...) if d.NextBlock(0) { return d.Err("malformed method matcher: blocks are not supported") } } return nil } // Match returns true if r matches m. func (m MatchMethod) Match(r *http.Request) bool { for _, method := range m { if r.Method == method { return true } } return false } // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // // Example: // // expression method('PUT', 'POST') func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) { return CELMatcherImpl( "method", "method_request_list", []*cel.Type{cel.ListType(cel.StringType)}, func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err } return MatchMethod(strList.([]string)), nil }, ) } // CaddyModule returns the Caddy module information. func (MatchQuery) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.query", New: func() caddy.Module { return new(MatchQuery) }, } } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(map[string][]string) } for d.Next() { for _, query := range d.RemainingArgs() { if query == "" { continue } before, after, found := strings.Cut(query, "=") if !found { return d.Errf("malformed query matcher token: %s; must be in param=val format", d.Val()) } url.Values(*m).Add(before, after) } if d.NextBlock(0) { return d.Err("malformed query matcher: blocks are not supported") } } return nil } // Match returns true if r matches m. An empty m matches an empty query string. func (m MatchQuery) Match(r *http.Request) bool { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) // parse query string just once, for efficiency parsed, err := url.ParseQuery(r.URL.RawQuery) if err != nil { // Illegal query string. Likely bad escape sequence or unescaped literals. // Note that semicolons in query string have a controversial history. Summaries: // - https://github.com/golang/go/issues/50034 // - https://github.com/golang/go/issues/25192 // Despite the URL WHATWG spec mandating the use of & separators for query strings, // every URL parser implementation is different, and Filippo Valsorda rightly wrote: // "Relying on parser alignment for security is doomed." Overall conclusion is that // splitting on & and rejecting ; in key=value pairs is safer than accepting raw ;. // We regard the Go team's decision as sound and thus reject malformed query strings. return false } for param, vals := range m { param = repl.ReplaceAll(param, "") paramVal, found := parsed[param] if found { for _, v := range vals { v = repl.ReplaceAll(v, "") if paramVal[0] == v || v == "*" { return true } } } } return len(m) == 0 && len(r.URL.Query()) == 0 } // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // // Example: // // expression query({'sort': 'asc'}) || query({'foo': ['*bar*', 'baz']}) func (MatchQuery) CELLibrary(_ caddy.Context) (cel.Library, error) { return CELMatcherImpl( "query", "query_matcher_request_map", []*cel.Type{CELTypeJSON}, func(data ref.Val) (RequestMatcher, error) { mapStrListStr, err := CELValueToMapStrList(data) if err != nil { return nil, err } return MatchQuery(url.Values(mapStrListStr)), nil }, ) } // CaddyModule returns the Caddy module information. func (MatchHeader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.header", New: func() caddy.Module { return new(MatchHeader) }, } } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(map[string][]string) } for d.Next() { var field, val string if !d.Args(&field) { return d.Errf("malformed header matcher: expected field") } if strings.HasPrefix(field, "!") { if len(field) == 1 { return d.Errf("malformed header matcher: must have field name following ! character") } field = field[1:] headers := *m headers[field] = nil m = &headers if d.NextArg() { return d.Errf("malformed header matcher: null matching headers cannot have a field value") } } else { if !d.NextArg() { return d.Errf("malformed header matcher: expected both field and value") } // If multiple header matchers with the same header field are defined, // we want to add the existing to the list of headers (will be OR'ed) val = d.Val() http.Header(*m).Add(field, val) } if d.NextBlock(0) { return d.Err("malformed header matcher: blocks are not supported") } } return nil } // Match returns true if r matches m. func (m MatchHeader) Match(r *http.Request) bool { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) return matchHeaders(r.Header, http.Header(m), r.Host, repl) } // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // // Example: // // expression header({'content-type': 'image/png'}) // expression header({'foo': ['bar', 'baz']}) // match bar or baz func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) { return CELMatcherImpl( "header", "header_matcher_request_map", []*cel.Type{CELTypeJSON}, func(data ref.Val) (RequestMatcher, error) { mapStrListStr, err := CELValueToMapStrList(data) if err != nil { return nil, err } return MatchHeader(http.Header(mapStrListStr)), nil }, ) } // getHeaderFieldVals returns the field values for the given fieldName from input. // The host parameter should be obtained from the http.Request.Host field since // net/http removes it from the header map. func getHeaderFieldVals(input http.Header, fieldName, host string) []string { fieldName = textproto.CanonicalMIMEHeaderKey(fieldName) if fieldName == "Host" && host != "" { return []string{host} } return input[fieldName] } // matchHeaders returns true if input matches the criteria in against without regex. // The host parameter should be obtained from the http.Request.Host field since // net/http removes it from the header map. func matchHeaders(input, against http.Header, host string, repl *caddy.Replacer) bool { for field, allowedFieldVals := range against { actualFieldVals := getHeaderFieldVals(input, field, host) if allowedFieldVals != nil && len(allowedFieldVals) == 0 && actualFieldVals != nil { // a non-nil but empty list of allowed values means // match if the header field exists at all continue } if allowedFieldVals == nil && actualFieldVals == nil { // a nil list means match if the header does not exist at all continue } var match bool fieldVals: for _, actualFieldVal := range actualFieldVals { for _, allowedFieldVal := range allowedFieldVals { if repl != nil { allowedFieldVal = repl.ReplaceAll(allowedFieldVal, "") } switch { case allowedFieldVal == "*": match = true case strings.HasPrefix(allowedFieldVal, "*") && strings.HasSuffix(allowedFieldVal, "*"): match = strings.Contains(actualFieldVal, allowedFieldVal[1:len(allowedFieldVal)-1]) case strings.HasPrefix(allowedFieldVal, "*"): match = strings.HasSuffix(actualFieldVal, allowedFieldVal[1:]) case strings.HasSuffix(allowedFieldVal, "*"): match = strings.HasPrefix(actualFieldVal, allowedFieldVal[:len(allowedFieldVal)-1]) default: match = actualFieldVal == allowedFieldVal } if match { break fieldVals } } } if !match { return false } } return true } // CaddyModule returns the Caddy module information. func (MatchHeaderRE) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.header_regexp", New: func() caddy.Module { return new(MatchHeaderRE) }, } } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(map[string]*MatchRegexp) } for d.Next() { var first, second, third string if !d.Args(&first, &second) { return d.ArgErr() } var name, field, val string if d.Args(&third) { name = first field = second val = third } else { field = first val = second } // If there's already a pattern for this field // then we would end up overwriting the old one if (*m)[field] != nil { return d.Errf("header_regexp matcher can only be used once per named matcher, per header field: %s", field) } (*m)[field] = &MatchRegexp{Pattern: val, Name: name} if d.NextBlock(0) { return d.Err("malformed header_regexp matcher: blocks are not supported") } } return nil } // Match returns true if r matches m. func (m MatchHeaderRE) Match(r *http.Request) bool { for field, rm := range m { actualFieldVals := getHeaderFieldVals(r.Header, field, r.Host) match := false fieldVal: for _, actualFieldVal := range actualFieldVals { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) if rm.Match(actualFieldVal, repl) { match = true break fieldVal } } if !match { return false } } return true } // Provision compiles m's regular expressions. func (m MatchHeaderRE) Provision(ctx caddy.Context) error { for _, rm := range m { err := rm.Provision(ctx) if err != nil { return err } } return nil } // Validate validates m's regular expressions. func (m MatchHeaderRE) Validate() error { for _, rm := range m { err := rm.Validate() if err != nil { return err } } return nil } // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // // Example: // // expression header_regexp('foo', 'Field', 'fo+') func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { unnamedPattern, err := CELMatcherImpl( "header_regexp", "header_regexp_request_string_string", []*cel.Type{cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err } strParams := params.([]string) matcher := MatchHeaderRE{} matcher[strParams[0]] = &MatchRegexp{Pattern: strParams[1], Name: ""} err = matcher.Provision(ctx) return matcher, err }, ) if err != nil { return nil, err } namedPattern, err := CELMatcherImpl( "header_regexp", "header_regexp_request_string_string_string", []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err } strParams := params.([]string) matcher := MatchHeaderRE{} matcher[strParams[1]] = &MatchRegexp{Pattern: strParams[2], Name: strParams[0]} err = matcher.Provision(ctx) return matcher, err }, ) if err != nil { return nil, err } envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...) prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...) return NewMatcherCELLibrary(envOpts, prgOpts), nil } // CaddyModule returns the Caddy module information. func (MatchProtocol) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.protocol", New: func() caddy.Module { return new(MatchProtocol) }, } } // Match returns true if r matches m. func (m MatchProtocol) Match(r *http.Request) bool { switch string(m) { case "grpc": return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc") case "https": return r.TLS != nil case "http": return r.TLS == nil case "http/1.0": return r.ProtoMajor == 1 && r.ProtoMinor == 0 case "http/1.0+": return r.ProtoAtLeast(1, 0) case "http/1.1": return r.ProtoMajor == 1 && r.ProtoMinor == 1 case "http/1.1+": return r.ProtoAtLeast(1, 1) case "http/2": return r.ProtoMajor == 2 case "http/2+": return r.ProtoAtLeast(2, 0) case "http/3": return r.ProtoMajor == 3 case "http/3+": return r.ProtoAtLeast(3, 0) } return false } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { var proto string if !d.Args(&proto) { return d.Err("expected exactly one protocol") } *m = MatchProtocol(proto) } return nil } // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // // Example: // // expression protocol('https') func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) { return CELMatcherImpl( "protocol", "protocol_request_string", []*cel.Type{cel.StringType}, func(data ref.Val) (RequestMatcher, error) { protocolStr, ok := data.(types.String) if !ok { return nil, errors.New("protocol argument was not a string") } return MatchProtocol(strings.ToLower(string(protocolStr))), nil }, ) } // CaddyModule returns the Caddy module information. func (MatchNot) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.not", New: func() caddy.Module { return new(MatchNot) }, } } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchNot) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { matcherSet, err := ParseCaddyfileNestedMatcherSet(d) if err != nil { return err } m.MatcherSetsRaw = append(m.MatcherSetsRaw, matcherSet) } return nil } // UnmarshalJSON satisfies json.Unmarshaler. It puts the JSON // bytes directly into m's MatcherSetsRaw field. func (m *MatchNot) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &m.MatcherSetsRaw) } // MarshalJSON satisfies json.Marshaler by marshaling // m's raw matcher sets. func (m MatchNot) MarshalJSON() ([]byte, error) { return json.Marshal(m.MatcherSetsRaw) } // Provision loads the matcher modules to be negated. func (m *MatchNot) Provision(ctx caddy.Context) error { matcherSets, err := ctx.LoadModule(m, "MatcherSetsRaw") if err != nil { return fmt.Errorf("loading matcher sets: %v", err) } for _, modMap := range matcherSets.([]map[string]any) { var ms MatcherSet for _, modIface := range modMap { ms = append(ms, modIface.(RequestMatcher)) } m.MatcherSets = append(m.MatcherSets, ms) } return nil } // Match returns true if r matches m. Since this matcher negates // the embedded matchers, false is returned if any of its matcher // sets return true. func (m MatchNot) Match(r *http.Request) bool { for _, ms := range m.MatcherSets { if ms.Match(r) { return false } } return true } // CaddyModule returns the Caddy module information. func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.remote_ip", New: func() caddy.Module { return new(MatchRemoteIP) }, } } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { for d.NextArg() { if d.Val() == "forwarded" { if len(m.Ranges) > 0 { return d.Err("if used, 'forwarded' must be first argument") } m.Forwarded = true continue } if d.Val() == "private_ranges" { m.Ranges = append(m.Ranges, []string{ "192.168.0.0/16", "172.16.0.0/12", "10.0.0.0/8", "127.0.0.1/8", "fd00::/8", "::1", }...) continue } m.Ranges = append(m.Ranges, d.Val()) } if d.NextBlock(0) { return d.Err("malformed remote_ip matcher: blocks are not supported") } } return nil } // CELLibrary produces options that expose this matcher for use in CEL // expression matchers. // // Example: // // expression remote_ip('forwarded', '192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8') func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) { return CELMatcherImpl( // name of the macro, this is the function name that users see when writing expressions. "remote_ip", // name of the function that the macro will be rewritten to call. "remote_ip_match_request_list", // internal data type of the MatchPath value. []*cel.Type{cel.ListType(cel.StringType)}, // function to convert a constant list of strings to a MatchPath instance. func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err } m := MatchRemoteIP{} for _, input := range strList.([]string) { if input == "forwarded" { if len(m.Ranges) > 0 { return nil, errors.New("if used, 'forwarded' must be first argument") } m.Forwarded = true continue } m.Ranges = append(m.Ranges, input) } err = m.Provision(ctx) return m, err }, ) } // Provision parses m's IP ranges, either from IP or CIDR expressions. func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { m.logger = ctx.Logger() for _, str := range m.Ranges { // Exclude the zone_id from the IP if strings.Contains(str, "%") { split := strings.Split(str, "%") str = split[0] // write zone identifiers in m.zones for matching later m.zones = append(m.zones, split[1]) } else { m.zones = append(m.zones, "") } if strings.Contains(str, "/") { ipNet, err := netip.ParsePrefix(str) if err != nil { return fmt.Errorf("parsing CIDR expression '%s': %v", str, err) } m.cidrs = append(m.cidrs, &ipNet) } else { ipAddr, err := netip.ParseAddr(str) if err != nil { return fmt.Errorf("invalid IP address: '%s': %v", str, err) } ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen()) m.cidrs = append(m.cidrs, &ipNew) } } return nil } func (m MatchRemoteIP) getClientIP(r *http.Request) (netip.Addr, string, error) { remote := r.RemoteAddr zoneID := "" if m.Forwarded { if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" { remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0]) } } ipStr, _, err := net.SplitHostPort(remote) if err != nil { ipStr = remote // OK; probably didn't have a port } // Some IPv6-Adresses can contain zone identifiers at the end, // which are separated with "%" if strings.Contains(ipStr, "%") { split := strings.Split(ipStr, "%") ipStr = split[0] zoneID = split[1] } ipAddr, err := netip.ParseAddr(ipStr) if err != nil { return netip.IPv4Unspecified(), "", err } return ipAddr, zoneID, nil } // Match returns true if r matches m. func (m MatchRemoteIP) Match(r *http.Request) bool { clientIP, zoneID, err := m.getClientIP(r) if err != nil { m.logger.Error("getting client IP", zap.Error(err)) return false } zoneFilter := true for i, ipRange := range m.cidrs { if ipRange.Contains(clientIP) { // Check if there are zone filters assigned and if they match. if m.zones[i] == "" || zoneID == m.zones[i] { return true } zoneFilter = false } } if !zoneFilter { m.logger.Debug("zone ID from remote did not match", zap.String("zone", zoneID)) } return false } // MatchRegexp is an embedable type for matching // using regular expressions. It adds placeholders // to the request's replacer. type MatchRegexp struct { // A unique name for this regular expression. Optional, // but useful to prevent overwriting captures from other // regexp matchers. Name string `json:"name,omitempty"` // The regular expression to evaluate, in RE2 syntax, // which is the same general syntax used by Go, Perl, // and Python. For details, see // [Go's regexp package](https://golang.org/pkg/regexp/). // Captures are accessible via placeholders. Unnamed // capture groups are exposed as their numeric, 1-based // index, while named capture groups are available by // the capture group name. Pattern string `json:"pattern"` compiled *regexp.Regexp phPrefix string } // Provision compiles the regular expression. func (mre *MatchRegexp) Provision(caddy.Context) error { re, err := regexp.Compile(mre.Pattern) if err != nil { return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err) } mre.compiled = re mre.phPrefix = regexpPlaceholderPrefix if mre.Name != "" { mre.phPrefix += "." + mre.Name } return nil } // Validate ensures mre is set up correctly. func (mre *MatchRegexp) Validate() error { if mre.Name != "" && !wordRE.MatchString(mre.Name) { return fmt.Errorf("invalid regexp name (must contain only word characters): %s", mre.Name) } return nil } // Match returns true if input matches the compiled regular // expression in mre. It sets values on the replacer repl // associated with capture groups, using the given scope // (namespace). func (mre *MatchRegexp) Match(input string, repl *caddy.Replacer) bool { matches := mre.compiled.FindStringSubmatch(input) if matches == nil { return false } // save all capture groups, first by index for i, match := range matches { key := mre.phPrefix + "." + strconv.Itoa(i) repl.Set(key, match) } // then by name for i, name := range mre.compiled.SubexpNames() { if i != 0 && name != "" { key := mre.phPrefix + "." + name repl.Set(key, matches[i]) } } return true } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { // If this is the second iteration of the loop // then there's more than one path_regexp matcher // and we would end up overwriting the old one if mre.Pattern != "" { return d.Err("regular expression can only be used once per named matcher") } args := d.RemainingArgs() switch len(args) { case 1: mre.Pattern = args[0] case 2: mre.Name = args[0] mre.Pattern = args[1] default: return d.ArgErr() } if d.NextBlock(0) { return d.Err("malformed path_regexp matcher: blocks are not supported") } } return nil } // ParseCaddyfileNestedMatcher parses the Caddyfile tokens for a nested // matcher set, and returns its raw module map value. func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) { matcherMap := make(map[string]RequestMatcher) // 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 { mod, err := caddy.GetModule("http.matchers." + matcherName) if err != nil { return nil, d.Errf("getting matcher module '%s': %v", matcherName, err) } unm, ok := mod.New().(caddyfile.Unmarshaler) if !ok { return nil, d.Errf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) } err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens)) if err != nil { return nil, err } rm, ok := unm.(RequestMatcher) if !ok { return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) } matcherMap[matcherName] = rm } // we should now have a functional matcher, but we also // need to be able to marshal as JSON, otherwise config // adaptation will be missing the matchers! matcherSet := make(caddy.ModuleMap) for name, matcher := range matcherMap { jsonBytes, err := json.Marshal(matcher) if err != nil { return nil, fmt.Errorf("marshaling %T matcher: %v", matcher, err) } matcherSet[name] = jsonBytes } return matcherSet, nil } var ( wordRE = regexp.MustCompile(`\w+`) ) const regexpPlaceholderPrefix = "http.regexp" // MatcherErrorVarKey is the key used for the variable that // holds an optional error emitted from a request matcher, // to short-circuit the handler chain, since matchers cannot // return errors via the RequestMatcher interface. const MatcherErrorVarKey = "matchers.error" // Interface guards var ( _ RequestMatcher = (*MatchHost)(nil) _ caddy.Provisioner = (*MatchHost)(nil) _ RequestMatcher = (*MatchPath)(nil) _ RequestMatcher = (*MatchPathRE)(nil) _ caddy.Provisioner = (*MatchPathRE)(nil) _ RequestMatcher = (*MatchMethod)(nil) _ RequestMatcher = (*MatchQuery)(nil) _ RequestMatcher = (*MatchHeader)(nil) _ RequestMatcher = (*MatchHeaderRE)(nil) _ caddy.Provisioner = (*MatchHeaderRE)(nil) _ RequestMatcher = (*MatchProtocol)(nil) _ RequestMatcher = (*MatchRemoteIP)(nil) _ caddy.Provisioner = (*MatchRemoteIP)(nil) _ RequestMatcher = (*MatchNot)(nil) _ caddy.Provisioner = (*MatchNot)(nil) _ caddy.Provisioner = (*MatchRegexp)(nil) _ caddyfile.Unmarshaler = (*MatchHost)(nil) _ caddyfile.Unmarshaler = (*MatchPath)(nil) _ caddyfile.Unmarshaler = (*MatchPathRE)(nil) _ caddyfile.Unmarshaler = (*MatchMethod)(nil) _ caddyfile.Unmarshaler = (*MatchQuery)(nil) _ caddyfile.Unmarshaler = (*MatchHeader)(nil) _ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil) _ caddyfile.Unmarshaler = (*MatchProtocol)(nil) _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil) _ caddyfile.Unmarshaler = (*VarsMatcher)(nil) _ caddyfile.Unmarshaler = (*MatchVarsRE)(nil) _ CELLibraryProducer = (*MatchHost)(nil) _ CELLibraryProducer = (*MatchPath)(nil) _ CELLibraryProducer = (*MatchPathRE)(nil) _ CELLibraryProducer = (*MatchMethod)(nil) _ CELLibraryProducer = (*MatchQuery)(nil) _ CELLibraryProducer = (*MatchHeader)(nil) _ CELLibraryProducer = (*MatchHeaderRE)(nil) _ CELLibraryProducer = (*MatchProtocol)(nil) _ CELLibraryProducer = (*MatchRemoteIP)(nil) // _ CELLibraryProducer = (*VarsMatcher)(nil) // _ CELLibraryProducer = (*MatchVarsRE)(nil) _ json.Marshaler = (*MatchNot)(nil) _ json.Unmarshaler = (*MatchNot)(nil) ) caddy-2.6.2/modules/caddyhttp/matchers_test.go000066400000000000000000000661421435007237400214530ustar00rootroot00000000000000// 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 caddyhttp import ( "context" "fmt" "net/http" "net/http/httptest" "net/url" "os" "testing" "github.com/caddyserver/caddy/v2" ) func TestHostMatcher(t *testing.T) { err := os.Setenv("GO_BENCHMARK_DOMAIN", "localhost") if err != nil { t.Errorf("error while setting up environment: %v", err) } for i, tc := range []struct { match MatchHost input string expect bool }{ { match: MatchHost{}, input: "example.com", expect: false, }, { match: MatchHost{"example.com"}, input: "example.com", expect: true, }, { match: MatchHost{"EXAMPLE.COM"}, input: "example.com", expect: true, }, { match: MatchHost{"example.com"}, input: "EXAMPLE.COM", expect: true, }, { match: MatchHost{"example.com"}, input: "foo.example.com", expect: false, }, { match: MatchHost{"example.com"}, input: "EXAMPLE.COM", expect: true, }, { match: MatchHost{"foo.example.com"}, input: "foo.example.com", expect: true, }, { match: MatchHost{"foo.example.com"}, input: "bar.example.com", expect: false, }, { match: MatchHost{"*.example.com"}, input: "example.com", expect: false, }, { match: MatchHost{"*.example.com"}, input: "SUB.EXAMPLE.COM", expect: true, }, { match: MatchHost{"*.example.com"}, input: "foo.example.com", expect: true, }, { match: MatchHost{"*.example.com"}, input: "foo.bar.example.com", expect: false, }, { match: MatchHost{"*.example.com", "example.net"}, input: "example.net", expect: true, }, { match: MatchHost{"example.net", "*.example.com"}, input: "foo.example.com", expect: true, }, { match: MatchHost{"*.example.net", "*.*.example.com"}, input: "foo.bar.example.com", expect: true, }, { match: MatchHost{"*.example.net", "sub.*.example.com"}, input: "sub.foo.example.com", expect: true, }, { match: MatchHost{"*.example.net", "sub.*.example.com"}, input: "sub.foo.example.net", expect: false, }, { match: MatchHost{"www.*.*"}, input: "www.example.com", expect: true, }, { match: MatchHost{"example.com"}, input: "example.com:5555", expect: true, }, { match: MatchHost{"{env.GO_BENCHMARK_DOMAIN}"}, input: "localhost", expect: true, }, { match: MatchHost{"{env.GO_NONEXISTENT}"}, input: "localhost", expect: false, }, } { req := &http.Request{Host: tc.input} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) actual := tc.match.Match(req) if actual != tc.expect { t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) continue } } } func TestPathMatcher(t *testing.T) { for i, tc := range []struct { match MatchPath // not URI-encoded because not parsing from a URI input string // should be valid URI encoding (escaped) since it will become part of a request expect bool provisionErr bool }{ { match: MatchPath{}, input: "/", expect: false, }, { match: MatchPath{"/"}, input: "/", expect: true, }, { match: MatchPath{"/foo/bar"}, input: "/", expect: false, }, { match: MatchPath{"/foo/bar"}, input: "/foo/bar", expect: true, }, { match: MatchPath{"/foo/bar/"}, input: "/foo/bar", expect: false, }, { match: MatchPath{"/foo/bar/"}, input: "/foo/bar/", expect: true, }, { match: MatchPath{"/foo/bar/", "/other"}, input: "/other/", expect: false, }, { match: MatchPath{"/foo/bar/", "/other"}, input: "/other", expect: true, }, { match: MatchPath{"*.ext"}, input: "/foo/bar.ext", expect: true, }, { match: MatchPath{"*.php"}, input: "/index.PHP", expect: true, }, { match: MatchPath{"*.ext"}, input: "/foo/bar.ext", expect: true, }, { match: MatchPath{"/foo/*/baz"}, input: "/foo/bar/baz", expect: true, }, { match: MatchPath{"/foo/*/baz/bam"}, input: "/foo/bar/bam", expect: false, }, { match: MatchPath{"*substring*"}, input: "/foo/substring/bar.txt", expect: true, }, { match: MatchPath{"/foo"}, input: "/foo/bar", expect: false, }, { match: MatchPath{"/foo"}, input: "/foo/bar", expect: false, }, { match: MatchPath{"/foo"}, input: "/FOO", expect: true, }, { match: MatchPath{"/foo*"}, input: "/FOOOO", expect: true, }, { match: MatchPath{"*.php"}, input: "/foo/index.php. .", expect: true, }, { match: MatchPath{"/foo/bar.txt"}, input: "/foo/BAR.txt", expect: true, }, { match: MatchPath{"/foo*"}, input: "//foo/bar", expect: true, }, { match: MatchPath{"/foo"}, input: "//foo", expect: true, }, { match: MatchPath{"//foo"}, input: "/foo", expect: false, }, { match: MatchPath{"//foo"}, input: "//foo", expect: true, }, { match: MatchPath{"/foo//*"}, input: "/foo//bar", expect: true, }, { match: MatchPath{"/foo//*"}, input: "/foo/%2Fbar", expect: true, }, { match: MatchPath{"/foo/%2F*"}, input: "/foo/%2Fbar", expect: true, }, { match: MatchPath{"/foo/%2F*"}, input: "/foo//bar", expect: false, }, { match: MatchPath{"/foo//bar"}, input: "/foo//bar", expect: true, }, { match: MatchPath{"/foo/*//bar"}, input: "/foo///bar", expect: true, }, { match: MatchPath{"/foo/%*//bar"}, input: "/foo///bar", expect: true, }, { match: MatchPath{"/foo/%*//bar"}, input: "/foo//%2Fbar", expect: true, }, { match: MatchPath{"/foo*"}, input: "/%2F/foo", expect: true, }, { match: MatchPath{"*"}, input: "/", expect: true, }, { match: MatchPath{"*"}, input: "/foo/bar", expect: true, }, { match: MatchPath{"**"}, input: "/", expect: true, }, { match: MatchPath{"**"}, input: "/foo/bar", expect: true, }, // notice these next three test cases are the same normalized path but are written differently { match: MatchPath{"/%25@.txt"}, input: "/%25@.txt", expect: true, }, { match: MatchPath{"/%25@.txt"}, input: "/%25%40.txt", expect: true, }, { match: MatchPath{"/%25%40.txt"}, input: "/%25%40.txt", expect: true, }, { match: MatchPath{"/bands/*/*"}, input: "/bands/AC%2FDC/T.N.T", expect: false, // because * operates in normalized space }, { match: MatchPath{"/bands/%*/%*"}, input: "/bands/AC%2FDC/T.N.T", expect: true, }, { match: MatchPath{"/bands/%*/%*"}, input: "/bands/AC/DC/T.N.T", expect: false, }, { match: MatchPath{"/bands/%*"}, input: "/bands/AC/DC", expect: false, // not a suffix match }, { match: MatchPath{"/bands/%*"}, input: "/bands/AC%2FDC", expect: true, }, { match: MatchPath{"/foo%2fbar/baz"}, input: "/foo%2Fbar/baz", expect: true, }, { match: MatchPath{"/foo%2fbar/baz"}, input: "/foo/bar/baz", expect: false, }, { match: MatchPath{"/foo/bar/baz"}, input: "/foo%2fbar/baz", expect: true, }, } { err := tc.match.Provision(caddy.Context{}) if err == nil && tc.provisionErr { t.Errorf("Test %d %v: Expected error provisioning, but there was no error", i, tc.match) } if err != nil && !tc.provisionErr { t.Errorf("Test %d %v: Expected no error provisioning, but there was an error: %v", i, tc.match, err) } if tc.provisionErr { continue // if it's not supposed to provision properly, pointless to test it } u, err := url.ParseRequestURI(tc.input) if err != nil { t.Fatalf("Test %d (%v): Invalid request URI (should be rejected by Go's HTTP server): %v", i, tc.input, err) } req := &http.Request{URL: u} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) actual := tc.match.Match(req) if actual != tc.expect { t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) continue } } } func TestPathMatcherWindows(t *testing.T) { // only Windows has this bug where it will ignore // trailing dots and spaces in a filename, but we // test for it on all platforms to be more consistent req := &http.Request{URL: &url.URL{Path: "/index.php . . .."}} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) match := MatchPath{"*.php"} matched := match.Match(req) if !matched { t.Errorf("Expected to match; should ignore trailing dots and spaces") } } func TestPathREMatcher(t *testing.T) { for i, tc := range []struct { match MatchPathRE input string expect bool expectRepl map[string]string }{ { match: MatchPathRE{}, input: "/", expect: true, }, { match: MatchPathRE{MatchRegexp{Pattern: "/"}}, input: "/", expect: true, }, { match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}}, input: "/foo", expect: true, }, { match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}}, input: "/foo/", expect: true, }, { match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}}, input: "//foo", expect: true, }, { match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}}, input: "//foo/", expect: true, }, { match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}}, input: "/%2F/foo/", expect: true, }, { match: MatchPathRE{MatchRegexp{Pattern: "/bar"}}, input: "/foo/", expect: false, }, { match: MatchPathRE{MatchRegexp{Pattern: "^/bar"}}, input: "/foo/bar", expect: false, }, { match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(.*)/baz$", Name: "name"}}, input: "/foo/bar/baz", expect: true, expectRepl: map[string]string{"name.1": "bar"}, }, { match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(?P.*)/baz$", Name: "name"}}, input: "/foo/bar/baz", expect: true, expectRepl: map[string]string{"name.myparam": "bar"}, }, { match: MatchPathRE{MatchRegexp{Pattern: "^/%@.txt"}}, input: "/%25@.txt", expect: true, }, { match: MatchPathRE{MatchRegexp{Pattern: "^/%25@.txt"}}, input: "/%25@.txt", expect: false, }, } { // compile the regexp and validate its name err := tc.match.Provision(caddy.Context{}) if err != nil { t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err) continue } err = tc.match.Validate() if err != nil { t.Errorf("Test %d %v: Validating: %v", i, tc.match, err) continue } // set up the fake request and its Replacer u, err := url.ParseRequestURI(tc.input) if err != nil { t.Fatalf("Test %d: Bad input URI: %v", i, err) } req := &http.Request{URL: u} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) actual := tc.match.Match(req) if actual != tc.expect { t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'", i, tc.match.Pattern, tc.expect, actual, tc.input) continue } for key, expectVal := range tc.expectRepl { placeholder := fmt.Sprintf("{http.regexp.%s}", key) actualVal := repl.ReplaceAll(placeholder, "") if actualVal != expectVal { t.Errorf("Test %d [%v]: Expected placeholder {http.regexp.%s} to be '%s' but got '%s'", i, tc.match.Pattern, key, expectVal, actualVal) continue } } } } func TestHeaderMatcher(t *testing.T) { repl := caddy.NewReplacer() repl.Set("a", "foobar") for i, tc := range []struct { match MatchHeader input http.Header // make sure these are canonical cased (std lib will do that in a real request) host string expect bool }{ { match: MatchHeader{"Field": []string{"foo"}}, input: http.Header{"Field": []string{"foo"}}, expect: true, }, { match: MatchHeader{"Field": []string{"foo", "bar"}}, input: http.Header{"Field": []string{"bar"}}, expect: true, }, { match: MatchHeader{"Field": []string{"foo", "bar"}}, input: http.Header{"Alakazam": []string{"kapow"}}, expect: false, }, { match: MatchHeader{"Field": []string{"foo", "bar"}}, input: http.Header{"Field": []string{"kapow"}}, expect: false, }, { match: MatchHeader{"Field": []string{"foo", "bar"}}, input: http.Header{"Field": []string{"kapow", "foo"}}, expect: true, }, { match: MatchHeader{"Field1": []string{"foo"}, "Field2": []string{"bar"}}, input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"bar"}}, expect: true, }, { match: MatchHeader{"field1": []string{"foo"}, "field2": []string{"bar"}}, input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"bar"}}, expect: true, }, { match: MatchHeader{"field1": []string{"foo"}, "field2": []string{"bar"}}, input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"kapow"}}, expect: false, }, { match: MatchHeader{"field1": []string{"*"}}, input: http.Header{"Field1": []string{"foo"}}, expect: true, }, { match: MatchHeader{"field1": []string{"*"}}, input: http.Header{"Field2": []string{"foo"}}, expect: false, }, { match: MatchHeader{"Field1": []string{"foo*"}}, input: http.Header{"Field1": []string{"foo"}}, expect: true, }, { match: MatchHeader{"Field1": []string{"foo*"}}, input: http.Header{"Field1": []string{"asdf", "foobar"}}, expect: true, }, { match: MatchHeader{"Field1": []string{"*bar"}}, input: http.Header{"Field1": []string{"asdf", "foobar"}}, expect: true, }, { match: MatchHeader{"host": []string{"localhost"}}, input: http.Header{}, host: "localhost", expect: true, }, { match: MatchHeader{"host": []string{"localhost"}}, input: http.Header{}, host: "caddyserver.com", expect: false, }, { match: MatchHeader{"Must-Not-Exist": nil}, input: http.Header{}, expect: true, }, { match: MatchHeader{"Must-Not-Exist": nil}, input: http.Header{"Must-Not-Exist": []string{"do not match"}}, expect: false, }, { match: MatchHeader{"Foo": []string{"{a}"}}, input: http.Header{"Foo": []string{"foobar"}}, expect: true, }, { match: MatchHeader{"Foo": []string{"{a}"}}, input: http.Header{"Foo": []string{"asdf"}}, expect: false, }, { match: MatchHeader{"Foo": []string{"{a}*"}}, input: http.Header{"Foo": []string{"foobar-baz"}}, expect: true, }, } { req := &http.Request{Header: tc.input, Host: tc.host} ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) actual := tc.match.Match(req) if actual != tc.expect { t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) continue } } } func TestQueryMatcher(t *testing.T) { for i, tc := range []struct { scenario string match MatchQuery input string expect bool }{ { scenario: "non match against a specific value", match: MatchQuery{"debug": []string{"1"}}, input: "/", expect: false, }, { scenario: "match against a specific value", match: MatchQuery{"debug": []string{"1"}}, input: "/?debug=1", expect: true, }, { scenario: "match against a wildcard", match: MatchQuery{"debug": []string{"*"}}, input: "/?debug=something", expect: true, }, { scenario: "non match against a wildcarded", match: MatchQuery{"debug": []string{"*"}}, input: "/?other=something", expect: false, }, { scenario: "match against an empty value", match: MatchQuery{"debug": []string{""}}, input: "/?debug", expect: true, }, { scenario: "non match against an empty value", match: MatchQuery{"debug": []string{""}}, input: "/?someparam", expect: false, }, { scenario: "empty matcher value should match empty query", match: MatchQuery{}, input: "/?", expect: true, }, { scenario: "nil matcher value should NOT match a non-empty query", match: MatchQuery{}, input: "/?foo=bar", expect: false, }, { scenario: "non-nil matcher should NOT match an empty query", match: MatchQuery{"": nil}, input: "/?", expect: false, }, { scenario: "match against a placeholder value", match: MatchQuery{"debug": []string{"{http.vars.debug}"}}, input: "/?debug=1", expect: true, }, { scenario: "match against a placeholder key", match: MatchQuery{"{http.vars.key}": []string{"1"}}, input: "/?somekey=1", expect: true, }, } { u, _ := url.Parse(tc.input) req := &http.Request{URL: u} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) repl.Set("http.vars.debug", "1") repl.Set("http.vars.key", "somekey") req = req.WithContext(ctx) actual := tc.match.Match(req) if actual != tc.expect { t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) continue } } } func TestHeaderREMatcher(t *testing.T) { for i, tc := range []struct { match MatchHeaderRE input http.Header // make sure these are canonical cased (std lib will do that in a real request) host string expect bool expectRepl map[string]string }{ { match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "foo"}}, input: http.Header{"Field": []string{"foo"}}, expect: true, }, { match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "$foo^"}}, input: http.Header{"Field": []string{"foobar"}}, expect: false, }, { match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}}, input: http.Header{"Field": []string{"foobar"}}, expect: true, expectRepl: map[string]string{"name.1": "bar"}, }, { match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo.*$", Name: "name"}}, input: http.Header{"Field": []string{"barfoo", "foobar"}}, expect: true, }, { match: MatchHeaderRE{"host": &MatchRegexp{Pattern: "^localhost$", Name: "name"}}, input: http.Header{}, host: "localhost", expect: true, }, { match: MatchHeaderRE{"host": &MatchRegexp{Pattern: "^local$", Name: "name"}}, input: http.Header{}, host: "localhost", expect: false, }, } { // compile the regexp and validate its name err := tc.match.Provision(caddy.Context{}) if err != nil { t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err) continue } err = tc.match.Validate() if err != nil { t.Errorf("Test %d %v: Validating: %v", i, tc.match, err) continue } // set up the fake request and its Replacer req := &http.Request{Header: tc.input, URL: new(url.URL), Host: tc.host} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) actual := tc.match.Match(req) if actual != tc.expect { t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'", i, tc.match, tc.expect, actual, tc.input) continue } for key, expectVal := range tc.expectRepl { placeholder := fmt.Sprintf("{http.regexp.%s}", key) actualVal := repl.ReplaceAll(placeholder, "") if actualVal != expectVal { t.Errorf("Test %d [%v]: Expected placeholder {http.regexp.%s} to be '%s' but got '%s'", i, tc.match, key, expectVal, actualVal) continue } } } } func BenchmarkHeaderREMatcher(b *testing.B) { i := 0 match := MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}} input := http.Header{"Field": []string{"foobar"}} var host string err := match.Provision(caddy.Context{}) if err != nil { b.Errorf("Test %d %v: Provisioning: %v", i, match, err) } err = match.Validate() if err != nil { b.Errorf("Test %d %v: Validating: %v", i, match, err) } // set up the fake request and its Replacer req := &http.Request{Header: input, URL: new(url.URL), Host: host} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) for run := 0; run < b.N; run++ { match.Match(req) } } func TestVarREMatcher(t *testing.T) { for i, tc := range []struct { desc string match MatchVarsRE input VarsMiddleware expect bool expectRepl map[string]string }{ { desc: "match static value within var set by the VarsMiddleware succeeds", match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "foo"}}, input: VarsMiddleware{"Var1": "here is foo val"}, expect: true, }, { desc: "value set by VarsMiddleware not satisfying regexp matcher fails to match", match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "$foo^"}}, input: VarsMiddleware{"Var1": "foobar"}, expect: false, }, { desc: "successfully matched value is captured and its placeholder is added to replacer", match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}}, input: VarsMiddleware{"Var1": "foobar"}, expect: true, expectRepl: map[string]string{"name.1": "bar"}, }, { desc: "matching against a value of standard variables succeeds", match: MatchVarsRE{"{http.request.method}": &MatchRegexp{Pattern: "^G.[tT]$"}}, input: VarsMiddleware{}, expect: true, }, { desc: "matching against value of var set by the VarsMiddleware and referenced by its placeholder succeeds", match: MatchVarsRE{"{http.vars.Var1}": &MatchRegexp{Pattern: "[vV]ar[0-9]"}}, input: VarsMiddleware{"Var1": "var1Value"}, expect: true, }, } { tc := tc // capture range value t.Run(tc.desc, func(t *testing.T) { t.Parallel() // compile the regexp and validate its name err := tc.match.Provision(caddy.Context{}) if err != nil { t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err) return } err = tc.match.Validate() if err != nil { t.Errorf("Test %d %v: Validating: %v", i, tc.match, err) return } // set up the fake request and its Replacer req := &http.Request{URL: new(url.URL), Method: http.MethodGet} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any)) req = req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) tc.input.ServeHTTP(httptest.NewRecorder(), req, emptyHandler) actual := tc.match.Match(req) if actual != tc.expect { t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'", i, tc.match, tc.expect, actual, tc.input) return } for key, expectVal := range tc.expectRepl { placeholder := fmt.Sprintf("{http.regexp.%s}", key) actualVal := repl.ReplaceAll(placeholder, "") if actualVal != expectVal { t.Errorf("Test %d [%v]: Expected placeholder {http.regexp.%s} to be '%s' but got '%s'", i, tc.match, key, expectVal, actualVal) return } } }) } } func TestNotMatcher(t *testing.T) { for i, tc := range []struct { host, path string match MatchNot expect bool }{ { host: "example.com", path: "/", match: MatchNot{}, expect: true, }, { host: "example.com", path: "/foo", match: MatchNot{ MatcherSets: []MatcherSet{ { MatchPath{"/foo"}, }, }, }, expect: false, }, { host: "example.com", path: "/bar", match: MatchNot{ MatcherSets: []MatcherSet{ { MatchPath{"/foo"}, }, }, }, expect: true, }, { host: "example.com", path: "/bar", match: MatchNot{ MatcherSets: []MatcherSet{ { MatchPath{"/foo"}, }, { MatchHost{"example.com"}, }, }, }, expect: false, }, { host: "example.com", path: "/bar", match: MatchNot{ MatcherSets: []MatcherSet{ { MatchPath{"/bar"}, }, { MatchHost{"example.com"}, }, }, }, expect: false, }, { host: "example.com", path: "/foo", match: MatchNot{ MatcherSets: []MatcherSet{ { MatchPath{"/bar"}, }, { MatchHost{"sub.example.com"}, }, }, }, expect: true, }, { host: "example.com", path: "/foo", match: MatchNot{ MatcherSets: []MatcherSet{ { MatchPath{"/foo"}, MatchHost{"example.com"}, }, }, }, expect: false, }, { host: "example.com", path: "/foo", match: MatchNot{ MatcherSets: []MatcherSet{ { MatchPath{"/bar"}, MatchHost{"example.com"}, }, }, }, expect: true, }, } { req := &http.Request{Host: tc.host, URL: &url.URL{Path: tc.path}} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) actual := tc.match.Match(req) if actual != tc.expect { t.Errorf("Test %d %+v: Expected %t, got %t for: host=%s path=%s'", i, tc.match, tc.expect, actual, tc.host, tc.path) continue } } } func BenchmarkLargeHostMatcher(b *testing.B) { // this benchmark simulates a large host matcher (thousands of entries) where each // value is an exact hostname (not a placeholder or wildcard) - compare the results // of this with and without the binary search (comment out the various fast path // sections in Match) to conduct experiments const n = 10000 lastHost := fmt.Sprintf("%d.example.com", n-1) req := &http.Request{Host: lastHost} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) matcher := make(MatchHost, n) for i := 0; i < n; i++ { matcher[i] = fmt.Sprintf("%d.example.com", i) } err := matcher.Provision(caddy.Context{}) if err != nil { b.Fatal(err) } b.ResetTimer() for i := 0; i < b.N; i++ { matcher.Match(req) } } func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) { req := &http.Request{Host: "localhost"} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) match := MatchHost{"localhost"} b.ResetTimer() for i := 0; i < b.N; i++ { match.Match(req) } } func BenchmarkHostMatcherWithPlaceholder(b *testing.B) { err := os.Setenv("GO_BENCHMARK_DOMAIN", "localhost") if err != nil { b.Errorf("error while setting up environment: %v", err) } req := &http.Request{Host: "localhost"} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) match := MatchHost{"{env.GO_BENCHMARK_DOMAIN}"} b.ResetTimer() for i := 0; i < b.N; i++ { match.Match(req) } } caddy-2.6.2/modules/caddyhttp/metrics.go000066400000000000000000000133561435007237400202530ustar00rootroot00000000000000package caddyhttp import ( "context" "net/http" "sync" "time" "github.com/caddyserver/caddy/v2/internal/metrics" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) // Metrics configures metrics observations. // EXPERIMENTAL and subject to change or removal. type Metrics struct{} var httpMetrics = struct { init sync.Once requestInFlight *prometheus.GaugeVec requestCount *prometheus.CounterVec requestErrors *prometheus.CounterVec requestDuration *prometheus.HistogramVec requestSize *prometheus.HistogramVec responseSize *prometheus.HistogramVec responseDuration *prometheus.HistogramVec }{ init: sync.Once{}, } func initHTTPMetrics() { const ns, sub = "caddy", "http" basicLabels := []string{"server", "handler"} httpMetrics.requestInFlight = promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: ns, Subsystem: sub, Name: "requests_in_flight", Help: "Number of requests currently handled by this server.", }, basicLabels) httpMetrics.requestErrors = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: ns, Subsystem: sub, Name: "request_errors_total", Help: "Number of requests resulting in middleware errors.", }, basicLabels) httpMetrics.requestCount = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: ns, Subsystem: sub, Name: "requests_total", Help: "Counter of HTTP(S) requests made.", }, basicLabels) // TODO: allow these to be customized in the config durationBuckets := prometheus.DefBuckets sizeBuckets := prometheus.ExponentialBuckets(256, 4, 8) httpLabels := []string{"server", "handler", "code", "method"} httpMetrics.requestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Namespace: ns, Subsystem: sub, Name: "request_duration_seconds", Help: "Histogram of round-trip request durations.", Buckets: durationBuckets, }, httpLabels) httpMetrics.requestSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ Namespace: ns, Subsystem: sub, Name: "request_size_bytes", Help: "Total size of the request. Includes body", Buckets: sizeBuckets, }, httpLabels) httpMetrics.responseSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ Namespace: ns, Subsystem: sub, Name: "response_size_bytes", Help: "Size of the returned response.", Buckets: sizeBuckets, }, httpLabels) httpMetrics.responseDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Namespace: ns, Subsystem: sub, Name: "response_duration_seconds", Help: "Histogram of times to first byte in response bodies.", Buckets: durationBuckets, }, httpLabels) } // serverNameFromContext extracts the current server name from the context. // Returns "UNKNOWN" if none is available (should probably never happen). func serverNameFromContext(ctx context.Context) string { srv, ok := ctx.Value(ServerCtxKey).(*Server) if !ok || srv == nil || srv.name == "" { return "UNKNOWN" } return srv.name } type metricsInstrumentedHandler struct { handler string mh MiddlewareHandler } func newMetricsInstrumentedHandler(handler string, mh MiddlewareHandler) *metricsInstrumentedHandler { httpMetrics.init.Do(func() { initHTTPMetrics() }) return &metricsInstrumentedHandler{handler, mh} } func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { server := serverNameFromContext(r.Context()) labels := prometheus.Labels{"server": server, "handler": h.handler} method := metrics.SanitizeMethod(r.Method) // the "code" value is set later, but initialized here to eliminate the possibility // of a panic statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""} inFlight := httpMetrics.requestInFlight.With(labels) inFlight.Inc() defer inFlight.Dec() start := time.Now() // This is a _bit_ of a hack - it depends on the ShouldBufferFunc always // being called when the headers are written. // Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader. writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool { statusLabels["code"] = metrics.SanitizeCode(status) ttfb := time.Since(start).Seconds() httpMetrics.responseDuration.With(statusLabels).Observe(ttfb) return false }) wrec := NewResponseRecorder(w, nil, writeHeaderRecorder) err := h.mh.ServeHTTP(wrec, r, next) dur := time.Since(start).Seconds() httpMetrics.requestCount.With(labels).Inc() if err != nil { httpMetrics.requestErrors.With(labels).Inc() return err } // If the code hasn't been set yet, and we didn't encounter an error, we're // probably falling through with an empty handler. if statusLabels["code"] == "" { // we still sanitize it, even though it's likely to be 0. A 200 is // returned on fallthrough so we want to reflect that. statusLabels["code"] = metrics.SanitizeCode(wrec.Status()) } httpMetrics.requestDuration.With(statusLabels).Observe(dur) httpMetrics.requestSize.With(statusLabels).Observe(float64(computeApproximateRequestSize(r))) httpMetrics.responseSize.With(statusLabels).Observe(float64(wrec.Size())) return nil } // taken from https://github.com/prometheus/client_golang/blob/6007b2b5cae01203111de55f753e76d8dac1f529/prometheus/promhttp/instrument_server.go#L298 func computeApproximateRequestSize(r *http.Request) int { s := 0 if r.URL != nil { s += len(r.URL.String()) } s += len(r.Method) s += len(r.Proto) for name, values := range r.Header { s += len(name) for _, value := range values { s += len(value) } } s += len(r.Host) // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. if r.ContentLength != -1 { s += int(r.ContentLength) } return s } caddy-2.6.2/modules/caddyhttp/metrics_test.go000066400000000000000000000047051435007237400213100ustar00rootroot00000000000000package caddyhttp import ( "context" "errors" "net/http" "net/http/httptest" "testing" "github.com/prometheus/client_golang/prometheus/testutil" ) func TestServerNameFromContext(t *testing.T) { ctx := context.Background() expected := "UNKNOWN" if actual := serverNameFromContext(ctx); actual != expected { t.Errorf("Not equal: expected %q, but got %q", expected, actual) } in := "foo" ctx = context.WithValue(ctx, ServerCtxKey, &Server{name: in}) if actual := serverNameFromContext(ctx); actual != in { t.Errorf("Not equal: expected %q, but got %q", in, actual) } } func TestMetricsInstrumentedHandler(t *testing.T) { handlerErr := errors.New("oh noes") response := []byte("hello world!") h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { if actual := testutil.ToFloat64(httpMetrics.requestInFlight); actual != 1.0 { t.Errorf("Not same: expected %#v, but got %#v", 1.0, actual) } if handlerErr == nil { w.Write(response) } return handlerErr }) mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { return h.ServeHTTP(w, r) }) ih := newMetricsInstrumentedHandler("bar", mh) r := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() if actual := ih.ServeHTTP(w, r, h); actual != handlerErr { t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual) } if actual := testutil.ToFloat64(httpMetrics.requestInFlight); actual != 0.0 { t.Errorf("Not same: expected %#v, but got %#v", 0.0, actual) } handlerErr = nil if err := ih.ServeHTTP(w, r, h); err != nil { t.Errorf("Received unexpected error: %v", err) } // an empty handler - no errors, no header written mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { return nil }) ih = newMetricsInstrumentedHandler("empty", mh) r = httptest.NewRequest("GET", "/", nil) w = httptest.NewRecorder() if err := ih.ServeHTTP(w, r, h); err != nil { t.Errorf("Received unexpected error: %v", err) } if actual := w.Result().StatusCode; actual != 200 { t.Errorf("Not same: expected status code %#v, but got %#v", 200, actual) } if actual := w.Result().Header; len(actual) != 0 { t.Errorf("Not empty: expected headers to be empty, but got %#v", actual) } } type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error { return f(w, r, h) } caddy-2.6.2/modules/caddyhttp/push/000077500000000000000000000000001435007237400172255ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/push/caddyfile.go000066400000000000000000000061651435007237400215100ustar00rootroot00000000000000// 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 push import ( "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" ) func init() { httpcaddyfile.RegisterHandlerDirective("push", parseCaddyfile) } // parseCaddyfile sets up the push handler. Syntax: // // push [] [] { // [GET|HEAD] // headers { // [+] [ []] // - // } // } // // A single resource can be specified inline without opening a // block for the most common/simple case. Or, a block can be // opened and multiple resources can be specified, one per // line, optionally preceded by the method. The headers // subdirective can be used to customize the headers that // are set on each (synthetic) push request, using the same // syntax as the 'header' directive for request headers. // Placeholders are accepted in resource and header field // name and value and replacement tokens. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { handler := new(Handler) for h.Next() { if h.NextArg() { handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) } // optional block for outerNesting := h.Nesting(); h.NextBlock(outerNesting); { switch h.Val() { case "headers": if h.NextArg() { return nil, h.ArgErr() } for innerNesting := h.Nesting(); h.NextBlock(innerNesting); { var err error // include current token, which we treat as an argument here args := []string{h.Val()} args = append(args, h.RemainingArgs()...) if handler.Headers == nil { handler.Headers = new(HeaderConfig) } switch len(args) { case 1: err = headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], "", "") case 2: err = headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], args[1], "") case 3: err = headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], args[1], args[2]) default: return nil, h.ArgErr() } if err != nil { return nil, h.Err(err.Error()) } } case "GET", "HEAD": method := h.Val() if !h.NextArg() { return nil, h.ArgErr() } target := h.Val() handler.Resources = append(handler.Resources, Resource{ Method: method, Target: target, }) default: handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) } } } return handler, nil } caddy-2.6.2/modules/caddyhttp/push/handler.go000066400000000000000000000173041435007237400211760ustar00rootroot00000000000000// 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 push import ( "fmt" "net/http" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "go.uber.org/zap" ) func init() { caddy.RegisterModule(Handler{}) } // Handler is a middleware for HTTP/2 server push. Note that // HTTP/2 server push has been deprecated by some clients and // its use is discouraged unless you can accurately predict // which resources actually need to be pushed to the client; // it can be difficult to know what the client already has // cached. Pushing unnecessary resources results in worse // performance. Consider using HTTP 103 Early Hints instead. // // This handler supports pushing from Link headers; in other // words, if the eventual response has Link headers, this // handler will push the resources indicated by those headers, // even without specifying any resources in its config. type Handler struct { // The resources to push. Resources []Resource `json:"resources,omitempty"` // Headers to modify for the push requests. Headers *HeaderConfig `json:"headers,omitempty"` logger *zap.Logger } // CaddyModule returns the Caddy module information. func (Handler) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.push", New: func() caddy.Module { return new(Handler) }, } } // Provision sets up h. func (h *Handler) Provision(ctx caddy.Context) error { h.logger = ctx.Logger() if h.Headers != nil { err := h.Headers.Provision(ctx) if err != nil { return fmt.Errorf("provisioning header operations: %v", err) } } return nil } func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { pusher, ok := w.(http.Pusher) if !ok { return next.ServeHTTP(w, r) } // short-circuit recursive pushes if _, ok := r.Header[pushHeader]; ok { return next.ServeHTTP(w, r) } repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server) shouldLogCredentials := server.Logs != nil && server.Logs.ShouldLogCredentials // create header for push requests hdr := h.initializePushHeaders(r, repl) // push first! for _, resource := range h.Resources { h.logger.Debug("pushing resource", zap.String("uri", r.RequestURI), zap.String("push_method", resource.Method), zap.String("push_target", resource.Target), zap.Object("push_headers", caddyhttp.LoggableHTTPHeader{ Header: hdr, ShouldLogCredentials: shouldLogCredentials, })) err := pusher.Push(repl.ReplaceAll(resource.Target, "."), &http.PushOptions{ Method: resource.Method, Header: hdr, }) if err != nil { // usually this means either that push is not // supported or concurrent streams are full break } } // wrap the response writer so that we can initiate push of any resources // described in Link header fields before the response is written lp := linkPusher{ ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w}, handler: h, pusher: pusher, header: hdr, request: r, } // serve only after pushing! if err := next.ServeHTTP(lp, r); err != nil { return err } return nil } func (h Handler) initializePushHeaders(r *http.Request, repl *caddy.Replacer) http.Header { hdr := make(http.Header) // prevent recursive pushes hdr.Set(pushHeader, "1") // set initial header fields; since exactly how headers should // be implemented for server push is not well-understood, we // are being conservative for now like httpd is: // https://httpd.apache.org/docs/2.4/en/howto/http2.html#push // we only copy some well-known, safe headers that are likely // crucial when requesting certain kinds of content for _, fieldName := range safeHeaders { if vals, ok := r.Header[fieldName]; ok { hdr[fieldName] = vals } } // user can customize the push request headers if h.Headers != nil { h.Headers.ApplyTo(hdr, repl) } return hdr } // servePreloadLinks parses Link headers from upstream and pushes // resources described by them. If a resource has the "nopush" // attribute or describes an external entity (meaning, the resource // URI includes a scheme), it will not be pushed. func (h Handler) servePreloadLinks(pusher http.Pusher, hdr http.Header, resources []string) { for _, resource := range resources { for _, resource := range parseLinkHeader(resource) { if _, ok := resource.params["nopush"]; ok { continue } if isRemoteResource(resource.uri) { continue } err := pusher.Push(resource.uri, &http.PushOptions{ Header: hdr, }) if err != nil { return } } } } // Resource represents a request for a resource to push. type Resource struct { // Method is the request method, which must be GET or HEAD. // Default is GET. Method string `json:"method,omitempty"` // Target is the path to the resource being pushed. Target string `json:"target,omitempty"` } // HeaderConfig configures headers for synthetic push requests. type HeaderConfig struct { headers.HeaderOps } // linkPusher is a http.ResponseWriter that intercepts // the WriteHeader() call to ensure that any resources // described by Link response headers get pushed before // the response is allowed to be written. type linkPusher struct { *caddyhttp.ResponseWriterWrapper handler Handler pusher http.Pusher header http.Header request *http.Request } func (lp linkPusher) WriteHeader(statusCode int) { if links, ok := lp.ResponseWriter.Header()["Link"]; ok { // only initiate these pushes if it hasn't been done yet if val := caddyhttp.GetVar(lp.request.Context(), pushedLink); val == nil { lp.handler.logger.Debug("pushing Link resources", zap.Strings("linked", links)) caddyhttp.SetVar(lp.request.Context(), pushedLink, true) lp.handler.servePreloadLinks(lp.pusher, lp.header, links) } } lp.ResponseWriter.WriteHeader(statusCode) } // isRemoteResource returns true if resource starts with // a scheme or is a protocol-relative URI. func isRemoteResource(resource string) bool { return strings.HasPrefix(resource, "//") || strings.HasPrefix(resource, "http://") || strings.HasPrefix(resource, "https://") } // safeHeaders is a list of header fields that are // safe to copy to push requests implicitly. It is // assumed that requests for certain kinds of content // would fail without these fields present. var safeHeaders = []string{ "Accept-Encoding", "Accept-Language", "Accept", "Cache-Control", "User-Agent", } // pushHeader is a header field that gets added to push requests // in order to avoid recursive/infinite pushes. const pushHeader = "Caddy-Push" // pushedLink is the key for the variable on the request // context that we use to remember whether we have already // pushed resources from Link headers yet; otherwise, if // multiple push handlers are invoked, it would repeat the // pushing of Link headers. const pushedLink = "http.handlers.push.pushed_link" // Interface guards var ( _ caddy.Provisioner = (*Handler)(nil) _ caddyhttp.MiddlewareHandler = (*Handler)(nil) _ caddyhttp.HTTPInterfaces = (*linkPusher)(nil) ) caddy-2.6.2/modules/caddyhttp/push/link.go000066400000000000000000000036211435007237400205130ustar00rootroot00000000000000// 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 push import ( "strings" ) // linkResource contains the results of a parsed Link header. type linkResource struct { uri string params map[string]string } // parseLinkHeader is responsible for parsing Link header // and returning list of found resources. // // Accepted formats are: // // Link: ; as=script // Link: ; as=script,; as=style // Link: ; // // where begins with a forward slash (/). func parseLinkHeader(header string) []linkResource { resources := []linkResource{} if header == "" { return resources } for _, link := range strings.Split(header, comma) { l := linkResource{params: make(map[string]string)} li, ri := strings.Index(link, "<"), strings.Index(link, ">") if li == -1 || ri == -1 { continue } l.uri = strings.TrimSpace(link[li+1 : ri]) for _, param := range strings.Split(strings.TrimSpace(link[ri+1:]), semicolon) { before, after, isCut := strings.Cut(strings.TrimSpace(param), equal) key := strings.TrimSpace(before) if key == "" { continue } if isCut { l.params[key] = strings.TrimSpace(after) } else { l.params[key] = key } } resources = append(resources, l) } return resources } const ( comma = "," semicolon = ";" equal = "=" ) caddy-2.6.2/modules/caddyhttp/push/link_test.go000066400000000000000000000051251435007237400215530ustar00rootroot00000000000000// 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 push import ( "reflect" "testing" ) func TestParseLinkHeader(t *testing.T) { testCases := []struct { header string expectedResources []linkResource }{ { header: "; as=script", expectedResources: []linkResource{{uri: "/resource", params: map[string]string{"as": "script"}}}, }, { header: "", expectedResources: []linkResource{{uri: "/resource", params: map[string]string{}}}, }, { header: "; nopush", expectedResources: []linkResource{{uri: "/resource", params: map[string]string{"nopush": "nopush"}}}, }, { header: ";nopush;rel=next", expectedResources: []linkResource{{uri: "/resource", params: map[string]string{"nopush": "nopush", "rel": "next"}}}, }, { header: ";nopush;rel=next,;nopush", expectedResources: []linkResource{ {uri: "/resource", params: map[string]string{"nopush": "nopush", "rel": "next"}}, {uri: "/resource2", params: map[string]string{"nopush": "nopush"}}, }, }, { header: ",", expectedResources: []linkResource{ {uri: "/resource", params: map[string]string{}}, {uri: "/resource2", params: map[string]string{}}, }, }, { header: "malformed", expectedResources: []linkResource{}, }, { header: " ; ", expectedResources: []linkResource{{uri: "/resource", params: map[string]string{}}}, }, } for i, test := range testCases { actualResources := parseLinkHeader(test.header) if !reflect.DeepEqual(actualResources, test.expectedResources) { t.Errorf("Test %d (header: %s) - expected resources %v, got %v", i, test.header, test.expectedResources, actualResources) } } } caddy-2.6.2/modules/caddyhttp/replacer.go000066400000000000000000000337451435007237400204060ustar00rootroot00000000000000// 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 caddyhttp import ( "bytes" "context" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rsa" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/asn1" "encoding/base64" "encoding/pem" "fmt" "io" "net" "net/http" "net/netip" "net/textproto" "net/url" "path" "strconv" "strings" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/google/uuid" ) // NewTestReplacer creates a replacer for an http.Request // for use in tests that are not in this package func NewTestReplacer(req *http.Request) *caddy.Replacer { repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) *req = *req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, nil) return repl } func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.ResponseWriter) { SetVar(req.Context(), "start_time", time.Now()) SetVar(req.Context(), "uuid", new(requestID)) httpVars := func(key string) (any, bool) { if req != nil { // query string parameters if strings.HasPrefix(key, reqURIQueryReplPrefix) { vals := req.URL.Query()[key[len(reqURIQueryReplPrefix):]] // always return true, since the query param might // be present only in some requests return strings.Join(vals, ","), true } // request header fields if strings.HasPrefix(key, reqHeaderReplPrefix) { field := key[len(reqHeaderReplPrefix):] vals := req.Header[textproto.CanonicalMIMEHeaderKey(field)] // always return true, since the header field might // be present only in some requests return strings.Join(vals, ","), true } // cookies if strings.HasPrefix(key, reqCookieReplPrefix) { name := key[len(reqCookieReplPrefix):] for _, cookie := range req.Cookies() { if strings.EqualFold(name, cookie.Name) { // always return true, since the cookie might // be present only in some requests return cookie.Value, true } } } // http.request.tls.* if strings.HasPrefix(key, reqTLSReplPrefix) { return getReqTLSReplacement(req, key) } switch key { case "http.request.method": return req.Method, true case "http.request.scheme": if req.TLS != nil { return "https", true } return "http", true case "http.request.proto": return req.Proto, true case "http.request.host": host, _, err := net.SplitHostPort(req.Host) if err != nil { return req.Host, true // OK; there probably was no port } return host, true case "http.request.port": _, port, _ := net.SplitHostPort(req.Host) if portNum, err := strconv.Atoi(port); err == nil { return portNum, true } return port, true case "http.request.hostport": return req.Host, true case "http.request.remote": return req.RemoteAddr, true case "http.request.remote.host": host, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { return req.RemoteAddr, true } return host, true case "http.request.remote.port": _, port, _ := net.SplitHostPort(req.RemoteAddr) if portNum, err := strconv.Atoi(port); err == nil { return portNum, true } return port, true // current URI, including any internal rewrites case "http.request.uri": return req.URL.RequestURI(), true case "http.request.uri.path": return req.URL.Path, true case "http.request.uri.path.file": _, file := path.Split(req.URL.Path) return file, true case "http.request.uri.path.dir": dir, _ := path.Split(req.URL.Path) return dir, true case "http.request.uri.path.file.base": return strings.TrimSuffix(path.Base(req.URL.Path), path.Ext(req.URL.Path)), true case "http.request.uri.path.file.ext": return path.Ext(req.URL.Path), true case "http.request.uri.query": return req.URL.RawQuery, true case "http.request.duration": start := GetVar(req.Context(), "start_time").(time.Time) return time.Since(start), true case "http.request.duration_ms": start := GetVar(req.Context(), "start_time").(time.Time) return time.Since(start).Seconds() * 1e3, true // multiply seconds to preserve decimal (see #4666) case "http.request.uuid": id := GetVar(req.Context(), "uuid").(*requestID) return id.String(), true case "http.request.body": if req.Body == nil { return "", true } // normally net/http will close the body for us, but since we // are replacing it with a fake one, we have to ensure we close // the real body ourselves when we're done defer req.Body.Close() // read the request body into a buffer (can't pool because we // don't know its lifetime and would have to make a copy anyway) buf := new(bytes.Buffer) _, _ = io.Copy(buf, req.Body) // can't handle error, so just ignore it req.Body = io.NopCloser(buf) // replace real body with buffered data return buf.String(), true // original request, before any internal changes case "http.request.orig_method": or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request) return or.Method, true case "http.request.orig_uri": or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request) return or.RequestURI, true case "http.request.orig_uri.path": or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request) return or.URL.Path, true case "http.request.orig_uri.path.file": or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request) _, file := path.Split(or.URL.Path) return file, true case "http.request.orig_uri.path.dir": or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request) dir, _ := path.Split(or.URL.Path) return dir, true case "http.request.orig_uri.query": or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request) return or.URL.RawQuery, true } // remote IP range/prefix (e.g. keep top 24 bits of 1.2.3.4 => "1.2.3.0/24") // syntax: "/V4,V6" where V4 = IPv4 bits, and V6 = IPv6 bits; if no comma, then same bit length used for both // (EXPERIMENTAL) if strings.HasPrefix(key, "http.request.remote.host/") { host, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { host = req.RemoteAddr // assume no port, I guess? } addr, err := netip.ParseAddr(host) if err != nil { return host, true // not an IP address } // extract the bits from the end of the placeholder (start after "/") then split on "," bitsBoth := key[strings.Index(key, "/")+1:] ipv4BitsStr, ipv6BitsStr, cutOK := strings.Cut(bitsBoth, ",") bitsStr := ipv4BitsStr if addr.Is6() && cutOK { bitsStr = ipv6BitsStr } // convert to integer then compute prefix bits, err := strconv.Atoi(bitsStr) if err != nil { return "", true } prefix, err := addr.Prefix(bits) if err != nil { return "", true } return prefix.String(), true } // hostname labels if strings.HasPrefix(key, reqHostLabelsReplPrefix) { idxStr := key[len(reqHostLabelsReplPrefix):] idx, err := strconv.Atoi(idxStr) if err != nil || idx < 0 { return "", false } reqHost, _, err := net.SplitHostPort(req.Host) if err != nil { reqHost = req.Host // OK; assume there was no port } hostLabels := strings.Split(reqHost, ".") if idx >= len(hostLabels) { return "", true } return hostLabels[len(hostLabels)-idx-1], true } // path parts if strings.HasPrefix(key, reqURIPathReplPrefix) { idxStr := key[len(reqURIPathReplPrefix):] idx, err := strconv.Atoi(idxStr) if err != nil { return "", false } pathParts := strings.Split(req.URL.Path, "/") if len(pathParts) > 0 && pathParts[0] == "" { pathParts = pathParts[1:] } if idx < 0 { return "", false } if idx >= len(pathParts) { return "", true } return pathParts[idx], true } // middleware variables if strings.HasPrefix(key, varsReplPrefix) { varName := key[len(varsReplPrefix):] tbl := req.Context().Value(VarsCtxKey).(map[string]any) raw := tbl[varName] // variables can be dynamic, so always return true // even when it may not be set; treat as empty then return raw, true } } if w != nil { // response header fields if strings.HasPrefix(key, respHeaderReplPrefix) { field := key[len(respHeaderReplPrefix):] vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)] // always return true, since the header field might // be present only in some responses return strings.Join(vals, ","), true } } switch { case key == "http.shutting_down": server := req.Context().Value(ServerCtxKey).(*Server) server.shutdownAtMu.RLock() defer server.shutdownAtMu.RUnlock() return !server.shutdownAt.IsZero(), true case key == "http.time_until_shutdown": server := req.Context().Value(ServerCtxKey).(*Server) server.shutdownAtMu.RLock() defer server.shutdownAtMu.RUnlock() if server.shutdownAt.IsZero() { return nil, true } return time.Until(server.shutdownAt), true } return nil, false } repl.Map(httpVars) } func getReqTLSReplacement(req *http.Request, key string) (any, bool) { if req == nil || req.TLS == nil { return nil, false } if len(key) < len(reqTLSReplPrefix) { return nil, false } field := strings.ToLower(key[len(reqTLSReplPrefix):]) if strings.HasPrefix(field, "client.") { cert := getTLSPeerCert(req.TLS) if cert == nil { return nil, false } // subject alternate names (SANs) if strings.HasPrefix(field, "client.san.") { field = field[len("client.san."):] var fieldName string var fieldValue any switch { case strings.HasPrefix(field, "dns_names"): fieldName = "dns_names" fieldValue = cert.DNSNames case strings.HasPrefix(field, "emails"): fieldName = "emails" fieldValue = cert.EmailAddresses case strings.HasPrefix(field, "ips"): fieldName = "ips" fieldValue = cert.IPAddresses case strings.HasPrefix(field, "uris"): fieldName = "uris" fieldValue = cert.URIs default: return nil, false } field = field[len(fieldName):] // if no index was specified, return the whole list if field == "" { return fieldValue, true } if len(field) < 2 || field[0] != '.' { return nil, false } field = field[1:] // trim '.' between field name and index // get the numeric index idx, err := strconv.Atoi(field) if err != nil || idx < 0 { return nil, false } // access the indexed element and return it switch v := fieldValue.(type) { case []string: if idx >= len(v) { return nil, true } return v[idx], true case []net.IP: if idx >= len(v) { return nil, true } return v[idx], true case []*url.URL: if idx >= len(v) { return nil, true } return v[idx], true } } switch field { case "client.fingerprint": return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true case "client.public_key", "client.public_key_sha256": if cert.PublicKey == nil { return nil, true } pubKeyBytes, err := marshalPublicKey(cert.PublicKey) if err != nil { return nil, true } if strings.HasSuffix(field, "_sha256") { return fmt.Sprintf("%x", sha256.Sum256(pubKeyBytes)), true } return fmt.Sprintf("%x", pubKeyBytes), true case "client.issuer": return cert.Issuer, true case "client.serial": return cert.SerialNumber, true case "client.subject": return cert.Subject, true case "client.certificate_pem": block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw} return pem.EncodeToMemory(&block), true case "client.certificate_der_base64": return base64.StdEncoding.EncodeToString(cert.Raw), true default: return nil, false } } switch field { case "version": return caddytls.ProtocolName(req.TLS.Version), true case "cipher_suite": return tls.CipherSuiteName(req.TLS.CipherSuite), true case "resumed": return req.TLS.DidResume, true case "proto": return req.TLS.NegotiatedProtocol, true case "proto_mutual": // req.TLS.NegotiatedProtocolIsMutual is deprecated - it's always true. return true, true case "server_name": return req.TLS.ServerName, true } return nil, false } // marshalPublicKey returns the byte encoding of pubKey. func marshalPublicKey(pubKey any) ([]byte, error) { switch key := pubKey.(type) { case *rsa.PublicKey: return asn1.Marshal(key) case *ecdsa.PublicKey: return elliptic.Marshal(key.Curve, key.X, key.Y), nil case ed25519.PublicKey: return key, nil } return nil, fmt.Errorf("unrecognized public key type: %T", pubKey) } // getTLSPeerCert retrieves the first peer certificate from a TLS session. // Returns nil if no peer cert is in use. func getTLSPeerCert(cs *tls.ConnectionState) *x509.Certificate { if len(cs.PeerCertificates) == 0 { return nil } return cs.PeerCertificates[0] } type requestID struct { value string } // Lazy generates UUID string or return cached value if present func (rid *requestID) String() string { if rid.value == "" { if id, err := uuid.NewRandom(); err == nil { rid.value = id.String() } } return rid.value } const ( reqCookieReplPrefix = "http.request.cookie." reqHeaderReplPrefix = "http.request.header." reqHostLabelsReplPrefix = "http.request.host.labels." reqTLSReplPrefix = "http.request.tls." reqURIPathReplPrefix = "http.request.uri.path." reqURIQueryReplPrefix = "http.request.uri.query." respHeaderReplPrefix = "http.response.header." varsReplPrefix = "http.vars." ) caddy-2.6.2/modules/caddyhttp/replacer_test.go000066400000000000000000000126071435007237400214370ustar00rootroot00000000000000// 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 caddyhttp import ( "context" "crypto/tls" "crypto/x509" "encoding/pem" "net/http" "net/http/httptest" "testing" "github.com/caddyserver/caddy/v2" ) func TestHTTPVarReplacement(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz", nil) repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) req.Host = "example.com:80" req.RemoteAddr = "192.168.159.32:1234" clientCert := []byte(`-----BEGIN CERTIFICATE----- MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+ fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV 3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH 9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g= -----END CERTIFICATE-----`) block, _ := pem.Decode(clientCert) if block == nil { t.Fatalf("failed to decode PEM certificate") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("failed to decode PEM certificate: %v", err) } req.TLS = &tls.ConnectionState{ Version: tls.VersionTLS13, HandshakeComplete: true, ServerName: "example.com", CipherSuite: tls.TLS_AES_256_GCM_SHA384, PeerCertificates: []*x509.Certificate{cert}, NegotiatedProtocol: "h2", NegotiatedProtocolIsMutual: true, } res := httptest.NewRecorder() addHTTPVarsToReplacer(repl, req, res) for i, tc := range []struct { get string expect string }{ { get: "http.request.scheme", expect: "https", }, { get: "http.request.method", expect: http.MethodGet, }, { get: "http.request.host", expect: "example.com", }, { get: "http.request.port", expect: "80", }, { get: "http.request.hostport", expect: "example.com:80", }, { get: "http.request.remote.host", expect: "192.168.159.32", }, { get: "http.request.remote.host/24", expect: "192.168.159.0/24", }, { get: "http.request.remote.host/24,32", expect: "192.168.159.0/24", }, { get: "http.request.remote.host/999", expect: "", }, { get: "http.request.remote.port", expect: "1234", }, { get: "http.request.host.labels.0", expect: "com", }, { get: "http.request.host.labels.1", expect: "example", }, { get: "http.request.host.labels.2", expect: "", }, { get: "http.request.uri.path.file", expect: "bar.tar.gz", }, { get: "http.request.uri.path.file.base", expect: "bar.tar", }, { // not ideal, but also most correct, given that files can have dots (example: index..html) TODO: maybe this isn't right.. get: "http.request.uri.path.file.ext", expect: ".gz", }, { get: "http.request.tls.cipher_suite", expect: "TLS_AES_256_GCM_SHA384", }, { get: "http.request.tls.proto", expect: "h2", }, { get: "http.request.tls.proto_mutual", expect: "true", }, { get: "http.request.tls.resumed", expect: "false", }, { get: "http.request.tls.server_name", expect: "example.com", }, { get: "http.request.tls.version", expect: "tls1.3", }, { get: "http.request.tls.client.fingerprint", expect: "9f57b7b497cceacc5459b76ac1c3afedbc12b300e728071f55f84168ff0f7702", }, { get: "http.request.tls.client.issuer", expect: "CN=Caddy Test CA", }, { get: "http.request.tls.client.serial", expect: "2", }, { get: "http.request.tls.client.subject", expect: "CN=client.localdomain", }, { get: "http.request.tls.client.san.dns_names", expect: "[localhost]", }, { get: "http.request.tls.client.san.dns_names.0", expect: "localhost", }, { get: "http.request.tls.client.san.dns_names.1", expect: "", }, { get: "http.request.tls.client.san.ips", expect: "[127.0.0.1]", }, { get: "http.request.tls.client.san.ips.0", expect: "127.0.0.1", }, { get: "http.request.tls.client.certificate_pem", expect: string(clientCert) + "\n", // returned value comes with a newline appended to it }, } { actual, got := repl.GetString(tc.get) if !got { t.Errorf("Test %d: Expected to recognize the placeholder name, but didn't", i) } if actual != tc.expect { t.Errorf("Test %d: Expected %s to be '%s' but got '%s'", i, tc.get, tc.expect, actual) } } } caddy-2.6.2/modules/caddyhttp/requestbody/000077500000000000000000000000001435007237400206145ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/requestbody/caddyfile.go000066400000000000000000000026641435007237400230770ustar00rootroot00000000000000// 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 requestbody import ( "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/dustin/go-humanize" ) func init() { httpcaddyfile.RegisterHandlerDirective("request_body", parseCaddyfile) } func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { rb := new(RequestBody) for h.Next() { // configuration should be in a block for h.NextBlock(0) { switch h.Val() { case "max_size": var sizeStr string if !h.AllArgs(&sizeStr) { return nil, h.ArgErr() } size, err := humanize.ParseBytes(sizeStr) if err != nil { return nil, h.Errf("parsing max_size: %v", err) } rb.MaxSize = int64(size) default: return nil, h.Errf("unrecognized servers option '%s'", h.Val()) } } } return rb, nil } caddy-2.6.2/modules/caddyhttp/requestbody/requestbody.go000066400000000000000000000040431435007237400235120ustar00rootroot00000000000000// 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 requestbody import ( "io" "net/http" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { caddy.RegisterModule(RequestBody{}) } // RequestBody is a middleware for manipulating the request body. type RequestBody struct { // The maximum number of bytes to allow reading from the body by a later handler. // If more bytes are read, an error with HTTP status 413 is returned. MaxSize int64 `json:"max_size,omitempty"` } // CaddyModule returns the Caddy module information. func (RequestBody) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.request_body", New: func() caddy.Module { return new(RequestBody) }, } } func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { if r.Body == nil { return next.ServeHTTP(w, r) } if rb.MaxSize > 0 { r.Body = errorWrapper{http.MaxBytesReader(w, r.Body, rb.MaxSize)} } return next.ServeHTTP(w, r) } // errorWrapper wraps errors that are returned from Read() // so that they can be associated with a proper status code. type errorWrapper struct { io.ReadCloser } func (ew errorWrapper) Read(p []byte) (n int, err error) { n, err = ew.ReadCloser.Read(p) if err != nil && err.Error() == "http: request body too large" { err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err) } return } // Interface guard var _ caddyhttp.MiddlewareHandler = (*RequestBody)(nil) caddy-2.6.2/modules/caddyhttp/responsematchers.go000066400000000000000000000065351435007237400221730ustar00rootroot00000000000000// 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 caddyhttp import ( "net/http" "strconv" "strings" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) // ResponseMatcher is a type which can determine if an // HTTP response matches some criteria. type ResponseMatcher struct { // If set, one of these status codes would be required. // A one-digit status can be used to represent all codes // in that class (e.g. 3 for all 3xx codes). StatusCode []int `json:"status_code,omitempty"` // If set, each header specified must be one of the // specified values, with the same logic used by the // [request header matcher](/docs/json/apps/http/servers/routes/match/header/). Headers http.Header `json:"headers,omitempty"` } // Match returns true if the given statusCode and hdr match rm. func (rm ResponseMatcher) Match(statusCode int, hdr http.Header) bool { if !rm.matchStatusCode(statusCode) { return false } return matchHeaders(hdr, rm.Headers, "", nil) } func (rm ResponseMatcher) matchStatusCode(statusCode int) bool { if rm.StatusCode == nil { return true } for _, code := range rm.StatusCode { if StatusCodeMatches(statusCode, code) { return true } } return false } // ParseNamedResponseMatcher parses the tokens of a named response matcher. // // @name { // header [] // status // } // // Or, single line syntax: // // @name [header []] | [status ] // func ParseNamedResponseMatcher(d *caddyfile.Dispenser, matchers map[string]ResponseMatcher) error { for d.Next() { definitionName := d.Val() if _, ok := matchers[definitionName]; ok { return d.Errf("matcher is defined more than once: %s", definitionName) } matcher := ResponseMatcher{} for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { switch d.Val() { case "header": if matcher.Headers == nil { matcher.Headers = http.Header{} } // reuse the header request matcher's unmarshaler headerMatcher := MatchHeader(matcher.Headers) err := headerMatcher.UnmarshalCaddyfile(d.NewFromNextSegment()) if err != nil { return err } matcher.Headers = http.Header(headerMatcher) case "status": if matcher.StatusCode == nil { matcher.StatusCode = []int{} } args := d.RemainingArgs() if len(args) == 0 { return d.ArgErr() } for _, arg := range args { if len(arg) == 3 && strings.HasSuffix(arg, "xx") { arg = arg[:1] } statusNum, err := strconv.Atoi(arg) if err != nil { return d.Errf("bad status value '%s': %v", arg, err) } matcher.StatusCode = append(matcher.StatusCode, statusNum) } default: return d.Errf("unrecognized response matcher %s", d.Val()) } } matchers[definitionName] = matcher } return nil } caddy-2.6.2/modules/caddyhttp/responsematchers_test.go000066400000000000000000000065521435007237400232310ustar00rootroot00000000000000// 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 caddyhttp import ( "net/http" "testing" ) func TestResponseMatcher(t *testing.T) { for i, tc := range []struct { require ResponseMatcher status int hdr http.Header // make sure these are canonical cased (std lib will do that in a real request) expect bool }{ { require: ResponseMatcher{}, status: 200, expect: true, }, { require: ResponseMatcher{ StatusCode: []int{200}, }, status: 200, expect: true, }, { require: ResponseMatcher{ StatusCode: []int{2}, }, status: 200, expect: true, }, { require: ResponseMatcher{ StatusCode: []int{201}, }, status: 200, expect: false, }, { require: ResponseMatcher{ StatusCode: []int{2}, }, status: 301, expect: false, }, { require: ResponseMatcher{ StatusCode: []int{3}, }, status: 301, expect: true, }, { require: ResponseMatcher{ StatusCode: []int{3}, }, status: 399, expect: true, }, { require: ResponseMatcher{ StatusCode: []int{3}, }, status: 400, expect: false, }, { require: ResponseMatcher{ StatusCode: []int{3, 4}, }, status: 400, expect: true, }, { require: ResponseMatcher{ StatusCode: []int{3, 401}, }, status: 401, expect: true, }, { require: ResponseMatcher{ Headers: http.Header{ "Foo": []string{"bar"}, }, }, hdr: http.Header{"Foo": []string{"bar"}}, expect: true, }, { require: ResponseMatcher{ Headers: http.Header{ "Foo2": []string{"bar"}, }, }, hdr: http.Header{"Foo": []string{"bar"}}, expect: false, }, { require: ResponseMatcher{ Headers: http.Header{ "Foo": []string{"bar", "baz"}, }, }, hdr: http.Header{"Foo": []string{"baz"}}, expect: true, }, { require: ResponseMatcher{ Headers: http.Header{ "Foo": []string{"bar"}, "Foo2": []string{"baz"}, }, }, hdr: http.Header{"Foo": []string{"baz"}}, expect: false, }, { require: ResponseMatcher{ Headers: http.Header{ "Foo": []string{"bar"}, "Foo2": []string{"baz"}, }, }, hdr: http.Header{"Foo": []string{"bar"}, "Foo2": []string{"baz"}}, expect: true, }, { require: ResponseMatcher{ Headers: http.Header{ "Foo": []string{"foo*"}, }, }, hdr: http.Header{"Foo": []string{"foobar"}}, expect: true, }, { require: ResponseMatcher{ Headers: http.Header{ "Foo": []string{"foo*"}, }, }, hdr: http.Header{"Foo": []string{"foobar"}}, expect: true, }, } { actual := tc.require.Match(tc.status, tc.hdr) if actual != tc.expect { t.Errorf("Test %d %v: Expected %t, got %t for HTTP %d %v", i, tc.require, tc.expect, actual, tc.status, tc.hdr) continue } } } caddy-2.6.2/modules/caddyhttp/responsewriter.go000066400000000000000000000222121435007237400216670ustar00rootroot00000000000000// 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 caddyhttp import ( "bufio" "bytes" "fmt" "io" "net" "net/http" ) // ResponseWriterWrapper wraps an underlying ResponseWriter and // promotes its Pusher/Flusher/Hijacker methods as well. To use // this type, embed a pointer to it within your own struct type // that implements the http.ResponseWriter interface, then call // methods on the embedded value. You can make sure your type // wraps correctly by asserting that it implements the // HTTPInterfaces interface. type ResponseWriterWrapper struct { http.ResponseWriter } // Hijack implements http.Hijacker. It simply calls the underlying // ResponseWriter's Hijack method if there is one, or returns // ErrNotImplemented otherwise. func (rww *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hj, ok := rww.ResponseWriter.(http.Hijacker); ok { return hj.Hijack() } return nil, nil, ErrNotImplemented } // Flush implements http.Flusher. It simply calls the underlying // ResponseWriter's Flush method if there is one. func (rww *ResponseWriterWrapper) Flush() { if f, ok := rww.ResponseWriter.(http.Flusher); ok { f.Flush() } } // Push implements http.Pusher. It simply calls the underlying // ResponseWriter's Push method if there is one, or returns // ErrNotImplemented otherwise. func (rww *ResponseWriterWrapper) Push(target string, opts *http.PushOptions) error { if pusher, ok := rww.ResponseWriter.(http.Pusher); ok { return pusher.Push(target, opts) } return ErrNotImplemented } // ReadFrom implements io.ReaderFrom. It simply calls the underlying // ResponseWriter's ReadFrom method if there is one, otherwise it defaults // to io.Copy. func (rww *ResponseWriterWrapper) ReadFrom(r io.Reader) (n int64, err error) { if rf, ok := rww.ResponseWriter.(io.ReaderFrom); ok { return rf.ReadFrom(r) } return io.Copy(rww.ResponseWriter, r) } // HTTPInterfaces mix all the interfaces that middleware ResponseWriters need to support. type HTTPInterfaces interface { http.ResponseWriter http.Pusher http.Flusher http.Hijacker } // ErrNotImplemented is returned when an underlying // ResponseWriter does not implement the required method. var ErrNotImplemented = fmt.Errorf("method not implemented") type responseRecorder struct { *ResponseWriterWrapper statusCode int buf *bytes.Buffer shouldBuffer ShouldBufferFunc size int wroteHeader bool stream bool } // NewResponseRecorder returns a new ResponseRecorder that can be // used instead of a standard http.ResponseWriter. The recorder is // useful for middlewares which need to buffer a response and // potentially process its entire body before actually writing the // response to the underlying writer. Of course, buffering the entire // body has a memory overhead, but sometimes there is no way to avoid // buffering the whole response, hence the existence of this type. // Still, if at all practical, handlers should strive to stream // responses by wrapping Write and WriteHeader methods instead of // buffering whole response bodies. // // Buffering is actually optional. The shouldBuffer function will // be called just before the headers are written. If it returns // true, the headers and body will be buffered by this recorder // and not written to the underlying writer; if false, the headers // will be written immediately and the body will be streamed out // directly to the underlying writer. If shouldBuffer is nil, // the response will never be buffered and will always be streamed // directly to the writer. // // You can know if shouldBuffer returned true by calling Buffered(). // // The provided buffer buf should be obtained from a pool for best // performance (see the sync.Pool type). // // Proper usage of a recorder looks like this: // // rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuffer) // err := next.ServeHTTP(rec, req) // if err != nil { // return err // } // if !rec.Buffered() { // return nil // } // // process the buffered response here // // The header map is not buffered; i.e. the ResponseRecorder's Header() // method returns the same header map of the underlying ResponseWriter. // This is a crucial design decision to allow HTTP trailers to be // flushed properly (https://github.com/caddyserver/caddy/issues/3236). // // Once you are ready to write the response, there are two ways you can // do it. The easier way is to have the recorder do it: // // rec.WriteResponse() // // This writes the recorded response headers as well as the buffered body. // Or, you may wish to do it yourself, especially if you manipulated the // buffered body. First you will need to write the headers with the // recorded status code, then write the body (this example writes the // recorder's body buffer, but you might have your own body to write // instead): // // w.WriteHeader(rec.Status()) // io.Copy(w, rec.Buffer()) // // As a special case, 1xx responses are not buffered nor recorded // because they are not the final response; they are passed through // directly to the underlying ResponseWriter. func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, shouldBuffer ShouldBufferFunc) ResponseRecorder { return &responseRecorder{ ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w}, buf: buf, shouldBuffer: shouldBuffer, } } // WriteHeader writes the headers with statusCode to the wrapped // ResponseWriter unless the response is to be buffered instead. // 1xx responses are never buffered. func (rr *responseRecorder) WriteHeader(statusCode int) { if rr.wroteHeader { return } // save statusCode always, in case HTTP middleware upgrades websocket // connections by manually setting headers and writing status 101 rr.statusCode = statusCode // 1xx responses aren't final; just informational if statusCode < 100 || statusCode > 199 { rr.wroteHeader = true // decide whether we should buffer the response if rr.shouldBuffer == nil { rr.stream = true } else { rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header()) } } // if informational or not buffered, immediately write header if rr.stream || (100 <= statusCode && statusCode <= 199) { rr.ResponseWriterWrapper.WriteHeader(statusCode) } } func (rr *responseRecorder) Write(data []byte) (int, error) { rr.WriteHeader(http.StatusOK) var n int var err error if rr.stream { n, err = rr.ResponseWriterWrapper.Write(data) } else { n, err = rr.buf.Write(data) } rr.size += n return n, err } func (rr *responseRecorder) ReadFrom(r io.Reader) (int64, error) { rr.WriteHeader(http.StatusOK) var n int64 var err error if rr.stream { if rf, ok := rr.ResponseWriter.(io.ReaderFrom); ok { n, err = rf.ReadFrom(r) } else { n, err = io.Copy(rr.ResponseWriter, r) } } else { n, err = rr.buf.ReadFrom(r) } rr.size += int(n) return n, err } // Status returns the status code that was written, if any. func (rr *responseRecorder) Status() int { return rr.statusCode } // Size returns the number of bytes written, // not including the response headers. func (rr *responseRecorder) Size() int { return rr.size } // Buffer returns the body buffer that rr was created with. // You should still have your original pointer, though. func (rr *responseRecorder) Buffer() *bytes.Buffer { return rr.buf } // Buffered returns whether rr has decided to buffer the response. func (rr *responseRecorder) Buffered() bool { return !rr.stream } func (rr *responseRecorder) WriteResponse() error { if rr.stream { return nil } if rr.statusCode == 0 { // could happen if no handlers actually wrote anything, // and this prevents a panic; status must be > 0 rr.statusCode = http.StatusOK } rr.ResponseWriterWrapper.WriteHeader(rr.statusCode) _, err := io.Copy(rr.ResponseWriterWrapper, rr.buf) return err } // ResponseRecorder is a http.ResponseWriter that records // responses instead of writing them to the client. See // docs for NewResponseRecorder for proper usage. type ResponseRecorder interface { HTTPInterfaces Status() int Buffer() *bytes.Buffer Buffered() bool Size() int WriteResponse() error } // ShouldBufferFunc is a function that returns true if the // response should be buffered, given the pending HTTP status // code and response headers. type ShouldBufferFunc func(status int, header http.Header) bool // Interface guards var ( _ HTTPInterfaces = (*ResponseWriterWrapper)(nil) _ ResponseRecorder = (*responseRecorder)(nil) // Implementing ReaderFrom can be such a significant // optimization that it should probably be required! // see PR #5022 (25%-50% speedup) _ io.ReaderFrom = (*ResponseWriterWrapper)(nil) _ io.ReaderFrom = (*responseRecorder)(nil) ) caddy-2.6.2/modules/caddyhttp/responsewriter_test.go000066400000000000000000000100321435007237400227230ustar00rootroot00000000000000package caddyhttp import ( "bytes" "fmt" "io" "net/http" "strings" "testing" ) type responseWriterSpy interface { http.ResponseWriter Written() string CalledReadFrom() bool } var ( _ responseWriterSpy = (*baseRespWriter)(nil) _ responseWriterSpy = (*readFromRespWriter)(nil) ) // a barebones http.ResponseWriter mock type baseRespWriter []byte func (brw *baseRespWriter) Write(d []byte) (int, error) { *brw = append(*brw, d...) return len(d), nil } func (brw *baseRespWriter) Header() http.Header { return nil } func (brw *baseRespWriter) WriteHeader(statusCode int) {} func (brw *baseRespWriter) Written() string { return string(*brw) } func (brw *baseRespWriter) CalledReadFrom() bool { return false } // an http.ResponseWriter mock that supports ReadFrom type readFromRespWriter struct { baseRespWriter called bool } func (rf *readFromRespWriter) ReadFrom(r io.Reader) (int64, error) { rf.called = true return io.Copy(&rf.baseRespWriter, r) } func (rf *readFromRespWriter) CalledReadFrom() bool { return rf.called } func TestResponseWriterWrapperReadFrom(t *testing.T) { tests := map[string]struct { responseWriter responseWriterSpy wantReadFrom bool }{ "no ReadFrom": { responseWriter: &baseRespWriter{}, wantReadFrom: false, }, "has ReadFrom": { responseWriter: &readFromRespWriter{}, wantReadFrom: true, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { // what we expect middlewares to do: type myWrapper struct { *ResponseWriterWrapper } wrapped := myWrapper{ ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: tt.responseWriter}, } const srcData = "boo!" // hides everything but Read, since strings.Reader implements WriteTo it would // take precedence over our ReadFrom. src := struct{ io.Reader }{strings.NewReader(srcData)} fmt.Println(name) if _, err := io.Copy(wrapped, src); err != nil { t.Errorf("Copy() err = %v", err) } if got := tt.responseWriter.Written(); got != srcData { t.Errorf("data = %q, want %q", got, srcData) } if tt.responseWriter.CalledReadFrom() != tt.wantReadFrom { if tt.wantReadFrom { t.Errorf("ReadFrom() should have been called") } else { t.Errorf("ReadFrom() should not have been called") } } }) } } func TestResponseRecorderReadFrom(t *testing.T) { tests := map[string]struct { responseWriter responseWriterSpy shouldBuffer bool wantReadFrom bool }{ "buffered plain": { responseWriter: &baseRespWriter{}, shouldBuffer: true, wantReadFrom: false, }, "streamed plain": { responseWriter: &baseRespWriter{}, shouldBuffer: false, wantReadFrom: false, }, "buffered ReadFrom": { responseWriter: &readFromRespWriter{}, shouldBuffer: true, wantReadFrom: false, }, "streamed ReadFrom": { responseWriter: &readFromRespWriter{}, shouldBuffer: false, wantReadFrom: true, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { var buf bytes.Buffer rr := NewResponseRecorder(tt.responseWriter, &buf, func(status int, header http.Header) bool { return tt.shouldBuffer }) const srcData = "boo!" // hides everything but Read, since strings.Reader implements WriteTo it would // take precedence over our ReadFrom. src := struct{ io.Reader }{strings.NewReader(srcData)} if _, err := io.Copy(rr, src); err != nil { t.Errorf("Copy() err = %v", err) } wantStreamed := srcData wantBuffered := "" if tt.shouldBuffer { wantStreamed = "" wantBuffered = srcData } if got := tt.responseWriter.Written(); got != wantStreamed { t.Errorf("streamed data = %q, want %q", got, wantStreamed) } if got := buf.String(); got != wantBuffered { t.Errorf("buffered data = %q, want %q", got, wantBuffered) } if tt.responseWriter.CalledReadFrom() != tt.wantReadFrom { if tt.wantReadFrom { t.Errorf("ReadFrom() should have been called") } else { t.Errorf("ReadFrom() should not have been called") } } }) } } caddy-2.6.2/modules/caddyhttp/reverseproxy/000077500000000000000000000000001435007237400210235ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/reverseproxy/addresses.go000066400000000000000000000101461435007237400233310ustar00rootroot00000000000000// 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 reverseproxy import ( "fmt" "net" "net/url" "strings" "github.com/caddyserver/caddy/v2" ) // parseUpstreamDialAddress parses configuration inputs for // the dial address, including support for a scheme in front // as a shortcut for the port number, and a network type, // for example 'unix' to dial a unix socket. // // TODO: the logic in this function is kind of sensitive, we // need to write tests before making any more changes to it func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) { var network, scheme, host, port string if strings.Contains(upstreamAddr, "://") { // we get a parsing error if a placeholder is specified // so we return a more user-friendly error message instead // to explain what to do instead if strings.Contains(upstreamAddr, "{") { return "", "", fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme") } toURL, err := url.Parse(upstreamAddr) if err != nil { return "", "", fmt.Errorf("parsing upstream URL: %v", err) } // there is currently no way to perform a URL rewrite between choosing // a backend and proxying to it, so we cannot allow extra components // in backend URLs if toURL.Path != "" || toURL.RawQuery != "" || toURL.Fragment != "" { return "", "", fmt.Errorf("for now, URLs for proxy upstreams only support scheme, host, and port components") } // ensure the port and scheme aren't in conflict urlPort := toURL.Port() if toURL.Scheme == "http" && urlPort == "443" { return "", "", fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)") } if toURL.Scheme == "https" && urlPort == "80" { return "", "", fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)") } if toURL.Scheme == "h2c" && urlPort == "443" { return "", "", fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)") } // if port is missing, attempt to infer from scheme if toURL.Port() == "" { var toPort string switch toURL.Scheme { case "", "http", "h2c": toPort = "80" case "https": toPort = "443" } toURL.Host = net.JoinHostPort(toURL.Hostname(), toPort) } scheme, host, port = toURL.Scheme, toURL.Hostname(), toURL.Port() } else { // extract network manually, since caddy.ParseNetworkAddress() will always add one if beforeSlash, afterSlash, slashFound := strings.Cut(upstreamAddr, "/"); slashFound { network = strings.ToLower(strings.TrimSpace(beforeSlash)) upstreamAddr = afterSlash } var err error host, port, err = net.SplitHostPort(upstreamAddr) if err != nil { host = upstreamAddr } // we can assume a port if only a hostname is specified, but use of a // placeholder without a port likely means a port will be filled in if port == "" && !strings.Contains(host, "{") { port = "80" } } // special case network to support both unix and h2c at the same time if network == "unix+h2c" { network = "unix" scheme = "h2c" } // for simplest possible config, we only need to include // the network portion if the user specified one if network != "" { return caddy.JoinNetworkAddress(network, host, port), scheme, nil } // if the host is a placeholder, then we don't want to join with an empty port, // because that would just append an extra ':' at the end of the address. if port == "" && strings.Contains(host, "{") { return host, scheme, nil } return net.JoinHostPort(host, port), scheme, nil } caddy-2.6.2/modules/caddyhttp/reverseproxy/admin.go000066400000000000000000000061531435007237400224470ustar00rootroot00000000000000// 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 reverseproxy import ( "encoding/json" "fmt" "net/http" "github.com/caddyserver/caddy/v2" ) func init() { caddy.RegisterModule(adminUpstreams{}) } // adminUpstreams is a module that provides the // /reverse_proxy/upstreams endpoint for the Caddy admin // API. This allows for checking the health of configured // reverse proxy upstreams in the pool. type adminUpstreams struct{} // upstreamResults holds the status of a particular upstream type upstreamStatus struct { Address string `json:"address"` NumRequests int `json:"num_requests"` Fails int `json:"fails"` } // CaddyModule returns the Caddy module information. func (adminUpstreams) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "admin.api.reverse_proxy", New: func() caddy.Module { return new(adminUpstreams) }, } } // Routes returns a route for the /reverse_proxy/upstreams endpoint. func (al adminUpstreams) Routes() []caddy.AdminRoute { return []caddy.AdminRoute{ { Pattern: "/reverse_proxy/upstreams", Handler: caddy.AdminHandlerFunc(al.handleUpstreams), }, } } // handleUpstreams reports the status of the reverse proxy // upstream pool. func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) error { if r.Method != http.MethodGet { return caddy.APIError{ HTTPStatus: http.StatusMethodNotAllowed, Err: fmt.Errorf("method not allowed"), } } // Prep for a JSON response w.Header().Set("Content-Type", "application/json") enc := json.NewEncoder(w) // Collect the results to respond with results := []upstreamStatus{} // Iterate over the upstream pool (needs to be fast) var rangeErr error hosts.Range(func(key, val any) bool { address, ok := key.(string) if !ok { rangeErr = caddy.APIError{ HTTPStatus: http.StatusInternalServerError, Err: fmt.Errorf("could not type assert upstream address"), } return false } upstream, ok := val.(*Host) if !ok { rangeErr = caddy.APIError{ HTTPStatus: http.StatusInternalServerError, Err: fmt.Errorf("could not type assert upstream struct"), } return false } results = append(results, upstreamStatus{ Address: address, NumRequests: upstream.NumRequests(), Fails: upstream.Fails(), }) return true }) // If an error happened during the range, return it if rangeErr != nil { return rangeErr } err := enc.Encode(results) if err != nil { return caddy.APIError{ HTTPStatus: http.StatusInternalServerError, Err: err, } } return nil } caddy-2.6.2/modules/caddyhttp/reverseproxy/ascii.go000066400000000000000000000034531435007237400224470ustar00rootroot00000000000000// 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. // Most of the code in this file was initially borrowed from the Go // standard library and modified; It had this copyright notice: // Copyright 2021 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Original source, copied because the package was marked internal: // https://github.com/golang/go/blob/5c489514bc5e61ad9b5b07bd7d8ec65d66a0512a/src/net/http/internal/ascii/print.go package reverseproxy // asciiEqualFold is strings.EqualFold, ASCII only. It reports whether s and t // are equal, ASCII-case-insensitively. func asciiEqualFold(s, t string) bool { if len(s) != len(t) { return false } for i := 0; i < len(s); i++ { if asciiLower(s[i]) != asciiLower(t[i]) { return false } } return true } // asciiLower returns the ASCII lowercase version of b. func asciiLower(b byte) byte { if 'A' <= b && b <= 'Z' { return b + ('a' - 'A') } return b } // asciiIsPrint returns whether s is ASCII and printable according to // https://tools.ietf.org/html/rfc20#section-4.2. func asciiIsPrint(s string) bool { for i := 0; i < len(s); i++ { if s[i] < ' ' || s[i] > '~' { return false } } return true } caddy-2.6.2/modules/caddyhttp/reverseproxy/ascii_test.go000066400000000000000000000052721435007237400235070ustar00rootroot00000000000000// 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. // Most of the code in this file was initially borrowed from the Go // standard library and modified; It had this copyright notice: // Copyright 2021 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Original source, copied because the package was marked internal: // https://github.com/golang/go/blob/5c489514bc5e61ad9b5b07bd7d8ec65d66a0512a/src/net/http/internal/ascii/print_test.go package reverseproxy import "testing" func TestEqualFold(t *testing.T) { var tests = []struct { name string a, b string want bool }{ { name: "empty", want: true, }, { name: "simple match", a: "CHUNKED", b: "chunked", want: true, }, { name: "same string", a: "chunked", b: "chunked", want: true, }, { name: "Unicode Kelvin symbol", a: "chunKed", // This "K" is 'KELVIN SIGN' (\u212A) b: "chunked", want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := asciiEqualFold(tt.a, tt.b); got != tt.want { t.Errorf("AsciiEqualFold(%q,%q): got %v want %v", tt.a, tt.b, got, tt.want) } }) } } func TestIsPrint(t *testing.T) { var tests = []struct { name string in string want bool }{ { name: "empty", want: true, }, { name: "ASCII low", in: "This is a space: ' '", want: true, }, { name: "ASCII high", in: "This is a tilde: '~'", want: true, }, { name: "ASCII low non-print", in: "This is a unit separator: \x1F", want: false, }, { name: "Ascii high non-print", in: "This is a Delete: \x7F", want: false, }, { name: "Unicode letter", in: "Today it's 280K outside: it's freezing!", // This "K" is 'KELVIN SIGN' (\u212A) want: false, }, { name: "Unicode emoji", in: "Gophers like 🧀", want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := asciiIsPrint(tt.in); got != tt.want { t.Errorf("IsASCIIPrint(%q): got %v want %v", tt.in, got, tt.want) } }) } } caddy-2.6.2/modules/caddyhttp/reverseproxy/caddyfile.go000066400000000000000000001144331435007237400233040ustar00rootroot00000000000000// 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 reverseproxy import ( "net" "net/http" "reflect" "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/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" "github.com/dustin/go-humanize" ) func init() { httpcaddyfile.RegisterHandlerDirective("reverse_proxy", parseCaddyfile) httpcaddyfile.RegisterHandlerDirective("copy_response", parseCopyResponseCaddyfile) httpcaddyfile.RegisterHandlerDirective("copy_response_headers", parseCopyResponseHeadersCaddyfile) } func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { rp := new(Handler) err := rp.UnmarshalCaddyfile(h.Dispenser) if err != nil { return nil, err } err = rp.FinalizeUnmarshalCaddyfile(h) if err != nil { return nil, err } return rp, nil } // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // reverse_proxy [] [] { // # backends // to // dynamic [...] // // # load balancing // lb_policy [] // lb_retries // lb_try_duration // lb_try_interval // lb_retry_match // // # active health checking // health_uri // health_port // health_interval // health_timeout // health_status // health_body // health_headers { // [] // } // // # passive health checking // fail_duration // max_fails // unhealthy_status // unhealthy_latency // unhealthy_request_count // // # streaming // flush_interval // buffer_requests // buffer_responses // max_buffer_size // // # request manipulation // trusted_proxies [private_ranges] // header_up [+|-] [ []] // header_down [+|-] [ []] // method // rewrite // // # round trip // transport { // ... // } // // # optionally intercept responses from upstream // @name { // status // header [] // } // replace_status [] // handle_response [] { // // // # special directives only available in handle_response // copy_response [] [] { // status // } // copy_response_headers [] { // include // exclude // } // } // } // // Proxy upstream addresses should be network dial addresses such // as `host:port`, or a URL such as `scheme://host:port`. Scheme // and port may be inferred from other parts of the address/URL; if // either are missing, defaults to HTTP. // // The FinalizeUnmarshalCaddyfile method should be called after this // to finalize parsing of "handle_response" blocks, if possible. func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // currently, all backends must use the same scheme/protocol (the // underlying JSON does not yet support per-backend transports) var commonScheme string // we'll wait until the very end of parsing before // validating and encoding the transport var transport http.RoundTripper var transportModuleName string // collect the response matchers defined as subdirectives // prefixed with "@" for use with "handle_response" blocks h.responseMatchers = make(map[string]caddyhttp.ResponseMatcher) // appendUpstream creates an upstream for address and adds // it to the list. If the address starts with "srv+" it is // treated as a SRV-based upstream, and any port will be // dropped. appendUpstream := func(address string) error { isSRV := strings.HasPrefix(address, "srv+") if isSRV { address = strings.TrimPrefix(address, "srv+") } dialAddr, scheme, err := parseUpstreamDialAddress(address) if err != nil { return d.WrapErr(err) } // the underlying JSON does not yet support different // transports (protocols or schemes) to each backend, // so we remember the last one we see and compare them if commonScheme != "" && scheme != commonScheme { return d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'", commonScheme, scheme) } commonScheme = scheme if isSRV { if host, _, err := net.SplitHostPort(dialAddr); err == nil { dialAddr = host } h.Upstreams = append(h.Upstreams, &Upstream{LookupSRV: dialAddr}) } else { h.Upstreams = append(h.Upstreams, &Upstream{Dial: dialAddr}) } return nil } for d.Next() { for _, up := range d.RemainingArgs() { err := appendUpstream(up) if err != nil { return err } } for d.NextBlock(0) { // if the subdirective has an "@" prefix then we // parse it as a response matcher for use with "handle_response" if strings.HasPrefix(d.Val(), matcherPrefix) { err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), h.responseMatchers) if err != nil { return err } continue } switch d.Val() { case "to": args := d.RemainingArgs() if len(args) == 0 { return d.ArgErr() } for _, up := range args { err := appendUpstream(up) if err != nil { return err } } case "dynamic": if !d.NextArg() { return d.ArgErr() } if h.DynamicUpstreams != nil { return d.Err("dynamic upstreams already specified") } dynModule := d.Val() modID := "http.reverse_proxy.upstreams." + dynModule unm, err := caddyfile.UnmarshalModule(d, modID) if err != nil { return err } source, ok := unm.(UpstreamSource) if !ok { return d.Errf("module %s (%T) is not an UpstreamSource", modID, unm) } h.DynamicUpstreamsRaw = caddyconfig.JSONModuleObject(source, "source", dynModule, nil) case "lb_policy": if !d.NextArg() { return d.ArgErr() } if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil { return d.Err("load balancing selection policy already specified") } name := d.Val() modID := "http.reverse_proxy.selection_policies." + name unm, err := caddyfile.UnmarshalModule(d, modID) if err != nil { return err } sel, ok := unm.(Selector) if !ok { return d.Errf("module %s (%T) is not a reverseproxy.Selector", modID, unm) } if h.LoadBalancing == nil { h.LoadBalancing = new(LoadBalancing) } h.LoadBalancing.SelectionPolicyRaw = caddyconfig.JSONModuleObject(sel, "policy", name, nil) case "lb_retries": if !d.NextArg() { return d.ArgErr() } tries, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("bad lb_retries number '%s': %v", d.Val(), err) } if h.LoadBalancing == nil { h.LoadBalancing = new(LoadBalancing) } h.LoadBalancing.Retries = tries case "lb_try_duration": if !d.NextArg() { return d.ArgErr() } if h.LoadBalancing == nil { h.LoadBalancing = new(LoadBalancing) } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad duration value %s: %v", d.Val(), err) } h.LoadBalancing.TryDuration = caddy.Duration(dur) case "lb_try_interval": if !d.NextArg() { return d.ArgErr() } if h.LoadBalancing == nil { h.LoadBalancing = new(LoadBalancing) } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad interval value '%s': %v", d.Val(), err) } h.LoadBalancing.TryInterval = caddy.Duration(dur) case "lb_retry_match": matcherSet, err := caddyhttp.ParseCaddyfileNestedMatcherSet(d) if err != nil { return d.Errf("failed to parse lb_retry_match: %v", err) } if h.LoadBalancing == nil { h.LoadBalancing = new(LoadBalancing) } h.LoadBalancing.RetryMatchRaw = append(h.LoadBalancing.RetryMatchRaw, matcherSet) case "health_uri": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Active == nil { h.HealthChecks.Active = new(ActiveHealthChecks) } h.HealthChecks.Active.URI = d.Val() case "health_path": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Active == nil { h.HealthChecks.Active = new(ActiveHealthChecks) } h.HealthChecks.Active.Path = d.Val() caddy.Log().Named("config.adapter.caddyfile").Warn("the 'health_path' subdirective is deprecated, please use 'health_uri' instead!") case "health_port": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Active == nil { h.HealthChecks.Active = new(ActiveHealthChecks) } portNum, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("bad port number '%s': %v", d.Val(), err) } h.HealthChecks.Active.Port = portNum case "health_headers": healthHeaders := make(http.Header) for nesting := d.Nesting(); d.NextBlock(nesting); { key := d.Val() values := d.RemainingArgs() if len(values) == 0 { values = append(values, "") } healthHeaders[key] = values } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Active == nil { h.HealthChecks.Active = new(ActiveHealthChecks) } h.HealthChecks.Active.Headers = healthHeaders case "health_interval": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Active == nil { h.HealthChecks.Active = new(ActiveHealthChecks) } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad interval value %s: %v", d.Val(), err) } h.HealthChecks.Active.Interval = caddy.Duration(dur) case "health_timeout": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Active == nil { h.HealthChecks.Active = new(ActiveHealthChecks) } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value %s: %v", d.Val(), err) } h.HealthChecks.Active.Timeout = caddy.Duration(dur) case "health_status": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Active == nil { h.HealthChecks.Active = new(ActiveHealthChecks) } val := d.Val() if len(val) == 3 && strings.HasSuffix(val, "xx") { val = val[:1] } statusNum, err := strconv.Atoi(val) if err != nil { return d.Errf("bad status value '%s': %v", d.Val(), err) } h.HealthChecks.Active.ExpectStatus = statusNum case "health_body": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Active == nil { h.HealthChecks.Active = new(ActiveHealthChecks) } h.HealthChecks.Active.ExpectBody = d.Val() case "max_fails": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Passive == nil { h.HealthChecks.Passive = new(PassiveHealthChecks) } maxFails, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("invalid maximum fail count '%s': %v", d.Val(), err) } h.HealthChecks.Passive.MaxFails = maxFails case "fail_duration": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Passive == nil { h.HealthChecks.Passive = new(PassiveHealthChecks) } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad duration value '%s': %v", d.Val(), err) } h.HealthChecks.Passive.FailDuration = caddy.Duration(dur) case "unhealthy_request_count": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Passive == nil { h.HealthChecks.Passive = new(PassiveHealthChecks) } maxConns, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("invalid maximum connection count '%s': %v", d.Val(), err) } h.HealthChecks.Passive.UnhealthyRequestCount = maxConns case "unhealthy_status": args := d.RemainingArgs() if len(args) == 0 { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Passive == nil { h.HealthChecks.Passive = new(PassiveHealthChecks) } for _, arg := range args { if len(arg) == 3 && strings.HasSuffix(arg, "xx") { arg = arg[:1] } statusNum, err := strconv.Atoi(arg) if err != nil { return d.Errf("bad status value '%s': %v", d.Val(), err) } h.HealthChecks.Passive.UnhealthyStatus = append(h.HealthChecks.Passive.UnhealthyStatus, statusNum) } case "unhealthy_latency": if !d.NextArg() { return d.ArgErr() } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) } if h.HealthChecks.Passive == nil { h.HealthChecks.Passive = new(PassiveHealthChecks) } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad duration value '%s': %v", d.Val(), err) } h.HealthChecks.Passive.UnhealthyLatency = caddy.Duration(dur) case "flush_interval": if !d.NextArg() { return d.ArgErr() } if fi, err := strconv.Atoi(d.Val()); err == nil { h.FlushInterval = caddy.Duration(fi) } else { dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad duration value '%s': %v", d.Val(), err) } h.FlushInterval = caddy.Duration(dur) } case "buffer_requests": if d.NextArg() { return d.ArgErr() } h.BufferRequests = true case "buffer_responses": if d.NextArg() { return d.ArgErr() } h.BufferResponses = true case "max_buffer_size": if !d.NextArg() { return d.ArgErr() } size, err := humanize.ParseBytes(d.Val()) if err != nil { return d.Errf("invalid byte size '%s': %v", d.Val(), err) } if d.NextArg() { return d.ArgErr() } h.MaxBufferSize = int64(size) case "trusted_proxies": for d.NextArg() { if d.Val() == "private_ranges" { h.TrustedProxies = append(h.TrustedProxies, []string{ "192.168.0.0/16", "172.16.0.0/12", "10.0.0.0/8", "127.0.0.1/8", "fd00::/8", "::1", }...) continue } h.TrustedProxies = append(h.TrustedProxies, d.Val()) } case "header_up": var err error if h.Headers == nil { h.Headers = new(headers.Handler) } if h.Headers.Request == nil { h.Headers.Request = new(headers.HeaderOps) } args := d.RemainingArgs() switch len(args) { case 1: err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], "", "") case 2: // some lint checks, I guess if strings.EqualFold(args[0], "host") && (args[1] == "{hostport}" || args[1] == "{http.request.hostport}") { caddy.Log().Named("caddyfile").Warn("Unnecessary header_up Host: the reverse proxy's default behavior is to pass headers to the upstream") } if strings.EqualFold(args[0], "x-forwarded-for") && (args[1] == "{remote}" || args[1] == "{http.request.remote}" || args[1] == "{remote_host}" || args[1] == "{http.request.remote.host}") { caddy.Log().Named("caddyfile").Warn("Unnecessary header_up X-Forwarded-For: the reverse proxy's default behavior is to pass headers to the upstream") } if strings.EqualFold(args[0], "x-forwarded-proto") && (args[1] == "{scheme}" || args[1] == "{http.request.scheme}") { caddy.Log().Named("caddyfile").Warn("Unnecessary header_up X-Forwarded-Proto: the reverse proxy's default behavior is to pass headers to the upstream") } if strings.EqualFold(args[0], "x-forwarded-host") && (args[1] == "{host}" || args[1] == "{http.request.host}" || args[1] == "{hostport}" || args[1] == "{http.request.hostport}") { caddy.Log().Named("caddyfile").Warn("Unnecessary header_up X-Forwarded-Host: the reverse proxy's default behavior is to pass headers to the upstream") } err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], args[1], "") case 3: err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], args[1], args[2]) default: return d.ArgErr() } if err != nil { return d.Err(err.Error()) } case "header_down": var err error if h.Headers == nil { h.Headers = new(headers.Handler) } if h.Headers.Response == nil { h.Headers.Response = &headers.RespHeaderOps{ HeaderOps: new(headers.HeaderOps), } } args := d.RemainingArgs() switch len(args) { case 1: err = headers.CaddyfileHeaderOp(h.Headers.Response.HeaderOps, args[0], "", "") case 2: err = headers.CaddyfileHeaderOp(h.Headers.Response.HeaderOps, args[0], args[1], "") case 3: err = headers.CaddyfileHeaderOp(h.Headers.Response.HeaderOps, args[0], args[1], args[2]) default: return d.ArgErr() } if err != nil { return d.Err(err.Error()) } case "method": if !d.NextArg() { return d.ArgErr() } if h.Rewrite == nil { h.Rewrite = &rewrite.Rewrite{} } h.Rewrite.Method = d.Val() if d.NextArg() { return d.ArgErr() } case "rewrite": if !d.NextArg() { return d.ArgErr() } if h.Rewrite == nil { h.Rewrite = &rewrite.Rewrite{} } h.Rewrite.URI = d.Val() if d.NextArg() { return d.ArgErr() } case "transport": if !d.NextArg() { return d.ArgErr() } if h.TransportRaw != nil { return d.Err("transport already specified") } transportModuleName = d.Val() modID := "http.reverse_proxy.transport." + transportModuleName unm, err := caddyfile.UnmarshalModule(d, modID) if err != nil { return err } rt, ok := unm.(http.RoundTripper) if !ok { return d.Errf("module %s (%T) is not a RoundTripper", modID, unm) } transport = rt case "handle_response": // delegate the parsing of handle_response to the caller, // since we need the httpcaddyfile.Helper to parse subroutes. // See h.FinalizeUnmarshalCaddyfile h.handleResponseSegments = append(h.handleResponseSegments, d.NewFromNextSegment()) case "replace_status": args := d.RemainingArgs() if len(args) != 1 && len(args) != 2 { return d.Errf("must have one or two arguments: an optional response matcher, and a status code") } responseHandler := caddyhttp.ResponseHandler{} if len(args) == 2 { if !strings.HasPrefix(args[0], matcherPrefix) { return d.Errf("must use a named response matcher, starting with '@'") } foundMatcher, ok := h.responseMatchers[args[0]] if !ok { return d.Errf("no named response matcher defined with name '%s'", args[0][1:]) } responseHandler.Match = &foundMatcher responseHandler.StatusCode = caddyhttp.WeakString(args[1]) } else if len(args) == 1 { responseHandler.StatusCode = caddyhttp.WeakString(args[0]) } // make sure there's no block, cause it doesn't make sense if d.NextBlock(1) { return d.Errf("cannot define routes for 'replace_status', use 'handle_response' instead.") } h.HandleResponse = append( h.HandleResponse, responseHandler, ) default: return d.Errf("unrecognized subdirective %s", d.Val()) } } } // if the scheme inferred from the backends' addresses is // HTTPS, we will need a non-nil transport to enable TLS, // or if H2C, to set the transport versions. if (commonScheme == "https" || commonScheme == "h2c") && transport == nil { transport = new(HTTPTransport) transportModuleName = "http" } // verify transport configuration, and finally encode it if transport != nil { if te, ok := transport.(TLSTransport); ok { if commonScheme == "https" && !te.TLSEnabled() { err := te.EnableTLS(new(TLSConfig)) if err != nil { return err } } if commonScheme == "http" && te.TLSEnabled() { return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)") } if te, ok := transport.(*HTTPTransport); ok && commonScheme == "h2c" { te.Versions = []string{"h2c", "2"} } } else if commonScheme == "https" { return d.Errf("upstreams are configured for HTTPS but transport module does not support TLS: %T", transport) } // no need to encode empty default transport if !reflect.DeepEqual(transport, new(HTTPTransport)) { h.TransportRaw = caddyconfig.JSONModuleObject(transport, "protocol", transportModuleName, nil) } } return nil } // FinalizeUnmarshalCaddyfile finalizes the Caddyfile parsing which // requires having an httpcaddyfile.Helper to function, to parse subroutes. func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error { for _, d := range h.handleResponseSegments { // consume the "handle_response" token d.Next() args := d.RemainingArgs() // TODO: Remove this check at some point in the future if len(args) == 2 { return d.Errf("configuring 'handle_response' for status code replacement is no longer supported. Use 'replace_status' instead.") } if len(args) > 1 { return d.Errf("too many arguments for 'handle_response': %s", args) } var matcher *caddyhttp.ResponseMatcher if len(args) == 1 { // the first arg should always be a matcher. if !strings.HasPrefix(args[0], matcherPrefix) { return d.Errf("must use a named response matcher, starting with '@'") } foundMatcher, ok := h.responseMatchers[args[0]] if !ok { return d.Errf("no named response matcher defined with name '%s'", args[0][1:]) } matcher = &foundMatcher } // parse the block as routes handler, err := httpcaddyfile.ParseSegmentAsSubroute(helper.WithDispenser(d.NewFromNextSegment())) if err != nil { return err } subroute, ok := handler.(*caddyhttp.Subroute) if !ok { return helper.Errf("segment was not parsed as a subroute") } h.HandleResponse = append( h.HandleResponse, caddyhttp.ResponseHandler{ Match: matcher, Routes: subroute.Routes, }, ) } // move the handle_response entries without a matcher to the end. // we can't use sort.SliceStable because it will reorder the rest of the // entries which may be undesirable because we don't have a good // heuristic to use for sorting. withoutMatchers := []caddyhttp.ResponseHandler{} withMatchers := []caddyhttp.ResponseHandler{} for _, hr := range h.HandleResponse { if hr.Match == nil { withoutMatchers = append(withoutMatchers, hr) } else { withMatchers = append(withMatchers, hr) } } h.HandleResponse = append(withMatchers, withoutMatchers...) // clean up the bits we only needed for adapting h.handleResponseSegments = nil h.responseMatchers = nil return nil } // UnmarshalCaddyfile deserializes Caddyfile tokens into h. // // transport http { // read_buffer // write_buffer // max_response_header // dial_timeout // dial_fallback_delay // response_header_timeout // expect_continue_timeout // resolvers // tls // tls_client_auth | // tls_insecure_skip_verify // tls_timeout // tls_trusted_ca_certs // tls_server_name // tls_renegotiation // tls_except_ports // keepalive [off|] // keepalive_interval // keepalive_idle_conns // keepalive_idle_conns_per_host // versions // compression off // max_conns_per_host // max_idle_conns_per_host // } func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { for d.NextBlock(0) { switch d.Val() { case "read_buffer": if !d.NextArg() { return d.ArgErr() } size, err := humanize.ParseBytes(d.Val()) if err != nil { return d.Errf("invalid read buffer size '%s': %v", d.Val(), err) } h.ReadBufferSize = int(size) case "write_buffer": if !d.NextArg() { return d.ArgErr() } size, err := humanize.ParseBytes(d.Val()) if err != nil { return d.Errf("invalid write buffer size '%s': %v", d.Val(), err) } h.WriteBufferSize = int(size) case "read_timeout": if !d.NextArg() { return d.ArgErr() } timeout, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("invalid read timeout duration '%s': %v", d.Val(), err) } h.ReadTimeout = caddy.Duration(timeout) case "write_timeout": if !d.NextArg() { return d.ArgErr() } timeout, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("invalid write timeout duration '%s': %v", d.Val(), err) } h.WriteTimeout = caddy.Duration(timeout) case "max_response_header": if !d.NextArg() { return d.ArgErr() } size, err := humanize.ParseBytes(d.Val()) if err != nil { return d.Errf("invalid max response header size '%s': %v", d.Val(), err) } h.MaxResponseHeaderSize = int64(size) case "dial_timeout": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value '%s': %v", d.Val(), err) } h.DialTimeout = caddy.Duration(dur) case "dial_fallback_delay": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad fallback delay value '%s': %v", d.Val(), err) } h.FallbackDelay = caddy.Duration(dur) case "response_header_timeout": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value '%s': %v", d.Val(), err) } h.ResponseHeaderTimeout = caddy.Duration(dur) case "expect_continue_timeout": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value '%s': %v", d.Val(), err) } h.ExpectContinueTimeout = caddy.Duration(dur) case "resolvers": if h.Resolver == nil { h.Resolver = new(UpstreamResolver) } h.Resolver.Addresses = d.RemainingArgs() if len(h.Resolver.Addresses) == 0 { return d.Errf("must specify at least one resolver address") } case "tls": if h.TLS == nil { h.TLS = new(TLSConfig) } case "tls_client_auth": if h.TLS == nil { h.TLS = new(TLSConfig) } args := d.RemainingArgs() switch len(args) { case 1: h.TLS.ClientCertificateAutomate = args[0] case 2: h.TLS.ClientCertificateFile = args[0] h.TLS.ClientCertificateKeyFile = args[1] default: return d.ArgErr() } case "tls_insecure_skip_verify": if d.NextArg() { return d.ArgErr() } if h.TLS == nil { h.TLS = new(TLSConfig) } h.TLS.InsecureSkipVerify = true case "tls_timeout": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value '%s': %v", d.Val(), err) } if h.TLS == nil { h.TLS = new(TLSConfig) } h.TLS.HandshakeTimeout = caddy.Duration(dur) case "tls_trusted_ca_certs": args := d.RemainingArgs() if len(args) == 0 { return d.ArgErr() } if h.TLS == nil { h.TLS = new(TLSConfig) } h.TLS.RootCAPEMFiles = args case "tls_server_name": if !d.NextArg() { return d.ArgErr() } if h.TLS == nil { h.TLS = new(TLSConfig) } h.TLS.ServerName = d.Val() case "tls_renegotiation": if h.TLS == nil { h.TLS = new(TLSConfig) } if !d.NextArg() { return d.ArgErr() } switch renegotiation := d.Val(); renegotiation { case "never", "once", "freely": h.TLS.Renegotiation = renegotiation default: return d.ArgErr() } case "tls_except_ports": if h.TLS == nil { h.TLS = new(TLSConfig) } h.TLS.ExceptPorts = d.RemainingArgs() if len(h.TLS.ExceptPorts) == 0 { return d.ArgErr() } case "keepalive": if !d.NextArg() { return d.ArgErr() } if h.KeepAlive == nil { h.KeepAlive = new(KeepAlive) } if d.Val() == "off" { var disable bool h.KeepAlive.Enabled = &disable break } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad duration value '%s': %v", d.Val(), err) } h.KeepAlive.IdleConnTimeout = caddy.Duration(dur) case "keepalive_interval": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad interval value '%s': %v", d.Val(), err) } if h.KeepAlive == nil { h.KeepAlive = new(KeepAlive) } h.KeepAlive.ProbeInterval = caddy.Duration(dur) case "keepalive_idle_conns": if !d.NextArg() { return d.ArgErr() } num, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("bad integer value '%s': %v", d.Val(), err) } if h.KeepAlive == nil { h.KeepAlive = new(KeepAlive) } h.KeepAlive.MaxIdleConns = num case "keepalive_idle_conns_per_host": if !d.NextArg() { return d.ArgErr() } num, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("bad integer value '%s': %v", d.Val(), err) } if h.KeepAlive == nil { h.KeepAlive = new(KeepAlive) } h.KeepAlive.MaxIdleConnsPerHost = num case "versions": h.Versions = d.RemainingArgs() if len(h.Versions) == 0 { return d.ArgErr() } case "compression": if d.NextArg() { if d.Val() == "off" { var disable bool h.Compression = &disable } } case "max_conns_per_host": if !d.NextArg() { return d.ArgErr() } num, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("bad integer value '%s': %v", d.Val(), err) } h.MaxConnsPerHost = num default: return d.Errf("unrecognized subdirective %s", d.Val()) } } } return nil } func parseCopyResponseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { crh := new(CopyResponseHandler) err := crh.UnmarshalCaddyfile(h.Dispenser) if err != nil { return nil, err } return crh, nil } // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // copy_response [] [] { // status // } func (h *CopyResponseHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { args := d.RemainingArgs() if len(args) == 1 { if num, err := strconv.Atoi(args[0]); err == nil && num > 0 { h.StatusCode = caddyhttp.WeakString(args[0]) break } } for d.NextBlock(0) { switch d.Val() { case "status": if !d.NextArg() { return d.ArgErr() } h.StatusCode = caddyhttp.WeakString(d.Val()) default: return d.Errf("unrecognized subdirective '%s'", d.Val()) } } } return nil } func parseCopyResponseHeadersCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { crh := new(CopyResponseHeadersHandler) err := crh.UnmarshalCaddyfile(h.Dispenser) if err != nil { return nil, err } return crh, nil } // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // copy_response_headers [] { // include // exclude // } func (h *CopyResponseHeadersHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { args := d.RemainingArgs() if len(args) > 0 { return d.ArgErr() } for d.NextBlock(0) { switch d.Val() { case "include": h.Include = append(h.Include, d.RemainingArgs()...) case "exclude": h.Exclude = append(h.Exclude, d.RemainingArgs()...) default: return d.Errf("unrecognized subdirective '%s'", d.Val()) } } } return nil } // UnmarshalCaddyfile deserializes Caddyfile tokens into h. // // dynamic srv [] { // service // proto // name // refresh // resolvers // dial_timeout // dial_fallback_delay // } func (u *SRVUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { args := d.RemainingArgs() if len(args) > 1 { return d.ArgErr() } if len(args) > 0 { u.Name = args[0] } for d.NextBlock(0) { switch d.Val() { case "service": if !d.NextArg() { return d.ArgErr() } if u.Service != "" { return d.Errf("srv service has already been specified") } u.Service = d.Val() case "proto": if !d.NextArg() { return d.ArgErr() } if u.Proto != "" { return d.Errf("srv proto has already been specified") } u.Proto = d.Val() case "name": if !d.NextArg() { return d.ArgErr() } if u.Name != "" { return d.Errf("srv name has already been specified") } u.Name = d.Val() case "refresh": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("parsing refresh interval duration: %v", err) } u.Refresh = caddy.Duration(dur) case "resolvers": if u.Resolver == nil { u.Resolver = new(UpstreamResolver) } u.Resolver.Addresses = d.RemainingArgs() if len(u.Resolver.Addresses) == 0 { return d.Errf("must specify at least one resolver address") } case "dial_timeout": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value '%s': %v", d.Val(), err) } u.DialTimeout = caddy.Duration(dur) case "dial_fallback_delay": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad delay value '%s': %v", d.Val(), err) } u.FallbackDelay = caddy.Duration(dur) default: return d.Errf("unrecognized srv option '%s'", d.Val()) } } } return nil } // UnmarshalCaddyfile deserializes Caddyfile tokens into h. // // dynamic a [ // port // refresh // resolvers // dial_timeout // dial_fallback_delay // } func (u *AUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { args := d.RemainingArgs() if len(args) > 2 { return d.ArgErr() } if len(args) > 0 { u.Name = args[0] if len(args) == 2 { u.Port = args[1] } } for d.NextBlock(0) { switch d.Val() { case "name": if !d.NextArg() { return d.ArgErr() } if u.Name != "" { return d.Errf("a name has already been specified") } u.Name = d.Val() case "port": if !d.NextArg() { return d.ArgErr() } if u.Port != "" { return d.Errf("a port has already been specified") } u.Port = d.Val() case "refresh": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("parsing refresh interval duration: %v", err) } u.Refresh = caddy.Duration(dur) case "resolvers": if u.Resolver == nil { u.Resolver = new(UpstreamResolver) } u.Resolver.Addresses = d.RemainingArgs() if len(u.Resolver.Addresses) == 0 { return d.Errf("must specify at least one resolver address") } case "dial_timeout": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value '%s': %v", d.Val(), err) } u.DialTimeout = caddy.Duration(dur) case "dial_fallback_delay": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad delay value '%s': %v", d.Val(), err) } u.FallbackDelay = caddy.Duration(dur) default: return d.Errf("unrecognized srv option '%s'", d.Val()) } } } return nil } // UnmarshalCaddyfile deserializes Caddyfile tokens into h. // // dynamic multi { // [...] // } func (u *MultiUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { return d.ArgErr() } for nesting := d.Nesting(); d.NextBlock(nesting); { dynModule := d.Val() modID := "http.reverse_proxy.upstreams." + dynModule unm, err := caddyfile.UnmarshalModule(d, modID) if err != nil { return err } source, ok := unm.(UpstreamSource) if !ok { return d.Errf("module %s (%T) is not an UpstreamSource", modID, unm) } u.SourcesRaw = append(u.SourcesRaw, caddyconfig.JSONModuleObject(source, "source", dynModule, nil)) } } return nil } const matcherPrefix = "@" // Interface guards var ( _ caddyfile.Unmarshaler = (*Handler)(nil) _ caddyfile.Unmarshaler = (*HTTPTransport)(nil) _ caddyfile.Unmarshaler = (*SRVUpstreams)(nil) _ caddyfile.Unmarshaler = (*AUpstreams)(nil) _ caddyfile.Unmarshaler = (*MultiUpstreams)(nil) ) caddy-2.6.2/modules/caddyhttp/reverseproxy/command.go000066400000000000000000000151061435007237400227730ustar00rootroot00000000000000// 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 reverseproxy import ( "encoding/json" "flag" "fmt" "net/http" "strconv" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddytls" "go.uber.org/zap" ) func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "reverse-proxy", Func: cmdReverseProxy, Usage: "[--from ] [--to ] [--change-host-header]", Short: "A quick and production-ready reverse proxy", Long: ` A simple but production-ready reverse proxy. Useful for quick deployments, demos, and development. Simply shuttles HTTP(S) traffic from the --from address to the --to address. Multiple --to addresses may be specified by repeating the flag. Unless otherwise specified in the addresses, the --from address will be assumed to be HTTPS if a hostname is given, and the --to address will be assumed to be HTTP. If the --from address has a host or IP, Caddy will attempt to serve the proxy over HTTPS with a certificate (unless overridden by the HTTP scheme or port). If --change-host-header is set, the Host header on the request will be modified from its original incoming value to the address of the upstream. (Otherwise, by default, all incoming headers are passed through unmodified.) `, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("reverse-proxy", flag.ExitOnError) fs.String("from", "localhost", "Address on which to receive traffic") fs.Var(&reverseProxyCmdTo, "to", "Upstream address(es) to which traffic should be sent") fs.Bool("change-host-header", false, "Set upstream Host header to address of upstream") fs.Bool("insecure", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING SSL CERTIFICATES!)") fs.Bool("internal-certs", false, "Use internal CA for issuing certs") fs.Bool("debug", false, "Enable verbose debug logs") return fs }(), }) } func cmdReverseProxy(fs caddycmd.Flags) (int, error) { caddy.TrapSignals() from := fs.String("from") changeHost := fs.Bool("change-host-header") insecure := fs.Bool("insecure") internalCerts := fs.Bool("internal-certs") debug := fs.Bool("debug") httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort) httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort) if len(reverseProxyCmdTo) == 0 { return caddy.ExitCodeFailedStartup, fmt.Errorf("--to is required") } // set up the downstream address; assume missing information from given parts fromAddr, err := httpcaddyfile.ParseAddress(from) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid downstream address %s: %v", from, err) } if fromAddr.Path != "" { return caddy.ExitCodeFailedStartup, fmt.Errorf("paths are not allowed: %s", from) } if fromAddr.Scheme == "" { if fromAddr.Port == httpPort || fromAddr.Host == "" { fromAddr.Scheme = "http" } else { fromAddr.Scheme = "https" } } if fromAddr.Port == "" { if fromAddr.Scheme == "http" { fromAddr.Port = httpPort } else if fromAddr.Scheme == "https" { fromAddr.Port = httpsPort } } // set up the upstream address; assume missing information from given parts // mixing schemes isn't supported, so use first defined (if available) toAddresses := make([]string, len(reverseProxyCmdTo)) var toScheme string for i, toLoc := range reverseProxyCmdTo { addr, scheme, err := parseUpstreamDialAddress(toLoc) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err) } if scheme != "" && toScheme == "" { toScheme = scheme } toAddresses[i] = addr } // proceed to build the handler and server ht := HTTPTransport{} if toScheme == "https" { ht.TLS = new(TLSConfig) if insecure { ht.TLS.InsecureSkipVerify = true } } upstreamPool := UpstreamPool{} for _, toAddr := range toAddresses { upstreamPool = append(upstreamPool, &Upstream{ Dial: toAddr, }) } handler := Handler{ TransportRaw: caddyconfig.JSONModuleObject(ht, "protocol", "http", nil), Upstreams: upstreamPool, } if changeHost { handler.Headers = &headers.Handler{ Request: &headers.HeaderOps{ Set: http.Header{ "Host": []string{"{http.reverse_proxy.upstream.hostport}"}, }, }, } } route := caddyhttp.Route{ HandlersRaw: []json.RawMessage{ caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", nil), }, } if fromAddr.Host != "" { route.MatcherSetsRaw = []caddy.ModuleMap{ { "host": caddyconfig.JSON(caddyhttp.MatchHost{fromAddr.Host}, nil), }, } } server := &caddyhttp.Server{ Routes: caddyhttp.RouteList{route}, Listen: []string{":" + fromAddr.Port}, } httpApp := caddyhttp.App{ Servers: map[string]*caddyhttp.Server{"proxy": server}, } appsRaw := caddy.ModuleMap{ "http": caddyconfig.JSON(httpApp, nil), } if internalCerts && fromAddr.Host != "" { tlsApp := caddytls.TLS{ Automation: &caddytls.AutomationConfig{ Policies: []*caddytls.AutomationPolicy{{ Subjects: []string{fromAddr.Host}, IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)}, }}, }, } appsRaw["tls"] = caddyconfig.JSON(tlsApp, nil) } var false bool cfg := &caddy.Config{ Admin: &caddy.AdminConfig{Disabled: true, Config: &caddy.ConfigSettings{ Persist: &false, }, }, AppsRaw: appsRaw, } if debug { cfg.Logging = &caddy.Logging{ Logs: map[string]*caddy.CustomLog{ "default": {Level: zap.DebugLevel.CapitalString()}, }, } } err = caddy.Run(cfg) if err != nil { return caddy.ExitCodeFailedStartup, err } for _, to := range toAddresses { fmt.Printf("Caddy proxying %s -> %s\n", fromAddr.String(), to) } if len(toAddresses) > 1 { fmt.Println("Load balancing policy: random") } select {} } // reverseProxyCmdTo holds the parsed values from repeated use of the --to flag. var reverseProxyCmdTo caddycmd.StringSlice caddy-2.6.2/modules/caddyhttp/reverseproxy/copyresponse.go000066400000000000000000000141641435007237400241110ustar00rootroot00000000000000// 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 reverseproxy import ( "fmt" "net/http" "strconv" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { caddy.RegisterModule(CopyResponseHandler{}) caddy.RegisterModule(CopyResponseHeadersHandler{}) } // CopyResponseHandler is a special HTTP handler which may // only be used within reverse_proxy's handle_response routes, // to copy the proxy response. EXPERIMENTAL, subject to change. type CopyResponseHandler struct { // To write the upstream response's body but with a different // status code, set this field to the desired status code. StatusCode caddyhttp.WeakString `json:"status_code,omitempty"` ctx caddy.Context } // CaddyModule returns the Caddy module information. func (CopyResponseHandler) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.copy_response", New: func() caddy.Module { return new(CopyResponseHandler) }, } } // Provision ensures that h is set up properly before use. func (h *CopyResponseHandler) Provision(ctx caddy.Context) error { h.ctx = ctx return nil } // ServeHTTP implements the Handler interface. func (h CopyResponseHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, _ caddyhttp.Handler) error { repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) hrc, ok := req.Context().Value(proxyHandleResponseContextCtxKey).(*handleResponseContext) // don't allow this to be used outside of handle_response routes if !ok { return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("cannot use 'copy_response' outside of reverse_proxy's handle_response routes")) } // allow a custom status code to be written; otherwise the // status code from the upstream response is written if codeStr := h.StatusCode.String(); codeStr != "" { intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, "")) if err != nil { return caddyhttp.Error(http.StatusInternalServerError, err) } hrc.response.StatusCode = intVal } // make sure the reverse_proxy handler doesn't try to call // finalizeResponse again after we've already done it here. hrc.isFinalized = true // write the response return hrc.handler.finalizeResponse(rw, req, hrc.response, repl, hrc.start, hrc.logger) } // CopyResponseHeadersHandler is a special HTTP handler which may // only be used within reverse_proxy's handle_response routes, // to copy headers from the proxy response. EXPERIMENTAL; // subject to change. type CopyResponseHeadersHandler struct { // A list of header fields to copy from the response. // Cannot be defined at the same time as Exclude. Include []string `json:"include,omitempty"` // A list of header fields to skip copying from the response. // Cannot be defined at the same time as Include. Exclude []string `json:"exclude,omitempty"` includeMap map[string]struct{} excludeMap map[string]struct{} ctx caddy.Context } // CaddyModule returns the Caddy module information. func (CopyResponseHeadersHandler) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.copy_response_headers", New: func() caddy.Module { return new(CopyResponseHeadersHandler) }, } } // Validate ensures the h's configuration is valid. func (h *CopyResponseHeadersHandler) Validate() error { if len(h.Exclude) > 0 && len(h.Include) > 0 { return fmt.Errorf("cannot define both 'exclude' and 'include' lists at the same time") } return nil } // Provision ensures that h is set up properly before use. func (h *CopyResponseHeadersHandler) Provision(ctx caddy.Context) error { h.ctx = ctx // Optimize the include list by converting it to a map if len(h.Include) > 0 { h.includeMap = map[string]struct{}{} } for _, field := range h.Include { h.includeMap[http.CanonicalHeaderKey(field)] = struct{}{} } // Optimize the exclude list by converting it to a map if len(h.Exclude) > 0 { h.excludeMap = map[string]struct{}{} } for _, field := range h.Exclude { h.excludeMap[http.CanonicalHeaderKey(field)] = struct{}{} } return nil } // ServeHTTP implements the Handler interface. func (h CopyResponseHeadersHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next caddyhttp.Handler) error { hrc, ok := req.Context().Value(proxyHandleResponseContextCtxKey).(*handleResponseContext) // don't allow this to be used outside of handle_response routes if !ok { return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("cannot use 'copy_response_headers' outside of reverse_proxy's handle_response routes")) } for field, values := range hrc.response.Header { // Check the include list first, skip // the header if it's _not_ in this list. if len(h.includeMap) > 0 { if _, ok := h.includeMap[field]; !ok { continue } } // Then, check the exclude list, skip // the header if it _is_ in this list. if len(h.excludeMap) > 0 { if _, ok := h.excludeMap[field]; ok { continue } } // Copy all the values for the header. for _, value := range values { rw.Header().Add(field, value) } } return next.ServeHTTP(rw, req) } // Interface guards var ( _ caddyhttp.MiddlewareHandler = (*CopyResponseHandler)(nil) _ caddyfile.Unmarshaler = (*CopyResponseHandler)(nil) _ caddy.Provisioner = (*CopyResponseHandler)(nil) _ caddyhttp.MiddlewareHandler = (*CopyResponseHeadersHandler)(nil) _ caddyfile.Unmarshaler = (*CopyResponseHeadersHandler)(nil) _ caddy.Provisioner = (*CopyResponseHeadersHandler)(nil) _ caddy.Validator = (*CopyResponseHeadersHandler)(nil) ) caddy-2.6.2/modules/caddyhttp/reverseproxy/fastcgi/000077500000000000000000000000001435007237400224435ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go000066400000000000000000000304721435007237400247240ustar00rootroot00000000000000// 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 fastcgi import ( "encoding/json" "net/http" "strconv" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver" "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" ) func init() { httpcaddyfile.RegisterDirective("php_fastcgi", parsePHPFastCGI) } // UnmarshalCaddyfile deserializes Caddyfile tokens into h. // // transport fastcgi { // root // split // env // resolve_root_symlink // dial_timeout // read_timeout // write_timeout // capture_stderr // } func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { for d.NextBlock(0) { switch d.Val() { case "root": if !d.NextArg() { return d.ArgErr() } t.Root = d.Val() case "split": t.SplitPath = d.RemainingArgs() if len(t.SplitPath) == 0 { return d.ArgErr() } case "env": args := d.RemainingArgs() if len(args) != 2 { return d.ArgErr() } if t.EnvVars == nil { t.EnvVars = make(map[string]string) } t.EnvVars[args[0]] = args[1] case "resolve_root_symlink": if d.NextArg() { return d.ArgErr() } t.ResolveRootSymlink = true case "dial_timeout": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value %s: %v", d.Val(), err) } t.DialTimeout = caddy.Duration(dur) case "read_timeout": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value %s: %v", d.Val(), err) } t.ReadTimeout = caddy.Duration(dur) case "write_timeout": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value %s: %v", d.Val(), err) } t.WriteTimeout = caddy.Duration(dur) case "capture_stderr": if d.NextArg() { return d.ArgErr() } t.CaptureStderr = true default: return d.Errf("unrecognized subdirective %s", d.Val()) } } } return nil } // parsePHPFastCGI parses the php_fastcgi directive, which has the same syntax // as the reverse_proxy directive (in fact, the reverse_proxy's directive // Unmarshaler is invoked by this function) but the resulting proxy is specially // configured for most™️ PHP apps over FastCGI. A line such as this: // // php_fastcgi localhost:7777 // // is equivalent to a route consisting of: // // # Add trailing slash for directory requests // @canonicalPath { // file {path}/index.php // not path */ // } // redir @canonicalPath {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 localhost:7777 { // transport fastcgi { // split .php // } // } // // Thus, this directive produces multiple handlers, each with a different // matcher because multiple consecutive handlers are necessary to support // the common PHP use case. If this "common" config is not compatible // with a user's PHP requirements, they can use a manual approach based // on the example above to configure it precisely as they need. // // If a matcher is specified by the user, for example: // // php_fastcgi /subpath localhost:7777 // // then the resulting handlers are wrapped in a subroute that uses the // user's matcher as a prerequisite to enter the subroute. In other // words, the directive's matcher is necessary, but not sufficient. func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { if !h.Next() { return nil, h.ArgErr() } // set up the transport for FastCGI, and specifically PHP fcgiTransport := Transport{} // set up the set of file extensions allowed to execute PHP code extensions := []string{".php"} // set the default index file for the try_files rewrites indexFile := "index.php" // set up for explicitly overriding try_files tryFiles := []string{} // if the user specified a matcher token, use that // matcher in a route that wraps both of our routes; // either way, strip the matcher token and pass // the remaining tokens to the unmarshaler so that // we can gain the rest of the reverse_proxy syntax userMatcherSet, err := h.ExtractMatcherSet() if err != nil { return nil, err } // make a new dispenser from the remaining tokens so that we // can reset the dispenser back to this point for the // reverse_proxy unmarshaler to read from it as well dispenser := h.NewFromNextSegment() // read the subdirectives that we allow as overrides to // the php_fastcgi shortcut // NOTE: we delete the tokens as we go so that the reverse_proxy // unmarshal doesn't see these subdirectives which it cannot handle for dispenser.Next() { for dispenser.NextBlock(0) { // ignore any sub-subdirectives that might // have the same name somewhere within // the reverse_proxy passthrough tokens if dispenser.Nesting() != 1 { continue } // parse the php_fastcgi subdirectives switch dispenser.Val() { case "root": if !dispenser.NextArg() { return nil, dispenser.ArgErr() } fcgiTransport.Root = dispenser.Val() dispenser.Delete() dispenser.Delete() case "split": extensions = dispenser.RemainingArgs() dispenser.Delete() for range extensions { dispenser.Delete() } if len(extensions) == 0 { return nil, dispenser.ArgErr() } case "env": args := dispenser.RemainingArgs() dispenser.Delete() for range args { dispenser.Delete() } if len(args) != 2 { return nil, dispenser.ArgErr() } if fcgiTransport.EnvVars == nil { fcgiTransport.EnvVars = make(map[string]string) } fcgiTransport.EnvVars[args[0]] = args[1] case "index": args := dispenser.RemainingArgs() dispenser.Delete() for range args { dispenser.Delete() } if len(args) != 1 { return nil, dispenser.ArgErr() } indexFile = args[0] case "try_files": args := dispenser.RemainingArgs() dispenser.Delete() for range args { dispenser.Delete() } if len(args) < 1 { return nil, dispenser.ArgErr() } tryFiles = args case "resolve_root_symlink": args := dispenser.RemainingArgs() dispenser.Delete() for range args { dispenser.Delete() } fcgiTransport.ResolveRootSymlink = true case "dial_timeout": if !dispenser.NextArg() { return nil, dispenser.ArgErr() } dur, err := caddy.ParseDuration(dispenser.Val()) if err != nil { return nil, dispenser.Errf("bad timeout value %s: %v", dispenser.Val(), err) } fcgiTransport.DialTimeout = caddy.Duration(dur) dispenser.Delete() dispenser.Delete() case "read_timeout": if !dispenser.NextArg() { return nil, dispenser.ArgErr() } dur, err := caddy.ParseDuration(dispenser.Val()) if err != nil { return nil, dispenser.Errf("bad timeout value %s: %v", dispenser.Val(), err) } fcgiTransport.ReadTimeout = caddy.Duration(dur) dispenser.Delete() dispenser.Delete() case "write_timeout": if !dispenser.NextArg() { return nil, dispenser.ArgErr() } dur, err := caddy.ParseDuration(dispenser.Val()) if err != nil { return nil, dispenser.Errf("bad timeout value %s: %v", dispenser.Val(), err) } fcgiTransport.WriteTimeout = caddy.Duration(dur) dispenser.Delete() dispenser.Delete() case "capture_stderr": args := dispenser.RemainingArgs() dispenser.Delete() for range args { dispenser.Delete() } fcgiTransport.CaptureStderr = true } } } // reset the dispenser after we're done so that the reverse_proxy // unmarshaler can read it from the start dispenser.Reset() // set up a route list that we'll append to routes := caddyhttp.RouteList{} // set the list of allowed path segments on which to split fcgiTransport.SplitPath = extensions // if the index is turned off, we skip the redirect and try_files if indexFile != "off" { // route to redirect to canonical path if index PHP file redirMatcherSet := caddy.ModuleMap{ "file": h.JSON(fileserver.MatchFile{ TryFiles: []string{"{http.request.uri.path}/" + indexFile}, }), "not": h.JSON(caddyhttp.MatchNot{ MatcherSetsRaw: []caddy.ModuleMap{ { "path": h.JSON(caddyhttp.MatchPath{"*/"}), }, }, }), } redirHandler := caddyhttp.StaticResponse{ StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)), Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}}, } redirRoute := caddyhttp.Route{ MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)}, } // if tryFiles wasn't overridden, use a reasonable default if len(tryFiles) == 0 { tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile} } // route to rewrite to PHP index file rewriteMatcherSet := caddy.ModuleMap{ "file": h.JSON(fileserver.MatchFile{ TryFiles: tryFiles, SplitPath: extensions, }), } rewriteHandler := rewrite.Rewrite{ URI: "{http.matchers.file.relative}", } rewriteRoute := caddyhttp.Route{ MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)}, } routes = append(routes, redirRoute, rewriteRoute) } // route to actually reverse proxy requests to PHP files; // match only requests that are for PHP files pathList := []string{} for _, ext := range extensions { pathList = append(pathList, "*"+ext) } rpMatcherSet := caddy.ModuleMap{ "path": h.JSON(pathList), } // create the reverse proxy handler which uses our FastCGI transport rpHandler := &reverseproxy.Handler{ TransportRaw: caddyconfig.JSONModuleObject(fcgiTransport, "protocol", "fastcgi", nil), } // the rest of the config is specified by the user // using the reverse_proxy directive syntax err = rpHandler.UnmarshalCaddyfile(dispenser) if err != nil { return nil, err } err = rpHandler.FinalizeUnmarshalCaddyfile(h) if err != nil { return nil, err } // create the final reverse proxy route which is // conditional on matching PHP files rpRoute := caddyhttp.Route{ MatcherSetsRaw: []caddy.ModuleMap{rpMatcherSet}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rpHandler, "handler", "reverse_proxy", nil)}, } subroute := caddyhttp.Subroute{ Routes: append(routes, rpRoute), } // the user's matcher is a prerequisite for ours, so // wrap ours in a subroute and return that if userMatcherSet != nil { return []httpcaddyfile.ConfigValue{ { Class: "route", Value: caddyhttp.Route{ MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)}, }, }, }, nil } // otherwise, return the literal subroute instead of // individual routes, to ensure they stay together and // are treated as a single unit, without necessarily // creating an actual subroute in the output return []httpcaddyfile.ConfigValue{ { Class: "route", Value: subroute, }, }, nil } caddy-2.6.2/modules/caddyhttp/reverseproxy/fastcgi/client.go000066400000000000000000000222371435007237400242560ustar00rootroot00000000000000// 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. // Forked Jan. 2015 from http://bitbucket.org/PinIdea/fcgi_client // (which is forked from https://code.google.com/p/go-fastcgi-client/). // This fork contains several fixes and improvements by Matt Holt and // other contributors to the Caddy project. // Copyright 2012 Junqing Tan and The Go Authors // Use of this source code is governed by a BSD-style // Part of source code is from Go fcgi package package fastcgi import ( "bufio" "bytes" "io" "mime/multipart" "net" "net/http" "net/http/httputil" "net/textproto" "net/url" "os" "path/filepath" "strconv" "strings" "time" "go.uber.org/zap" ) // FCGIListenSockFileno describes listen socket file number. const FCGIListenSockFileno uint8 = 0 // FCGIHeaderLen describes header length. const FCGIHeaderLen uint8 = 8 // Version1 describes the version. const Version1 uint8 = 1 // FCGINullRequestID describes the null request ID. const FCGINullRequestID uint8 = 0 // FCGIKeepConn describes keep connection mode. const FCGIKeepConn uint8 = 1 const ( // BeginRequest is the begin request flag. BeginRequest uint8 = iota + 1 // AbortRequest is the abort request flag. AbortRequest // EndRequest is the end request flag. EndRequest // Params is the parameters flag. Params // Stdin is the standard input flag. Stdin // Stdout is the standard output flag. Stdout // Stderr is the standard error flag. Stderr // Data is the data flag. Data // GetValues is the get values flag. GetValues // GetValuesResult is the get values result flag. GetValuesResult // UnknownType is the unknown type flag. UnknownType // MaxType is the maximum type flag. MaxType = UnknownType ) const ( // Responder is the responder flag. Responder uint8 = iota + 1 // Authorizer is the authorizer flag. Authorizer // Filter is the filter flag. Filter ) const ( // RequestComplete is the completed request flag. RequestComplete uint8 = iota // CantMultiplexConns is the multiplexed connections flag. CantMultiplexConns // Overloaded is the overloaded flag. Overloaded // UnknownRole is the unknown role flag. UnknownRole ) const ( // MaxConns is the maximum connections flag. MaxConns string = "MAX_CONNS" // MaxRequests is the maximum requests flag. MaxRequests string = "MAX_REQS" // MultiplexConns is the multiplex connections flag. MultiplexConns string = "MPXS_CONNS" ) const ( maxWrite = 65500 // 65530 may work, but for compatibility maxPad = 255 ) // for padding so we don't have to allocate all the time // not synchronized because we don't care what the contents are var pad [maxPad]byte // client implements a FastCGI client, which is a standard for // interfacing external applications with Web servers. type client struct { rwc net.Conn // keepAlive bool // TODO: implement reqID uint16 stderr bool logger *zap.Logger } // Do made the request and returns a io.Reader that translates the data read // from fcgi responder out of fcgi packet before returning it. func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) { writer := &streamWriter{c: c} writer.buf = bufPool.Get().(*bytes.Buffer) writer.buf.Reset() defer bufPool.Put(writer.buf) err = writer.writeBeginRequest(uint16(Responder), 0) if err != nil { return } writer.recType = Params err = writer.writePairs(p) if err != nil { return } writer.recType = Stdin if req != nil { _, err = io.Copy(writer, req) if err != nil { return nil, err } } err = writer.FlushStream() if err != nil { return nil, err } r = &streamReader{c: c} return } // clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer // that closes the client connection. type clientCloser struct { rwc net.Conn r *streamReader io.Reader status int logger *zap.Logger } func (f clientCloser) Close() error { stderr := f.r.stderr.Bytes() if len(stderr) == 0 { return f.rwc.Close() } if f.status >= 400 { f.logger.Error("stderr", zap.ByteString("body", stderr)) } else { f.logger.Warn("stderr", zap.ByteString("body", stderr)) } return f.rwc.Close() } // Request returns a HTTP Response with Header and Body // from fcgi responder func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) { r, err := c.Do(p, req) if err != nil { return } rb := bufio.NewReader(r) tp := textproto.NewReader(rb) resp = new(http.Response) // Parse the response headers. mimeHeader, err := tp.ReadMIMEHeader() if err != nil && err != io.EOF { return } resp.Header = http.Header(mimeHeader) if resp.Header.Get("Status") != "" { statusNumber, statusInfo, statusIsCut := strings.Cut(resp.Header.Get("Status"), " ") resp.StatusCode, err = strconv.Atoi(statusNumber) if err != nil { return } if statusIsCut { resp.Status = statusInfo } } else { resp.StatusCode = http.StatusOK } // TODO: fixTransferEncoding ? resp.TransferEncoding = resp.Header["Transfer-Encoding"] resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) // wrap the response body in our closer closer := clientCloser{ rwc: c.rwc, r: r.(*streamReader), Reader: rb, status: resp.StatusCode, logger: noopLogger, } if chunked(resp.TransferEncoding) { closer.Reader = httputil.NewChunkedReader(rb) } if c.stderr { closer.logger = c.logger } resp.Body = closer return } // Get issues a GET request to the fcgi responder. func (c *client) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) { p["REQUEST_METHOD"] = "GET" p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10) return c.Request(p, body) } // Head issues a HEAD request to the fcgi responder. func (c *client) Head(p map[string]string) (resp *http.Response, err error) { p["REQUEST_METHOD"] = "HEAD" p["CONTENT_LENGTH"] = "0" return c.Request(p, nil) } // Options issues an OPTIONS request to the fcgi responder. func (c *client) Options(p map[string]string) (resp *http.Response, err error) { p["REQUEST_METHOD"] = "OPTIONS" p["CONTENT_LENGTH"] = "0" return c.Request(p, nil) } // Post issues a POST request to the fcgi responder. with request body // in the format that bodyType specified func (c *client) Post(p map[string]string, method string, bodyType string, body io.Reader, l int64) (resp *http.Response, err error) { if p == nil { p = make(map[string]string) } p["REQUEST_METHOD"] = strings.ToUpper(method) if len(p["REQUEST_METHOD"]) == 0 || p["REQUEST_METHOD"] == "GET" { p["REQUEST_METHOD"] = "POST" } p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10) if len(bodyType) > 0 { p["CONTENT_TYPE"] = bodyType } else { p["CONTENT_TYPE"] = "application/x-www-form-urlencoded" } return c.Request(p, body) } // PostForm issues a POST to the fcgi responder, with form // as a string key to a list values (url.Values) func (c *client) PostForm(p map[string]string, data url.Values) (resp *http.Response, err error) { body := bytes.NewReader([]byte(data.Encode())) return c.Post(p, "POST", "application/x-www-form-urlencoded", body, int64(body.Len())) } // PostFile issues a POST to the fcgi responder in multipart(RFC 2046) standard, // with form as a string key to a list values (url.Values), // and/or with file as a string key to a list file path. func (c *client) PostFile(p map[string]string, data url.Values, file map[string]string) (resp *http.Response, err error) { buf := &bytes.Buffer{} writer := multipart.NewWriter(buf) bodyType := writer.FormDataContentType() for key, val := range data { for _, v0 := range val { err = writer.WriteField(key, v0) if err != nil { return } } } for key, val := range file { fd, e := os.Open(val) if e != nil { return nil, e } defer fd.Close() part, e := writer.CreateFormFile(key, filepath.Base(val)) if e != nil { return nil, e } _, err = io.Copy(part, fd) if err != nil { return } } err = writer.Close() if err != nil { return } return c.Post(p, "POST", bodyType, buf, int64(buf.Len())) } // SetReadTimeout sets the read timeout for future calls that read from the // fcgi responder. A zero value for t means no timeout will be set. func (c *client) SetReadTimeout(t time.Duration) error { if t != 0 { return c.rwc.SetReadDeadline(time.Now().Add(t)) } return nil } // SetWriteTimeout sets the write timeout for future calls that send data to // the fcgi responder. A zero value for t means no timeout will be set. func (c *client) SetWriteTimeout(t time.Duration) error { if t != 0 { return c.rwc.SetWriteDeadline(time.Now().Add(t)) } return nil } // Checks whether chunked is part of the encodings stack func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" } caddy-2.6.2/modules/caddyhttp/reverseproxy/fastcgi/client_test.go000066400000000000000000000170351435007237400253150ustar00rootroot00000000000000// 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. // NOTE: These tests were adapted from the original // repository from which this package was forked. // The tests are slow (~10s) and in dire need of rewriting. // As such, the tests have been disabled to speed up // automated builds until they can be properly written. package fastcgi import ( "bytes" "crypto/md5" "encoding/binary" "fmt" "io" "log" "math/rand" "net" "net/http" "net/http/fcgi" "net/url" "os" "path/filepath" "strconv" "strings" "testing" "time" ) // test fcgi protocol includes: // Get, Post, Post in multipart/form-data, and Post with files // each key should be the md5 of the value or the file uploaded // specify remote fcgi responder ip:port to test with php // test failed if the remote fcgi(script) failed md5 verification // and output "FAILED" in response const ( scriptFile = "/tank/www/fcgic_test.php" //ipPort = "remote-php-serv:59000" ipPort = "127.0.0.1:59000" ) var globalt *testing.T type FastCGIServer struct{} func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { if err := req.ParseMultipartForm(100000000); err != nil { log.Printf("[ERROR] failed to parse: %v", err) } stat := "PASSED" fmt.Fprintln(resp, "-") fileNum := 0 { length := 0 for k0, v0 := range req.Form { h := md5.New() _, _ = io.WriteString(h, v0[0]) _md5 := fmt.Sprintf("%x", h.Sum(nil)) length += len(k0) length += len(v0[0]) // echo error when key != _md5(val) if _md5 != k0 { fmt.Fprintln(resp, "server:err ", _md5, k0) stat = "FAILED" } } if req.MultipartForm != nil { fileNum = len(req.MultipartForm.File) for kn, fns := range req.MultipartForm.File { //fmt.Fprintln(resp, "server:filekey ", kn ) length += len(kn) for _, f := range fns { fd, err := f.Open() if err != nil { log.Println("server:", err) return } h := md5.New() l0, err := io.Copy(h, fd) if err != nil { log.Println(err) return } length += int(l0) defer fd.Close() md5 := fmt.Sprintf("%x", h.Sum(nil)) //fmt.Fprintln(resp, "server:filemd5 ", md5 ) if kn != md5 { fmt.Fprintln(resp, "server:err ", md5, kn) stat = "FAILED" } //fmt.Fprintln(resp, "server:filename ", f.Filename ) } } } fmt.Fprintln(resp, "server:got data length", length) } fmt.Fprintln(resp, "-"+stat+"-POST(", len(req.Form), ")-FILE(", fileNum, ")--") } func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) { conn, err := net.Dial("tcp", ipPort) if err != nil { log.Println("err:", err) return } fcgi := client{rwc: conn, reqID: 1} length := 0 var resp *http.Response switch reqType { case 0: if len(data) > 0 { length = len(data) rd := bytes.NewReader(data) resp, err = fcgi.Post(fcgiParams, "", "", rd, int64(rd.Len())) } else if len(posts) > 0 { values := url.Values{} for k, v := range posts { values.Set(k, v) length += len(k) + 2 + len(v) } resp, err = fcgi.PostForm(fcgiParams, values) } else { rd := bytes.NewReader(data) resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len())) } default: values := url.Values{} for k, v := range posts { values.Set(k, v) length += len(k) + 2 + len(v) } for k, v := range files { fi, _ := os.Lstat(v) length += len(k) + int(fi.Size()) } resp, err = fcgi.PostFile(fcgiParams, values, files) } if err != nil { log.Println("err:", err) return } defer resp.Body.Close() content, _ = io.ReadAll(resp.Body) log.Println("c: send data length ≈", length, string(content)) conn.Close() time.Sleep(250 * time.Millisecond) if bytes.Contains(content, []byte("FAILED")) { globalt.Error("Server return failed message") } return } func generateRandFile(size int) (p string, m string) { p = filepath.Join(os.TempDir(), "fcgict"+strconv.Itoa(rand.Int())) // open output file fo, err := os.Create(p) if err != nil { panic(err) } // close fo on exit and check for its returned error defer func() { if err := fo.Close(); err != nil { panic(err) } }() h := md5.New() for i := 0; i < size/16; i++ { buf := make([]byte, 16) binary.PutVarint(buf, rand.Int63()) if _, err := fo.Write(buf); err != nil { log.Printf("[ERROR] failed to write buffer: %v\n", err) } if _, err := h.Write(buf); err != nil { log.Printf("[ERROR] failed to write buffer: %v\n", err) } } m = fmt.Sprintf("%x", h.Sum(nil)) return } func DisabledTest(t *testing.T) { // TODO: test chunked reader globalt = t rand.Seed(time.Now().UTC().UnixNano()) // server go func() { listener, err := net.Listen("tcp", ipPort) if err != nil { log.Println("listener creation failed: ", err) } srv := new(FastCGIServer) if err := fcgi.Serve(listener, srv); err != nil { log.Print("[ERROR] failed to start server: ", err) } }() time.Sleep(250 * time.Millisecond) // init fcgiParams := make(map[string]string) fcgiParams["REQUEST_METHOD"] = "GET" fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1" //fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1" fcgiParams["SCRIPT_FILENAME"] = scriptFile // simple GET log.Println("test:", "get") sendFcgi(0, fcgiParams, nil, nil, nil) // simple post data log.Println("test:", "post") sendFcgi(0, fcgiParams, []byte("c4ca4238a0b923820dcc509a6f75849b=1&7b8b965ad4bca0e41ab51de7b31363a1=n"), nil, nil) log.Println("test:", "post data (more than 60KB)") data := "" for i := 0x00; i < 0xff; i++ { v0 := strings.Repeat(fmt.Sprint(i), 256) h := md5.New() _, _ = io.WriteString(h, v0) k0 := fmt.Sprintf("%x", h.Sum(nil)) data += k0 + "=" + url.QueryEscape(v0) + "&" } sendFcgi(0, fcgiParams, []byte(data), nil, nil) log.Println("test:", "post form (use url.Values)") p0 := make(map[string]string, 1) p0["c4ca4238a0b923820dcc509a6f75849b"] = "1" p0["7b8b965ad4bca0e41ab51de7b31363a1"] = "n" sendFcgi(1, fcgiParams, nil, p0, nil) log.Println("test:", "post forms (256 keys, more than 1MB)") p1 := make(map[string]string, 1) for i := 0x00; i < 0xff; i++ { v0 := strings.Repeat(fmt.Sprint(i), 4096) h := md5.New() _, _ = io.WriteString(h, v0) k0 := fmt.Sprintf("%x", h.Sum(nil)) p1[k0] = v0 } sendFcgi(1, fcgiParams, nil, p1, nil) log.Println("test:", "post file (1 file, 500KB)) ") f0 := make(map[string]string, 1) path0, m0 := generateRandFile(500000) f0[m0] = path0 sendFcgi(1, fcgiParams, nil, p1, f0) log.Println("test:", "post multiple files (2 files, 5M each) and forms (256 keys, more than 1MB data") path1, m1 := generateRandFile(5000000) f0[m1] = path1 sendFcgi(1, fcgiParams, nil, p1, f0) log.Println("test:", "post only files (2 files, 5M each)") sendFcgi(1, fcgiParams, nil, nil, f0) log.Println("test:", "post only 1 file") delete(f0, "m0") sendFcgi(1, fcgiParams, nil, nil, f0) if err := os.Remove(path0); err != nil { log.Println("[ERROR] failed to remove path: ", err) } if err := os.Remove(path1); err != nil { log.Println("[ERROR] failed to remove path: ", err) } } caddy-2.6.2/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go000066400000000000000000000317631435007237400244240ustar00rootroot00000000000000// 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 fastcgi import ( "crypto/tls" "fmt" "net" "net/http" "path/filepath" "strconv" "strings" "time" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" "github.com/caddyserver/caddy/v2/modules/caddytls" "go.uber.org/zap" "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2" ) var noopLogger = zap.NewNop() func init() { caddy.RegisterModule(Transport{}) } // Transport facilitates FastCGI communication. type Transport struct { // Use this directory as the fastcgi root directory. Defaults to the root // directory of the parent virtual host. Root string `json:"root,omitempty"` // The path in the URL will be split into two, with the first piece ending // with the value of SplitPath. The first piece will be assumed as the // actual resource (CGI script) name, and the second piece will be set to // PATH_INFO for the CGI script to use. // // Future enhancements should be careful to avoid CVE-2019-11043, // which can be mitigated with use of a try_files-like behavior // that 404s if the fastcgi path info is not found. SplitPath []string `json:"split_path,omitempty"` // Path declared as root directory will be resolved to its absolute value // after the evaluation of any symbolic links. // Due to the nature of PHP opcache, root directory path is cached: when // using a symlinked directory as root this could generate errors when // symlink is changed without php-fpm being restarted; enabling this // directive will set $_SERVER['DOCUMENT_ROOT'] to the real directory path. ResolveRootSymlink bool `json:"resolve_root_symlink,omitempty"` // Extra environment variables. EnvVars map[string]string `json:"env,omitempty"` // The duration used to set a deadline when connecting to an upstream. Default: `3s`. DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` // The duration used to set a deadline when reading from the FastCGI server. ReadTimeout caddy.Duration `json:"read_timeout,omitempty"` // The duration used to set a deadline when sending to the FastCGI server. WriteTimeout caddy.Duration `json:"write_timeout,omitempty"` // Capture and log any messages sent by the upstream on stderr. Logs at WARN // level by default. If the response has a 4xx or 5xx status ERROR level will // be used instead. CaptureStderr bool `json:"capture_stderr,omitempty"` serverSoftware string logger *zap.Logger } // CaddyModule returns the Caddy module information. func (Transport) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.transport.fastcgi", New: func() caddy.Module { return new(Transport) }, } } // Provision sets up t. func (t *Transport) Provision(ctx caddy.Context) error { t.logger = ctx.Logger() if t.Root == "" { t.Root = "{http.vars.root}" } version, _ := caddy.Version() t.serverSoftware = "Caddy/" + version // Set a relatively short default dial timeout. // This is helpful to make load-balancer retries more speedy. if t.DialTimeout == 0 { t.DialTimeout = caddy.Duration(3 * time.Second) } return nil } // RoundTrip implements http.RoundTripper. func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server) // Disallow null bytes in the request path, because // PHP upstreams may do bad things, like execute a // non-PHP file as PHP code. See #4574 if strings.Contains(r.URL.Path, "\x00") { return nil, caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("invalid request path")) } env, err := t.buildEnv(r) if err != nil { return nil, fmt.Errorf("building environment: %v", err) } ctx := r.Context() // extract dial information from request (should have been embedded by the reverse proxy) network, address := "tcp", r.URL.Host if dialInfo, ok := reverseproxy.GetDialInfo(ctx); ok { network = dialInfo.Network address = dialInfo.Address } logCreds := server.Logs != nil && server.Logs.ShouldLogCredentials loggableReq := caddyhttp.LoggableHTTPRequest{ Request: r, ShouldLogCredentials: logCreds, } loggableEnv := loggableEnv{vars: env, logCredentials: logCreds} logger := t.logger.With( zap.Object("request", loggableReq), zap.Object("env", loggableEnv), ) logger.Debug("roundtrip", zap.String("dial", address), zap.Object("env", loggableEnv), zap.Object("request", loggableReq)) // connect to the backend dialer := net.Dialer{Timeout: time.Duration(t.DialTimeout)} conn, err := dialer.DialContext(ctx, network, address) if err != nil { return nil, fmt.Errorf("dialing backend: %v", err) } defer func() { // conn will be closed with the response body unless there's an error if err != nil { conn.Close() } }() // create the client that will facilitate the protocol client := client{ rwc: conn, reqID: 1, logger: logger, } // read/write timeouts if err = client.SetReadTimeout(time.Duration(t.ReadTimeout)); err != nil { return nil, fmt.Errorf("setting read timeout: %v", err) } if err = client.SetWriteTimeout(time.Duration(t.WriteTimeout)); err != nil { return nil, fmt.Errorf("setting write timeout: %v", err) } contentLength := r.ContentLength if contentLength == 0 { contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) } var resp *http.Response switch r.Method { case http.MethodHead: resp, err = client.Head(env) case http.MethodGet: resp, err = client.Get(env, r.Body, contentLength) case http.MethodOptions: resp, err = client.Options(env) default: resp, err = client.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength) } if err != nil { return nil, err } return resp, nil } // buildEnv returns a set of CGI environment variables for the request. func (t Transport) buildEnv(r *http.Request) (envVars, error) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) var env envVars // Separate remote IP and port; more lenient than net.SplitHostPort var ip, port string if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 { ip = r.RemoteAddr[:idx] port = r.RemoteAddr[idx+1:] } else { ip = r.RemoteAddr } // Remove [] from IPv6 addresses ip = strings.Replace(ip, "[", "", 1) ip = strings.Replace(ip, "]", "", 1) // make sure file root is absolute root, err := filepath.Abs(repl.ReplaceAll(t.Root, ".")) if err != nil { return nil, err } if t.ResolveRootSymlink { root, err = filepath.EvalSymlinks(root) if err != nil { return nil, err } } fpath := r.URL.Path scriptName := fpath docURI := fpath // split "actual path" from "path info" if configured var pathInfo string if splitPos := t.splitPos(fpath); splitPos > -1 { docURI = fpath[:splitPos] pathInfo = fpath[splitPos:] // Strip PATH_INFO from SCRIPT_NAME scriptName = strings.TrimSuffix(scriptName, pathInfo) } // Try to grab the path remainder from a file matcher // if we didn't get a split result here. // See https://github.com/caddyserver/caddy/issues/3718 if pathInfo == "" { if remainder, ok := repl.GetString("http.matchers.file.remainder"); ok { pathInfo = remainder } } // SCRIPT_FILENAME is the absolute path of SCRIPT_NAME scriptFilename := caddyhttp.SanitizedPathJoin(root, scriptName) // Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875 // Info: https://tools.ietf.org/html/rfc3875#section-4.1.13 if scriptName != "" && !strings.HasPrefix(scriptName, "/") { scriptName = "/" + scriptName } // Get the request URL from context. The context stores the original URL in case // it was changed by a middleware such as rewrite. By default, we pass the // original URI in as the value of REQUEST_URI (the user can overwrite this // if desired). Most PHP apps seem to want the original URI. Besides, this is // how nginx defaults: http://stackoverflow.com/a/12485156/1048862 origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request) requestScheme := "http" if r.TLS != nil { requestScheme = "https" } reqHost, reqPort, err := net.SplitHostPort(r.Host) if err != nil { // whatever, just assume there was no port reqHost = r.Host } authUser := "" if val, ok := repl.Get("http.auth.user.id"); ok { authUser = val.(string) } // Some variables are unused but cleared explicitly to prevent // the parent environment from interfering. env = envVars{ // Variables defined in CGI 1.1 spec "AUTH_TYPE": "", // Not used "CONTENT_LENGTH": r.Header.Get("Content-Length"), "CONTENT_TYPE": r.Header.Get("Content-Type"), "GATEWAY_INTERFACE": "CGI/1.1", "PATH_INFO": pathInfo, "QUERY_STRING": r.URL.RawQuery, "REMOTE_ADDR": ip, "REMOTE_HOST": ip, // For speed, remote host lookups disabled "REMOTE_PORT": port, "REMOTE_IDENT": "", // Not used "REMOTE_USER": authUser, "REQUEST_METHOD": r.Method, "REQUEST_SCHEME": requestScheme, "SERVER_NAME": reqHost, "SERVER_PROTOCOL": r.Proto, "SERVER_SOFTWARE": t.serverSoftware, // Other variables "DOCUMENT_ROOT": root, "DOCUMENT_URI": docURI, "HTTP_HOST": r.Host, // added here, since not always part of headers "REQUEST_URI": origReq.URL.RequestURI(), "SCRIPT_FILENAME": scriptFilename, "SCRIPT_NAME": scriptName, } // compliance with the CGI specification requires that // PATH_TRANSLATED should only exist if PATH_INFO is defined. // Info: https://www.ietf.org/rfc/rfc3875 Page 14 if env["PATH_INFO"] != "" { env["PATH_TRANSLATED"] = caddyhttp.SanitizedPathJoin(root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html } // compliance with the CGI specification requires that // the SERVER_PORT variable MUST be set to the TCP/IP port number on which this request is received from the client // even if the port is the default port for the scheme and could otherwise be omitted from a URI. // https://tools.ietf.org/html/rfc3875#section-4.1.15 if reqPort != "" { env["SERVER_PORT"] = reqPort } else if requestScheme == "http" { env["SERVER_PORT"] = "80" } else if requestScheme == "https" { env["SERVER_PORT"] = "443" } // Some web apps rely on knowing HTTPS or not if r.TLS != nil { env["HTTPS"] = "on" // and pass the protocol details in a manner compatible with apache's mod_ssl // (which is why these have a SSL_ prefix and not TLS_). v, ok := tlsProtocolStrings[r.TLS.Version] if ok { env["SSL_PROTOCOL"] = v } // and pass the cipher suite in a manner compatible with apache's mod_ssl for _, cs := range caddytls.SupportedCipherSuites() { if cs.ID == r.TLS.CipherSuite { env["SSL_CIPHER"] = cs.Name break } } } // Add env variables from config (with support for placeholders in values) for key, value := range t.EnvVars { env[key] = repl.ReplaceAll(value, "") } // Add all HTTP headers to env variables for field, val := range r.Header { header := strings.ToUpper(field) header = headerNameReplacer.Replace(header) env["HTTP_"+header] = strings.Join(val, ", ") } return env, nil } // splitPos returns the index where path should // be split based on t.SplitPath. func (t Transport) splitPos(path string) int { // TODO: from v1... // if httpserver.CaseSensitivePath { // return strings.Index(path, r.SplitPath) // } if len(t.SplitPath) == 0 { return 0 } lowerPath := strings.ToLower(path) for _, split := range t.SplitPath { if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 { return idx + len(split) } } return -1 } type envVars map[string]string // loggableEnv is a simple type to allow for speeding up zap log encoding. type loggableEnv struct { vars envVars logCredentials bool } func (env loggableEnv) MarshalLogObject(enc zapcore.ObjectEncoder) error { for k, v := range env.vars { if !env.logCredentials { switch strings.ToLower(k) { case "http_cookie", "http_set_cookie", "http_authorization", "http_proxy_authorization": v = "" } } enc.AddString(k, v) } return nil } // Map of supported protocols to Apache ssl_mod format // Note that these are slightly different from SupportedProtocols in caddytls/config.go var tlsProtocolStrings = map[uint16]string{ tls.VersionTLS10: "TLSv1", tls.VersionTLS11: "TLSv1.1", tls.VersionTLS12: "TLSv1.2", tls.VersionTLS13: "TLSv1.3", } var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") // Interface guards var ( _ zapcore.ObjectMarshaler = (*loggableEnv)(nil) _ caddy.Provisioner = (*Transport)(nil) _ http.RoundTripper = (*Transport)(nil) ) caddy-2.6.2/modules/caddyhttp/reverseproxy/fastcgi/header.go000066400000000000000000000017341435007237400242270ustar00rootroot00000000000000// 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 fastcgi type header struct { Version uint8 Type uint8 ID uint16 ContentLength uint16 PaddingLength uint8 Reserved uint8 } func (h *header) init(recType uint8, reqID uint16, contentLength int) { h.Version = 1 h.Type = recType h.ID = reqID h.ContentLength = uint16(contentLength) h.PaddingLength = uint8(-contentLength & 7) } caddy-2.6.2/modules/caddyhttp/reverseproxy/fastcgi/pool.go000066400000000000000000000013371435007237400237470ustar00rootroot00000000000000// 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 fastcgi import ( "bytes" "sync" ) var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } caddy-2.6.2/modules/caddyhttp/reverseproxy/fastcgi/reader.go000066400000000000000000000020411435007237400242310ustar00rootroot00000000000000// 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 fastcgi import ( "bytes" "io" ) type streamReader struct { c *client rec record stderr bytes.Buffer } func (w *streamReader) Read(p []byte) (n int, err error) { for !w.rec.hasMore() { err = w.rec.fill(w.c.rwc) if err != nil { return 0, err } // standard error output if w.rec.h.Type == Stderr { if _, err = io.Copy(&w.stderr, &w.rec); err != nil { return 0, err } } } return w.rec.Read(p) } caddy-2.6.2/modules/caddyhttp/reverseproxy/fastcgi/record.go000066400000000000000000000025261435007237400242550ustar00rootroot00000000000000// 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 fastcgi import ( "encoding/binary" "errors" "io" ) type record struct { h header lr io.LimitedReader padding int64 } func (rec *record) fill(r io.Reader) (err error) { rec.lr.N = rec.padding rec.lr.R = r if _, err = io.Copy(io.Discard, rec); err != nil { return } if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil { return } if rec.h.Version != 1 { err = errors.New("fcgi: invalid header version") return } if rec.h.Type == EndRequest { err = io.EOF return } rec.lr.N = int64(rec.h.ContentLength) rec.padding = int64(rec.h.PaddingLength) return } func (rec *record) Read(p []byte) (n int, err error) { return rec.lr.Read(p) } func (rec *record) hasMore() bool { return rec.lr.N > 0 } caddy-2.6.2/modules/caddyhttp/reverseproxy/fastcgi/writer.go000066400000000000000000000066531435007237400243200ustar00rootroot00000000000000// 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 fastcgi import ( "bytes" "encoding/binary" ) // streamWriter abstracts out the separation of a stream into discrete records. // It only writes maxWrite bytes at a time. type streamWriter struct { c *client h header buf *bytes.Buffer recType uint8 } func (w *streamWriter) writeRecord(recType uint8, content []byte) (err error) { w.h.init(recType, w.c.reqID, len(content)) w.buf.Write(pad[:8]) w.writeHeader() w.buf.Write(content) w.buf.Write(pad[:w.h.PaddingLength]) _, err = w.buf.WriteTo(w.c.rwc) return err } func (w *streamWriter) writeBeginRequest(role uint16, flags uint8) error { b := [8]byte{byte(role >> 8), byte(role), flags} return w.writeRecord(BeginRequest, b[:]) } func (w *streamWriter) Write(p []byte) (int, error) { // init header if w.buf.Len() < 8 { w.buf.Write(pad[:8]) } nn := 0 for len(p) > 0 { n := len(p) nl := maxWrite + 8 - w.buf.Len() if n > nl { n = nl w.buf.Write(p[:n]) if err := w.Flush(); err != nil { return nn, err } // reset headers w.buf.Write(pad[:8]) } else { w.buf.Write(p[:n]) } nn += n p = p[n:] } return nn, nil } func (w *streamWriter) endStream() error { // send empty record to close the stream return w.writeRecord(w.recType, nil) } func (w *streamWriter) writePairs(pairs map[string]string) error { b := make([]byte, 8) nn := 0 // init headers w.buf.Write(b) for k, v := range pairs { m := 8 + len(k) + len(v) if m > maxWrite { // param data size exceed 65535 bytes" vl := maxWrite - 8 - len(k) v = v[:vl] } n := encodeSize(b, uint32(len(k))) n += encodeSize(b[n:], uint32(len(v))) m = n + len(k) + len(v) if (nn + m) > maxWrite { if err := w.Flush(); err != nil { return err } // reset headers w.buf.Write(b) nn = 0 } nn += m w.buf.Write(b[:n]) w.buf.WriteString(k) w.buf.WriteString(v) } return w.FlushStream() } func encodeSize(b []byte, size uint32) int { if size > 127 { size |= 1 << 31 binary.BigEndian.PutUint32(b, size) return 4 } b[0] = byte(size) return 1 } // writeHeader populate header wire data in buf, it abuses buffer.Bytes() modification func (w *streamWriter) writeHeader() { h := w.buf.Bytes()[:8] h[0] = w.h.Version h[1] = w.h.Type binary.BigEndian.PutUint16(h[2:4], w.h.ID) binary.BigEndian.PutUint16(h[4:6], w.h.ContentLength) h[6] = w.h.PaddingLength h[7] = w.h.Reserved } // Flush write buffer data to the underlying connection, it assumes header data is the first 8 bytes of buf func (w *streamWriter) Flush() error { w.h.init(w.recType, w.c.reqID, w.buf.Len()-8) w.writeHeader() w.buf.Write(pad[:w.h.PaddingLength]) _, err := w.buf.WriteTo(w.c.rwc) return err } // FlushStream flush data then end current stream func (w *streamWriter) FlushStream() error { if err := w.Flush(); err != nil { return err } return w.endStream() } caddy-2.6.2/modules/caddyhttp/reverseproxy/forwardauth/000077500000000000000000000000001435007237400233515ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go000066400000000000000000000171541435007237400256340ustar00rootroot00000000000000// 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 forwardauth import ( "encoding/json" "net/http" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" ) func init() { httpcaddyfile.RegisterDirective("forward_auth", parseCaddyfile) } // parseCaddyfile parses the forward_auth directive, which has the same syntax // as the reverse_proxy directive (in fact, the reverse_proxy's directive // Unmarshaler is invoked by this function) but the resulting proxy is specially // configured for most™️ auth gateways that support forward auth. The typical // config which looks something like this: // // forward_auth auth-gateway:9091 { // uri /authenticate?redirect=https://auth.example.com // copy_headers Remote-User Remote-Email // } // // is equivalent to a reverse_proxy directive like this: // // reverse_proxy auth-gateway:9091 { // method GET // rewrite /authenticate?redirect=https://auth.example.com // // header_up X-Forwarded-Method {method} // header_up X-Forwarded-Uri {uri} // // @good status 2xx // handle_response @good { // request_header { // Remote-User {http.reverse_proxy.header.Remote-User} // Remote-Email {http.reverse_proxy.header.Remote-Email} // } // } // } func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { if !h.Next() { return nil, h.ArgErr() } // if the user specified a matcher token, use that // matcher in a route that wraps both of our routes; // either way, strip the matcher token and pass // the remaining tokens to the unmarshaler so that // we can gain the rest of the reverse_proxy syntax userMatcherSet, err := h.ExtractMatcherSet() if err != nil { return nil, err } // make a new dispenser from the remaining tokens so that we // can reset the dispenser back to this point for the // reverse_proxy unmarshaler to read from it as well dispenser := h.NewFromNextSegment() // create the reverse proxy handler rpHandler := &reverseproxy.Handler{ // set up defaults for header_up; reverse_proxy already deals with // adding the other three X-Forwarded-* headers, but for this flow, // we want to also send along the incoming method and URI since this // request will have a rewritten URI and method. Headers: &headers.Handler{ Request: &headers.HeaderOps{ Set: http.Header{ "X-Forwarded-Method": []string{"{http.request.method}"}, "X-Forwarded-Uri": []string{"{http.request.uri}"}, }, }, }, // we always rewrite the method to GET, which implicitly // turns off sending the incoming request's body, which // allows later middleware handlers to consume it Rewrite: &rewrite.Rewrite{ Method: "GET", }, HandleResponse: []caddyhttp.ResponseHandler{}, } // collect the headers to copy from the auth response // onto the original request, so they can get passed // through to a backend app headersToCopy := make(map[string]string) // read the subdirectives for configuring the forward_auth shortcut // NOTE: we delete the tokens as we go so that the reverse_proxy // unmarshal doesn't see these subdirectives which it cannot handle for dispenser.Next() { for dispenser.NextBlock(0) { // ignore any sub-subdirectives that might // have the same name somewhere within // the reverse_proxy passthrough tokens if dispenser.Nesting() != 1 { continue } // parse the forward_auth subdirectives switch dispenser.Val() { case "uri": if !dispenser.NextArg() { return nil, dispenser.ArgErr() } rpHandler.Rewrite.URI = dispenser.Val() dispenser.Delete() dispenser.Delete() case "copy_headers": args := dispenser.RemainingArgs() hadBlock := false for nesting := dispenser.Nesting(); dispenser.NextBlock(nesting); { hadBlock = true args = append(args, dispenser.Val()) } dispenser.Delete() // directive name if hadBlock { dispenser.Delete() // opening brace dispenser.Delete() // closing brace } for range args { dispenser.Delete() } for _, headerField := range args { if strings.Contains(headerField, ">") { parts := strings.Split(headerField, ">") headersToCopy[parts[0]] = parts[1] } else { headersToCopy[headerField] = headerField } } if len(headersToCopy) == 0 { return nil, dispenser.ArgErr() } } } } // reset the dispenser after we're done so that the reverse_proxy // unmarshaler can read it from the start dispenser.Reset() // the auth target URI must not be empty if rpHandler.Rewrite.URI == "" { return nil, dispenser.Errf("the 'uri' subdirective is required") } // set up handler for good responses; when a response // has 2xx status, then we will copy some headers from // the response onto the original request, and allow // handling to continue down the middleware chain, // by _not_ executing a terminal handler. goodResponseHandler := caddyhttp.ResponseHandler{ Match: &caddyhttp.ResponseMatcher{ StatusCode: []int{2}, }, Routes: []caddyhttp.Route{}, } handler := &headers.Handler{ Request: &headers.HeaderOps{ Set: http.Header{}, }, } // the list of headers to copy may be empty, but that's okay; we // need at least one handler in the routes for the response handling // logic in reverse_proxy to not skip this entry as empty. for from, to := range headersToCopy { handler.Request.Set.Set(to, "{http.reverse_proxy.header."+http.CanonicalHeaderKey(from)+"}") } goodResponseHandler.Routes = append( goodResponseHandler.Routes, caddyhttp.Route{ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject( handler, "handler", "headers", nil, )}, }, ) // note that when a response has any other status than 2xx, then we // use the reverse proxy's default behaviour of copying the response // back to the client, so we don't need to explicitly add a response // handler specifically for that behaviour; we do need the 2xx handler // though, to make handling fall through to handlers deeper in the chain. rpHandler.HandleResponse = append(rpHandler.HandleResponse, goodResponseHandler) // the rest of the config is specified by the user // using the reverse_proxy directive syntax err = rpHandler.UnmarshalCaddyfile(dispenser) if err != nil { return nil, err } err = rpHandler.FinalizeUnmarshalCaddyfile(h) if err != nil { return nil, err } // create the final reverse proxy route rpRoute := caddyhttp.Route{ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject( rpHandler, "handler", "reverse_proxy", nil, )}, } // apply the user's matcher if any if userMatcherSet != nil { rpRoute.MatcherSetsRaw = []caddy.ModuleMap{userMatcherSet} } return []httpcaddyfile.ConfigValue{ { Class: "route", Value: rpRoute, }, }, nil } caddy-2.6.2/modules/caddyhttp/reverseproxy/healthchecks.go000066400000000000000000000325711435007237400240100ustar00rootroot00000000000000// 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 reverseproxy import ( "context" "fmt" "io" "net" "net/http" "net/url" "regexp" "runtime/debug" "strconv" "strings" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap" ) // HealthChecks configures active and passive health checks. type HealthChecks struct { // Active health checks run in the background on a timer. To // minimally enable active health checks, set either path or // port (or both). Note that active health check status // (healthy/unhealthy) is stored per-proxy-handler, not // globally; this allows different handlers to use different // criteria to decide what defines a healthy backend. // // Active health checks do not run for dynamic upstreams. Active *ActiveHealthChecks `json:"active,omitempty"` // Passive health checks monitor proxied requests for errors or timeouts. // To minimally enable passive health checks, specify at least an empty // config object. Passive health check state is shared (stored globally), // so a failure from one handler will be counted by all handlers; but // the tolerances or standards for what defines healthy/unhealthy backends // is configured per-proxy-handler. // // Passive health checks technically do operate on dynamic upstreams, // but are only effective for very busy proxies where the list of // upstreams is mostly stable. This is because the shared/global // state of upstreams is cleaned up when the upstreams are no longer // used. Since dynamic upstreams are allocated dynamically at each // request (specifically, each iteration of the proxy loop per request), // they are also cleaned up after every request. Thus, if there is a // moment when no requests are actively referring to a particular // upstream host, the passive health check state will be reset because // it will be garbage-collected. It is usually better for the dynamic // upstream module to only return healthy, available backends instead. Passive *PassiveHealthChecks `json:"passive,omitempty"` } // ActiveHealthChecks holds configuration related to active // health checks (that is, health checks which occur in a // background goroutine independently). type ActiveHealthChecks struct { // DEPRECATED: Use 'uri' instead. This field will be removed. TODO: remove this field Path string `json:"path,omitempty"` // The URI (path and query) to use for health checks URI string `json:"uri,omitempty"` // The port to use (if different from the upstream's dial // address) for health checks. Port int `json:"port,omitempty"` // HTTP headers to set on health check requests. Headers http.Header `json:"headers,omitempty"` // How frequently to perform active health checks (default 30s). Interval caddy.Duration `json:"interval,omitempty"` // How long to wait for a response from a backend before // considering it unhealthy (default 5s). Timeout caddy.Duration `json:"timeout,omitempty"` // The maximum response body to download from the backend // during a health check. MaxSize int64 `json:"max_size,omitempty"` // The HTTP status code to expect from a healthy backend. ExpectStatus int `json:"expect_status,omitempty"` // A regular expression against which to match the response // body of a healthy backend. ExpectBody string `json:"expect_body,omitempty"` uri *url.URL httpClient *http.Client bodyRegexp *regexp.Regexp logger *zap.Logger } // PassiveHealthChecks holds configuration related to passive // health checks (that is, health checks which occur during // the normal flow of request proxying). type PassiveHealthChecks struct { // How long to remember a failed request to a backend. A duration > 0 // enables passive health checking. Default is 0. FailDuration caddy.Duration `json:"fail_duration,omitempty"` // The number of failed requests within the FailDuration window to // consider a backend as "down". Must be >= 1; default is 1. Requires // that FailDuration be > 0. MaxFails int `json:"max_fails,omitempty"` // Limits the number of simultaneous requests to a backend by // marking the backend as "down" if it has this many concurrent // requests or more. UnhealthyRequestCount int `json:"unhealthy_request_count,omitempty"` // Count the request as failed if the response comes back with // one of these status codes. UnhealthyStatus []int `json:"unhealthy_status,omitempty"` // Count the request as failed if the response takes at least this // long to receive. UnhealthyLatency caddy.Duration `json:"unhealthy_latency,omitempty"` logger *zap.Logger } // CircuitBreaker is a type that can act as an early-warning // system for the health checker when backends are getting // overloaded. This interface is still experimental and is // subject to change. type CircuitBreaker interface { OK() bool RecordMetric(statusCode int, latency time.Duration) } // activeHealthChecker runs active health checks on a // regular basis and blocks until // h.HealthChecks.Active.stopChan is closed. func (h *Handler) activeHealthChecker() { defer func() { if err := recover(); err != nil { h.HealthChecks.Active.logger.Error("active health checker panicked", zap.Any("error", err), zap.ByteString("stack", debug.Stack())) } }() ticker := time.NewTicker(time.Duration(h.HealthChecks.Active.Interval)) h.doActiveHealthCheckForAllHosts() for { select { case <-ticker.C: h.doActiveHealthCheckForAllHosts() case <-h.ctx.Done(): ticker.Stop() return } } } // doActiveHealthCheckForAllHosts immediately performs a // health checks for all upstream hosts configured by h. func (h *Handler) doActiveHealthCheckForAllHosts() { for _, upstream := range h.Upstreams { go func(upstream *Upstream) { defer func() { if err := recover(); err != nil { h.HealthChecks.Active.logger.Error("active health check panicked", zap.Any("error", err), zap.ByteString("stack", debug.Stack())) } }() networkAddr, err := caddy.NewReplacer().ReplaceOrErr(upstream.Dial, true, true) if err != nil { h.HealthChecks.Active.logger.Error("invalid use of placeholders in dial address for active health checks", zap.String("address", networkAddr), zap.Error(err), ) return } addr, err := caddy.ParseNetworkAddress(networkAddr) if err != nil { h.HealthChecks.Active.logger.Error("bad network address", zap.String("address", networkAddr), zap.Error(err), ) return } if hcp := uint(upstream.activeHealthCheckPort); hcp != 0 { if addr.IsUnixNetwork() { addr.Network = "tcp" // I guess we just assume TCP since we are using a port?? } addr.StartPort, addr.EndPort = hcp, hcp } if upstream.LookupSRV == "" && addr.PortRangeSize() != 1 { h.HealthChecks.Active.logger.Error("multiple addresses (upstream must map to only one address)", zap.String("address", networkAddr), ) return } hostAddr := addr.JoinHostPort(0) dialAddr := hostAddr if addr.IsUnixNetwork() { // this will be used as the Host portion of a http.Request URL, and // paths to socket files would produce an error when creating URL, // so use a fake Host value instead; unix sockets are usually local hostAddr = "localhost" } err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, upstream) if err != nil { h.HealthChecks.Active.logger.Error("active health check failed", zap.String("address", hostAddr), zap.Error(err), ) } }(upstream) } } // doActiveHealthCheck performs a health check to upstream which // can be reached at address hostAddr. The actual address for // the request will be built according to active health checker // config. The health status of the host will be updated // according to whether it passes the health check. An error is // returned only if the health check fails to occur or if marking // the host's health status fails. func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstream *Upstream) error { // create the URL for the request that acts as a health check scheme := "http" if ht, ok := h.Transport.(TLSTransport); ok && ht.TLSEnabled() { // this is kind of a hacky way to know if we should use HTTPS, but whatever scheme = "https" } u := &url.URL{ Scheme: scheme, Host: hostAddr, } // if we have a provisioned uri, use that, otherwise use // the deprecated Path option if h.HealthChecks.Active.uri != nil { u.Path = h.HealthChecks.Active.uri.Path u.RawQuery = h.HealthChecks.Active.uri.RawQuery } else { u.Path = h.HealthChecks.Active.Path } // adjust the port, if configured to be different if h.HealthChecks.Active.Port != 0 { portStr := strconv.Itoa(h.HealthChecks.Active.Port) host, _, err := net.SplitHostPort(hostAddr) if err != nil { host = hostAddr } u.Host = net.JoinHostPort(host, portStr) } // attach dialing information to this request ctx := h.ctx.Context ctx = context.WithValue(ctx, caddy.ReplacerCtxKey, caddy.NewReplacer()) ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{ dialInfoVarKey: dialInfo, }) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return fmt.Errorf("making request: %v", err) } for key, hdrs := range h.HealthChecks.Active.Headers { if strings.ToLower(key) == "host" { req.Host = h.HealthChecks.Active.Headers.Get(key) } else { req.Header[key] = hdrs } } markUnhealthy := func() { // dispatch an event that the host newly became unhealthy if upstream.setHealthy(false) { h.events.Emit(h.ctx, "unhealthy", map[string]any{"host": hostAddr}) } } // do the request, being careful to tame the response body resp, err := h.HealthChecks.Active.httpClient.Do(req) if err != nil { h.HealthChecks.Active.logger.Info("HTTP request failed", zap.String("host", hostAddr), zap.Error(err), ) markUnhealthy() return nil } var body io.Reader = resp.Body if h.HealthChecks.Active.MaxSize > 0 { body = io.LimitReader(body, h.HealthChecks.Active.MaxSize) } defer func() { // drain any remaining body so connection could be re-used _, _ = io.Copy(io.Discard, body) resp.Body.Close() }() // if status code is outside criteria, mark down if h.HealthChecks.Active.ExpectStatus > 0 { if !caddyhttp.StatusCodeMatches(resp.StatusCode, h.HealthChecks.Active.ExpectStatus) { h.HealthChecks.Active.logger.Info("unexpected status code", zap.Int("status_code", resp.StatusCode), zap.String("host", hostAddr), ) markUnhealthy() return nil } } else if resp.StatusCode < 200 || resp.StatusCode >= 400 { h.HealthChecks.Active.logger.Info("status code out of tolerances", zap.Int("status_code", resp.StatusCode), zap.String("host", hostAddr), ) markUnhealthy() return nil } // if body does not match regex, mark down if h.HealthChecks.Active.bodyRegexp != nil { bodyBytes, err := io.ReadAll(body) if err != nil { h.HealthChecks.Active.logger.Info("failed to read response body", zap.String("host", hostAddr), zap.Error(err), ) markUnhealthy() return nil } if !h.HealthChecks.Active.bodyRegexp.Match(bodyBytes) { h.HealthChecks.Active.logger.Info("response body failed expectations", zap.String("host", hostAddr), ) markUnhealthy() return nil } } // passed health check parameters, so mark as healthy if upstream.setHealthy(true) { h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr)) h.events.Emit(h.ctx, "healthy", map[string]any{"host": hostAddr}) } return nil } // countFailure is used with passive health checks. It // remembers 1 failure for upstream for the configured // duration. If passive health checks are disabled or // failure expiry is 0, this is a no-op. func (h *Handler) countFailure(upstream *Upstream) { // only count failures if passive health checking is enabled // and if failures are configured have a non-zero expiry if h.HealthChecks == nil || h.HealthChecks.Passive == nil { return } failDuration := time.Duration(h.HealthChecks.Passive.FailDuration) if failDuration == 0 { return } // count failure immediately err := upstream.Host.countFail(1) if err != nil { h.HealthChecks.Passive.logger.Error("could not count failure", zap.String("host", upstream.Dial), zap.Error(err)) return } // forget it later go func(host *Host, failDuration time.Duration) { defer func() { if err := recover(); err != nil { h.HealthChecks.Passive.logger.Error("passive health check failure forgetter panicked", zap.Any("error", err), zap.ByteString("stack", debug.Stack())) } }() timer := time.NewTimer(failDuration) select { case <-h.ctx.Done(): if !timer.Stop() { <-timer.C } case <-timer.C: } err := host.countFail(-1) if err != nil { h.HealthChecks.Passive.logger.Error("could not forget failure", zap.String("host", upstream.Dial), zap.Error(err)) } }(upstream.Host, failDuration) } caddy-2.6.2/modules/caddyhttp/reverseproxy/hosts.go000066400000000000000000000207531435007237400225210ustar00rootroot00000000000000// 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 reverseproxy import ( "context" "fmt" "net" "net/http" "strconv" "sync/atomic" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) // UpstreamPool is a collection of upstreams. type UpstreamPool []*Upstream // Upstream bridges this proxy's configuration to the // state of the backend host it is correlated with. // Upstream values must not be copied. type Upstream struct { *Host `json:"-"` // The [network address](/docs/conventions#network-addresses) // to dial to connect to the upstream. Must represent precisely // one socket (i.e. no port ranges). A valid network address // either has a host and port or is a unix socket address. // // Placeholders may be used to make the upstream dynamic, but be // aware of the health check implications of this: a single // upstream that represents numerous (perhaps arbitrary) backends // can be considered down if one or enough of the arbitrary // backends is down. Also be aware of open proxy vulnerabilities. Dial string `json:"dial,omitempty"` // DEPRECATED: Use the SRVUpstreams module instead // (http.reverse_proxy.upstreams.srv). This field will be // removed in a future version of Caddy. TODO: Remove this field. // // If DNS SRV records are used for service discovery with this // upstream, specify the DNS name for which to look up SRV // records here, instead of specifying a dial address. LookupSRV string `json:"lookup_srv,omitempty"` // The maximum number of simultaneous requests to allow to // this upstream. If set, overrides the global passive health // check UnhealthyRequestCount value. MaxRequests int `json:"max_requests,omitempty"` // TODO: This could be really useful, to bind requests // with certain properties to specific backends // HeaderAffinity string // IPAffinity string activeHealthCheckPort int healthCheckPolicy *PassiveHealthChecks cb CircuitBreaker unhealthy int32 // accessed atomically; status from active health checker } func (u Upstream) String() string { if u.LookupSRV != "" { return u.LookupSRV } return u.Dial } // Available returns true if the remote host // is available to receive requests. This is // the method that should be used by selection // policies, etc. to determine if a backend // should be able to be sent a request. func (u *Upstream) Available() bool { return u.Healthy() && !u.Full() } // Healthy returns true if the remote host // is currently known to be healthy or "up". // It consults the circuit breaker, if any. func (u *Upstream) Healthy() bool { healthy := u.healthy() if healthy && u.healthCheckPolicy != nil { healthy = u.Host.Fails() < u.healthCheckPolicy.MaxFails } if healthy && u.cb != nil { healthy = u.cb.OK() } return healthy } // Full returns true if the remote host // cannot receive more requests at this time. func (u *Upstream) Full() bool { return u.MaxRequests > 0 && u.Host.NumRequests() >= u.MaxRequests } // fillDialInfo returns a filled DialInfo for upstream u, using the request // context. If the upstream has a SRV lookup configured, that is done and a // returned address is chosen; otherwise, the upstream's regular dial address // field is used. Note that the returned value is not a pointer. func (u *Upstream) fillDialInfo(r *http.Request) (DialInfo, error) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) var addr caddy.NetworkAddress if u.LookupSRV != "" { // perform DNS lookup for SRV records and choose one - TODO: deprecated srvName := repl.ReplaceAll(u.LookupSRV, "") _, records, err := net.DefaultResolver.LookupSRV(r.Context(), "", "", srvName) if err != nil { return DialInfo{}, err } addr.Network = "tcp" addr.Host = records[0].Target addr.StartPort, addr.EndPort = uint(records[0].Port), uint(records[0].Port) } else { // use provided dial address var err error dial := repl.ReplaceAll(u.Dial, "") addr, err = caddy.ParseNetworkAddress(dial) if err != nil { return DialInfo{}, fmt.Errorf("upstream %s: invalid dial address %s: %v", u.Dial, dial, err) } if numPorts := addr.PortRangeSize(); numPorts != 1 { return DialInfo{}, fmt.Errorf("upstream %s: dial address must represent precisely one socket: %s represents %d", u.Dial, dial, numPorts) } } return DialInfo{ Upstream: u, Network: addr.Network, Address: addr.JoinHostPort(0), Host: addr.Host, Port: strconv.Itoa(int(addr.StartPort)), }, nil } func (u *Upstream) fillHost() { host := new(Host) existingHost, loaded := hosts.LoadOrStore(u.String(), host) if loaded { host = existingHost.(*Host) } u.Host = host } // Host is the basic, in-memory representation of the state of a remote host. // Its fields are accessed atomically and Host values must not be copied. type Host struct { numRequests int64 // must be 64-bit aligned on 32-bit systems (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG) fails int64 } // NumRequests returns the number of active requests to the upstream. func (h *Host) NumRequests() int { return int(atomic.LoadInt64(&h.numRequests)) } // Fails returns the number of recent failures with the upstream. func (h *Host) Fails() int { return int(atomic.LoadInt64(&h.fails)) } // countRequest mutates the active request count by // delta. It returns an error if the adjustment fails. func (h *Host) countRequest(delta int) error { result := atomic.AddInt64(&h.numRequests, int64(delta)) if result < 0 { return fmt.Errorf("count below 0: %d", result) } return nil } // countFail mutates the recent failures count by // delta. It returns an error if the adjustment fails. func (h *Host) countFail(delta int) error { result := atomic.AddInt64(&h.fails, int64(delta)) if result < 0 { return fmt.Errorf("count below 0: %d", result) } return nil } // healthy returns true if the upstream is not actively marked as unhealthy. // (This returns the status only from the "active" health checks.) func (u *Upstream) healthy() bool { return atomic.LoadInt32(&u.unhealthy) == 0 } // SetHealthy sets the upstream has healthy or unhealthy // and returns true if the new value is different. This // sets the status only for the "active" health checks. func (u *Upstream) setHealthy(healthy bool) bool { var unhealthy, compare int32 = 1, 0 if healthy { unhealthy, compare = 0, 1 } return atomic.CompareAndSwapInt32(&u.unhealthy, compare, unhealthy) } // DialInfo contains information needed to dial a // connection to an upstream host. This information // may be different than that which is represented // in a URL (for example, unix sockets don't have // a host that can be represented in a URL, but // they certainly have a network name and address). type DialInfo struct { // Upstream is the Upstream associated with // this DialInfo. It may be nil. Upstream *Upstream // The network to use. This should be one of // the values that is accepted by net.Dial: // https://golang.org/pkg/net/#Dial Network string // The address to dial. Follows the same // semantics and rules as net.Dial. Address string // Host and Port are components of Address. Host, Port string } // String returns the Caddy network address form // by joining the network and address with a // forward slash. func (di DialInfo) String() string { return caddy.JoinNetworkAddress(di.Network, di.Host, di.Port) } // GetDialInfo gets the upstream dialing info out of the context, // and returns true if there was a valid value; false otherwise. func GetDialInfo(ctx context.Context) (DialInfo, bool) { dialInfo, ok := caddyhttp.GetVar(ctx, dialInfoVarKey).(DialInfo) return dialInfo, ok } // hosts is the global repository for hosts that are // currently in use by active configuration(s). This // allows the state of remote hosts to be preserved // through config reloads. var hosts = caddy.NewUsagePool() // dialInfoVarKey is the key used for the variable that holds // the dial info for the upstream connection. const dialInfoVarKey = "reverse_proxy.dial_info" caddy-2.6.2/modules/caddyhttp/reverseproxy/httptransport.go000066400000000000000000000500551435007237400243130ustar00rootroot00000000000000// 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 reverseproxy import ( "context" "crypto/tls" "crypto/x509" "encoding/base64" "fmt" weakrand "math/rand" "net" "net/http" "os" "reflect" "strings" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" "go.uber.org/zap" "golang.org/x/net/http2" ) func init() { caddy.RegisterModule(HTTPTransport{}) } // HTTPTransport is essentially a configuration wrapper for http.Transport. // It defines a JSON structure useful when configuring the HTTP transport // for Caddy's reverse proxy. It builds its http.Transport at Provision. type HTTPTransport struct { // TODO: It's possible that other transports (like fastcgi) might be // able to borrow/use at least some of these config fields; if so, // maybe move them into a type called CommonTransport and embed it? // Configures the DNS resolver used to resolve the IP address of upstream hostnames. Resolver *UpstreamResolver `json:"resolver,omitempty"` // Configures TLS to the upstream. Setting this to an empty struct // is sufficient to enable TLS with reasonable defaults. TLS *TLSConfig `json:"tls,omitempty"` // Configures HTTP Keep-Alive (enabled by default). Should only be // necessary if rigorous testing has shown that tuning this helps // improve performance. KeepAlive *KeepAlive `json:"keep_alive,omitempty"` // Whether to enable compression to upstream. Default: true Compression *bool `json:"compression,omitempty"` // Maximum number of connections per host. Default: 0 (no limit) MaxConnsPerHost int `json:"max_conns_per_host,omitempty"` // How long to wait before timing out trying to connect to // an upstream. Default: `3s`. DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` // How long to wait before spawning an RFC 6555 Fast Fallback // connection. A negative value disables this. Default: `300ms`. FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"` // How long to wait for reading response headers from server. Default: No timeout. ResponseHeaderTimeout caddy.Duration `json:"response_header_timeout,omitempty"` // The length of time to wait for a server's first response // headers after fully writing the request headers if the // request has a header "Expect: 100-continue". Default: No timeout. ExpectContinueTimeout caddy.Duration `json:"expect_continue_timeout,omitempty"` // The maximum bytes to read from response headers. Default: `10MiB`. MaxResponseHeaderSize int64 `json:"max_response_header_size,omitempty"` // The size of the write buffer in bytes. Default: `4KiB`. WriteBufferSize int `json:"write_buffer_size,omitempty"` // The size of the read buffer in bytes. Default: `4KiB`. ReadBufferSize int `json:"read_buffer_size,omitempty"` // The maximum time to wait for next read from backend. Default: no timeout. ReadTimeout caddy.Duration `json:"read_timeout,omitempty"` // The maximum time to wait for next write to backend. Default: no timeout. WriteTimeout caddy.Duration `json:"write_timeout,omitempty"` // The versions of HTTP to support. As a special case, "h2c" // can be specified to use H2C (HTTP/2 over Cleartext) to the // upstream (this feature is experimental and subject to // change or removal). Default: ["1.1", "2"] Versions []string `json:"versions,omitempty"` // The pre-configured underlying HTTP transport. Transport *http.Transport `json:"-"` h2cTransport *http2.Transport } // CaddyModule returns the Caddy module information. func (HTTPTransport) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.transport.http", New: func() caddy.Module { return new(HTTPTransport) }, } } // Provision sets up h.Transport with a *http.Transport // that is ready to use. func (h *HTTPTransport) Provision(ctx caddy.Context) error { if len(h.Versions) == 0 { h.Versions = []string{"1.1", "2"} } rt, err := h.NewTransport(ctx) if err != nil { return err } h.Transport = rt return nil } // NewTransport builds a standard-lib-compatible http.Transport value from h. func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, error) { // Set keep-alive defaults if it wasn't otherwise configured if h.KeepAlive == nil { h.KeepAlive = &KeepAlive{ ProbeInterval: caddy.Duration(30 * time.Second), IdleConnTimeout: caddy.Duration(2 * time.Minute), MaxIdleConnsPerHost: 32, // seems about optimal, see #2805 } } // Set a relatively short default dial timeout. // This is helpful to make load-balancer retries more speedy. if h.DialTimeout == 0 { h.DialTimeout = caddy.Duration(3 * time.Second) } dialer := &net.Dialer{ Timeout: time.Duration(h.DialTimeout), FallbackDelay: time.Duration(h.FallbackDelay), } if h.Resolver != nil { err := h.Resolver.ParseAddresses() if err != nil { return nil, err } d := &net.Dialer{ Timeout: time.Duration(h.DialTimeout), FallbackDelay: time.Duration(h.FallbackDelay), } dialer.Resolver = &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { //nolint:gosec addr := h.Resolver.netAddrs[weakrand.Intn(len(h.Resolver.netAddrs))] return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0)) }, } } // Set up the dialer to pull the correct information from the context dialContext := func(ctx context.Context, network, address string) (net.Conn, error) { // the proper dialing information should be embedded into the request's context if dialInfo, ok := GetDialInfo(ctx); ok { network = dialInfo.Network address = dialInfo.Address } conn, err := dialer.DialContext(ctx, network, address) if err != nil { // identify this error as one that occurred during // dialing, which can be important when trying to // decide whether to retry a request return nil, DialError{err} } // if read/write timeouts are configured and this is a TCP connection, enforce the timeouts // by wrapping the connection with our own type if tcpConn, ok := conn.(*net.TCPConn); ok && (h.ReadTimeout > 0 || h.WriteTimeout > 0) { conn = &tcpRWTimeoutConn{ TCPConn: tcpConn, readTimeout: time.Duration(h.ReadTimeout), writeTimeout: time.Duration(h.WriteTimeout), logger: caddyCtx.Logger(), } } return conn, nil } rt := &http.Transport{ DialContext: dialContext, MaxConnsPerHost: h.MaxConnsPerHost, ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout), ExpectContinueTimeout: time.Duration(h.ExpectContinueTimeout), MaxResponseHeaderBytes: h.MaxResponseHeaderSize, WriteBufferSize: h.WriteBufferSize, ReadBufferSize: h.ReadBufferSize, } if h.TLS != nil { rt.TLSHandshakeTimeout = time.Duration(h.TLS.HandshakeTimeout) var err error rt.TLSClientConfig, err = h.TLS.MakeTLSClientConfig(caddyCtx) if err != nil { return nil, fmt.Errorf("making TLS client config: %v", err) } } if h.KeepAlive != nil { dialer.KeepAlive = time.Duration(h.KeepAlive.ProbeInterval) if h.KeepAlive.Enabled != nil { rt.DisableKeepAlives = !*h.KeepAlive.Enabled } rt.MaxIdleConns = h.KeepAlive.MaxIdleConns rt.MaxIdleConnsPerHost = h.KeepAlive.MaxIdleConnsPerHost rt.IdleConnTimeout = time.Duration(h.KeepAlive.IdleConnTimeout) } if h.Compression != nil { rt.DisableCompression = !*h.Compression } if sliceContains(h.Versions, "2") { if err := http2.ConfigureTransport(rt); err != nil { return nil, err } } // if h2c is enabled, configure its transport (std lib http.Transport // does not "HTTP/2 over cleartext TCP") if sliceContains(h.Versions, "h2c") { // crafting our own http2.Transport doesn't allow us to utilize // most of the customizations/preferences on the http.Transport, // because, for some reason, only http2.ConfigureTransport() // is allowed to set the unexported field that refers to a base // http.Transport config; oh well h2t := &http2.Transport{ // kind of a hack, but for plaintext/H2C requests, pretend to dial TLS DialTLSContext: func(ctx context.Context, network, address string, _ *tls.Config) (net.Conn, error) { return dialContext(ctx, network, address) }, AllowHTTP: true, } if h.Compression != nil { h2t.DisableCompression = !*h.Compression } h.h2cTransport = h2t } return rt, nil } // replaceTLSServername checks TLS servername to see if it needs replacing // if it does need replacing, it creates a new cloned HTTPTransport object to avoid any races // and does the replacing of the TLS servername on that and returns the new object // if no replacement is necessary it returns the original func (h *HTTPTransport) replaceTLSServername(repl *caddy.Replacer) *HTTPTransport { // check whether we have TLS and need to replace the servername in the TLSClientConfig if h.TLSEnabled() && strings.Contains(h.TLS.ServerName, "{") { // make a new h, "copy" the parts we don't need to touch, add a new *tls.Config and replace servername newtransport := &HTTPTransport{ Resolver: h.Resolver, TLS: h.TLS, KeepAlive: h.KeepAlive, Compression: h.Compression, MaxConnsPerHost: h.MaxConnsPerHost, DialTimeout: h.DialTimeout, FallbackDelay: h.FallbackDelay, ResponseHeaderTimeout: h.ResponseHeaderTimeout, ExpectContinueTimeout: h.ExpectContinueTimeout, MaxResponseHeaderSize: h.MaxResponseHeaderSize, WriteBufferSize: h.WriteBufferSize, ReadBufferSize: h.ReadBufferSize, Versions: h.Versions, Transport: h.Transport.Clone(), h2cTransport: h.h2cTransport, } newtransport.Transport.TLSClientConfig.ServerName = repl.ReplaceAll(newtransport.Transport.TLSClientConfig.ServerName, "") return newtransport } return h } // RoundTrip implements http.RoundTripper. func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { // Try to replace TLS servername if needed repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) transport := h.replaceTLSServername(repl) transport.SetScheme(req) // if H2C ("HTTP/2 over cleartext") is enabled and the upstream request is // HTTP without TLS, use the alternate H2C-capable transport instead if req.URL.Scheme == "http" && h.h2cTransport != nil { return h.h2cTransport.RoundTrip(req) } return transport.Transport.RoundTrip(req) } // SetScheme ensures that the outbound request req // has the scheme set in its URL; the underlying // http.Transport requires a scheme to be set. // // This method may be used by other transport modules // that wrap/use this one. func (h *HTTPTransport) SetScheme(req *http.Request) { if req.URL.Scheme != "" { return } if h.shouldUseTLS(req) { req.URL.Scheme = "https" } else { req.URL.Scheme = "http" } } // shouldUseTLS returns true if TLS should be used for req. func (h *HTTPTransport) shouldUseTLS(req *http.Request) bool { if h.TLS == nil { return false } port := req.URL.Port() for i := range h.TLS.ExceptPorts { if h.TLS.ExceptPorts[i] == port { return false } } return true } // TLSEnabled returns true if TLS is enabled. func (h HTTPTransport) TLSEnabled() bool { return h.TLS != nil } // EnableTLS enables TLS on the transport. func (h *HTTPTransport) EnableTLS(base *TLSConfig) error { h.TLS = base return nil } // Cleanup implements caddy.CleanerUpper and closes any idle connections. func (h HTTPTransport) Cleanup() error { if h.Transport == nil { return nil } h.Transport.CloseIdleConnections() return nil } // TLSConfig holds configuration related to the TLS configuration for the // transport/client. type TLSConfig struct { // Optional list of base64-encoded DER-encoded CA certificates to trust. RootCAPool []string `json:"root_ca_pool,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"` // PEM-encoded client certificate filename to present to servers. ClientCertificateFile string `json:"client_certificate_file,omitempty"` // PEM-encoded key to use with the client certificate. ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"` // If specified, Caddy will use and automate a client certificate // with this subject name. ClientCertificateAutomate string `json:"client_certificate_automate,omitempty"` // If true, TLS verification of server certificates will be disabled. // This is insecure and may be removed in the future. Do not use this // option except in testing or local development environments. InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"` // The duration to allow a TLS handshake to a server. Default: No timeout. HandshakeTimeout caddy.Duration `json:"handshake_timeout,omitempty"` // The server name used when verifying the certificate received in the TLS // handshake. By default, this will use the upstream address' host part. // You only need to override this if your upstream address does not match the // certificate the upstream is likely to use. For example if the upstream // address is an IP address, then you would need to configure this to the // hostname being served by the upstream server. Currently, this does not // support placeholders because the TLS config is not provisioned on each // connection, so a static value must be used. ServerName string `json:"server_name,omitempty"` // TLS renegotiation level. TLS renegotiation is the act of performing // subsequent handshakes on a connection after the first. // The level can be: // - "never": (the default) disables renegotiation. // - "once": allows a remote server to request renegotiation once per connection. // - "freely": allows a remote server to repeatedly request renegotiation. Renegotiation string `json:"renegotiation,omitempty"` // Skip TLS ports specifies a list of upstream ports on which TLS should not be // attempted even if it is configured. Handy when using dynamic upstreams that // return HTTP and HTTPS endpoints too. // When specified, TLS will automatically be configured on the transport. // The value can be a list of any valid tcp port numbers, default empty. ExceptPorts []string `json:"except_ports,omitempty"` } // MakeTLSClientConfig returns a tls.Config usable by a client to a backend. // If there is no custom TLS configuration, a nil config may be returned. func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) { cfg := new(tls.Config) // client auth if t.ClientCertificateFile != "" && t.ClientCertificateKeyFile == "" { return nil, fmt.Errorf("client_certificate_file specified without client_certificate_key_file") } if t.ClientCertificateFile == "" && t.ClientCertificateKeyFile != "" { return nil, fmt.Errorf("client_certificate_key_file specified without client_certificate_file") } if t.ClientCertificateFile != "" && t.ClientCertificateKeyFile != "" { cert, err := tls.LoadX509KeyPair(t.ClientCertificateFile, t.ClientCertificateKeyFile) if err != nil { return nil, fmt.Errorf("loading client certificate key pair: %v", err) } cfg.Certificates = []tls.Certificate{cert} } if t.ClientCertificateAutomate != "" { // TODO: use or enable ctx.IdentityCredentials() ... tlsAppIface, err := ctx.App("tls") if err != nil { return nil, fmt.Errorf("getting tls app: %v", err) } tlsApp := tlsAppIface.(*caddytls.TLS) err = tlsApp.Manage([]string{t.ClientCertificateAutomate}) if err != nil { return nil, fmt.Errorf("managing client certificate: %v", err) } cfg.GetClientCertificate = func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { certs := tlsApp.AllMatchingCertificates(t.ClientCertificateAutomate) var err error for _, cert := range certs { err = cri.SupportsCertificate(&cert.Certificate) if err == nil { return &cert.Certificate, nil } } if err == nil { err = fmt.Errorf("no client certificate found for automate name: %s", t.ClientCertificateAutomate) } return nil, err } } // trusted root CAs if len(t.RootCAPool) > 0 || len(t.RootCAPEMFiles) > 0 { rootPool := x509.NewCertPool() for _, encodedCACert := range t.RootCAPool { caCert, err := decodeBase64DERCert(encodedCACert) if err != nil { return nil, fmt.Errorf("parsing CA certificate: %v", err) } rootPool.AddCert(caCert) } for _, pemFile := range t.RootCAPEMFiles { pemData, err := os.ReadFile(pemFile) if err != nil { return nil, fmt.Errorf("failed reading ca cert: %v", err) } rootPool.AppendCertsFromPEM(pemData) } cfg.RootCAs = rootPool } // Renegotiation switch t.Renegotiation { case "never", "": cfg.Renegotiation = tls.RenegotiateNever case "once": cfg.Renegotiation = tls.RenegotiateOnceAsClient case "freely": cfg.Renegotiation = tls.RenegotiateFreelyAsClient default: return nil, fmt.Errorf("invalid TLS renegotiation level: %v", t.Renegotiation) } // override for the server name used verify the TLS handshake cfg.ServerName = t.ServerName // throw all security out the window cfg.InsecureSkipVerify = t.InsecureSkipVerify // only return a config if it's not empty if reflect.DeepEqual(cfg, new(tls.Config)) { return nil, nil } return cfg, nil } // KeepAlive holds configuration pertaining to HTTP Keep-Alive. type KeepAlive struct { // Whether HTTP Keep-Alive is enabled. Default: `true` Enabled *bool `json:"enabled,omitempty"` // How often to probe for liveness. Default: `30s`. ProbeInterval caddy.Duration `json:"probe_interval,omitempty"` // Maximum number of idle connections. Default: `0`, which means no limit. MaxIdleConns int `json:"max_idle_conns,omitempty"` // Maximum number of idle connections per host. Default: `32`. MaxIdleConnsPerHost int `json:"max_idle_conns_per_host,omitempty"` // How long connections should be kept alive when idle. Default: `2m`. IdleConnTimeout caddy.Duration `json:"idle_timeout,omitempty"` } // tcpRWTimeoutConn enforces read/write timeouts for a TCP connection. // If it fails to set deadlines, the error is logged but does not abort // the read/write attempt (ignoring the error is consistent with what // the standard library does: https://github.com/golang/go/blob/c5da4fb7ac5cb7434b41fc9a1df3bee66c7f1a4d/src/net/http/server.go#L981-L986) type tcpRWTimeoutConn struct { *net.TCPConn readTimeout, writeTimeout time.Duration logger *zap.Logger } func (c *tcpRWTimeoutConn) Read(b []byte) (int, error) { if c.readTimeout > 0 { err := c.TCPConn.SetReadDeadline(time.Now().Add(c.readTimeout)) if err != nil { c.logger.Error("failed to set read deadline", zap.Error(err)) } } return c.TCPConn.Read(b) } func (c *tcpRWTimeoutConn) Write(b []byte) (int, error) { if c.writeTimeout > 0 { err := c.TCPConn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) if err != nil { c.logger.Error("failed to set write deadline", zap.Error(err)) } } return c.TCPConn.Write(b) } // decodeBase64DERCert base64-decodes, then DER-decodes, certStr. func decodeBase64DERCert(certStr string) (*x509.Certificate, error) { // decode base64 derBytes, err := base64.StdEncoding.DecodeString(certStr) if err != nil { return nil, err } // parse the DER-encoded certificate return x509.ParseCertificate(derBytes) } // 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 } // Interface guards var ( _ caddy.Provisioner = (*HTTPTransport)(nil) _ http.RoundTripper = (*HTTPTransport)(nil) _ caddy.CleanerUpper = (*HTTPTransport)(nil) _ TLSTransport = (*HTTPTransport)(nil) ) caddy-2.6.2/modules/caddyhttp/reverseproxy/metrics.go000066400000000000000000000034631435007237400230260ustar00rootroot00000000000000package reverseproxy import ( "runtime/debug" "sync" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "go.uber.org/zap" ) var reverseProxyMetrics = struct { init sync.Once upstreamsHealthy *prometheus.GaugeVec logger *zap.Logger }{} func initReverseProxyMetrics(handler *Handler) { const ns, sub = "caddy", "reverse_proxy" upstreamsLabels := []string{"upstream"} reverseProxyMetrics.upstreamsHealthy = promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: ns, Subsystem: sub, Name: "upstreams_healthy", Help: "Health status of reverse proxy upstreams.", }, upstreamsLabels) reverseProxyMetrics.logger = handler.logger.Named("reverse_proxy.metrics") } type metricsUpstreamsHealthyUpdater struct { handler *Handler } func newMetricsUpstreamsHealthyUpdater(handler *Handler) *metricsUpstreamsHealthyUpdater { reverseProxyMetrics.init.Do(func() { initReverseProxyMetrics(handler) }) return &metricsUpstreamsHealthyUpdater{handler} } func (m *metricsUpstreamsHealthyUpdater) Init() { go func() { defer func() { if err := recover(); err != nil { reverseProxyMetrics.logger.Error("upstreams healthy metrics updater panicked", zap.Any("error", err), zap.ByteString("stack", debug.Stack())) } }() m.update() ticker := time.NewTicker(10 * time.Second) for { select { case <-ticker.C: m.update() case <-m.handler.ctx.Done(): ticker.Stop() return } } }() } func (m *metricsUpstreamsHealthyUpdater) update() { for _, upstream := range m.handler.Upstreams { labels := prometheus.Labels{"upstream": upstream.Dial} gaugeValue := 0.0 if upstream.Healthy() { gaugeValue = 1.0 } reverseProxyMetrics.upstreamsHealthy.With(labels).Set(gaugeValue) } } caddy-2.6.2/modules/caddyhttp/reverseproxy/reverseproxy.go000066400000000000000000001466371435007237400241500ustar00rootroot00000000000000// 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 reverseproxy import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/http/httptrace" "net/netip" "net/textproto" "net/url" "regexp" "runtime" "strconv" "strings" "sync" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" "go.uber.org/zap" "golang.org/x/net/http/httpguts" ) var supports1xx bool func init() { // Caddy requires at least Go 1.18, but Early Hints requires Go 1.19; thus we can simply check for 1.18 in version string // TODO: remove this once our minimum Go version is 1.19 supports1xx = !strings.Contains(runtime.Version(), "go1.18") caddy.RegisterModule(Handler{}) } // Handler implements a highly configurable and production-ready reverse proxy. // // Upon proxying, this module sets the following placeholders (which can be used // both within and after this handler; for example, in response headers): // // Placeholder | Description // ------------|------------- // `{http.reverse_proxy.upstream.address}` | The full address to the upstream as given in the config // `{http.reverse_proxy.upstream.hostport}` | The host:port of the upstream // `{http.reverse_proxy.upstream.host}` | The host of the upstream // `{http.reverse_proxy.upstream.port}` | The port of the upstream // `{http.reverse_proxy.upstream.requests}` | The approximate current number of requests to the upstream // `{http.reverse_proxy.upstream.max_requests}` | The maximum approximate number of requests allowed to the upstream // `{http.reverse_proxy.upstream.fails}` | The number of recent failed requests to the upstream // `{http.reverse_proxy.upstream.latency}` | How long it took the proxy upstream to write the response header. // `{http.reverse_proxy.upstream.latency_ms}` | Same as 'latency', but in milliseconds. // `{http.reverse_proxy.upstream.duration}` | Time spent proxying to the upstream, including writing response body to client. // `{http.reverse_proxy.upstream.duration_ms}` | Same as 'upstream.duration', but in milliseconds. // `{http.reverse_proxy.duration}` | Total time spent proxying, including selecting an upstream, retries, and writing response. // `{http.reverse_proxy.duration_ms}` | Same as 'duration', but in milliseconds. type Handler struct { // Configures the method of transport for the proxy. A transport // is what performs the actual "round trip" to the backend. // The default transport is plaintext HTTP. TransportRaw json.RawMessage `json:"transport,omitempty" caddy:"namespace=http.reverse_proxy.transport inline_key=protocol"` // A circuit breaker may be used to relieve pressure on a backend // that is beginning to exhibit symptoms of stress or latency. // By default, there is no circuit breaker. CBRaw json.RawMessage `json:"circuit_breaker,omitempty" caddy:"namespace=http.reverse_proxy.circuit_breakers inline_key=type"` // Load balancing distributes load/requests between backends. LoadBalancing *LoadBalancing `json:"load_balancing,omitempty"` // Health checks update the status of backends, whether they are // up or down. Down backends will not be proxied to. HealthChecks *HealthChecks `json:"health_checks,omitempty"` // Upstreams is the static list of backends to proxy to. Upstreams UpstreamPool `json:"upstreams,omitempty"` // A module for retrieving the list of upstreams dynamically. Dynamic // upstreams are retrieved at every iteration of the proxy loop for // each request (i.e. before every proxy attempt within every request). // Active health checks do not work on dynamic upstreams, and passive // health checks are only effective on dynamic upstreams if the proxy // server is busy enough that concurrent requests to the same backends // are continuous. Instead of health checks for dynamic upstreams, it // is recommended that the dynamic upstream module only return available // backends in the first place. DynamicUpstreamsRaw json.RawMessage `json:"dynamic_upstreams,omitempty" caddy:"namespace=http.reverse_proxy.upstreams inline_key=source"` // Adjusts how often to flush the response buffer. By default, // no periodic flushing is done. A negative value disables // response buffering, and flushes immediately after each // write to the client. This option is ignored when the upstream's // response is recognized as a streaming response, or if its // content length is -1; for such responses, writes are flushed // to the client immediately. // // Normally, a request will be canceled if the client disconnects // before the response is received from the backend. If explicitly // set to -1, client disconnection will be ignored and the request // will be completed to help facilitate low-latency streaming. FlushInterval caddy.Duration `json:"flush_interval,omitempty"` // A list of IP ranges (supports CIDR notation) from which // X-Forwarded-* header values should be trusted. By default, // no proxies are trusted, so existing values will be ignored // when setting these headers. If the proxy is trusted, then // existing values will be used when constructing the final // header values. TrustedProxies []string `json:"trusted_proxies,omitempty"` // Headers manipulates headers between Caddy and the backend. // By default, all headers are passed-thru without changes, // with the exceptions of special hop-by-hop headers. // // X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host // are also set implicitly. Headers *headers.Handler `json:"headers,omitempty"` // If true, the entire request body will be read and buffered // in memory before being proxied to the backend. This should // be avoided if at all possible for performance reasons, but // could be useful if the backend is intolerant of read latency. BufferRequests bool `json:"buffer_requests,omitempty"` // If true, the entire response body will be read and buffered // in memory before being proxied to the client. This should // be avoided if at all possible for performance reasons, but // could be useful if the backend has tighter memory constraints. BufferResponses bool `json:"buffer_responses,omitempty"` // If body buffering is enabled, the maximum size of the buffers // used for the requests and responses (in bytes). MaxBufferSize int64 `json:"max_buffer_size,omitempty"` // If configured, rewrites the copy of the upstream request. // Allows changing the request method and URI (path and query). // Since the rewrite is applied to the copy, it does not persist // past the reverse proxy handler. // If the method is changed to `GET` or `HEAD`, the request body // will not be copied to the backend. This allows a later request // handler -- either in a `handle_response` route, or after -- to // read the body. // By default, no rewrite is performed, and the method and URI // from the incoming request is used as-is for proxying. Rewrite *rewrite.Rewrite `json:"rewrite,omitempty"` // List of handlers and their associated matchers to evaluate // after successful roundtrips. The first handler that matches // the response from a backend will be invoked. The response // body from the backend will not be written to the client; // it is up to the handler to finish handling the response. // If passive health checks are enabled, any errors from the // handler chain will not affect the health status of the // backend. // // Three new placeholders are available in this handler chain: // - `{http.reverse_proxy.status_code}` The status code from the response // - `{http.reverse_proxy.status_text}` The status text from the response // - `{http.reverse_proxy.header.*}` The headers from the response HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"` Transport http.RoundTripper `json:"-"` CB CircuitBreaker `json:"-"` DynamicUpstreams UpstreamSource `json:"-"` // Holds the parsed CIDR ranges from TrustedProxies trustedProxies []netip.Prefix // Holds the named response matchers from the Caddyfile while adapting responseMatchers map[string]caddyhttp.ResponseMatcher // Holds the handle_response Caddyfile tokens while adapting handleResponseSegments []*caddyfile.Dispenser // Stores upgraded requests (hijacked connections) for proper cleanup connections map[io.ReadWriteCloser]openConnection connectionsMu *sync.Mutex ctx caddy.Context logger *zap.Logger events *caddyevents.App } // CaddyModule returns the Caddy module information. func (Handler) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.reverse_proxy", New: func() caddy.Module { return new(Handler) }, } } // Provision ensures that h is set up properly before use. func (h *Handler) Provision(ctx caddy.Context) error { eventAppIface, err := ctx.App("events") if err != nil { return fmt.Errorf("getting events app: %v", err) } h.events = eventAppIface.(*caddyevents.App) h.ctx = ctx h.logger = ctx.Logger() h.connections = make(map[io.ReadWriteCloser]openConnection) h.connectionsMu = new(sync.Mutex) // verify SRV compatibility - TODO: LookupSRV deprecated; will be removed for i, v := range h.Upstreams { if v.LookupSRV == "" { continue } if h.HealthChecks != nil && h.HealthChecks.Active != nil { return fmt.Errorf(`upstream: lookup_srv is incompatible with active health checks: %d: {"dial": %q, "lookup_srv": %q}`, i, v.Dial, v.LookupSRV) } if v.Dial != "" { return fmt.Errorf(`upstream: specifying dial address is incompatible with lookup_srv: %d: {"dial": %q, "lookup_srv": %q}`, i, v.Dial, v.LookupSRV) } } // start by loading modules if h.TransportRaw != nil { mod, err := ctx.LoadModule(h, "TransportRaw") if err != nil { return fmt.Errorf("loading transport: %v", err) } h.Transport = mod.(http.RoundTripper) } if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil { mod, err := ctx.LoadModule(h.LoadBalancing, "SelectionPolicyRaw") if err != nil { return fmt.Errorf("loading load balancing selection policy: %s", err) } h.LoadBalancing.SelectionPolicy = mod.(Selector) } if h.CBRaw != nil { mod, err := ctx.LoadModule(h, "CBRaw") if err != nil { return fmt.Errorf("loading circuit breaker: %s", err) } h.CB = mod.(CircuitBreaker) } if h.DynamicUpstreamsRaw != nil { mod, err := ctx.LoadModule(h, "DynamicUpstreamsRaw") if err != nil { return fmt.Errorf("loading upstream source module: %v", err) } h.DynamicUpstreams = mod.(UpstreamSource) } // parse trusted proxy CIDRs ahead of time for _, str := range h.TrustedProxies { if strings.Contains(str, "/") { ipNet, err := netip.ParsePrefix(str) if err != nil { return fmt.Errorf("parsing CIDR expression: '%s': %v", str, err) } h.trustedProxies = append(h.trustedProxies, ipNet) } else { ipAddr, err := netip.ParseAddr(str) if err != nil { return fmt.Errorf("invalid IP address: '%s': %v", str, err) } ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen()) h.trustedProxies = append(h.trustedProxies, ipNew) } } // ensure any embedded headers handler module gets provisioned // (see https://caddy.community/t/set-cookie-manipulation-in-reverse-proxy/7666?u=matt // for what happens if we forget to provision it) if h.Headers != nil { err := h.Headers.Provision(ctx) if err != nil { return fmt.Errorf("provisioning embedded headers handler: %v", err) } } if h.Rewrite != nil { err := h.Rewrite.Provision(ctx) if err != nil { return fmt.Errorf("provisioning rewrite: %v", err) } } // set up transport if h.Transport == nil { t := &HTTPTransport{} err := t.Provision(ctx) if err != nil { return fmt.Errorf("provisioning default transport: %v", err) } h.Transport = t } // set up load balancing if h.LoadBalancing == nil { h.LoadBalancing = new(LoadBalancing) } if h.LoadBalancing.SelectionPolicy == nil { h.LoadBalancing.SelectionPolicy = RandomSelection{} } if h.LoadBalancing.TryDuration > 0 && h.LoadBalancing.TryInterval == 0 { // a non-zero try_duration with a zero try_interval // will always spin the CPU for try_duration if the // upstream is local or low-latency; avoid that by // defaulting to a sane wait period between attempts h.LoadBalancing.TryInterval = caddy.Duration(250 * time.Millisecond) } lbMatcherSets, err := ctx.LoadModule(h.LoadBalancing, "RetryMatchRaw") if err != nil { return err } err = h.LoadBalancing.RetryMatch.FromInterface(lbMatcherSets) if err != nil { return err } // set up upstreams for _, u := range h.Upstreams { h.provisionUpstream(u) } if h.HealthChecks != nil { // set defaults on passive health checks, if necessary if h.HealthChecks.Passive != nil { if h.HealthChecks.Passive.FailDuration > 0 && h.HealthChecks.Passive.MaxFails == 0 { h.HealthChecks.Passive.MaxFails = 1 } } // if active health checks are enabled, configure them and start a worker if h.HealthChecks.Active != nil && (h.HealthChecks.Active.Path != "" || h.HealthChecks.Active.URI != "" || h.HealthChecks.Active.Port != 0) { h.HealthChecks.Active.logger = h.logger.Named("health_checker.active") timeout := time.Duration(h.HealthChecks.Active.Timeout) if timeout == 0 { timeout = 5 * time.Second } if h.HealthChecks.Active.Path != "" { h.HealthChecks.Active.logger.Warn("the 'path' option is deprecated, please use 'uri' instead!") } // parse the URI string (supports path and query) if h.HealthChecks.Active.URI != "" { parsedURI, err := url.Parse(h.HealthChecks.Active.URI) if err != nil { return err } h.HealthChecks.Active.uri = parsedURI } h.HealthChecks.Active.httpClient = &http.Client{ Timeout: timeout, Transport: h.Transport, } for _, upstream := range h.Upstreams { // if there's an alternative port for health-check provided in the config, // then use it, otherwise use the port of upstream. if h.HealthChecks.Active.Port != 0 { upstream.activeHealthCheckPort = h.HealthChecks.Active.Port } } if h.HealthChecks.Active.Interval == 0 { h.HealthChecks.Active.Interval = caddy.Duration(30 * time.Second) } if h.HealthChecks.Active.ExpectBody != "" { var err error h.HealthChecks.Active.bodyRegexp, err = regexp.Compile(h.HealthChecks.Active.ExpectBody) if err != nil { return fmt.Errorf("expect_body: compiling regular expression: %v", err) } } go h.activeHealthChecker() } } // set up any response routes for i, rh := range h.HandleResponse { err := rh.Provision(ctx) if err != nil { return fmt.Errorf("provisioning response handler %d: %v", i, err) } } upstreamHealthyUpdater := newMetricsUpstreamsHealthyUpdater(h) upstreamHealthyUpdater.Init() return nil } // Cleanup cleans up the resources made by h. func (h *Handler) Cleanup() error { // close hijacked connections (both to client and backend) var err error h.connectionsMu.Lock() for _, oc := range h.connections { if oc.gracefulClose != nil { // this is potentially blocking while we have the lock on the connections // map, but that should be OK since the server has in theory shut down // and we are no longer using the connections map gracefulErr := oc.gracefulClose() if gracefulErr != nil && err == nil { err = gracefulErr } } closeErr := oc.conn.Close() if closeErr != nil && err == nil { err = closeErr } } h.connectionsMu.Unlock() // remove hosts from our config from the pool for _, upstream := range h.Upstreams { _, _ = hosts.Delete(upstream.String()) } return err } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) // prepare the request for proxying; this is needed only once clonedReq, err := h.prepareRequest(r, repl) if err != nil { return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("preparing request for upstream round-trip: %v", err)) } // we will need the original headers and Host value if // header operations are configured; this is so that each // retry can apply the modifications, because placeholders // may be used which depend on the selected upstream for // their values reqHost := clonedReq.Host reqHeader := clonedReq.Header start := time.Now() defer func() { // total proxying duration, including time spent on LB and retries repl.Set("http.reverse_proxy.duration", time.Since(start)) repl.Set("http.reverse_proxy.duration_ms", time.Since(start).Seconds()*1e3) // multiply seconds to preserve decimal (see #4666) }() // in the proxy loop, each iteration is an attempt to proxy the request, // and because we may retry some number of times, carry over the error // from previous tries because of the nuances of load balancing & retries var proxyErr error var retries int for { var done bool done, proxyErr = h.proxyLoopIteration(clonedReq, r, w, proxyErr, start, retries, repl, reqHeader, reqHost, next) if done { break } retries++ } if proxyErr != nil { return statusError(proxyErr) } return nil } // proxyLoopIteration implements an iteration of the proxy loop. Despite the enormous amount of local state // that has to be passed in, we brought this into its own method so that we could run defer more easily. // It returns true when the loop is done and should break; false otherwise. The error value returned should // be assigned to the proxyErr value for the next iteration of the loop (or the error handled after break). func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w http.ResponseWriter, proxyErr error, start time.Time, retries int, repl *caddy.Replacer, reqHeader http.Header, reqHost string, next caddyhttp.Handler) (bool, error) { // get the updated list of upstreams upstreams := h.Upstreams if h.DynamicUpstreams != nil { dUpstreams, err := h.DynamicUpstreams.GetUpstreams(r) if err != nil { h.logger.Error("failed getting dynamic upstreams; falling back to static upstreams", zap.Error(err)) } else { upstreams = dUpstreams for _, dUp := range dUpstreams { h.provisionUpstream(dUp) } h.logger.Debug("provisioned dynamic upstreams", zap.Int("count", len(dUpstreams))) defer func() { // these upstreams are dynamic, so they are only used for this iteration // of the proxy loop; be sure to let them go away when we're done with them for _, upstream := range dUpstreams { _, _ = hosts.Delete(upstream.String()) } }() } } // choose an available upstream upstream := h.LoadBalancing.SelectionPolicy.Select(upstreams, r, w) if upstream == nil { if proxyErr == nil { proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, fmt.Errorf("no upstreams available")) } if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) { return true, proxyErr } return false, proxyErr } // the dial address may vary per-request if placeholders are // used, so perform those replacements here; the resulting // DialInfo struct should have valid network address syntax dialInfo, err := upstream.fillDialInfo(r) if err != nil { return true, fmt.Errorf("making dial info: %v", err) } h.logger.Debug("selected upstream", zap.String("dial", dialInfo.Address), zap.Int("total_upstreams", len(upstreams))) // attach to the request information about how to dial the upstream; // this is necessary because the information cannot be sufficiently // or satisfactorily represented in a URL caddyhttp.SetVar(r.Context(), dialInfoVarKey, dialInfo) // set placeholders with information about this upstream repl.Set("http.reverse_proxy.upstream.address", dialInfo.String()) repl.Set("http.reverse_proxy.upstream.hostport", dialInfo.Address) repl.Set("http.reverse_proxy.upstream.host", dialInfo.Host) repl.Set("http.reverse_proxy.upstream.port", dialInfo.Port) repl.Set("http.reverse_proxy.upstream.requests", upstream.Host.NumRequests()) repl.Set("http.reverse_proxy.upstream.max_requests", upstream.MaxRequests) repl.Set("http.reverse_proxy.upstream.fails", upstream.Host.Fails()) // mutate request headers according to this upstream; // because we're in a retry loop, we have to copy // headers (and the r.Host value) from the original // so that each retry is identical to the first if h.Headers != nil && h.Headers.Request != nil { r.Header = make(http.Header) copyHeader(r.Header, reqHeader) r.Host = reqHost h.Headers.Request.ApplyToRequest(r) } // proxy the request to that upstream proxyErr = h.reverseProxy(w, r, origReq, repl, dialInfo, next) if proxyErr == nil || errors.Is(proxyErr, context.Canceled) { // context.Canceled happens when the downstream client // cancels the request, which is not our failure return true, nil } // if the roundtrip was successful, don't retry the request or // ding the health status of the upstream (an error can still // occur after the roundtrip if, for example, a response handler // after the roundtrip returns an error) if succ, ok := proxyErr.(roundtripSucceeded); ok { return true, succ.error } // remember this failure (if enabled) h.countFailure(upstream) // if we've tried long enough, break if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) { return true, proxyErr } return false, proxyErr } // prepareRequest clones req so that it can be safely modified without // changing the original request or introducing data races. It then // modifies it so that it is ready to be proxied, except for directing // to a specific upstream. This method adjusts headers and other relevant // properties of the cloned request and should be done just once (before // proxying) regardless of proxy retries. This assumes that no mutations // of the cloned request are performed by h during or after proxying. func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.Request, error) { req = cloneRequest(req) // if enabled, perform rewrites on the cloned request; if // the method is GET or HEAD, prevent the request body // from being copied to the upstream if h.Rewrite != nil { changed := h.Rewrite.Rewrite(req, repl) if changed && (h.Rewrite.Method == "GET" || h.Rewrite.Method == "HEAD") { req.ContentLength = 0 req.Body = nil } } // if enabled, buffer client request; this should only be // enabled if the upstream requires it and does not work // with "slow clients" (gunicorn, etc.) - this obviously // has a perf overhead and makes the proxy at risk of // exhausting memory and more susceptible to slowloris // attacks, so it is strongly recommended to only use this // feature if absolutely required, if read timeouts are // set, and if body size is limited if h.BufferRequests && req.Body != nil { req.Body = h.bufferedBody(req.Body) } if req.ContentLength == 0 { req.Body = nil // Issue golang/go#16036: nil Body for http.Transport retries } req.Close = false // if User-Agent is not set by client, then explicitly // disable it so it's not set to default value by std lib if _, ok := req.Header["User-Agent"]; !ok { req.Header.Set("User-Agent", "") } reqUpType := upgradeType(req.Header) removeConnectionHeaders(req.Header) // Remove hop-by-hop headers to the backend. Especially // important is "Connection" because we want a persistent // connection, regardless of what the client sent to us. // Issue golang/go#46313: don't skip if field is empty. for _, h := range hopHeaders { // Issue golang/go#21096: tell backend applications that care about trailer support // that we support trailers. (We do, but we don't go out of our way to // advertise that unless the incoming client request thought it was worth // mentioning.) if h == "Te" && httpguts.HeaderValuesContainsToken(req.Header["Te"], "trailers") { req.Header.Set("Te", "trailers") continue } req.Header.Del(h) } // After stripping all the hop-by-hop connection headers above, add back any // necessary for protocol upgrades, such as for websockets. if reqUpType != "" { req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", reqUpType) } // Add the supported X-Forwarded-* headers err := h.addForwardedHeaders(req) if err != nil { return nil, err } return req, nil } // addForwardedHeaders adds the de-facto standard X-Forwarded-* // headers to the request before it is sent upstream. // // These headers are security sensitive, so care is taken to only // use existing values for these headers from the incoming request // if the client IP is trusted (i.e. coming from a trusted proxy // sitting in front of this server). If the request didn't have // the headers at all, then they will be added with the values // that we can glean from the request. func (h Handler) addForwardedHeaders(req *http.Request) error { // Parse the remote IP, ignore the error as non-fatal, // but the remote IP is required to continue, so we // just return early. This should probably never happen // though, unless some other module manipulated the request's // remote address and used an invalid value. clientIP, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { // Remove the `X-Forwarded-*` headers to avoid upstreams // potentially trusting a header that came from the client req.Header.Del("X-Forwarded-For") req.Header.Del("X-Forwarded-Proto") req.Header.Del("X-Forwarded-Host") return nil } // Client IP may contain a zone if IPv6, so we need // to pull that out before parsing the IP if before, _, found := strings.Cut(clientIP, "%"); found { clientIP = before } ipAddr, err := netip.ParseAddr(clientIP) if err != nil { return fmt.Errorf("invalid IP address: '%s': %v", clientIP, err) } // Check if the client is a trusted proxy trusted := false for _, ipRange := range h.trustedProxies { if ipRange.Contains(ipAddr) { trusted = true break } } // If we aren't the first proxy, and the proxy is trusted, // retain prior X-Forwarded-For information as a comma+space // separated list and fold multiple headers into one. clientXFF := clientIP prior, ok, omit := allHeaderValues(req.Header, "X-Forwarded-For") if trusted && ok && prior != "" { clientXFF = prior + ", " + clientXFF } if !omit { req.Header.Set("X-Forwarded-For", clientXFF) } // Set X-Forwarded-Proto; many backend apps expect this, // so that they can properly craft URLs with the right // scheme to match the original request proto := "https" if req.TLS == nil { proto = "http" } prior, ok, omit = lastHeaderValue(req.Header, "X-Forwarded-Proto") if trusted && ok && prior != "" { proto = prior } if !omit { req.Header.Set("X-Forwarded-Proto", proto) } // Set X-Forwarded-Host; often this is redundant because // we pass through the request Host as-is, but in situations // where we proxy over HTTPS, the user may need to override // Host themselves, so it's helpful to send the original too. host := req.Host prior, ok, omit = lastHeaderValue(req.Header, "X-Forwarded-Host") if trusted && ok && prior != "" { host = prior } if !omit { req.Header.Set("X-Forwarded-Host", host) } return nil } // reverseProxy performs a round-trip to the given backend and processes the response with the client. // (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the // Go standard library which was used as the foundation.) func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origReq *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error { _ = di.Upstream.Host.countRequest(1) //nolint:errcheck defer di.Upstream.Host.countRequest(-1) // point the request to this upstream h.directRequest(req, di) server := req.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server) shouldLogCredentials := server.Logs != nil && server.Logs.ShouldLogCredentials if supports1xx { // Forward 1xx status codes, backported from https://github.com/golang/go/pull/53164 trace := &httptrace.ClientTrace{ Got1xxResponse: func(code int, header textproto.MIMEHeader) error { h := rw.Header() copyHeader(h, http.Header(header)) rw.WriteHeader(code) // Clear headers coming from the backend // (it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses) for k := range header { delete(h, k) } return nil }, } req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) } // if FlushInterval is explicitly configured to -1 (i.e. flush continuously to achieve // low-latency streaming), don't let the transport cancel the request if the client // disconnects: user probably wants us to finish sending the data to the upstream // regardless, and we should expect client disconnection in low-latency streaming // scenarios (see issue #4922) if h.FlushInterval == -1 { req = req.WithContext(ignoreClientGoneContext{req.Context(), h.ctx.Done()}) } // do the round-trip; emit debug log with values we know are // safe, or if there is no error, emit fuller log entry start := time.Now() res, err := h.Transport.RoundTrip(req) duration := time.Since(start) logger := h.logger.With( zap.String("upstream", di.Upstream.String()), zap.Duration("duration", duration), zap.Object("request", caddyhttp.LoggableHTTPRequest{ Request: req, ShouldLogCredentials: shouldLogCredentials, }), ) if err != nil { logger.Debug("upstream roundtrip", zap.Error(err)) return err } logger.Debug("upstream roundtrip", zap.Object("headers", caddyhttp.LoggableHTTPHeader{ Header: res.Header, ShouldLogCredentials: shouldLogCredentials, }), zap.Int("status", res.StatusCode)) // duration until upstream wrote response headers (roundtrip duration) repl.Set("http.reverse_proxy.upstream.latency", duration) repl.Set("http.reverse_proxy.upstream.latency_ms", duration.Seconds()*1e3) // multiply seconds to preserve decimal (see #4666) // update circuit breaker on current conditions if di.Upstream.cb != nil { di.Upstream.cb.RecordMetric(res.StatusCode, duration) } // perform passive health checks (if enabled) if h.HealthChecks != nil && h.HealthChecks.Passive != nil { // strike if the status code matches one that is "bad" for _, badStatus := range h.HealthChecks.Passive.UnhealthyStatus { if caddyhttp.StatusCodeMatches(res.StatusCode, badStatus) { h.countFailure(di.Upstream) } } // strike if the roundtrip took too long if h.HealthChecks.Passive.UnhealthyLatency > 0 && duration >= time.Duration(h.HealthChecks.Passive.UnhealthyLatency) { h.countFailure(di.Upstream) } } // if enabled, buffer the response body if h.BufferResponses { res.Body = h.bufferedBody(res.Body) } // see if any response handler is configured for this response from the backend for i, rh := range h.HandleResponse { if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) { continue } // if configured to only change the status code, // do that then continue regular proxy response if statusCodeStr := rh.StatusCode.String(); statusCodeStr != "" { statusCode, err := strconv.Atoi(repl.ReplaceAll(statusCodeStr, "")) if err != nil { return caddyhttp.Error(http.StatusInternalServerError, err) } if statusCode != 0 { res.StatusCode = statusCode } break } // otherwise, if there are any routes configured, execute those as the // actual response instead of what we got from the proxy backend if len(rh.Routes) == 0 { continue } // set up the replacer so that parts of the original response can be // used for routing decisions for field, value := range res.Header { repl.Set("http.reverse_proxy.header."+field, strings.Join(value, ",")) } repl.Set("http.reverse_proxy.status_code", res.StatusCode) repl.Set("http.reverse_proxy.status_text", res.Status) h.logger.Debug("handling response", zap.Int("handler", i)) // we make some data available via request context to child routes // so that they may inherit some options and functions from the // handler, and be able to copy the response. // we use the original request here, so that any routes from 'next' // see the original request rather than the proxy cloned request. hrc := &handleResponseContext{ handler: h, response: res, start: start, logger: logger, } ctx := origReq.Context() ctx = context.WithValue(ctx, proxyHandleResponseContextCtxKey, hrc) // pass the request through the response handler routes routeErr := rh.Routes.Compile(next).ServeHTTP(rw, origReq.WithContext(ctx)) // close the response body afterwards, since we don't need it anymore; // either a route had 'copy_response' which already consumed the body, // or some other terminal handler ran which doesn't need the response // body after that point (e.g. 'file_server' for X-Accel-Redirect flow), // or we fell through to subsequent handlers past this proxy // (e.g. forward auth's 2xx response flow). if !hrc.isFinalized { res.Body.Close() } // wrap any route error in roundtripSucceeded so caller knows that // the roundtrip was successful and to not retry if routeErr != nil { return roundtripSucceeded{routeErr} } // we're done handling the response, and we don't want to // fall through to the default finalize/copy behaviour return nil } // copy the response body and headers back to the upstream client return h.finalizeResponse(rw, req, res, repl, start, logger) } // finalizeResponse prepares and copies the response. func (h Handler) finalizeResponse( rw http.ResponseWriter, req *http.Request, res *http.Response, repl *caddy.Replacer, start time.Time, logger *zap.Logger, ) error { // deal with 101 Switching Protocols responses: (WebSocket, h2c, etc) if res.StatusCode == http.StatusSwitchingProtocols { h.handleUpgradeResponse(logger, rw, req, res) return nil } removeConnectionHeaders(res.Header) for _, h := range hopHeaders { res.Header.Del(h) } // apply any response header operations if h.Headers != nil && h.Headers.Response != nil { if h.Headers.Response.Require == nil || h.Headers.Response.Require.Match(res.StatusCode, res.Header) { h.Headers.Response.ApplyTo(res.Header, repl) } } copyHeader(rw.Header(), res.Header) // The "Trailer" header isn't included in the Transport's response, // at least for *http.Transport. Build it up from Trailer. announcedTrailers := len(res.Trailer) if announcedTrailers > 0 { trailerKeys := make([]string, 0, len(res.Trailer)) for k := range res.Trailer { trailerKeys = append(trailerKeys, k) } rw.Header().Add("Trailer", strings.Join(trailerKeys, ", ")) } rw.WriteHeader(res.StatusCode) err := h.copyResponse(rw, res.Body, h.flushInterval(req, res)) res.Body.Close() // close now, instead of defer, to populate res.Trailer if err != nil { // we're streaming the response and we've already written headers, so // there's nothing an error handler can do to recover at this point; // the standard lib's proxy panics at this point, but we'll just log // the error and abort the stream here h.logger.Error("aborting with incomplete response", zap.Error(err)) return nil } if len(res.Trailer) > 0 { // Force chunking if we saw a response trailer. // This prevents net/http from calculating the length for short // bodies and adding a Content-Length. if fl, ok := rw.(http.Flusher); ok { fl.Flush() } } // total duration spent proxying, including writing response body repl.Set("http.reverse_proxy.upstream.duration", time.Since(start)) repl.Set("http.reverse_proxy.upstream.duration_ms", time.Since(start).Seconds()*1e3) if len(res.Trailer) == announcedTrailers { copyHeader(rw.Header(), res.Trailer) return nil } for k, vv := range res.Trailer { k = http.TrailerPrefix + k for _, v := range vv { rw.Header().Add(k, v) } } return nil } // tryAgain takes the time that the handler was initially invoked, // the amount of retries already performed, as well as any error // currently obtained, and the request being tried, and returns // true if another attempt should be made at proxying the request. // If true is returned, it has already blocked long enough before // the next retry (i.e. no more sleeping is needed). If false is // returned, the handler should stop trying to proxy the request. func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int, proxyErr error, req *http.Request) bool { // no retries are configured if lb.TryDuration == 0 && lb.Retries == 0 { return false } // if we've tried long enough, break if lb.TryDuration > 0 && time.Since(start) >= time.Duration(lb.TryDuration) { return false } // if we've reached the retry limit, break if lb.Retries > 0 && retries >= lb.Retries { return false } // if the error occurred while dialing (i.e. a connection // could not even be established to the upstream), then it // should be safe to retry, since without a connection, no // HTTP request can be transmitted; but if the error is not // specifically a dialer error, we need to be careful if _, ok := proxyErr.(DialError); proxyErr != nil && !ok { // if the error occurred after a connection was established, // we have to assume the upstream received the request, and // retries need to be carefully decided, because some requests // are not idempotent if lb.RetryMatch == nil && req.Method != "GET" { // by default, don't retry requests if they aren't GET return false } if !lb.RetryMatch.AnyMatch(req) { return false } } // fast path; if the interval is zero, we don't need to wait if lb.TryInterval == 0 { return true } // otherwise, wait and try the next available host timer := time.NewTimer(time.Duration(lb.TryInterval)) select { case <-timer.C: return true case <-ctx.Done(): if !timer.Stop() { // if the timer has been stopped then read from the channel <-timer.C } return false } } // directRequest modifies only req.URL so that it points to the upstream // in the given DialInfo. It must modify ONLY the request URL. func (Handler) directRequest(req *http.Request, di DialInfo) { // we need a host, so set the upstream's host address reqHost := di.Address // if the port equates to the scheme, strip the port because // it's weird to make a request like http://example.com:80/. if (req.URL.Scheme == "http" && di.Port == "80") || (req.URL.Scheme == "https" && di.Port == "443") { reqHost = di.Host } req.URL.Host = reqHost } func (h Handler) provisionUpstream(upstream *Upstream) { // create or get the host representation for this upstream upstream.fillHost() // give it the circuit breaker, if any upstream.cb = h.CB // if the passive health checker has a non-zero UnhealthyRequestCount // but the upstream has no MaxRequests set (they are the same thing, // but the passive health checker is a default value for for upstreams // without MaxRequests), copy the value into this upstream, since the // value in the upstream (MaxRequests) is what is used during // availability checks if h.HealthChecks != nil && h.HealthChecks.Passive != nil { h.HealthChecks.Passive.logger = h.logger.Named("health_checker.passive") if h.HealthChecks.Passive.UnhealthyRequestCount > 0 && upstream.MaxRequests == 0 { upstream.MaxRequests = h.HealthChecks.Passive.UnhealthyRequestCount } } // upstreams need independent access to the passive // health check policy because passive health checks // run without access to h. if h.HealthChecks != nil { upstream.healthCheckPolicy = h.HealthChecks.Passive } } // bufferedBody reads originalBody into a buffer, then returns a reader for the buffer. // Always close the return value when done with it, just like if it was the original body! func (h Handler) bufferedBody(originalBody io.ReadCloser) io.ReadCloser { buf := bufPool.Get().(*bytes.Buffer) buf.Reset() if h.MaxBufferSize > 0 { n, err := io.CopyN(buf, originalBody, h.MaxBufferSize) if err != nil || n == h.MaxBufferSize { return bodyReadCloser{ Reader: io.MultiReader(buf, originalBody), buf: buf, body: originalBody, } } } else { _, _ = io.Copy(buf, originalBody) } originalBody.Close() // no point in keeping it open return bodyReadCloser{ Reader: buf, buf: buf, } } // cloneRequest makes a semi-deep clone of origReq. // // Most of this code is borrowed from the Go stdlib reverse proxy, // but we make a shallow-ish clone the request (deep clone only // the headers and URL) so we can avoid manipulating the original // request when using it to proxy upstream. This prevents request // corruption and data races. func cloneRequest(origReq *http.Request) *http.Request { req := new(http.Request) *req = *origReq if origReq.URL != nil { newURL := new(url.URL) *newURL = *origReq.URL if origReq.URL.User != nil { newURL.User = new(url.Userinfo) *newURL.User = *origReq.URL.User } // sanitize the request URL; we expect it to not contain the // scheme and host since those should be determined by r.TLS // and r.Host respectively, but some clients may include it // in the request-line, which is technically valid in HTTP, // but breaks reverseproxy behaviour, overriding how the // dialer will behave. See #4237 for context. newURL.Scheme = "" newURL.Host = "" req.URL = newURL } if origReq.Header != nil { req.Header = origReq.Header.Clone() } if origReq.Trailer != nil { req.Trailer = origReq.Trailer.Clone() } return req } func copyHeader(dst, src http.Header) { for k, vv := range src { for _, v := range vv { dst.Add(k, v) } } } // allHeaderValues gets all values for a given header field, // joined by a comma and space if more than one is set. If the // header field is nil, then the omit is true, meaning some // earlier logic in the server wanted to prevent this header from // getting written at all. If the header is empty, then ok is // false. Callers should still check that the value is not empty // (the header field may be set but have an empty value). func allHeaderValues(h http.Header, field string) (value string, ok bool, omit bool) { values, ok := h[http.CanonicalHeaderKey(field)] if ok && values == nil { return "", true, true } if len(values) == 0 { return "", false, false } return strings.Join(values, ", "), true, false } // lastHeaderValue gets the last value for a given header field // if more than one is set. If the header field is nil, then // the omit is true, meaning some earlier logic in the server // wanted to prevent this header from getting written at all. // If the header is empty, then ok is false. Callers should // still check that the value is not empty (the header field // may be set but have an empty value). func lastHeaderValue(h http.Header, field string) (value string, ok bool, omit bool) { values, ok := h[http.CanonicalHeaderKey(field)] if ok && values == nil { return "", true, true } if len(values) == 0 { return "", false, false } return values[len(values)-1], true, false } func upgradeType(h http.Header) string { if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") { return "" } return strings.ToLower(h.Get("Upgrade")) } // removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h. // See RFC 7230, section 6.1 func removeConnectionHeaders(h http.Header) { for _, f := range h["Connection"] { for _, sf := range strings.Split(f, ",") { if sf = textproto.TrimString(sf); sf != "" { h.Del(sf) } } } } // statusError returns an error value that has a status code. func statusError(err error) error { // errors proxying usually mean there is a problem with the upstream(s) statusCode := http.StatusBadGateway // timeout errors have a standard status code (see issue #4823) if err, ok := err.(net.Error); ok && err.Timeout() { statusCode = http.StatusGatewayTimeout } // if the client canceled the request (usually this means they closed // the connection, so they won't see any response), we can report it // as a client error (4xx) and not a server error (5xx); unfortunately // the Go standard library, at least at time of writing in late 2020, // obnoxiously wraps the exported, standard context.Canceled error with // an unexported garbage value that we have to do a substring check for: // https://github.com/golang/go/blob/6965b01ea248cabb70c3749fd218b36089a21efb/src/net/net.go#L416-L430 if errors.Is(err, context.Canceled) || strings.Contains(err.Error(), "operation was canceled") { // regrettably, there is no standard error code for "client closed connection", but // for historical reasons we can use a code that a lot of people are already using; // using 5xx is problematic for users; see #3748 statusCode = 499 } return caddyhttp.Error(statusCode, err) } // LoadBalancing has parameters related to load balancing. type LoadBalancing struct { // A selection policy is how to choose an available backend. // The default policy is random selection. SelectionPolicyRaw json.RawMessage `json:"selection_policy,omitempty" caddy:"namespace=http.reverse_proxy.selection_policies inline_key=policy"` // How many times to retry selecting available backends for each // request if the next available host is down. If try_duration is // also configured, then retries may stop early if the duration // is reached. By default, retries are disabled (zero). Retries int `json:"retries,omitempty"` // How long to try selecting available backends for each request // if the next available host is down. Clients will wait for up // to this long while the load balancer tries to find an available // upstream host. If retries is also configured, tries may stop // early if the maximum retries is reached. By default, retries // are disabled (zero duration). TryDuration caddy.Duration `json:"try_duration,omitempty"` // How long to wait between selecting the next host from the pool. // Default is 250ms if try_duration is enabled, otherwise zero. Only // relevant when a request to an upstream host fails. Be aware that // setting this to 0 with a non-zero try_duration can cause the CPU // to spin if all backends are down and latency is very low. TryInterval caddy.Duration `json:"try_interval,omitempty"` // A list of matcher sets that restricts with which requests retries are // allowed. A request must match any of the given matcher sets in order // to be retried if the connection to the upstream succeeded but the // subsequent round-trip failed. If the connection to the upstream failed, // a retry is always allowed. If unspecified, only GET requests will be // allowed to be retried. Note that a retry is done with the next available // host according to the load balancing policy. RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty" caddy:"namespace=http.matchers"` SelectionPolicy Selector `json:"-"` RetryMatch caddyhttp.MatcherSets `json:"-"` } // Selector selects an available upstream from the pool. type Selector interface { Select(UpstreamPool, *http.Request, http.ResponseWriter) *Upstream } // UpstreamSource gets the list of upstreams that can be used when // proxying a request. Returned upstreams will be load balanced and // health-checked. This should be a very fast function -- instant // if possible -- and the return value must be as stable as possible. // In other words, the list of upstreams should ideally not change much // across successive calls. If the list of upstreams changes or the // ordering is not stable, load balancing will suffer. This function // may be called during each retry, multiple times per request, and as // such, needs to be instantaneous. The returned slice will not be // modified. type UpstreamSource interface { GetUpstreams(*http.Request) ([]*Upstream, error) } // Hop-by-hop headers. These are removed when sent to the backend. // As of RFC 7230, hop-by-hop headers are required to appear in the // Connection header field. These are the headers defined by the // obsoleted RFC 2616 (section 13.5.1) and are used for backward // compatibility. var hopHeaders = []string{ "Alt-Svc", "Connection", "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "Te", // canonicalized version of "TE" "Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522 "Transfer-Encoding", "Upgrade", } // DialError is an error that specifically occurs // in a call to Dial or DialContext. type DialError struct{ error } // TLSTransport is implemented by transports // that are capable of using TLS. type TLSTransport interface { // TLSEnabled returns true if the transport // has TLS enabled, false otherwise. TLSEnabled() bool // EnableTLS enables TLS within the transport // if it is not already, using the provided // value as a basis for the TLS config. EnableTLS(base *TLSConfig) error } // roundtripSucceeded is an error type that is returned if the // roundtrip succeeded, but an error occurred after-the-fact. type roundtripSucceeded struct{ error } // bodyReadCloser is a reader that, upon closing, will return // its buffer to the pool and close the underlying body reader. type bodyReadCloser struct { io.Reader buf *bytes.Buffer body io.ReadCloser } func (brc bodyReadCloser) Close() error { bufPool.Put(brc.buf) if brc.body != nil { return brc.body.Close() } return nil } // bufPool is used for buffering requests and responses. var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } // handleResponseContext carries some contextual information about the // current proxy handling. type handleResponseContext struct { // handler is the active proxy handler instance, so that // routes like copy_response may inherit some config // options and have access to handler methods. handler *Handler // response is the actual response received from the proxy // roundtrip, to potentially be copied if a copy_response // handler is in the handle_response routes. response *http.Response // start is the time just before the proxy roundtrip was // performed, used for logging. start time.Time // logger is the prepared logger which is used to write logs // with the request, duration, and selected upstream attached. logger *zap.Logger // isFinalized is whether the response has been finalized, // i.e. copied and closed, to make sure that it doesn't // happen twice. isFinalized bool } // ignoreClientGoneContext is a special context.Context type // intended for use when doing a RoundTrip where you don't // want a client disconnection to cancel the request during // the roundtrip. Set its done field to a Done() channel // of a context that doesn't get canceled when the client // disconnects, such as caddy.Context.Done() instead. type ignoreClientGoneContext struct { context.Context done <-chan struct{} } func (c ignoreClientGoneContext) Done() <-chan struct{} { return c.done } // proxyHandleResponseContextCtxKey is the context key for the active proxy handler // so that handle_response routes can inherit some config options // from the proxy handler. const proxyHandleResponseContextCtxKey caddy.CtxKey = "reverse_proxy_handle_response_context" // Interface guards var ( _ caddy.Provisioner = (*Handler)(nil) _ caddy.CleanerUpper = (*Handler)(nil) _ caddyhttp.MiddlewareHandler = (*Handler)(nil) ) caddy-2.6.2/modules/caddyhttp/reverseproxy/selectionpolicies.go000066400000000000000000000371251435007237400250770ustar00rootroot00000000000000// 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 reverseproxy import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "hash/fnv" weakrand "math/rand" "net" "net/http" "strconv" "sync/atomic" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { caddy.RegisterModule(RandomSelection{}) caddy.RegisterModule(RandomChoiceSelection{}) caddy.RegisterModule(LeastConnSelection{}) caddy.RegisterModule(RoundRobinSelection{}) caddy.RegisterModule(FirstSelection{}) caddy.RegisterModule(IPHashSelection{}) caddy.RegisterModule(URIHashSelection{}) caddy.RegisterModule(HeaderHashSelection{}) caddy.RegisterModule(CookieHashSelection{}) weakrand.Seed(time.Now().UTC().UnixNano()) } // RandomSelection is a policy that selects // an available host at random. type RandomSelection struct{} // CaddyModule returns the Caddy module information. func (RandomSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.random", New: func() caddy.Module { return new(RandomSelection) }, } } // Select returns an available host, if any. func (r RandomSelection) Select(pool UpstreamPool, request *http.Request, _ http.ResponseWriter) *Upstream { return selectRandomHost(pool) } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *RandomSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { return d.ArgErr() } } return nil } // RandomChoiceSelection is a policy that selects // two or more available hosts at random, then // chooses the one with the least load. type RandomChoiceSelection struct { // The size of the sub-pool created from the larger upstream pool. The default value // is 2 and the maximum at selection time is the size of the upstream pool. Choose int `json:"choose,omitempty"` } // CaddyModule returns the Caddy module information. func (RandomChoiceSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.random_choose", New: func() caddy.Module { return new(RandomChoiceSelection) }, } } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *RandomChoiceSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if !d.NextArg() { return d.ArgErr() } chooseStr := d.Val() choose, err := strconv.Atoi(chooseStr) if err != nil { return d.Errf("invalid choice value '%s': %v", chooseStr, err) } r.Choose = choose } return nil } // Provision sets up r. func (r *RandomChoiceSelection) Provision(ctx caddy.Context) error { if r.Choose == 0 { r.Choose = 2 } return nil } // Validate ensures that r's configuration is valid. func (r RandomChoiceSelection) Validate() error { if r.Choose < 2 { return fmt.Errorf("choose must be at least 2") } return nil } // Select returns an available host, if any. func (r RandomChoiceSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream { k := r.Choose if k > len(pool) { k = len(pool) } choices := make([]*Upstream, k) for i, upstream := range pool { if !upstream.Available() { continue } j := weakrand.Intn(i + 1) //nolint:gosec if j < k { choices[j] = upstream } } return leastRequests(choices) } // LeastConnSelection is a policy that selects the // host with the least active requests. If multiple // hosts have the same fewest number, one is chosen // randomly. The term "conn" or "connection" is used // in this policy name due to its similar meaning in // other software, but our load balancer actually // counts active requests rather than connections, // since these days requests are multiplexed onto // shared connections. type LeastConnSelection struct{} // CaddyModule returns the Caddy module information. func (LeastConnSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.least_conn", New: func() caddy.Module { return new(LeastConnSelection) }, } } // Select selects the up host with the least number of connections in the // pool. If more than one host has the same least number of connections, // one of the hosts is chosen at random. func (LeastConnSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream { var bestHost *Upstream var count int leastReqs := -1 for _, host := range pool { if !host.Available() { continue } numReqs := host.NumRequests() if leastReqs == -1 || numReqs < leastReqs { leastReqs = numReqs count = 0 } // among hosts with same least connections, perform a reservoir // sample: https://en.wikipedia.org/wiki/Reservoir_sampling if numReqs == leastReqs { count++ if (weakrand.Int() % count) == 0 { //nolint:gosec bestHost = host } } } return bestHost } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *LeastConnSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { return d.ArgErr() } } return nil } // RoundRobinSelection is a policy that selects // a host based on round-robin ordering. type RoundRobinSelection struct { robin uint32 } // CaddyModule returns the Caddy module information. func (RoundRobinSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.round_robin", New: func() caddy.Module { return new(RoundRobinSelection) }, } } // Select returns an available host, if any. func (r *RoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream { n := uint32(len(pool)) if n == 0 { return nil } for i := uint32(0); i < n; i++ { robin := atomic.AddUint32(&r.robin, 1) host := pool[robin%n] if host.Available() { return host } } return nil } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *RoundRobinSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { return d.ArgErr() } } return nil } // FirstSelection is a policy that selects // the first available host. type FirstSelection struct{} // CaddyModule returns the Caddy module information. func (FirstSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.first", New: func() caddy.Module { return new(FirstSelection) }, } } // Select returns an available host, if any. func (FirstSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream { for _, host := range pool { if host.Available() { return host } } return nil } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *FirstSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { return d.ArgErr() } } return nil } // IPHashSelection is a policy that selects a host // based on hashing the remote IP of the request. type IPHashSelection struct{} // CaddyModule returns the Caddy module information. func (IPHashSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.ip_hash", New: func() caddy.Module { return new(IPHashSelection) }, } } // Select returns an available host, if any. func (IPHashSelection) Select(pool UpstreamPool, req *http.Request, _ http.ResponseWriter) *Upstream { clientIP, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { clientIP = req.RemoteAddr } return hostByHashing(pool, clientIP) } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *IPHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { return d.ArgErr() } } return nil } // URIHashSelection is a policy that selects a // host by hashing the request URI. type URIHashSelection struct{} // CaddyModule returns the Caddy module information. func (URIHashSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.uri_hash", New: func() caddy.Module { return new(URIHashSelection) }, } } // Select returns an available host, if any. func (URIHashSelection) Select(pool UpstreamPool, req *http.Request, _ http.ResponseWriter) *Upstream { return hostByHashing(pool, req.RequestURI) } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *URIHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { return d.ArgErr() } } return nil } // HeaderHashSelection is a policy that selects // a host based on a given request header. type HeaderHashSelection struct { // The HTTP header field whose value is to be hashed and used for upstream selection. Field string `json:"field,omitempty"` } // CaddyModule returns the Caddy module information. func (HeaderHashSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.header", New: func() caddy.Module { return new(HeaderHashSelection) }, } } // Select returns an available host, if any. func (s HeaderHashSelection) Select(pool UpstreamPool, req *http.Request, _ http.ResponseWriter) *Upstream { if s.Field == "" { return nil } // The Host header should be obtained from the req.Host field // since net/http removes it from the header map. if s.Field == "Host" && req.Host != "" { return hostByHashing(pool, req.Host) } val := req.Header.Get(s.Field) if val == "" { return RandomSelection{}.Select(pool, req, nil) } return hostByHashing(pool, val) } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (s *HeaderHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if !d.NextArg() { return d.ArgErr() } s.Field = d.Val() } return nil } // CookieHashSelection is a policy that selects // a host based on a given cookie name. type CookieHashSelection struct { // The HTTP cookie name whose value is to be hashed and used for upstream selection. Name string `json:"name,omitempty"` // Secret to hash (Hmac256) chosen upstream in cookie Secret string `json:"secret,omitempty"` } // CaddyModule returns the Caddy module information. func (CookieHashSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.selection_policies.cookie", New: func() caddy.Module { return new(CookieHashSelection) }, } } // Select returns an available host, if any. func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http.ResponseWriter) *Upstream { if s.Name == "" { s.Name = "lb" } cookie, err := req.Cookie(s.Name) // If there's no cookie, select new random host if err != nil || cookie == nil { return selectNewHostWithCookieHashSelection(pool, w, s.Secret, s.Name) } // If the cookie is present, loop over the available upstreams until we find a match cookieValue := cookie.Value for _, upstream := range pool { if !upstream.Available() { continue } sha, err := hashCookie(s.Secret, upstream.Dial) if err == nil && sha == cookieValue { return upstream } } // If there is no matching host, select new random host return selectNewHostWithCookieHashSelection(pool, w, s.Secret, s.Name) } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: // lb_policy cookie [ []] // // By default name is `lb` func (s *CookieHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { args := d.RemainingArgs() switch len(args) { case 1: case 2: s.Name = args[1] case 3: s.Name = args[1] s.Secret = args[2] default: return d.ArgErr() } return nil } // Select a new Host randomly and add a sticky session cookie func selectNewHostWithCookieHashSelection(pool []*Upstream, w http.ResponseWriter, cookieSecret string, cookieName string) *Upstream { randomHost := selectRandomHost(pool) if randomHost != nil { // Hash (HMAC with some key for privacy) the upstream.Dial string as the cookie value sha, err := hashCookie(cookieSecret, randomHost.Dial) if err == nil { // write the cookie. http.SetCookie(w, &http.Cookie{Name: cookieName, Value: sha, Path: "/", Secure: false}) } } return randomHost } // hashCookie hashes (HMAC 256) some data with the secret func hashCookie(secret string, data string) (string, error) { h := hmac.New(sha256.New, []byte(secret)) _, err := h.Write([]byte(data)) if err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } // selectRandomHost returns a random available host func selectRandomHost(pool []*Upstream) *Upstream { // use reservoir sampling because the number of available // hosts isn't known: https://en.wikipedia.org/wiki/Reservoir_sampling var randomHost *Upstream var count int for _, upstream := range pool { if !upstream.Available() { continue } // (n % 1 == 0) holds for all n, therefore a // upstream will always be chosen if there is at // least one available count++ if (weakrand.Int() % count) == 0 { //nolint:gosec randomHost = upstream } } return randomHost } // leastRequests returns the host with the // least number of active requests to it. // If more than one host has the same // least number of active requests, then // one of those is chosen at random. func leastRequests(upstreams []*Upstream) *Upstream { if len(upstreams) == 0 { return nil } var best []*Upstream var bestReqs int = -1 for _, upstream := range upstreams { if upstream == nil { continue } reqs := upstream.NumRequests() if reqs == 0 { return upstream } // If bestReqs was just initialized to -1 // we need to append upstream also if reqs <= bestReqs || bestReqs == -1 { bestReqs = reqs best = append(best, upstream) } } if len(best) == 0 { return nil } return best[weakrand.Intn(len(best))] //nolint:gosec } // hostByHashing returns an available host from pool based on a hashable string s. func hostByHashing(pool []*Upstream, s string) *Upstream { // Highest Random Weight (HRW, or "Rendezvous") hashing, // guarantees stability when the list of upstreams changes; // see https://medium.com/i0exception/rendezvous-hashing-8c00e2fb58b0, // https://randorithms.com/2020/12/26/rendezvous-hashing.html, // and https://en.wikipedia.org/wiki/Rendezvous_hashing. var highestHash uint32 var upstream *Upstream for _, up := range pool { if !up.Available() { continue } h := hash(s + up.String()) // important to hash key and server together if h > highestHash { highestHash = h upstream = up } } return upstream } // hash calculates a fast hash based on s. func hash(s string) uint32 { h := fnv.New32a() _, _ = h.Write([]byte(s)) return h.Sum32() } // Interface guards var ( _ Selector = (*RandomSelection)(nil) _ Selector = (*RandomChoiceSelection)(nil) _ Selector = (*LeastConnSelection)(nil) _ Selector = (*RoundRobinSelection)(nil) _ Selector = (*FirstSelection)(nil) _ Selector = (*IPHashSelection)(nil) _ Selector = (*URIHashSelection)(nil) _ Selector = (*HeaderHashSelection)(nil) _ Selector = (*CookieHashSelection)(nil) _ caddy.Validator = (*RandomChoiceSelection)(nil) _ caddy.Provisioner = (*RandomChoiceSelection)(nil) _ caddyfile.Unmarshaler = (*RandomChoiceSelection)(nil) ) caddy-2.6.2/modules/caddyhttp/reverseproxy/selectionpolicies_test.go000066400000000000000000000260401435007237400261300ustar00rootroot00000000000000// 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 reverseproxy import ( "net/http" "net/http/httptest" "testing" ) func testPool() UpstreamPool { return UpstreamPool{ {Host: new(Host), Dial: "0.0.0.1"}, {Host: new(Host), Dial: "0.0.0.2"}, {Host: new(Host), Dial: "0.0.0.3"}, } } func TestRoundRobinPolicy(t *testing.T) { pool := testPool() rrPolicy := new(RoundRobinSelection) req, _ := http.NewRequest("GET", "/", nil) h := rrPolicy.Select(pool, req, nil) // First selected host is 1, because counter starts at 0 // and increments before host is selected if h != pool[1] { t.Error("Expected first round robin host to be second host in the pool.") } h = rrPolicy.Select(pool, req, nil) if h != pool[2] { t.Error("Expected second round robin host to be third host in the pool.") } h = rrPolicy.Select(pool, req, nil) if h != pool[0] { t.Error("Expected third round robin host to be first host in the pool.") } // mark host as down pool[1].setHealthy(false) h = rrPolicy.Select(pool, req, nil) if h != pool[2] { t.Error("Expected to skip down host.") } // mark host as up pool[1].setHealthy(true) h = rrPolicy.Select(pool, req, nil) if h == pool[2] { t.Error("Expected to balance evenly among healthy hosts") } // mark host as full pool[1].countRequest(1) pool[1].MaxRequests = 1 h = rrPolicy.Select(pool, req, nil) if h != pool[2] { t.Error("Expected to skip full host.") } } func TestLeastConnPolicy(t *testing.T) { pool := testPool() lcPolicy := new(LeastConnSelection) req, _ := http.NewRequest("GET", "/", nil) pool[0].countRequest(10) pool[1].countRequest(10) h := lcPolicy.Select(pool, req, nil) if h != pool[2] { t.Error("Expected least connection host to be third host.") } pool[2].countRequest(100) h = lcPolicy.Select(pool, req, nil) if h != pool[0] && h != pool[1] { t.Error("Expected least connection host to be first or second host.") } } func TestIPHashPolicy(t *testing.T) { pool := testPool() ipHash := new(IPHashSelection) req, _ := http.NewRequest("GET", "/", nil) // We should be able to predict where every request is routed. req.RemoteAddr = "172.0.0.1:80" h := ipHash.Select(pool, req, nil) if h != pool[0] { t.Error("Expected ip hash policy host to be the first host.") } req.RemoteAddr = "172.0.0.2:80" h = ipHash.Select(pool, req, nil) if h != pool[0] { t.Error("Expected ip hash policy host to be the first host.") } req.RemoteAddr = "172.0.0.3:80" h = ipHash.Select(pool, req, nil) if h != pool[2] { t.Error("Expected ip hash policy host to be the third host.") } req.RemoteAddr = "172.0.0.4:80" h = ipHash.Select(pool, req, nil) if h != pool[1] { t.Error("Expected ip hash policy host to be the second host.") } // we should get the same results without a port req.RemoteAddr = "172.0.0.1" h = ipHash.Select(pool, req, nil) if h != pool[0] { t.Error("Expected ip hash policy host to be the first host.") } req.RemoteAddr = "172.0.0.2" h = ipHash.Select(pool, req, nil) if h != pool[0] { t.Error("Expected ip hash policy host to be the first host.") } req.RemoteAddr = "172.0.0.3" h = ipHash.Select(pool, req, nil) if h != pool[2] { t.Error("Expected ip hash policy host to be the third host.") } req.RemoteAddr = "172.0.0.4" h = ipHash.Select(pool, req, nil) if h != pool[1] { t.Error("Expected ip hash policy host to be the second host.") } // we should get a healthy host if the original host is unhealthy and a // healthy host is available req.RemoteAddr = "172.0.0.4" pool[1].setHealthy(false) h = ipHash.Select(pool, req, nil) if h != pool[2] { t.Error("Expected ip hash policy host to be the third host.") } req.RemoteAddr = "172.0.0.2" h = ipHash.Select(pool, req, nil) if h != pool[0] { t.Error("Expected ip hash policy host to be the first host.") } pool[1].setHealthy(true) req.RemoteAddr = "172.0.0.3" pool[2].setHealthy(false) h = ipHash.Select(pool, req, nil) if h != pool[1] { t.Error("Expected ip hash policy host to be the second host.") } req.RemoteAddr = "172.0.0.4" h = ipHash.Select(pool, req, nil) if h != pool[1] { t.Error("Expected ip hash policy host to be the second host.") } // We should be able to resize the host pool and still be able to predict // where a req will be routed with the same IP's used above pool = UpstreamPool{ {Host: new(Host), Dial: "0.0.0.2"}, {Host: new(Host), Dial: "0.0.0.3"}, } req.RemoteAddr = "172.0.0.1:80" h = ipHash.Select(pool, req, nil) if h != pool[1] { t.Error("Expected ip hash policy host to be the second host.") } req.RemoteAddr = "172.0.0.2:80" h = ipHash.Select(pool, req, nil) if h != pool[0] { t.Error("Expected ip hash policy host to be the first host.") } req.RemoteAddr = "172.0.0.3:80" h = ipHash.Select(pool, req, nil) if h != pool[1] { t.Error("Expected ip hash policy host to be the second host.") } req.RemoteAddr = "172.0.0.4:80" h = ipHash.Select(pool, req, nil) if h != pool[0] { t.Error("Expected ip hash policy host to be the first host.") } // We should get nil when there are no healthy hosts pool[0].setHealthy(false) pool[1].setHealthy(false) h = ipHash.Select(pool, req, nil) if h != nil { t.Error("Expected ip hash policy host to be nil.") } // Reproduce #4135 pool = UpstreamPool{ {Host: new(Host)}, {Host: new(Host)}, {Host: new(Host)}, {Host: new(Host)}, {Host: new(Host)}, {Host: new(Host)}, {Host: new(Host)}, {Host: new(Host)}, {Host: new(Host)}, } pool[0].setHealthy(false) pool[1].setHealthy(false) pool[2].setHealthy(false) pool[3].setHealthy(false) pool[4].setHealthy(false) pool[5].setHealthy(false) pool[6].setHealthy(false) pool[7].setHealthy(false) pool[8].setHealthy(true) // We should get a result back when there is one healthy host left. h = ipHash.Select(pool, req, nil) if h == nil { // If it is nil, it means we missed a host even though one is available t.Error("Expected ip hash policy host to not be nil, but it is nil.") } } func TestFirstPolicy(t *testing.T) { pool := testPool() firstPolicy := new(FirstSelection) req := httptest.NewRequest(http.MethodGet, "/", nil) h := firstPolicy.Select(pool, req, nil) if h != pool[0] { t.Error("Expected first policy host to be the first host.") } pool[0].setHealthy(false) h = firstPolicy.Select(pool, req, nil) if h != pool[1] { t.Error("Expected first policy host to be the second host.") } } func TestURIHashPolicy(t *testing.T) { pool := testPool() uriPolicy := new(URIHashSelection) request := httptest.NewRequest(http.MethodGet, "/test", nil) h := uriPolicy.Select(pool, request, nil) if h != pool[2] { t.Error("Expected uri policy host to be the third host.") } pool[2].setHealthy(false) h = uriPolicy.Select(pool, request, nil) if h != pool[1] { t.Error("Expected uri policy host to be the second host.") } request = httptest.NewRequest(http.MethodGet, "/test_2", nil) h = uriPolicy.Select(pool, request, nil) if h != pool[1] { t.Error("Expected uri policy host to be the second host.") } // We should be able to resize the host pool and still be able to predict // where a request will be routed with the same URI's used above pool = UpstreamPool{ {Host: new(Host)}, {Host: new(Host)}, } request = httptest.NewRequest(http.MethodGet, "/test", nil) h = uriPolicy.Select(pool, request, nil) if h != pool[0] { t.Error("Expected uri policy host to be the first host.") } pool[0].setHealthy(false) h = uriPolicy.Select(pool, request, nil) if h != pool[1] { t.Error("Expected uri policy host to be the first host.") } request = httptest.NewRequest(http.MethodGet, "/test_2", nil) h = uriPolicy.Select(pool, request, nil) if h != pool[1] { t.Error("Expected uri policy host to be the second host.") } pool[0].setHealthy(false) pool[1].setHealthy(false) h = uriPolicy.Select(pool, request, nil) if h != nil { t.Error("Expected uri policy policy host to be nil.") } } func TestLeastRequests(t *testing.T) { pool := testPool() pool[0].Dial = "localhost:8080" pool[1].Dial = "localhost:8081" pool[2].Dial = "localhost:8082" pool[0].setHealthy(true) pool[1].setHealthy(true) pool[2].setHealthy(true) pool[0].countRequest(10) pool[1].countRequest(20) pool[2].countRequest(30) result := leastRequests(pool) if result == nil { t.Error("Least request should not return nil") } if result != pool[0] { t.Error("Least request should return pool[0]") } } func TestRandomChoicePolicy(t *testing.T) { pool := testPool() pool[0].Dial = "localhost:8080" pool[1].Dial = "localhost:8081" pool[2].Dial = "localhost:8082" pool[0].setHealthy(false) pool[1].setHealthy(true) pool[2].setHealthy(true) pool[0].countRequest(10) pool[1].countRequest(20) pool[2].countRequest(30) request := httptest.NewRequest(http.MethodGet, "/test", nil) randomChoicePolicy := new(RandomChoiceSelection) randomChoicePolicy.Choose = 2 h := randomChoicePolicy.Select(pool, request, nil) if h == nil { t.Error("RandomChoicePolicy should not return nil") } if h == pool[0] { t.Error("RandomChoicePolicy should not choose pool[0]") } } func TestCookieHashPolicy(t *testing.T) { pool := testPool() pool[0].Dial = "localhost:8080" pool[1].Dial = "localhost:8081" pool[2].Dial = "localhost:8082" pool[0].setHealthy(true) pool[1].setHealthy(false) pool[2].setHealthy(false) request := httptest.NewRequest(http.MethodGet, "/test", nil) w := httptest.NewRecorder() cookieHashPolicy := new(CookieHashSelection) h := cookieHashPolicy.Select(pool, request, w) cookieServer1 := w.Result().Cookies()[0] if cookieServer1 == nil { t.Fatal("cookieHashPolicy should set a cookie") } if cookieServer1.Name != "lb" { t.Error("cookieHashPolicy should set a cookie with name lb") } if h != pool[0] { t.Error("Expected cookieHashPolicy host to be the first only available host.") } pool[1].setHealthy(true) pool[2].setHealthy(true) request = httptest.NewRequest(http.MethodGet, "/test", nil) w = httptest.NewRecorder() request.AddCookie(cookieServer1) h = cookieHashPolicy.Select(pool, request, w) if h != pool[0] { t.Error("Expected cookieHashPolicy host to stick to the first host (matching cookie).") } s := w.Result().Cookies() if len(s) != 0 { t.Error("Expected cookieHashPolicy to not set a new cookie.") } pool[0].setHealthy(false) request = httptest.NewRequest(http.MethodGet, "/test", nil) w = httptest.NewRecorder() request.AddCookie(cookieServer1) h = cookieHashPolicy.Select(pool, request, w) if h == pool[0] { t.Error("Expected cookieHashPolicy to select a new host.") } if w.Result().Cookies() == nil { t.Error("Expected cookieHashPolicy to set a new cookie.") } } caddy-2.6.2/modules/caddyhttp/reverseproxy/streaming.go000066400000000000000000000256701435007237400233550ustar00rootroot00000000000000// 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. // Most of the code in this file was initially borrowed from the Go // standard library and modified; It had this copyright notice: // Copyright 2011 The Go Authors package reverseproxy import ( "context" "encoding/binary" "io" "mime" "net/http" "sync" "time" "go.uber.org/zap" "golang.org/x/net/http/httpguts" ) func (h Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWriter, req *http.Request, res *http.Response) { reqUpType := upgradeType(req.Header) resUpType := upgradeType(res.Header) // Taken from https://github.com/golang/go/commit/5c489514bc5e61ad9b5b07bd7d8ec65d66a0512a // We know reqUpType is ASCII, it's checked by the caller. if !asciiIsPrint(resUpType) { h.logger.Debug("backend tried to switch to invalid protocol", zap.String("backend_upgrade", resUpType)) return } if !asciiEqualFold(reqUpType, resUpType) { h.logger.Debug("backend tried to switch to unexpected protocol via Upgrade header", zap.String("backend_upgrade", resUpType), zap.String("requested_upgrade", reqUpType)) return } hj, ok := rw.(http.Hijacker) if !ok { h.logger.Sugar().Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw) return } backConn, ok := res.Body.(io.ReadWriteCloser) if !ok { h.logger.Error("internal error: 101 switching protocols response with non-writable body") return } // adopted from https://github.com/golang/go/commit/8bcf2834afdf6a1f7937390903a41518715ef6f5 backConnCloseCh := make(chan struct{}) go func() { // Ensure that the cancelation of a request closes the backend. // See issue https://golang.org/issue/35559. select { case <-req.Context().Done(): case <-backConnCloseCh: } backConn.Close() }() defer close(backConnCloseCh) logger.Debug("upgrading connection") conn, brw, err := hj.Hijack() if err != nil { h.logger.Error("hijack failed on protocol switch", zap.Error(err)) return } defer conn.Close() start := time.Now() defer func() { logger.Debug("connection closed", zap.Duration("duration", time.Since(start))) }() copyHeader(rw.Header(), res.Header) res.Header = rw.Header() res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above if err := res.Write(brw); err != nil { h.logger.Debug("response write", zap.Error(err)) return } if err := brw.Flush(); err != nil { h.logger.Debug("response flush", zap.Error(err)) return } // Ensure the hijacked client connection, and the new connection established // with the backend, are both closed in the event of a server shutdown. This // is done by registering them. We also try to gracefully close connections // we recognize as websockets. gracefulClose := func(conn io.ReadWriteCloser) func() error { if isWebsocket(req) { return func() error { return writeCloseControl(conn) } } return nil } deleteFrontConn := h.registerConnection(conn, gracefulClose(conn)) deleteBackConn := h.registerConnection(backConn, gracefulClose(backConn)) defer deleteFrontConn() defer deleteBackConn() spc := switchProtocolCopier{user: conn, backend: backConn} errc := make(chan error, 1) go spc.copyToBackend(errc) go spc.copyFromBackend(errc) <-errc } // flushInterval returns the p.FlushInterval value, conditionally // overriding its value for a specific request/response. func (h Handler) flushInterval(req *http.Request, res *http.Response) time.Duration { resCTHeader := res.Header.Get("Content-Type") resCT, _, err := mime.ParseMediaType(resCTHeader) // For Server-Sent Events responses, flush immediately. // The MIME type is defined in https://www.w3.org/TR/eventsource/#text-event-stream if err == nil && resCT == "text/event-stream" { return -1 // negative means immediately } // We might have the case of streaming for which Content-Length might be unset. if res.ContentLength == -1 { return -1 } // for h2 and h2c upstream streaming data to client (issues #3556 and #3606) if h.isBidirectionalStream(req, res) { return -1 } return time.Duration(h.FlushInterval) } // isBidirectionalStream returns whether we should work in bi-directional stream mode. // // See https://github.com/caddyserver/caddy/pull/3620 for discussion of nuances. func (h Handler) isBidirectionalStream(req *http.Request, res *http.Response) bool { // We have to check the encoding here; only flush headers with identity encoding. // Non-identity encoding might combine with "encode" directive, and in that case, // if body size larger than enc.MinLength, upper level encode handle might have // Content-Encoding header to write. // (see https://github.com/caddyserver/caddy/issues/3606 for use case) ae := req.Header.Get("Accept-Encoding") return req.ProtoMajor == 2 && res.ProtoMajor == 2 && res.ContentLength == -1 && (ae == "identity" || ae == "") } func (h Handler) copyResponse(dst io.Writer, src io.Reader, flushInterval time.Duration) error { if flushInterval != 0 { if wf, ok := dst.(writeFlusher); ok { mlw := &maxLatencyWriter{ dst: wf, latency: flushInterval, } defer mlw.stop() // set up initial timer so headers get flushed even if body writes are delayed mlw.flushPending = true mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush) dst = mlw } } buf := streamingBufPool.Get().(*[]byte) defer streamingBufPool.Put(buf) _, err := h.copyBuffer(dst, src, *buf) return err } // copyBuffer returns any write errors or non-EOF read errors, and the amount // of bytes written. func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) { if len(buf) == 0 { buf = make([]byte, defaultBufferSize) } var written int64 for { nr, rerr := src.Read(buf) if rerr != nil && rerr != io.EOF && rerr != context.Canceled { // TODO: this could be useful to know (indeed, it revealed an error in our // fastcgi PoC earlier; but it's this single error report here that necessitates // a function separate from io.CopyBuffer, since io.CopyBuffer does not distinguish // between read or write errors; in a reverse proxy situation, write errors are not // something we need to report to the client, but read errors are a problem on our // end for sure. so we need to decide what we want.) // p.logf("copyBuffer: ReverseProxy read error during body copy: %v", rerr) h.logger.Error("reading from backend", zap.Error(rerr)) } if nr > 0 { nw, werr := dst.Write(buf[:nr]) if nw > 0 { written += int64(nw) } if werr != nil { return written, werr } if nr != nw { return written, io.ErrShortWrite } } if rerr != nil { if rerr == io.EOF { rerr = nil } return written, rerr } } } // registerConnection holds onto conn so it can be closed in the event // of a server shutdown. This is useful because hijacked connections or // connections dialed to backends don't close when server is shut down. // The caller should call the returned delete() function when the // connection is done to remove it from memory. func (h *Handler) registerConnection(conn io.ReadWriteCloser, gracefulClose func() error) (del func()) { h.connectionsMu.Lock() h.connections[conn] = openConnection{conn, gracefulClose} h.connectionsMu.Unlock() return func() { h.connectionsMu.Lock() delete(h.connections, conn) h.connectionsMu.Unlock() } } // writeCloseControl sends a best-effort Close control message to the given // WebSocket connection. Thanks to @pascaldekloe who provided inspiration // from his simple implementation of this I was able to learn from at: // github.com/pascaldekloe/websocket. func writeCloseControl(conn io.Writer) error { // https://github.com/pascaldekloe/websocket/blob/32050af67a5d/websocket.go#L119 var reason string // max 123 bytes (control frame payload limit is 125; status code takes 2) const goingAway uint16 = 1001 // TODO: we might need to ensure we are the exclusive writer by this point (io.Copy is stopped)? var writeBuf [127]byte const closeMessage = 8 const finalBit = 1 << 7 writeBuf[0] = closeMessage | finalBit writeBuf[1] = byte(len(reason) + 2) binary.BigEndian.PutUint16(writeBuf[2:4], goingAway) copy(writeBuf[4:], reason) // simply best-effort, but return error for logging purposes _, err := conn.Write(writeBuf[:4+len(reason)]) return err } // isWebsocket returns true if r looks to be an upgrade request for WebSockets. // It is a fairly naive check. func isWebsocket(r *http.Request) bool { return httpguts.HeaderValuesContainsToken(r.Header["Connection"], "upgrade") && httpguts.HeaderValuesContainsToken(r.Header["Upgrade"], "websocket") } // openConnection maps an open connection to // an optional function for graceful close. type openConnection struct { conn io.ReadWriteCloser gracefulClose func() error } type writeFlusher interface { io.Writer http.Flusher } type maxLatencyWriter struct { dst writeFlusher latency time.Duration // non-zero; negative means to flush immediately mu sync.Mutex // protects t, flushPending, and dst.Flush t *time.Timer flushPending bool } func (m *maxLatencyWriter) Write(p []byte) (n int, err error) { m.mu.Lock() defer m.mu.Unlock() n, err = m.dst.Write(p) if m.latency < 0 { m.dst.Flush() return } if m.flushPending { return } if m.t == nil { m.t = time.AfterFunc(m.latency, m.delayedFlush) } else { m.t.Reset(m.latency) } m.flushPending = true return } func (m *maxLatencyWriter) delayedFlush() { m.mu.Lock() defer m.mu.Unlock() if !m.flushPending { // if stop was called but AfterFunc already started this goroutine return } m.dst.Flush() m.flushPending = false } func (m *maxLatencyWriter) stop() { m.mu.Lock() defer m.mu.Unlock() m.flushPending = false if m.t != nil { m.t.Stop() } } // switchProtocolCopier exists so goroutines proxying data back and // forth have nice names in stacks. type switchProtocolCopier struct { user, backend io.ReadWriteCloser } func (c switchProtocolCopier) copyFromBackend(errc chan<- error) { _, err := io.Copy(c.user, c.backend) errc <- err } func (c switchProtocolCopier) copyToBackend(errc chan<- error) { _, err := io.Copy(c.backend, c.user) errc <- err } var streamingBufPool = sync.Pool{ New: func() any { // The Pool's New function should generally only return pointer // types, since a pointer can be put into the return interface // value without an allocation // - (from the package docs) b := make([]byte, defaultBufferSize) return &b }, } const defaultBufferSize = 32 * 1024 caddy-2.6.2/modules/caddyhttp/reverseproxy/streaming_test.go000066400000000000000000000010531435007237400244010ustar00rootroot00000000000000package reverseproxy import ( "bytes" "strings" "testing" ) func TestHandlerCopyResponse(t *testing.T) { h := Handler{} testdata := []string{ "", strings.Repeat("a", defaultBufferSize), strings.Repeat("123456789 123456789 123456789 12", 3000), } dst := bytes.NewBuffer(nil) for _, d := range testdata { src := bytes.NewBuffer([]byte(d)) dst.Reset() err := h.copyResponse(dst, src, 0) if err != nil { t.Errorf("failed with error: %v", err) } out := dst.String() if out != d { t.Errorf("bad read: got %q", out) } } } caddy-2.6.2/modules/caddyhttp/reverseproxy/upstreams.go000066400000000000000000000330621435007237400234010ustar00rootroot00000000000000package reverseproxy import ( "context" "encoding/json" "fmt" weakrand "math/rand" "net" "net/http" "strconv" "strings" "sync" "time" "github.com/caddyserver/caddy/v2" "go.uber.org/zap" ) func init() { caddy.RegisterModule(SRVUpstreams{}) caddy.RegisterModule(AUpstreams{}) caddy.RegisterModule(MultiUpstreams{}) } // SRVUpstreams provides upstreams from SRV lookups. // The lookup DNS name can be configured either by // its individual parts (that is, specifying the // service, protocol, and name separately) to form // the standard "_service._proto.name" domain, or // the domain can be specified directly in name by // leaving service and proto empty. See RFC 2782. // // Lookups are cached and refreshed at the configured // refresh interval. // // Returned upstreams are sorted by priority and weight. type SRVUpstreams struct { // The service label. Service string `json:"service,omitempty"` // The protocol label; either tcp or udp. Proto string `json:"proto,omitempty"` // The name label; or, if service and proto are // empty, the entire domain name to look up. Name string `json:"name,omitempty"` // The interval at which to refresh the SRV lookup. // Results are cached between lookups. Default: 1m Refresh caddy.Duration `json:"refresh,omitempty"` // Configures the DNS resolver used to resolve the // SRV address to SRV records. Resolver *UpstreamResolver `json:"resolver,omitempty"` // If Resolver is configured, how long to wait before // timing out trying to connect to the DNS server. DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` // If Resolver is configured, how long to wait before // spawning an RFC 6555 Fast Fallback connection. // A negative value disables this. FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"` resolver *net.Resolver logger *zap.Logger } // CaddyModule returns the Caddy module information. func (SRVUpstreams) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.upstreams.srv", New: func() caddy.Module { return new(SRVUpstreams) }, } } func (su *SRVUpstreams) Provision(ctx caddy.Context) error { su.logger = ctx.Logger() if su.Refresh == 0 { su.Refresh = caddy.Duration(time.Minute) } if su.Resolver != nil { err := su.Resolver.ParseAddresses() if err != nil { return err } d := &net.Dialer{ Timeout: time.Duration(su.DialTimeout), FallbackDelay: time.Duration(su.FallbackDelay), } su.resolver = &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { //nolint:gosec addr := su.Resolver.netAddrs[weakrand.Intn(len(su.Resolver.netAddrs))] return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0)) }, } } if su.resolver == nil { su.resolver = net.DefaultResolver } return nil } func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { suAddr, service, proto, name := su.expandedAddr(r) // first, use a cheap read-lock to return a cached result quickly srvsMu.RLock() cached := srvs[suAddr] srvsMu.RUnlock() if cached.isFresh() { return cached.upstreams, nil } // otherwise, obtain a write-lock to update the cached value srvsMu.Lock() defer srvsMu.Unlock() // check to see if it's still stale, since we're now in a different // lock from when we first checked freshness; another goroutine might // have refreshed it in the meantime before we re-obtained our lock cached = srvs[suAddr] if cached.isFresh() { return cached.upstreams, nil } su.logger.Debug("refreshing SRV upstreams", zap.String("service", service), zap.String("proto", proto), zap.String("name", name)) _, records, err := su.resolver.LookupSRV(r.Context(), service, proto, name) if err != nil { // From LookupSRV docs: "If the response contains invalid names, those records are filtered // out and an error will be returned alongside the remaining results, if any." Thus, we // only return an error if no records were also returned. if len(records) == 0 { return nil, err } su.logger.Warn("SRV records filtered", zap.Error(err)) } upstreams := make([]*Upstream, len(records)) for i, rec := range records { su.logger.Debug("discovered SRV record", zap.String("target", rec.Target), zap.Uint16("port", rec.Port), zap.Uint16("priority", rec.Priority), zap.Uint16("weight", rec.Weight)) addr := net.JoinHostPort(rec.Target, strconv.Itoa(int(rec.Port))) upstreams[i] = &Upstream{Dial: addr} } // before adding a new one to the cache (as opposed to replacing stale one), make room if cache is full if cached.freshness.IsZero() && len(srvs) >= 100 { for randomKey := range srvs { delete(srvs, randomKey) break } } srvs[suAddr] = srvLookup{ srvUpstreams: su, freshness: time.Now(), upstreams: upstreams, } return upstreams, nil } func (su SRVUpstreams) String() string { if su.Service == "" && su.Proto == "" { return su.Name } return su.formattedAddr(su.Service, su.Proto, su.Name) } // expandedAddr expands placeholders in the configured SRV domain labels. // The return values are: addr, the RFC 2782 representation of the SRV domain; // service, the service; proto, the protocol; and name, the name. // If su.Service and su.Proto are empty, name will be returned as addr instead. func (su SRVUpstreams) expandedAddr(r *http.Request) (addr, service, proto, name string) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) name = repl.ReplaceAll(su.Name, "") if su.Service == "" && su.Proto == "" { addr = name return } service = repl.ReplaceAll(su.Service, "") proto = repl.ReplaceAll(su.Proto, "") addr = su.formattedAddr(service, proto, name) return } // formattedAddr the RFC 2782 representation of the SRV domain, in // the form "_service._proto.name". func (SRVUpstreams) formattedAddr(service, proto, name string) string { return fmt.Sprintf("_%s._%s.%s", service, proto, name) } type srvLookup struct { srvUpstreams SRVUpstreams freshness time.Time upstreams []*Upstream } func (sl srvLookup) isFresh() bool { return time.Since(sl.freshness) < time.Duration(sl.srvUpstreams.Refresh) } // AUpstreams provides upstreams from A/AAAA lookups. // Results are cached and refreshed at the configured // refresh interval. type AUpstreams struct { // The domain name to look up. Name string `json:"name,omitempty"` // The port to use with the upstreams. Default: 80 Port string `json:"port,omitempty"` // The interval at which to refresh the A lookup. // Results are cached between lookups. Default: 1m Refresh caddy.Duration `json:"refresh,omitempty"` // Configures the DNS resolver used to resolve the // domain name to A records. Resolver *UpstreamResolver `json:"resolver,omitempty"` // If Resolver is configured, how long to wait before // timing out trying to connect to the DNS server. DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` // If Resolver is configured, how long to wait before // spawning an RFC 6555 Fast Fallback connection. // A negative value disables this. FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"` resolver *net.Resolver } // CaddyModule returns the Caddy module information. func (AUpstreams) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.upstreams.a", New: func() caddy.Module { return new(AUpstreams) }, } } func (au *AUpstreams) Provision(_ caddy.Context) error { if au.Refresh == 0 { au.Refresh = caddy.Duration(time.Minute) } if au.Port == "" { au.Port = "80" } if au.Resolver != nil { err := au.Resolver.ParseAddresses() if err != nil { return err } d := &net.Dialer{ Timeout: time.Duration(au.DialTimeout), FallbackDelay: time.Duration(au.FallbackDelay), } au.resolver = &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { //nolint:gosec addr := au.Resolver.netAddrs[weakrand.Intn(len(au.Resolver.netAddrs))] return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0)) }, } } if au.resolver == nil { au.resolver = net.DefaultResolver } return nil } func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) auStr := repl.ReplaceAll(au.String(), "") // first, use a cheap read-lock to return a cached result quickly aAaaaMu.RLock() cached := aAaaa[auStr] aAaaaMu.RUnlock() if cached.isFresh() { return cached.upstreams, nil } // otherwise, obtain a write-lock to update the cached value aAaaaMu.Lock() defer aAaaaMu.Unlock() // check to see if it's still stale, since we're now in a different // lock from when we first checked freshness; another goroutine might // have refreshed it in the meantime before we re-obtained our lock cached = aAaaa[auStr] if cached.isFresh() { return cached.upstreams, nil } name := repl.ReplaceAll(au.Name, "") port := repl.ReplaceAll(au.Port, "") ips, err := au.resolver.LookupIPAddr(r.Context(), name) if err != nil { return nil, err } upstreams := make([]*Upstream, len(ips)) for i, ip := range ips { upstreams[i] = &Upstream{ Dial: net.JoinHostPort(ip.String(), port), } } // before adding a new one to the cache (as opposed to replacing stale one), make room if cache is full if cached.freshness.IsZero() && len(srvs) >= 100 { for randomKey := range aAaaa { delete(aAaaa, randomKey) break } } aAaaa[auStr] = aLookup{ aUpstreams: au, freshness: time.Now(), upstreams: upstreams, } return upstreams, nil } func (au AUpstreams) String() string { return net.JoinHostPort(au.Name, au.Port) } type aLookup struct { aUpstreams AUpstreams freshness time.Time upstreams []*Upstream } func (al aLookup) isFresh() bool { return time.Since(al.freshness) < time.Duration(al.aUpstreams.Refresh) } // MultiUpstreams is a single dynamic upstream source that // aggregates the results of multiple dynamic upstream sources. // All configured sources will be queried in order, with their // results appended to the end of the list. Errors returned // from individual sources will be logged and the next source // will continue to be invoked. // // This module makes it easy to implement redundant cluster // failovers, especially in conjunction with the `first` load // balancing policy: if the first source returns an error or // no upstreams, the second source's upstreams will be used // naturally. type MultiUpstreams struct { // The list of upstream source modules to get upstreams from. // They will be queried in order, with their results appended // in the order they are returned. SourcesRaw []json.RawMessage `json:"sources,omitempty" caddy:"namespace=http.reverse_proxy.upstreams inline_key=source"` sources []UpstreamSource logger *zap.Logger } // CaddyModule returns the Caddy module information. func (MultiUpstreams) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.reverse_proxy.upstreams.multi", New: func() caddy.Module { return new(MultiUpstreams) }, } } func (mu *MultiUpstreams) Provision(ctx caddy.Context) error { mu.logger = ctx.Logger() if mu.SourcesRaw != nil { mod, err := ctx.LoadModule(mu, "SourcesRaw") if err != nil { return fmt.Errorf("loading upstream source modules: %v", err) } for _, src := range mod.([]any) { mu.sources = append(mu.sources, src.(UpstreamSource)) } } return nil } func (mu MultiUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { var upstreams []*Upstream for i, src := range mu.sources { select { case <-r.Context().Done(): return upstreams, context.Canceled default: } up, err := src.GetUpstreams(r) if err != nil { mu.logger.Error("upstream source returned error", zap.Int("source_idx", i), zap.Error(err)) } else if len(up) == 0 { mu.logger.Warn("upstream source returned 0 upstreams", zap.Int("source_idx", i)) } else { upstreams = append(upstreams, up...) } } return upstreams, nil } // UpstreamResolver holds the set of addresses of DNS resolvers of // upstream addresses type UpstreamResolver struct { // The addresses of DNS resolvers to use when looking up the addresses of proxy upstreams. // It accepts [network addresses](/docs/conventions#network-addresses) // with port range of only 1. If the host is an IP address, it will be dialed directly to resolve the upstream server. // If the host is not an IP address, the addresses are resolved using the [name resolution convention](https://golang.org/pkg/net/#hdr-Name_Resolution) of the Go standard library. // If the array contains more than 1 resolver address, one is chosen at random. Addresses []string `json:"addresses,omitempty"` netAddrs []caddy.NetworkAddress } // ParseAddresses parses all the configured network addresses // and ensures they're ready to be used. func (u *UpstreamResolver) ParseAddresses() error { for _, v := range u.Addresses { addr, err := caddy.ParseNetworkAddress(v) if err != nil { // If a port wasn't specified for the resolver, // try defaulting to 53 and parse again if strings.Contains(err.Error(), "missing port in address") { addr, err = caddy.ParseNetworkAddress(v + ":53") } if err != nil { return err } } if addr.PortRangeSize() != 1 { return fmt.Errorf("resolver address must have exactly one address; cannot call %v", addr) } u.netAddrs = append(u.netAddrs, addr) } return nil } var ( srvs = make(map[string]srvLookup) srvsMu sync.RWMutex aAaaa = make(map[string]aLookup) aAaaaMu sync.RWMutex ) // Interface guards var ( _ caddy.Provisioner = (*SRVUpstreams)(nil) _ UpstreamSource = (*SRVUpstreams)(nil) _ caddy.Provisioner = (*AUpstreams)(nil) _ UpstreamSource = (*AUpstreams)(nil) ) caddy-2.6.2/modules/caddyhttp/rewrite/000077500000000000000000000000001435007237400177275ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/rewrite/caddyfile.go000066400000000000000000000140751435007237400222110ustar00rootroot00000000000000// 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 rewrite import ( "encoding/json" "strconv" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { httpcaddyfile.RegisterHandlerDirective("rewrite", parseCaddyfileRewrite) httpcaddyfile.RegisterHandlerDirective("method", parseCaddyfileMethod) httpcaddyfile.RegisterHandlerDirective("uri", parseCaddyfileURI) httpcaddyfile.RegisterDirective("handle_path", parseCaddyfileHandlePath) } // parseCaddyfileRewrite sets up a basic rewrite handler from Caddyfile tokens. Syntax: // // rewrite [] // // Only URI components which are given in will be set in the resulting URI. // See the docs for the rewrite handler for more information. func parseCaddyfileRewrite(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { var rewr Rewrite for h.Next() { if !h.NextArg() { return nil, h.ArgErr() } rewr.URI = h.Val() if h.NextArg() { return nil, h.ArgErr() } } return rewr, nil } // parseCaddyfileMethod sets up a basic method rewrite handler from Caddyfile tokens. Syntax: // // method [] // func parseCaddyfileMethod(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { var rewr Rewrite for h.Next() { if !h.NextArg() { return nil, h.ArgErr() } rewr.Method = h.Val() if h.NextArg() { return nil, h.ArgErr() } } return rewr, nil } // parseCaddyfileURI sets up a handler for manipulating (but not "rewriting") the // URI from Caddyfile tokens. Syntax: // // uri [] strip_prefix|strip_suffix|replace|path_regexp [ []] // // If strip_prefix or strip_suffix are used, then will be stripped // only if it is the beginning or the end, respectively, of the URI path. If // replace is used, then will be replaced with across // the whole URI, up to times (or unlimited if unspecified). If // path_regexp is used, then regular expression replacements will be performed // on the path portion of the URI (and a limit cannot be set). func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { var rewr Rewrite for h.Next() { args := h.RemainingArgs() if len(args) < 2 { return nil, h.ArgErr() } switch args[0] { case "strip_prefix": if len(args) > 2 { return nil, h.ArgErr() } rewr.StripPathPrefix = args[1] if !strings.HasPrefix(rewr.StripPathPrefix, "/") { rewr.StripPathPrefix = "/" + rewr.StripPathPrefix } case "strip_suffix": if len(args) > 2 { return nil, h.ArgErr() } rewr.StripPathSuffix = args[1] case "replace": var find, replace, lim string switch len(args) { case 4: lim = args[3] fallthrough case 3: find = args[1] replace = args[2] default: return nil, h.ArgErr() } var limInt int if lim != "" { var err error limInt, err = strconv.Atoi(lim) if err != nil { return nil, h.Errf("limit must be an integer; invalid: %v", err) } } rewr.URISubstring = append(rewr.URISubstring, substrReplacer{ Find: find, Replace: replace, Limit: limInt, }) case "path_regexp": if len(args) != 3 { return nil, h.ArgErr() } find, replace := args[1], args[2] rewr.PathRegexp = append(rewr.PathRegexp, ®exReplacer{ Find: find, Replace: replace, }) default: return nil, h.Errf("unrecognized URI manipulation '%s'", args[0]) } } return rewr, nil } // parseCaddyfileHandlePath parses the handle_path directive. Syntax: // // handle_path [] { // // } // // Only path matchers (with a `/` prefix) are supported as this is a shortcut // for the handle directive with a strip_prefix rewrite. func parseCaddyfileHandlePath(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { if !h.Next() { return nil, h.ArgErr() } if !h.NextArg() { return nil, h.ArgErr() } // read the prefix to strip path := h.Val() if !strings.HasPrefix(path, "/") { return nil, h.Errf("path matcher must begin with '/', got %s", path) } // we only want to strip what comes before the '/' if // the user specified it (e.g. /api/* should only strip /api) var stripPath string if strings.HasSuffix(path, "/*") { stripPath = path[:len(path)-2] } else if strings.HasSuffix(path, "*") { stripPath = path[:len(path)-1] } else { stripPath = path } // the ParseSegmentAsSubroute function expects the cursor // to be at the token just before the block opening, // so we need to rewind because we already read past it h.Reset() h.Next() // parse the block contents as a subroute handler handler, err := httpcaddyfile.ParseSegmentAsSubroute(h) if err != nil { return nil, err } subroute, ok := handler.(*caddyhttp.Subroute) if !ok { return nil, h.Errf("segment was not parsed as a subroute") } // make a matcher on the path and everything below it pathMatcher := caddy.ModuleMap{ "path": h.JSON(caddyhttp.MatchPath{path}), } // build a route with a rewrite handler to strip the path prefix route := caddyhttp.Route{ HandlersRaw: []json.RawMessage{ caddyconfig.JSONModuleObject(Rewrite{ StripPathPrefix: stripPath, }, "handler", "rewrite", nil), }, } // prepend the route to the subroute subroute.Routes = append([]caddyhttp.Route{route}, subroute.Routes...) // build and return a route from the subroute return h.NewRoute(pathMatcher, subroute), nil } caddy-2.6.2/modules/caddyhttp/rewrite/rewrite.go000066400000000000000000000341411435007237400217420ustar00rootroot00000000000000// 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 rewrite import ( "fmt" "net/http" "net/url" "regexp" "strconv" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap" ) func init() { caddy.RegisterModule(Rewrite{}) } // Rewrite is a middleware which can rewrite/mutate HTTP requests. // // The Method and URI properties are "setters" (the request URI // will be overwritten with the given values). Other properties are // "modifiers" (they modify existing values in a differentiable // way). It is atypical to combine the use of setters and // modifiers in a single rewrite. // // To ensure consistent behavior, prefix and suffix stripping is // performed in the URL-decoded (unescaped, normalized) space by // default except for the specific bytes where an escape sequence // is used in the prefix or suffix pattern. // // For all modifiers, paths are cleaned before being modified so that // multiple, consecutive slashes are collapsed into a single slash, // and dot elements are resolved and removed. In the special case // of a prefix, suffix, or substring containing "//" (repeated slashes), // slashes will not be merged while cleaning the path so that // the rewrite can be interpreted literally. type Rewrite struct { // Changes the request's HTTP verb. Method string `json:"method,omitempty"` // Changes the request's URI, which consists of path and query string. // Only components of the URI that are specified will be changed. // For example, a value of "/foo.html" or "foo.html" will only change // the path and will preserve any existing query string. Similarly, a // value of "?a=b" will only change the query string and will not affect // the path. Both can also be changed: "/foo?a=b" - this sets both the // path and query string at the same time. // // You can also use placeholders. For example, to preserve the existing // query string, you might use: "?{http.request.uri.query}&a=b". Any // key-value pairs you add to the query string will not overwrite // existing values (individual pairs are append-only). // // To clear the query string, explicitly set an empty one: "?" URI string `json:"uri,omitempty"` // Strips the given prefix from the beginning of the URI path. // The prefix should be written in normalized (unescaped) form, // but if an escaping (`%xx`) is used, the path will be required // to have that same escape at that position in order to match. StripPathPrefix string `json:"strip_path_prefix,omitempty"` // Strips the given suffix from the end of the URI path. // The suffix should be written in normalized (unescaped) form, // but if an escaping (`%xx`) is used, the path will be required // to have that same escape at that position in order to match. StripPathSuffix string `json:"strip_path_suffix,omitempty"` // Performs substring replacements on the URI. URISubstring []substrReplacer `json:"uri_substring,omitempty"` // Performs regular expression replacements on the URI path. PathRegexp []*regexReplacer `json:"path_regexp,omitempty"` logger *zap.Logger } // CaddyModule returns the Caddy module information. func (Rewrite) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.rewrite", New: func() caddy.Module { return new(Rewrite) }, } } // Provision sets up rewr. func (rewr *Rewrite) Provision(ctx caddy.Context) error { rewr.logger = ctx.Logger() for i, rep := range rewr.PathRegexp { if rep.Find == "" { return fmt.Errorf("path_regexp find cannot be empty") } re, err := regexp.Compile(rep.Find) if err != nil { return fmt.Errorf("compiling regular expression %d: %v", i, err) } rep.re = re } return nil } func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) logger := rewr.logger.With( zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}), ) changed := rewr.Rewrite(r, repl) if changed { logger.Debug("rewrote request", zap.String("method", r.Method), zap.String("uri", r.RequestURI), ) } return next.ServeHTTP(w, r) } // rewrite performs the rewrites on r using repl, which should // have been obtained from r, but is passed in for efficiency. // It returns true if any changes were made to r. func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool { oldMethod := r.Method oldURI := r.RequestURI // method if rewr.Method != "" { r.Method = strings.ToUpper(repl.ReplaceAll(rewr.Method, "")) } // uri (path, query string and... fragment, because why not) if uri := rewr.URI; uri != "" { // find the bounds of each part of the URI that exist pathStart, qsStart, fragStart := -1, -1, -1 pathEnd, qsEnd := -1, -1 loop: for i, ch := range uri { switch { case ch == '?' && qsStart < 0: pathEnd, qsStart = i, i+1 case ch == '#' && fragStart < 0: // everything after fragment is fragment (very clear in RFC 3986 section 4.2) if qsStart < 0 { pathEnd = i } else { qsEnd = i } fragStart = i + 1 break loop case pathStart < 0 && qsStart < 0: pathStart = i } } if pathStart >= 0 && pathEnd < 0 { pathEnd = len(uri) } if qsStart >= 0 && qsEnd < 0 { qsEnd = len(uri) } // isolate the three main components of the URI var path, query, frag string if pathStart > -1 { path = uri[pathStart:pathEnd] } if qsStart > -1 { query = uri[qsStart:qsEnd] } if fragStart > -1 { frag = uri[fragStart:] } // build components which are specified, and store them // in a temporary variable so that they all read the // same version of the URI var newPath, newQuery, newFrag string if path != "" { // Since the 'uri' placeholder performs a URL-encode, // we need to intercept it so that it doesn't, because // otherwise we risk a double-encode of the path. uriPlaceholder := "{http.request.uri}" if strings.Contains(path, uriPlaceholder) { tmpUri := r.URL.Path if r.URL.RawQuery != "" { tmpUri += "?" + r.URL.RawQuery } path = strings.ReplaceAll(path, uriPlaceholder, tmpUri) } newPath = repl.ReplaceAll(path, "") } // before continuing, we need to check if a query string // snuck into the path component during replacements if before, after, found := strings.Cut(newPath, "?"); found { // recompute; new path contains a query string var injectedQuery string newPath, injectedQuery = before, after // don't overwrite explicitly-configured query string if query == "" { query = injectedQuery } } if query != "" { newQuery = buildQueryString(query, repl) } if frag != "" { newFrag = repl.ReplaceAll(frag, "") } // update the URI with the new components // only after building them if pathStart >= 0 { r.URL.Path = newPath } if qsStart >= 0 { r.URL.RawQuery = newQuery } if fragStart >= 0 { r.URL.Fragment = newFrag } } // strip path prefix or suffix if rewr.StripPathPrefix != "" { prefix := repl.ReplaceAll(rewr.StripPathPrefix, "") mergeSlashes := !strings.Contains(prefix, "//") changePath(r, func(escapedPath string) string { escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes) return trimPathPrefix(escapedPath, prefix) }) } if rewr.StripPathSuffix != "" { suffix := repl.ReplaceAll(rewr.StripPathSuffix, "") mergeSlashes := !strings.Contains(suffix, "//") changePath(r, func(escapedPath string) string { escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes) return reverse(trimPathPrefix(reverse(escapedPath), reverse(suffix))) }) } // substring replacements in URI for _, rep := range rewr.URISubstring { rep.do(r, repl) } // regular expression replacements on the path for _, rep := range rewr.PathRegexp { rep.do(r, repl) } // update the encoded copy of the URI r.RequestURI = r.URL.RequestURI() // return true if anything changed return r.Method != oldMethod || r.RequestURI != oldURI } // buildQueryString takes an input query string and // performs replacements on each component, returning // the resulting query string. This function appends // duplicate keys rather than replaces. func buildQueryString(qs string, repl *caddy.Replacer) string { var sb strings.Builder // first component must be key, which is the same // as if we just wrote a value in previous iteration wroteVal := true for len(qs) > 0 { // determine the end of this component, which will be at // the next equal sign or ampersand, whichever comes first nextEq, nextAmp := strings.Index(qs, "="), strings.Index(qs, "&") ampIsNext := nextAmp >= 0 && (nextAmp < nextEq || nextEq < 0) end := len(qs) // assume no delimiter remains... if ampIsNext { end = nextAmp // ...unless ampersand is first... } else if nextEq >= 0 && (nextEq < nextAmp || nextAmp < 0) { end = nextEq // ...or unless equal is first. } // consume the component and write the result comp := qs[:end] comp, _ = repl.ReplaceFunc(comp, func(name string, val any) (any, error) { if name == "http.request.uri.query" && wroteVal { return val, nil // already escaped } var valStr string switch v := val.(type) { case string: valStr = v case fmt.Stringer: valStr = v.String() case int: valStr = strconv.Itoa(v) default: valStr = fmt.Sprintf("%+v", v) } return url.QueryEscape(valStr), nil }) if end < len(qs) { end++ // consume delimiter } qs = qs[end:] // if previous iteration wrote a value, // that means we are writing a key if wroteVal { if sb.Len() > 0 && len(comp) > 0 { sb.WriteRune('&') } } else { sb.WriteRune('=') } sb.WriteString(comp) // remember for the next iteration that we just wrote a value, // which means the next iteration MUST write a key wroteVal = ampIsNext } return sb.String() } // trimPathPrefix is like strings.TrimPrefix, but customized for advanced URI // path prefix matching. The string prefix will be trimmed from the beginning // of escapedPath if escapedPath starts with prefix. Rather than a naive 1:1 // comparison of each byte to determine if escapedPath starts with prefix, // both strings are iterated in lock-step, and if prefix has a '%' encoding // at a particular position, escapedPath must also have the same encoding // representation for that character. In other words, if the prefix string // uses the escaped form for a character, escapedPath must literally use the // same escape at that position. Otherwise, all character comparisons are // performed in normalized/unescaped space. func trimPathPrefix(escapedPath, prefix string) string { var iPath, iPrefix int for { if iPath >= len(escapedPath) || iPrefix >= len(prefix) { break } prefixCh := prefix[iPrefix] ch := string(escapedPath[iPath]) if ch == "%" && prefixCh != '%' && len(escapedPath) >= iPath+3 { var err error ch, err = url.PathUnescape(escapedPath[iPath : iPath+3]) if err != nil { // should be impossible unless EscapedPath() is returning invalid values! return escapedPath } iPath += 2 } // prefix comparisons are case-insensitive to consistency with // path matcher, which is case-insensitive for good reasons if !strings.EqualFold(ch, string(prefixCh)) { return escapedPath } iPath++ iPrefix++ } // if we iterated through the entire prefix, we found it, so trim it if iPath >= len(prefix) { return escapedPath[iPath:] } // otherwise we did not find the prefix return escapedPath } func reverse(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) } // substrReplacer describes either a simple and fast substring replacement. type substrReplacer struct { // A substring to find. Supports placeholders. Find string `json:"find,omitempty"` // The substring to replace with. Supports placeholders. Replace string `json:"replace,omitempty"` // Maximum number of replacements per string. // Set to <= 0 for no limit (default). Limit int `json:"limit,omitempty"` } // do performs the substring replacement on r. func (rep substrReplacer) do(r *http.Request, repl *caddy.Replacer) { if rep.Find == "" { return } lim := rep.Limit if lim == 0 { lim = -1 } find := repl.ReplaceAll(rep.Find, "") replace := repl.ReplaceAll(rep.Replace, "") mergeSlashes := !strings.Contains(rep.Find, "//") changePath(r, func(pathOrRawPath string) string { return strings.Replace(caddyhttp.CleanPath(pathOrRawPath, mergeSlashes), find, replace, lim) }) r.URL.RawQuery = strings.Replace(r.URL.RawQuery, find, replace, lim) } // regexReplacer describes a replacement using a regular expression. type regexReplacer struct { // The regular expression to find. Find string `json:"find,omitempty"` // The substring to replace with. Supports placeholders and // regular expression capture groups. Replace string `json:"replace,omitempty"` re *regexp.Regexp } func (rep regexReplacer) do(r *http.Request, repl *caddy.Replacer) { if rep.Find == "" || rep.re == nil { return } replace := repl.ReplaceAll(rep.Replace, "") changePath(r, func(pathOrRawPath string) string { return rep.re.ReplaceAllString(pathOrRawPath, replace) }) } func changePath(req *http.Request, newVal func(pathOrRawPath string) string) { req.URL.RawPath = newVal(req.URL.EscapedPath()) if p, err := url.PathUnescape(req.URL.RawPath); err == nil && p != "" { req.URL.Path = p } else { req.URL.Path = newVal(req.URL.Path) } // RawPath is only set if it's different from the normalized Path (std lib) if req.URL.RawPath == req.URL.Path { req.URL.RawPath = "" } } // Interface guard var _ caddyhttp.MiddlewareHandler = (*Rewrite)(nil) caddy-2.6.2/modules/caddyhttp/rewrite/rewrite_test.go000066400000000000000000000312671435007237400230070ustar00rootroot00000000000000// 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 rewrite import ( "net/http" "regexp" "testing" "github.com/caddyserver/caddy/v2" ) func TestRewrite(t *testing.T) { repl := caddy.NewReplacer() for i, tc := range []struct { input, expect *http.Request rule Rewrite }{ { input: newRequest(t, "GET", "/"), expect: newRequest(t, "GET", "/"), }, { rule: Rewrite{Method: "GET", URI: "/"}, input: newRequest(t, "GET", "/"), expect: newRequest(t, "GET", "/"), }, { rule: Rewrite{Method: "POST"}, input: newRequest(t, "GET", "/"), expect: newRequest(t, "POST", "/"), }, { rule: Rewrite{URI: "/foo"}, input: newRequest(t, "GET", "/"), expect: newRequest(t, "GET", "/foo"), }, { rule: Rewrite{URI: "/foo"}, input: newRequest(t, "GET", "/bar"), expect: newRequest(t, "GET", "/foo"), }, { rule: Rewrite{URI: "foo"}, input: newRequest(t, "GET", "/"), expect: newRequest(t, "GET", "foo"), }, { rule: Rewrite{URI: "/foo{http.request.uri.path}"}, input: newRequest(t, "GET", "/bar"), expect: newRequest(t, "GET", "/foo/bar"), }, { rule: Rewrite{URI: "/index.php?p={http.request.uri.path}"}, input: newRequest(t, "GET", "/foo/bar"), expect: newRequest(t, "GET", "/index.php?p=%2Ffoo%2Fbar"), }, { rule: Rewrite{URI: "?a=b&{http.request.uri.query}"}, input: newRequest(t, "GET", "/"), expect: newRequest(t, "GET", "/?a=b"), }, { rule: Rewrite{URI: "/?c=d"}, input: newRequest(t, "GET", "/"), expect: newRequest(t, "GET", "/?c=d"), }, { rule: Rewrite{URI: "/?c=d"}, input: newRequest(t, "GET", "/?a=b"), expect: newRequest(t, "GET", "/?c=d"), }, { rule: Rewrite{URI: "?c=d"}, input: newRequest(t, "GET", "/foo"), expect: newRequest(t, "GET", "/foo?c=d"), }, { rule: Rewrite{URI: "/?c=d"}, input: newRequest(t, "GET", "/foo"), expect: newRequest(t, "GET", "/?c=d"), }, { rule: Rewrite{URI: "/?{http.request.uri.query}&c=d"}, input: newRequest(t, "GET", "/"), expect: newRequest(t, "GET", "/?c=d"), }, { rule: Rewrite{URI: "/foo?{http.request.uri.query}&c=d"}, input: newRequest(t, "GET", "/"), expect: newRequest(t, "GET", "/foo?c=d"), }, { rule: Rewrite{URI: "?{http.request.uri.query}&c=d"}, input: newRequest(t, "GET", "/foo"), expect: newRequest(t, "GET", "/foo?c=d"), }, { rule: Rewrite{URI: "{http.request.uri.path}?{http.request.uri.query}&c=d"}, input: newRequest(t, "GET", "/foo"), expect: newRequest(t, "GET", "/foo?c=d"), }, { rule: Rewrite{URI: "{http.request.uri.path}?{http.request.uri.query}&c=d"}, input: newRequest(t, "GET", "/foo"), expect: newRequest(t, "GET", "/foo?c=d"), }, { rule: Rewrite{URI: "/index.php?{http.request.uri.query}&c=d"}, input: newRequest(t, "GET", "/foo"), expect: newRequest(t, "GET", "/index.php?c=d"), }, { rule: Rewrite{URI: "?a=b&c=d"}, input: newRequest(t, "GET", "/foo"), expect: newRequest(t, "GET", "/foo?a=b&c=d"), }, { rule: Rewrite{URI: "/index.php?{http.request.uri.query}&c=d"}, input: newRequest(t, "GET", "/?a=b"), expect: newRequest(t, "GET", "/index.php?a=b&c=d"), }, { rule: Rewrite{URI: "/index.php?c=d&{http.request.uri.query}"}, input: newRequest(t, "GET", "/?a=b"), expect: newRequest(t, "GET", "/index.php?c=d&a=b"), }, { rule: Rewrite{URI: "/index.php?{http.request.uri.query}&p={http.request.uri.path}"}, input: newRequest(t, "GET", "/foo/bar?a=b"), expect: newRequest(t, "GET", "/index.php?a=b&p=%2Ffoo%2Fbar"), }, { rule: Rewrite{URI: "{http.request.uri.path}?"}, input: newRequest(t, "GET", "/foo/bar?a=b&c=d"), expect: newRequest(t, "GET", "/foo/bar"), }, { rule: Rewrite{URI: "?qs={http.request.uri.query}"}, input: newRequest(t, "GET", "/foo?a=b&c=d"), expect: newRequest(t, "GET", "/foo?qs=a%3Db%26c%3Dd"), }, { rule: Rewrite{URI: "/foo?{http.request.uri.query}#frag"}, input: newRequest(t, "GET", "/foo/bar?a=b"), expect: newRequest(t, "GET", "/foo?a=b#frag"), }, { rule: Rewrite{URI: "/foo{http.request.uri}"}, input: newRequest(t, "GET", "/bar?a=b"), expect: newRequest(t, "GET", "/foo/bar?a=b"), }, { rule: Rewrite{URI: "/foo{http.request.uri}"}, input: newRequest(t, "GET", "/bar"), expect: newRequest(t, "GET", "/foo/bar"), }, { rule: Rewrite{URI: "/foo{http.request.uri}?c=d"}, input: newRequest(t, "GET", "/bar?a=b"), expect: newRequest(t, "GET", "/foo/bar?c=d"), }, { rule: Rewrite{URI: "/foo{http.request.uri}?{http.request.uri.query}&c=d"}, input: newRequest(t, "GET", "/bar?a=b"), expect: newRequest(t, "GET", "/foo/bar?a=b&c=d"), }, { rule: Rewrite{URI: "{http.request.uri}"}, input: newRequest(t, "GET", "/bar?a=b"), expect: newRequest(t, "GET", "/bar?a=b"), }, { rule: Rewrite{URI: "{http.request.uri.path}bar?c=d"}, input: newRequest(t, "GET", "/foo/?a=b"), expect: newRequest(t, "GET", "/foo/bar?c=d"), }, { rule: Rewrite{URI: "/i{http.request.uri}"}, input: newRequest(t, "GET", "/%C2%B7%E2%88%B5.png"), expect: newRequest(t, "GET", "/i/%C2%B7%E2%88%B5.png"), }, { rule: Rewrite{URI: "/i{http.request.uri}"}, input: newRequest(t, "GET", "/·∵.png?a=b"), expect: newRequest(t, "GET", "/i/%C2%B7%E2%88%B5.png?a=b"), }, { rule: Rewrite{URI: "/i{http.request.uri}"}, input: newRequest(t, "GET", "/%C2%B7%E2%88%B5.png?a=b"), expect: newRequest(t, "GET", "/i/%C2%B7%E2%88%B5.png?a=b"), }, { rule: Rewrite{URI: "/bar#?"}, input: newRequest(t, "GET", "/foo#fragFirst?c=d"), // not a valid query string (is part of fragment) expect: newRequest(t, "GET", "/bar#?"), // I think this is right? but who knows; std lib drops fragment when parsing }, { rule: Rewrite{URI: "/bar"}, input: newRequest(t, "GET", "/foo#fragFirst?c=d"), expect: newRequest(t, "GET", "/bar#fragFirst?c=d"), }, { rule: Rewrite{StripPathPrefix: "/prefix"}, input: newRequest(t, "GET", "/foo/bar"), expect: newRequest(t, "GET", "/foo/bar"), }, { rule: Rewrite{StripPathPrefix: "/prefix"}, input: newRequest(t, "GET", "/prefix/foo/bar"), expect: newRequest(t, "GET", "/foo/bar"), }, { rule: Rewrite{StripPathPrefix: "/prefix"}, input: newRequest(t, "GET", "/prefix"), expect: newRequest(t, "GET", ""), }, { rule: Rewrite{StripPathPrefix: "/prefix"}, input: newRequest(t, "GET", "/"), expect: newRequest(t, "GET", "/"), }, { rule: Rewrite{StripPathPrefix: "/prefix"}, input: newRequest(t, "GET", "/prefix/foo%2Fbar"), expect: newRequest(t, "GET", "/foo%2Fbar"), }, { rule: Rewrite{StripPathPrefix: "/prefix"}, input: newRequest(t, "GET", "/foo/prefix/bar"), expect: newRequest(t, "GET", "/foo/prefix/bar"), }, { rule: Rewrite{StripPathPrefix: "//prefix"}, // scheme and host needed for URL parser to succeed in setting up test input: newRequest(t, "GET", "http://host//prefix/foo/bar"), expect: newRequest(t, "GET", "http://host/foo/bar"), }, { rule: Rewrite{StripPathPrefix: "//prefix"}, input: newRequest(t, "GET", "/prefix/foo/bar"), expect: newRequest(t, "GET", "/prefix/foo/bar"), }, { rule: Rewrite{StripPathPrefix: "/a%2Fb/c"}, input: newRequest(t, "GET", "/a%2Fb/c/d"), expect: newRequest(t, "GET", "/d"), }, { rule: Rewrite{StripPathPrefix: "/a%2Fb/c"}, input: newRequest(t, "GET", "/a%2fb/c/d"), expect: newRequest(t, "GET", "/d"), }, { rule: Rewrite{StripPathPrefix: "/a/b/c"}, input: newRequest(t, "GET", "/a%2Fb/c/d"), expect: newRequest(t, "GET", "/d"), }, { rule: Rewrite{StripPathPrefix: "/a%2Fb/c"}, input: newRequest(t, "GET", "/a/b/c/d"), expect: newRequest(t, "GET", "/a/b/c/d"), }, { rule: Rewrite{StripPathPrefix: "//a%2Fb/c"}, input: newRequest(t, "GET", "/a/b/c/d"), expect: newRequest(t, "GET", "/a/b/c/d"), }, { rule: Rewrite{StripPathSuffix: "/suffix"}, input: newRequest(t, "GET", "/foo/bar"), expect: newRequest(t, "GET", "/foo/bar"), }, { rule: Rewrite{StripPathSuffix: "suffix"}, input: newRequest(t, "GET", "/foo/bar/suffix"), expect: newRequest(t, "GET", "/foo/bar/"), }, { rule: Rewrite{StripPathSuffix: "suffix"}, input: newRequest(t, "GET", "/foo%2Fbar/suffix"), expect: newRequest(t, "GET", "/foo%2Fbar/"), }, { rule: Rewrite{StripPathSuffix: "%2fsuffix"}, input: newRequest(t, "GET", "/foo%2Fbar%2fsuffix"), expect: newRequest(t, "GET", "/foo%2Fbar"), }, { rule: Rewrite{StripPathSuffix: "/suffix"}, input: newRequest(t, "GET", "/foo/suffix/bar"), expect: newRequest(t, "GET", "/foo/suffix/bar"), }, { rule: Rewrite{URISubstring: []substrReplacer{{Find: "findme", Replace: "replaced"}}}, input: newRequest(t, "GET", "/foo/bar"), expect: newRequest(t, "GET", "/foo/bar"), }, { rule: Rewrite{URISubstring: []substrReplacer{{Find: "findme", Replace: "replaced"}}}, input: newRequest(t, "GET", "/foo/findme/bar"), expect: newRequest(t, "GET", "/foo/replaced/bar"), }, { rule: Rewrite{URISubstring: []substrReplacer{{Find: "findme", Replace: "replaced"}}}, input: newRequest(t, "GET", "/foo/findme%2Fbar"), expect: newRequest(t, "GET", "/foo/replaced%2Fbar"), }, { rule: Rewrite{PathRegexp: []*regexReplacer{{Find: "/{2,}", Replace: "/"}}}, input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"), expect: newRequest(t, "GET", "/foo/bar/baz?a=b//c"), }, } { // copy the original input just enough so that we can // compare it after the rewrite to see if it changed urlCopy := *tc.input.URL originalInput := &http.Request{ Method: tc.input.Method, RequestURI: tc.input.RequestURI, URL: &urlCopy, } // populate the replacer just enough for our tests repl.Set("http.request.uri", tc.input.RequestURI) repl.Set("http.request.uri.path", tc.input.URL.Path) repl.Set("http.request.uri.query", tc.input.URL.RawQuery) // we can't directly call Provision() without a valid caddy.Context // (TODO: fix that) so here we ad-hoc compile the regex for _, rep := range tc.rule.PathRegexp { re, err := regexp.Compile(rep.Find) if err != nil { t.Fatal(err) } rep.re = re } changed := tc.rule.Rewrite(tc.input, repl) if expected, actual := !reqEqual(originalInput, tc.input), changed; expected != actual { t.Errorf("Test %d: Expected changed=%t but was %t", i, expected, actual) } if expected, actual := tc.expect.Method, tc.input.Method; expected != actual { t.Errorf("Test %d: Expected Method='%s' but got '%s'", i, expected, actual) } if expected, actual := tc.expect.RequestURI, tc.input.RequestURI; expected != actual { t.Errorf("Test %d: Expected RequestURI='%s' but got '%s'", i, expected, actual) } if expected, actual := tc.expect.URL.String(), tc.input.URL.String(); expected != actual { t.Errorf("Test %d: Expected URL='%s' but got '%s'", i, expected, actual) } if expected, actual := tc.expect.URL.RequestURI(), tc.input.URL.RequestURI(); expected != actual { t.Errorf("Test %d: Expected URL.RequestURI()='%s' but got '%s'", i, expected, actual) } if expected, actual := tc.expect.URL.Fragment, tc.input.URL.Fragment; expected != actual { t.Errorf("Test %d: Expected URL.Fragment='%s' but got '%s'", i, expected, actual) } } } func newRequest(t *testing.T, method, uri string) *http.Request { req, err := http.NewRequest(method, uri, nil) if err != nil { t.Fatalf("error creating request: %v", err) } req.RequestURI = req.URL.RequestURI() // simulate incoming request return req } // reqEqual if r1 and r2 are equal enough for our purposes. func reqEqual(r1, r2 *http.Request) bool { if r1.Method != r2.Method { return false } if r1.RequestURI != r2.RequestURI { return false } if (r1.URL == nil && r2.URL != nil) || (r1.URL != nil && r2.URL == nil) { return false } if r1.URL == nil && r2.URL == nil { return true } return r1.URL.Scheme == r2.URL.Scheme && r1.URL.Host == r2.URL.Host && r1.URL.Path == r2.URL.Path && r1.URL.RawPath == r2.URL.RawPath && r1.URL.RawQuery == r2.URL.RawQuery && r1.URL.Fragment == r2.URL.Fragment } caddy-2.6.2/modules/caddyhttp/routes.go000066400000000000000000000312131435007237400201160ustar00rootroot00000000000000// 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 caddyhttp import ( "encoding/json" "fmt" "net/http" "github.com/caddyserver/caddy/v2" ) // Route consists of a set of rules for matching HTTP requests, // a list of handlers to execute, and optional flow control // parameters which customize the handling of HTTP requests // in a highly flexible and performant manner. type Route struct { // Group is an optional name for a group to which this // route belongs. Grouping a route makes it mutually // exclusive with others in its group; if a route belongs // to a group, only the first matching route in that group // will be executed. Group string `json:"group,omitempty"` // The matcher sets which will be used to qualify this // route for a request (essentially the "if" statement // of this route). Each matcher set is OR'ed, but matchers // within a set are AND'ed together. MatcherSetsRaw RawMatcherSets `json:"match,omitempty" caddy:"namespace=http.matchers"` // The list of handlers for this route. Upon matching a request, they are chained // together in a middleware fashion: requests flow from the first handler to the last // (top of the list to the bottom), with the possibility that any handler could stop // the chain and/or return an error. Responses flow back through the chain (bottom of // the list to the top) as they are written out to the client. // // Not all handlers call the next handler in the chain. For example, the reverse_proxy // handler always sends a request upstream or returns an error. Thus, configuring // handlers after reverse_proxy in the same route is illogical, since they would never // be executed. You will want to put handlers which originate the response at the very // end of your route(s). The documentation for a module should state whether it invokes // the next handler, but sometimes it is common sense. // // Some handlers manipulate the response. Remember that requests flow down the list, and // responses flow up the list. // // For example, if you wanted to use both `templates` and `encode` handlers, you would // need to put `templates` after `encode` in your route, because responses flow up. // Thus, `templates` will be able to parse and execute the plain-text response as a // template, and then return it up to the `encode` handler which will then compress it // into a binary format. // // If `templates` came before `encode`, then `encode` would write a compressed, // binary-encoded response to `templates` which would not be able to parse the response // properly. // // The correct order, then, is this: // // [ // {"handler": "encode"}, // {"handler": "templates"}, // {"handler": "file_server"} // ] // // The request flows ⬇️ DOWN (`encode` -> `templates` -> `file_server`). // // 1. First, `encode` will choose how to `encode` the response and wrap the response. // 2. Then, `templates` will wrap the response with a buffer. // 3. Finally, `file_server` will originate the content from a file. // // The response flows ⬆️ UP (`file_server` -> `templates` -> `encode`): // // 1. First, `file_server` will write the file to the response. // 2. That write will be buffered and then executed by `templates`. // 3. Lastly, the write from `templates` will flow into `encode` which will compress the stream. // // If you think of routes in this way, it will be easy and even fun to solve the puzzle of writing correct routes. HandlersRaw []json.RawMessage `json:"handle,omitempty" caddy:"namespace=http.handlers inline_key=handler"` // If true, no more routes will be executed after this one. Terminal bool `json:"terminal,omitempty"` // decoded values MatcherSets MatcherSets `json:"-"` Handlers []MiddlewareHandler `json:"-"` middleware []Middleware } // Empty returns true if the route has all zero/default values. func (r Route) Empty() bool { return len(r.MatcherSetsRaw) == 0 && len(r.MatcherSets) == 0 && len(r.HandlersRaw) == 0 && len(r.Handlers) == 0 && !r.Terminal && r.Group == "" } func (r Route) String() string { handlersRaw := "[" for _, hr := range r.HandlersRaw { handlersRaw += " " + string(hr) } handlersRaw += "]" return fmt.Sprintf(`{Group:"%s" MatcherSetsRaw:%s HandlersRaw:%s Terminal:%t}`, r.Group, r.MatcherSetsRaw, handlersRaw, r.Terminal) } // RouteList is a list of server routes that can // create a middleware chain. type RouteList []Route // Provision sets up both the matchers and handlers in the routes. func (routes RouteList) Provision(ctx caddy.Context) error { err := routes.ProvisionMatchers(ctx) if err != nil { return err } return routes.ProvisionHandlers(ctx, nil) } // ProvisionMatchers sets up all the matchers by loading the // matcher modules. Only call this method directly if you need // to set up matchers and handlers separately without having // to provision a second time; otherwise use Provision instead. func (routes RouteList) ProvisionMatchers(ctx caddy.Context) error { for i := range routes { // matchers matchersIface, err := ctx.LoadModule(&routes[i], "MatcherSetsRaw") if err != nil { return fmt.Errorf("route %d: loading matcher modules: %v", i, err) } err = routes[i].MatcherSets.FromInterface(matchersIface) if err != nil { return fmt.Errorf("route %d: %v", i, err) } } return nil } // ProvisionHandlers sets up all the handlers by loading the // handler modules. Only call this method directly if you need // to set up matchers and handlers separately without having // to provision a second time; otherwise use Provision instead. func (routes RouteList) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error { for i := range routes { handlersIface, err := ctx.LoadModule(&routes[i], "HandlersRaw") if err != nil { return fmt.Errorf("route %d: loading handler modules: %v", i, err) } for _, handler := range handlersIface.([]any) { routes[i].Handlers = append(routes[i].Handlers, handler.(MiddlewareHandler)) } // pre-compile the middleware handler chain for _, midhandler := range routes[i].Handlers { routes[i].middleware = append(routes[i].middleware, wrapMiddleware(ctx, midhandler, metrics)) } } return nil } // Compile prepares a middleware chain from the route list. // This should only be done once: after all the routes have // been provisioned, and before serving requests. func (routes RouteList) Compile(next Handler) Handler { mid := make([]Middleware, 0, len(routes)) for _, route := range routes { mid = append(mid, wrapRoute(route)) } stack := next for i := len(mid) - 1; i >= 0; i-- { stack = mid[i](stack) } return stack } // wrapRoute wraps route with a middleware and handler so that it can // be chained in and defer evaluation of its matchers to request-time. // Like wrapMiddleware, it is vital that this wrapping takes place in // its own stack frame so as to not overwrite the reference to the // intended route by looping and changing the reference each time. func wrapRoute(route Route) Middleware { return func(next Handler) Handler { return HandlerFunc(func(rw http.ResponseWriter, req *http.Request) error { // TODO: Update this comment, it seems we've moved the copy into the handler? // copy the next handler (it's an interface, so it's just // a very lightweight copy of a pointer); this is important // because this is a closure to the func below, which // re-assigns the value as it compiles the middleware stack; // if we don't make this copy, we'd affect the underlying // pointer for all future request (yikes); we could // alternatively solve this by moving the func below out of // this closure and into a standalone package-level func, // but I just thought this made more sense nextCopy := next // route must match at least one of the matcher sets if !route.MatcherSets.AnyMatch(req) { // allow matchers the opportunity to short circuit // the request and trigger the error handling chain err, ok := GetVar(req.Context(), MatcherErrorVarKey).(error) if ok { // clear out the error from context, otherwise // it will cascade to the error routes (#4916) SetVar(req.Context(), MatcherErrorVarKey, nil) // return the matcher's error return err } // call the next handler, and skip this one, // since the matcher didn't match return nextCopy.ServeHTTP(rw, req) } // if route is part of a group, ensure only the // first matching route in the group is applied if route.Group != "" { groups := req.Context().Value(routeGroupCtxKey).(map[string]struct{}) if _, ok := groups[route.Group]; ok { // this group has already been // satisfied by a matching route return nextCopy.ServeHTTP(rw, req) } // this matching route satisfies the group groups[route.Group] = struct{}{} } // make terminal routes terminate if route.Terminal { if _, ok := req.Context().Value(ErrorCtxKey).(error); ok { nextCopy = errorEmptyHandler } else { nextCopy = emptyHandler } } // compile this route's handler stack for i := len(route.middleware) - 1; i >= 0; i-- { nextCopy = route.middleware[i](nextCopy) } return nextCopy.ServeHTTP(rw, req) }) } } // wrapMiddleware wraps mh such that it can be correctly // appended to a list of middleware in preparation for // compiling into a handler chain. We can't do this inline // inside a loop, because it relies on a reference to mh // not changing until the execution of its handler (which // is deferred by multiple func closures). In other words, // we need to pull this particular MiddlewareHandler // pointer into its own stack frame to preserve it so it // won't be overwritten in future loop iterations. func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware { handlerToUse := mh if metrics != nil { // wrap the middleware with metrics instrumentation handlerToUse = newMetricsInstrumentedHandler(caddy.GetModuleName(mh), mh) } return func(next Handler) Handler { // copy the next handler (it's an interface, so it's // just a very lightweight copy of a pointer); this // is a safeguard against the handler changing the // value, which could affect future requests (yikes) nextCopy := next return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { // TODO: This is where request tracing could be implemented // TODO: see what the std lib gives us in terms of stack tracing too return handlerToUse.ServeHTTP(w, r, nextCopy) }) } } // MatcherSet is a set of matchers which // must all match in order for the request // to be matched successfully. type MatcherSet []RequestMatcher // Match returns true if the request matches all // matchers in mset or if there are no matchers. func (mset MatcherSet) Match(r *http.Request) bool { for _, m := range mset { if !m.Match(r) { return false } } return true } // RawMatcherSets is a group of matcher sets // in their raw, JSON form. type RawMatcherSets []caddy.ModuleMap // MatcherSets is a group of matcher sets capable // of checking whether a request matches any of // the sets. type MatcherSets []MatcherSet // AnyMatch returns true if req matches any of the // matcher sets in ms or if there are no matchers, // in which case the request always matches. func (ms MatcherSets) AnyMatch(req *http.Request) bool { for _, m := range ms { if m.Match(req) { return true } } return len(ms) == 0 } // FromInterface fills ms from an 'any' value obtained from LoadModule. func (ms *MatcherSets) FromInterface(matcherSets any) error { for _, matcherSetIfaces := range matcherSets.([]map[string]any) { var matcherSet MatcherSet for _, matcher := range matcherSetIfaces { reqMatcher, ok := matcher.(RequestMatcher) if !ok { return fmt.Errorf("decoded module is not a RequestMatcher: %#v", matcher) } matcherSet = append(matcherSet, reqMatcher) } *ms = append(*ms, matcherSet) } return nil } // TODO: Is this used? func (ms MatcherSets) String() string { result := "[" for _, matcherSet := range ms { for _, matcher := range matcherSet { result += fmt.Sprintf(" %#v", matcher) } } return result + " ]" } var routeGroupCtxKey = caddy.CtxKey("route_group") caddy-2.6.2/modules/caddyhttp/server.go000066400000000000000000000603211435007237400201050ustar00rootroot00000000000000// 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 caddyhttp import ( "context" "crypto/tls" "encoding/json" "fmt" "net" "net/http" "net/url" "runtime" "strings" "sync" "sync/atomic" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/certmagic" "github.com/lucas-clemente/quic-go" "github.com/lucas-clemente/quic-go/http3" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Server describes an HTTP server. type Server struct { activeRequests int64 // accessed atomically // Socket addresses to which to bind listeners. Accepts // [network addresses](/docs/conventions#network-addresses) // that may include port ranges. Listener addresses must // be unique; they cannot be repeated across all defined // servers. Listen []string `json:"listen,omitempty"` // A list of listener wrapper modules, which can modify the behavior // of the base listener. They are applied in the given order. ListenerWrappersRaw []json.RawMessage `json:"listener_wrappers,omitempty" caddy:"namespace=caddy.listeners inline_key=wrapper"` // How long to allow a read from a client's upload. Setting this // to a short, non-zero value can mitigate slowloris attacks, but // may also affect legitimately slow clients. ReadTimeout caddy.Duration `json:"read_timeout,omitempty"` // ReadHeaderTimeout is like ReadTimeout but for request headers. ReadHeaderTimeout caddy.Duration `json:"read_header_timeout,omitempty"` // WriteTimeout is how long to allow a write to a client. Note // that setting this to a small value when serving large files // may negatively affect legitimately slow clients. WriteTimeout caddy.Duration `json:"write_timeout,omitempty"` // IdleTimeout is the maximum time to wait for the next request // when keep-alives are enabled. If zero, a default timeout of // 5m is applied to help avoid resource exhaustion. IdleTimeout caddy.Duration `json:"idle_timeout,omitempty"` // KeepAliveInterval is the interval at which TCP keepalive packets // are sent to keep the connection alive at the TCP layer when no other // data is being transmitted. The default is 15s. KeepAliveInterval caddy.Duration `json:"keepalive_interval,omitempty"` // MaxHeaderBytes is the maximum size to parse from a client's // HTTP request headers. MaxHeaderBytes int `json:"max_header_bytes,omitempty"` // Routes describes how this server will handle requests. // Routes are executed sequentially. First a route's matchers // are evaluated, then its grouping. If it matches and has // not been mutually-excluded by its grouping, then its // handlers are executed sequentially. The sequence of invoked // handlers comprises a compiled middleware chain that flows // from each matching route and its handlers to the next. // // By default, all unrouted requests receive a 200 OK response // to indicate the server is working. Routes RouteList `json:"routes,omitempty"` // Errors is how this server will handle errors returned from any // of the handlers in the primary routes. If the primary handler // chain returns an error, the error along with its recommended // status code are bubbled back up to the HTTP server which // executes a separate error route, specified using this property. // The error routes work exactly like the normal routes. Errors *HTTPErrorConfig `json:"errors,omitempty"` // How to handle TLS connections. At least one policy is // required to enable HTTPS on this server if automatic // HTTPS is disabled or does not apply. TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies,omitempty"` // AutoHTTPS configures or disables automatic HTTPS within this server. // HTTPS is enabled automatically and by default when qualifying names // are present in a Host matcher and/or when the server is listening // only on the HTTPS port. AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"` // If true, will require that a request's Host header match // the value of the ServerName sent by the client's TLS // ClientHello; often a necessary safeguard when using TLS // client authentication. StrictSNIHost *bool `json:"strict_sni_host,omitempty"` // Enables access logging and configures how access logs are handled // in this server. To minimally enable access logs, simply set this // to a non-null, empty struct. Logs *ServerLogConfig `json:"logs,omitempty"` // Protocols specifies which HTTP protocols to enable. // Supported values are: // // - `h1` (HTTP/1.1) // - `h2` (HTTP/2) // - `h2c` (cleartext HTTP/2) // - `h3` (HTTP/3) // // If enabling `h2` or `h2c`, `h1` must also be enabled; // this is due to current limitations in the Go standard // library. // // HTTP/2 operates only over TLS (HTTPS). HTTP/3 opens // a UDP socket to serve QUIC connections. // // H2C operates over plain TCP if the client supports it; // however, because this is not implemented by the Go // standard library, other server options are not compatible // and will not be applied to H2C requests. Do not enable this // only to achieve maximum client compatibility. In practice, // very few clients implement H2C, and even fewer require it. // Enabling H2C can be useful for serving/proxying gRPC // if encryption is not possible or desired. // // We recommend for most users to simply let Caddy use the // default settings. // // Default: `[h1 h2 h3]` Protocols []string `json:"protocols,omitempty"` // If set, metrics observations will be enabled. // This setting is EXPERIMENTAL and subject to change. Metrics *Metrics `json:"metrics,omitempty"` name string primaryHandlerChain Handler errorHandlerChain Handler listenerWrappers []caddy.ListenerWrapper listeners []net.Listener tlsApp *caddytls.TLS events *caddyevents.App logger *zap.Logger accessLogger *zap.Logger errorLogger *zap.Logger ctx caddy.Context server *http.Server h3server *http3.Server h3listeners []net.PacketConn // TODO: we have to hold these because quic-go won't close listeners it didn't create addresses []caddy.NetworkAddress shutdownAt time.Time shutdownAtMu *sync.RWMutex // registered callback functions connStateFuncs []func(net.Conn, http.ConnState) connContextFuncs []func(ctx context.Context, c net.Conn) context.Context onShutdownFuncs []func() } // ServeHTTP is the entry point for all HTTP requests. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", "Caddy") // advertise HTTP/3, if enabled if s.h3server != nil { // keep track of active requests for QUIC transport purposes atomic.AddInt64(&s.activeRequests, 1) defer atomic.AddInt64(&s.activeRequests, -1) if r.ProtoMajor < 3 { err := s.h3server.SetQuicHeaders(w.Header()) if err != nil { s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err)) } } } // reject very long methods; probably a mistake or an attack if len(r.Method) > 32 { if s.shouldLogRequest(r) { s.accessLogger.Debug("rejecting request with long method", zap.String("method_trunc", r.Method[:32]), zap.String("remote_addr", r.RemoteAddr)) } w.WriteHeader(http.StatusMethodNotAllowed) return } repl := caddy.NewReplacer() r = PrepareRequest(r, repl, w, s) // encode the request for logging purposes before // it enters any handler chain; this is necessary // to capture the original request in case it gets // modified during handling shouldLogCredentials := s.Logs != nil && s.Logs.ShouldLogCredentials loggableReq := zap.Object("request", LoggableHTTPRequest{ Request: r, ShouldLogCredentials: shouldLogCredentials, }) errLog := s.errorLogger.With(loggableReq) var duration time.Duration if s.shouldLogRequest(r) { wrec := NewResponseRecorder(w, nil, nil) w = wrec // capture the original version of the request accLog := s.accessLogger.With(loggableReq) defer func() { // this request may be flagged as omitted from the logs if skipLog, ok := GetVar(r.Context(), SkipLogVar).(bool); ok && skipLog { return } repl.Set("http.response.status", wrec.Status()) // will be 0 if no response is written by us (Go will write 200 to client) repl.Set("http.response.size", wrec.Size()) repl.Set("http.response.duration", duration) repl.Set("http.response.duration_ms", duration.Seconds()*1e3) // multiply seconds to preserve decimal (see #4666) logger := accLog if s.Logs != nil { logger = s.Logs.wrapLogger(logger, r.Host) } log := logger.Info if wrec.Status() >= 400 { log = logger.Error } userID, _ := repl.GetString("http.auth.user.id") log("handled request", zap.String("user_id", userID), zap.Duration("duration", duration), zap.Int("size", wrec.Size()), zap.Int("status", wrec.Status()), zap.Object("resp_headers", LoggableHTTPHeader{ Header: wrec.Header(), ShouldLogCredentials: shouldLogCredentials, }), ) }() } start := time.Now() // guarantee ACME HTTP challenges; handle them // separately from any user-defined handlers if s.tlsApp.HandleHTTPChallenge(w, r) { duration = time.Since(start) return } // execute the primary handler chain err := s.primaryHandlerChain.ServeHTTP(w, r) duration = time.Since(start) // if no errors, we're done! if err == nil { return } // restore original request before invoking error handler chain (issue #3717) // TODO: this does not restore original headers, if modified (for efficiency) origReq := r.Context().Value(OriginalRequestCtxKey).(http.Request) r.Method = origReq.Method r.RemoteAddr = origReq.RemoteAddr r.RequestURI = origReq.RequestURI cloneURL(origReq.URL, r.URL) // prepare the error log logger := errLog if s.Logs != nil { logger = s.Logs.wrapLogger(logger, r.Host) } logger = logger.With(zap.Duration("duration", duration)) // get the values that will be used to log the error errStatus, errMsg, errFields := errLogValues(err) // add HTTP error information to request context r = s.Errors.WithError(r, err) if s.Errors != nil && len(s.Errors.Routes) > 0 { // execute user-defined error handling route err2 := s.errorHandlerChain.ServeHTTP(w, r) if err2 == nil { // user's error route handled the error response // successfully, so now just log the error logger.Debug(errMsg, errFields...) } else { // well... this is awkward errFields = append([]zapcore.Field{ zap.String("error", err2.Error()), zap.Namespace("first_error"), zap.String("msg", errMsg), }, errFields...) logger.Error("error handling handler error", errFields...) if handlerErr, ok := err.(HandlerError); ok { w.WriteHeader(handlerErr.StatusCode) } else { w.WriteHeader(http.StatusInternalServerError) } } } else { if errStatus >= 500 { logger.Error(errMsg, errFields...) } else { logger.Debug(errMsg, errFields...) } w.WriteHeader(errStatus) } } // wrapPrimaryRoute wraps stack (a compiled middleware handler chain) // in s.enforcementHandler which performs crucial security checks, etc. func (s *Server) wrapPrimaryRoute(stack Handler) Handler { return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return s.enforcementHandler(w, r, stack) }) } // enforcementHandler is an implicit middleware which performs // standard checks before executing the HTTP middleware chain. func (s *Server) enforcementHandler(w http.ResponseWriter, r *http.Request, next Handler) error { // enforce strict host matching, which ensures that the SNI // value (if any), matches the Host header; essential for // servers that rely on TLS ClientAuth sharing a listener // with servers that do not; if not enforced, client could // bypass by sending benign SNI then restricted Host header if s.StrictSNIHost != nil && *s.StrictSNIHost && r.TLS != nil { hostname, _, err := net.SplitHostPort(r.Host) if err != nil { hostname = r.Host // OK; probably lacked port } if !strings.EqualFold(r.TLS.ServerName, hostname) { err := fmt.Errorf("strict host matching: TLS ServerName (%s) and HTTP Host (%s) values differ", r.TLS.ServerName, hostname) r.Close = true return Error(http.StatusMisdirectedRequest, err) } } return next.ServeHTTP(w, r) } // listenersUseAnyPortOtherThan returns true if there are any // listeners in s that use a port which is not otherPort. func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool { for _, lnAddr := range s.Listen { laddrs, err := caddy.ParseNetworkAddress(lnAddr) if err != nil { continue } if uint(otherPort) > laddrs.EndPort || uint(otherPort) < laddrs.StartPort { return true } } return false } // hasListenerAddress returns true if s has a listener // at the given address fullAddr. Currently, fullAddr // must represent exactly one socket address (port // ranges are not supported) func (s *Server) hasListenerAddress(fullAddr string) bool { laddrs, err := caddy.ParseNetworkAddress(fullAddr) if err != nil { return false } if laddrs.PortRangeSize() != 1 { return false // TODO: support port ranges } for _, lnAddr := range s.Listen { thisAddrs, err := caddy.ParseNetworkAddress(lnAddr) if err != nil { continue } if thisAddrs.Network != laddrs.Network { continue } // Apparently, Linux requires all bound ports to be distinct // *regardless of host interface* even if the addresses are // in fact different; binding "192.168.0.1:9000" and then // ":9000" will fail for ":9000" because "address is already // in use" even though it's not, and the same bindings work // fine on macOS. I also found on Linux that listening on // "[::]:9000" would fail with a similar error, except with // the address "0.0.0.0:9000", as if deliberately ignoring // that I specified the IPv6 interface explicitly. This seems // to be a major bug in the Linux network stack and I don't // know why it hasn't been fixed yet, so for now we have to // special-case ourselves around Linux like a doting parent. // The second issue seems very similar to a discussion here: // https://github.com/nodejs/node/issues/9390 // // This is very easy to reproduce by creating an HTTP server // that listens to both addresses or just one with a host // interface; or for a more confusing reproduction, try // listening on "127.0.0.1:80" and ":443" and you'll see // the error, if you take away the GOOS condition below. // // So, an address is equivalent if the port is in the port // range, and if not on Linux, the host is the same... sigh. if (runtime.GOOS == "linux" || thisAddrs.Host == laddrs.Host) && (laddrs.StartPort <= thisAddrs.EndPort) && (laddrs.StartPort >= thisAddrs.StartPort) { return true } } return false } func (s *Server) hasTLSClientAuth() bool { for _, cp := range s.TLSConnPolicies { if cp.ClientAuthentication != nil && cp.ClientAuthentication.Active() { return true } } return false } // findLastRouteWithHostMatcher returns the index of the last route // in the server which has a host matcher. Used during Automatic HTTPS // to determine where to insert the HTTP->HTTPS redirect route, such // that it is after any other host matcher but before any "catch-all" // route without a host matcher. func (s *Server) findLastRouteWithHostMatcher() int { foundHostMatcher := false lastIndex := len(s.Routes) for i, route := range s.Routes { // since we want to break out of an inner loop, use a closure // to allow us to use 'return' when we found a host matcher found := (func() bool { for _, sets := range route.MatcherSets { for _, matcher := range sets { switch matcher.(type) { case *MatchHost: foundHostMatcher = true return true } } } return false })() // if we found the host matcher, change the lastIndex to // just after the current route if found { lastIndex = i + 1 } } // If we didn't actually find a host matcher, return 0 // because that means every defined route was a "catch-all". // See https://caddy.community/t/how-to-set-priority-in-caddyfile/13002/8 if !foundHostMatcher { return 0 } return lastIndex } // serveHTTP3 creates a QUIC listener, configures an HTTP/3 server if // not already done, and then uses that server to serve HTTP/3 over // the listener, with Server s as the handler. func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error { switch addr.Network { case "unix": addr.Network = "unixgram" case "tcp4": addr.Network = "udp4" case "tcp6": addr.Network = "udp6" default: addr.Network = "udp" // TODO: Maybe a better default is to not enable HTTP/3 if we do not know the network? } lnAny, err := addr.Listen(s.ctx, 0, net.ListenConfig{}) if err != nil { return err } ln := lnAny.(net.PacketConn) h3ln, err := caddy.ListenQUIC(ln, tlsCfg, &s.activeRequests) if err != nil { return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err) } // create HTTP/3 server if not done already if s.h3server == nil { s.h3server = &http3.Server{ Handler: s, TLSConfig: tlsCfg, MaxHeaderBytes: s.MaxHeaderBytes, // TODO: remove this config when draft versions are no longer supported (we have no need to support drafts) QuicConfig: &quic.Config{ Versions: []quic.VersionNumber{quic.Version1, quic.Version2}, }, } } s.h3listeners = append(s.h3listeners, lnAny.(net.PacketConn)) //nolint:errcheck go s.h3server.ServeListener(h3ln) return nil } // configureServer applies/binds the registered callback functions to the server. func (s *Server) configureServer(server *http.Server) { for _, f := range s.connStateFuncs { if server.ConnState != nil { baseConnStateFunc := server.ConnState server.ConnState = func(conn net.Conn, state http.ConnState) { baseConnStateFunc(conn, state) f(conn, state) } } else { server.ConnState = f } } for _, f := range s.connContextFuncs { if server.ConnContext != nil { baseConnContextFunc := server.ConnContext server.ConnContext = func(ctx context.Context, c net.Conn) context.Context { return f(baseConnContextFunc(ctx, c), c) } } else { server.ConnContext = f } } for _, f := range s.onShutdownFuncs { server.RegisterOnShutdown(f) } } // RegisterConnState registers f to be invoked on s.ConnState. func (s *Server) RegisterConnState(f func(net.Conn, http.ConnState)) { s.connStateFuncs = append(s.connStateFuncs, f) } // RegisterConnContext registers f to be invoked as part of s.ConnContext. func (s *Server) RegisterConnContext(f func(ctx context.Context, c net.Conn) context.Context) { s.connContextFuncs = append(s.connContextFuncs, f) } // RegisterOnShutdown registers f to be invoked on server shutdown. func (s *Server) RegisterOnShutdown(f func()) { s.onShutdownFuncs = append(s.onShutdownFuncs, f) } // HTTPErrorConfig determines how to handle errors // from the HTTP handlers. type HTTPErrorConfig struct { // The routes to evaluate after the primary handler // chain returns an error. In an error route, extra // placeholders are available: // // Placeholder | Description // ------------|--------------- // `{http.error.status_code}` | The recommended HTTP status code // `{http.error.status_text}` | The status text associated with the recommended status code // `{http.error.message}` | The error message // `{http.error.trace}` | The origin of the error // `{http.error.id}` | An identifier for this occurrence of the error Routes RouteList `json:"routes,omitempty"` } // WithError makes a shallow copy of r to add the error to its // context, and sets placeholders on the request's replacer // related to err. It returns the modified request which has // the error information in its context and replacer. It // overwrites any existing error values that are stored. func (*HTTPErrorConfig) WithError(r *http.Request, err error) *http.Request { // add the raw error value to the request context // so it can be accessed by error handlers c := context.WithValue(r.Context(), ErrorCtxKey, err) r = r.WithContext(c) // add error values to the replacer repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl.Set("http.error", err) if handlerErr, ok := err.(HandlerError); ok { repl.Set("http.error.status_code", handlerErr.StatusCode) repl.Set("http.error.status_text", http.StatusText(handlerErr.StatusCode)) repl.Set("http.error.id", handlerErr.ID) repl.Set("http.error.trace", handlerErr.Trace) if handlerErr.Err != nil { repl.Set("http.error.message", handlerErr.Err.Error()) } else { repl.Set("http.error.message", http.StatusText(handlerErr.StatusCode)) } } return r } // shouldLogRequest returns true if this request should be logged. func (s *Server) shouldLogRequest(r *http.Request) bool { if s.accessLogger == nil || s.Logs == nil { // logging is disabled return false } if _, ok := s.Logs.LoggerNames[r.Host]; ok { // this host is mapped to a particular logger name return true } for _, dh := range s.Logs.SkipHosts { // logging for this particular host is disabled if certmagic.MatchWildcard(r.Host, dh) { return false } } // if configured, this host is not mapped and thus must not be logged return !s.Logs.SkipUnmappedHosts } // protocol returns true if the protocol proto is configured/enabled. func (s *Server) protocol(proto string) bool { for _, p := range s.Protocols { if p == proto { return true } } return false } // Listeners returns the server's listeners. These are active listeners, // so calling Accept() or Close() on them will probably break things. // They are made available here for read-only purposes (e.g. Addr()) // and for type-asserting for purposes where you know what you're doing. // // EXPERIMENTAL: Subject to change or removal. func (s *Server) Listeners() []net.Listener { return s.listeners } // PrepareRequest fills the request r for use in a Caddy HTTP handler chain. w and s can // be nil, but the handlers will lose response placeholders and access to the server. func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter, s *Server) *http.Request { // set up the context for the request ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl) ctx = context.WithValue(ctx, ServerCtxKey, s) ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any)) ctx = context.WithValue(ctx, routeGroupCtxKey, make(map[string]struct{})) var url2 url.URL // avoid letting this escape to the heap ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2)) r = r.WithContext(ctx) // once the pointer to the request won't change // anymore, finish setting up the replacer addHTTPVarsToReplacer(repl, r, w) return r } // originalRequest returns a partial, shallow copy of // req, including: req.Method, deep copy of req.URL // (into the urlCopy parameter, which should be on the // stack), req.RequestURI, and req.RemoteAddr. Notably, // headers are not copied. This function is designed to // be very fast and efficient, and useful primarily for // read-only/logging purposes. func originalRequest(req *http.Request, urlCopy *url.URL) http.Request { cloneURL(req.URL, urlCopy) return http.Request{ Method: req.Method, RemoteAddr: req.RemoteAddr, RequestURI: req.RequestURI, URL: urlCopy, } } // cloneURL makes a copy of r.URL and returns a // new value that doesn't reference the original. func cloneURL(from, to *url.URL) { *to = *from if from.User != nil { userInfo := new(url.Userinfo) *userInfo = *from.User to.User = userInfo } } // Context keys for HTTP request context values. const ( // For referencing the server instance ServerCtxKey caddy.CtxKey = "server" // For the request's variable table VarsCtxKey caddy.CtxKey = "vars" // For a partial copy of the unmodified request that // originally came into the server's entry handler OriginalRequestCtxKey caddy.CtxKey = "original_request" ) caddy-2.6.2/modules/caddyhttp/standard/000077500000000000000000000000001435007237400200465ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/standard/imports.go000066400000000000000000000022341435007237400220730ustar00rootroot00000000000000package standard import ( // standard Caddy HTTP app modules _ "github.com/caddyserver/caddy/v2/modules/caddyhttp" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/brotli" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/gzip" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/zstd" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/map" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/push" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/forwardauth" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/tracing" ) caddy-2.6.2/modules/caddyhttp/staticerror.go000066400000000000000000000065161435007237400211460ustar00rootroot00000000000000// 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 caddyhttp import ( "fmt" "net/http" "strconv" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { caddy.RegisterModule(StaticError{}) } // StaticError implements a simple handler that returns an error. // This handler returns an error value, but does not write a response. // This is useful when you want the server to act as if an error // occurred; for example, to invoke your custom error handling logic. // // Since this handler does not write a response, the error information // is for use by the server to know how to handle the error. type StaticError struct { // The error message. Optional. Default is no error message. Error string `json:"error,omitempty"` // The recommended HTTP status code. Can be either an integer or a // string if placeholders are needed. Optional. Default is 500. StatusCode WeakString `json:"status_code,omitempty"` } // CaddyModule returns the Caddy module information. func (StaticError) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.error", New: func() caddy.Module { return new(StaticError) }, } } // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // error [] | [] { // message // } // // If there is just one argument (other than the matcher), it is considered // to be a status code if it's a valid positive integer of 3 digits. func (e *StaticError) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { args := d.RemainingArgs() switch len(args) { case 1: if len(args[0]) == 3 { if num, err := strconv.Atoi(args[0]); err == nil && num > 0 { e.StatusCode = WeakString(args[0]) break } } e.Error = args[0] case 2: e.Error = args[0] e.StatusCode = WeakString(args[1]) default: return d.ArgErr() } for d.NextBlock(0) { switch d.Val() { case "message": if e.Error != "" { return d.Err("message already specified") } if !d.AllArgs(&e.Error) { return d.ArgErr() } default: return d.Errf("unrecognized subdirective '%s'", d.Val()) } } } return nil } func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) statusCode := http.StatusInternalServerError if codeStr := e.StatusCode.String(); codeStr != "" { intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, "")) if err != nil { return Error(http.StatusInternalServerError, err) } statusCode = intVal } return Error(statusCode, fmt.Errorf("%s", e.Error)) } // Interface guard var ( _ MiddlewareHandler = (*StaticError)(nil) _ caddyfile.Unmarshaler = (*StaticError)(nil) ) caddy-2.6.2/modules/caddyhttp/staticresp.go000066400000000000000000000322411435007237400207600ustar00rootroot00000000000000// 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 caddyhttp import ( "bytes" "encoding/json" "flag" "fmt" "io" "net/http" "os" "strconv" "strings" "text/template" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" caddycmd "github.com/caddyserver/caddy/v2/cmd" "go.uber.org/zap" ) func init() { caddy.RegisterModule(StaticResponse{}) caddycmd.RegisterCommand(caddycmd.Command{ Name: "respond", Func: cmdRespond, Usage: `[--status ] [--body ] [--listen ] [--access-log] [--debug] [--header "Field: value"] `, Short: "Simple, hard-coded HTTP responses for development and testing", Long: ` Spins up a quick-and-clean HTTP server for development and testing purposes. With no options specified, this command listens on a random available port and answers HTTP requests with an empty 200 response. The listen address can be customized with the --listen flag and will always be printed to stdout. If the listen address includes a port range, multiple servers will be started. If a final, unnamed argument is given, it will be treated as a status code (same as the --status flag) if it is a 3-digit number. Otherwise, it is used as the response body (same as the --body flag). The --status and --body flags will always override this argument (for example, to write a body that literally says "404" but with a status code of 200, do '--status 200 404'). A body may be given in 3 ways: a flag, a final (and unnamed) argument to the command, or piped to stdin (if flag and argument are unset). Limited template evaluation is supported on the body, with the following variables: {{.N}} The server number (useful if using a port range) {{.Port}} The listener port {{.Address}} The listener address (See the docs for the text/template package in the Go standard library for information about using templates: https://pkg.go.dev/text/template) Access/request logging and more verbose debug logging can also be enabled. Response headers may be added using the --header flag for each header field. `, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("respond", flag.ExitOnError) fs.String("listen", ":0", "The address to which to bind the listener") fs.Int("status", http.StatusOK, "The response status code") fs.String("body", "", "The body of the HTTP response") fs.Bool("access-log", false, "Enable the access log") fs.Bool("debug", false, "Enable more verbose debug-level logging") fs.Var(&respondCmdHeaders, "header", "Set a header on the response (format: \"Field: value\"") return fs }(), }) } // StaticResponse implements a simple responder for static responses. type StaticResponse struct { // The HTTP status code to respond with. Can be an integer or, // if needing to use a placeholder, a string. // // If the status code is 103 (Early Hints), the response headers // will be written to the client immediately, the body will be // ignored, and the next handler will be invoked. This behavior // is EXPERIMENTAL while RFC 8297 is a draft, and may be changed // or removed. StatusCode WeakString `json:"status_code,omitempty"` // Header fields to set on the response; overwrites any existing // header fields of the same names after normalization. Headers http.Header `json:"headers,omitempty"` // The response body. If non-empty, the Content-Type header may // be added automatically if it is not explicitly configured nor // already set on the response; the default value is // "text/plain; charset=utf-8" unless the body is a valid JSON object // or array, in which case the value will be "application/json". // Other than those common special cases the Content-Type header // should be set explicitly if it is desired because MIME sniffing // is disabled for safety. Body string `json:"body,omitempty"` // If true, the server will close the client's connection // after writing the response. Close bool `json:"close,omitempty"` // Immediately and forcefully closes the connection without // writing a response. Interrupts any other HTTP streams on // the same connection. Abort bool `json:"abort,omitempty"` } // CaddyModule returns the Caddy module information. func (StaticResponse) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.static_response", New: func() caddy.Module { return new(StaticResponse) }, } } // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // respond [] | [] { // body // close // } // // If there is just one argument (other than the matcher), it is considered // to be a status code if it's a valid positive integer of 3 digits. func (s *StaticResponse) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { args := d.RemainingArgs() switch len(args) { case 1: if len(args[0]) == 3 { if num, err := strconv.Atoi(args[0]); err == nil && num > 0 { s.StatusCode = WeakString(args[0]) break } } s.Body = args[0] case 2: s.Body = args[0] s.StatusCode = WeakString(args[1]) default: return d.ArgErr() } for d.NextBlock(0) { switch d.Val() { case "body": if s.Body != "" { return d.Err("body already specified") } if !d.AllArgs(&s.Body) { return d.ArgErr() } case "close": if s.Close { return d.Err("close already specified") } s.Close = true default: return d.Errf("unrecognized subdirective '%s'", d.Val()) } } } return nil } func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { // close the connection immediately if s.Abort { panic(http.ErrAbortHandler) } // close the connection after responding if s.Close { r.Close = true w.Header().Set("Connection", "close") } repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) // set all headers for field, vals := range s.Headers { field = repl.ReplaceAll(field, "") newVals := make([]string, len(vals)) for i := range vals { newVals[i] = repl.ReplaceAll(vals[i], "") } w.Header()[field] = newVals } // implicitly set Content-Type header if we can do so safely // (this allows templates handler to eval templates successfully // or for clients to render JSON properly which is very common) body := repl.ReplaceKnown(s.Body, "") if body != "" && w.Header().Get("Content-Type") == "" { content := strings.TrimSpace(s.Body) if len(content) > 2 && (content[0] == '{' && content[len(content)-1] == '}' || (content[0] == '[' && content[len(content)-1] == ']')) && json.Valid([]byte(content)) { w.Header().Set("Content-Type", "application/json") } else { w.Header().Set("Content-Type", "text/plain; charset=utf-8") } } // do not allow Go to sniff the content-type, for safety if w.Header().Get("Content-Type") == "" { w.Header()["Content-Type"] = nil } // get the status code; if this handler exists in an error route, // use the recommended status code as the default; otherwise 200 statusCode := http.StatusOK if reqErr, ok := r.Context().Value(ErrorCtxKey).(error); ok { if handlerErr, ok := reqErr.(HandlerError); ok { if handlerErr.StatusCode > 0 { statusCode = handlerErr.StatusCode } } } if codeStr := s.StatusCode.String(); codeStr != "" { intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, "")) if err != nil { return Error(http.StatusInternalServerError, err) } statusCode = intVal } // write headers w.WriteHeader(statusCode) // write response body if statusCode != http.StatusEarlyHints && body != "" { fmt.Fprint(w, body) } // continue handling after Early Hints as they are not the final response if statusCode == http.StatusEarlyHints { return next.ServeHTTP(w, r) } return nil } func cmdRespond(fl caddycmd.Flags) (int, error) { caddy.TrapSignals() // get flag values listen := fl.String("listen") statusCodeFl := fl.Int("status") bodyFl := fl.String("body") accessLog := fl.Bool("access-log") debug := fl.Bool("debug") arg := fl.Arg(0) if fl.NArg() > 1 { return caddy.ExitCodeFailedStartup, fmt.Errorf("too many unflagged arguments") } // prefer status and body from explicit flags statusCode, body := statusCodeFl, bodyFl // figure out if status code was explicitly specified; this lets // us set a non-zero value as the default but is a little hacky var statusCodeFlagSpecified bool for _, fl := range os.Args { if fl == "--status" { statusCodeFlagSpecified = true break } } // try to determine what kind of parameter the unnamed argument is if arg != "" { // specifying body and status flags makes the argument redundant/unused if bodyFl != "" && statusCodeFlagSpecified { return caddy.ExitCodeFailedStartup, fmt.Errorf("unflagged argument \"%s\" is overridden by flags", arg) } // if a valid 3-digit number, treat as status code; otherwise body if argInt, err := strconv.Atoi(arg); err == nil && !statusCodeFlagSpecified { if argInt >= 100 && argInt <= 999 { statusCode = argInt } } else if body == "" { body = arg } } // if we still need a body, see if stdin is being piped if body == "" { stdinInfo, err := os.Stdin.Stat() if err != nil { return caddy.ExitCodeFailedStartup, err } if stdinInfo.Mode()&os.ModeNamedPipe != 0 { bodyBytes, err := io.ReadAll(os.Stdin) if err != nil { return caddy.ExitCodeFailedStartup, err } body = string(bodyBytes) } } // build headers map hdr := make(http.Header) for i, h := range respondCmdHeaders { key, val, found := strings.Cut(h, ":") key, val = strings.TrimSpace(key), strings.TrimSpace(val) if !found || key == "" || val == "" { return caddy.ExitCodeFailedStartup, fmt.Errorf("header %d: invalid format \"%s\" (expecting \"Field: value\")", i, h) } hdr.Set(key, val) } // expand listen address, if more than one port listenAddr, err := caddy.ParseNetworkAddress(listen) if err != nil { return caddy.ExitCodeFailedStartup, err } listenAddrs := make([]string, 0, listenAddr.PortRangeSize()) for offset := uint(0); offset < listenAddr.PortRangeSize(); offset++ { listenAddrs = append(listenAddrs, listenAddr.JoinHostPort(offset)) } // build each HTTP server httpApp := App{Servers: make(map[string]*Server)} for i, addr := range listenAddrs { var handlers []json.RawMessage // response body supports a basic template; evaluate it tplCtx := struct { N int // server number Port uint // only the port Address string // listener address }{ N: i, Port: listenAddr.StartPort + uint(i), Address: addr, } tpl, err := template.New("body").Parse(body) if err != nil { return caddy.ExitCodeFailedStartup, err } buf := new(bytes.Buffer) err = tpl.Execute(buf, tplCtx) if err != nil { return caddy.ExitCodeFailedStartup, err } // create route with handler handler := StaticResponse{ StatusCode: WeakString(fmt.Sprintf("%d", statusCode)), Headers: hdr, Body: buf.String(), } handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "static_response", nil)) route := Route{HandlersRaw: handlers} server := &Server{ Listen: []string{addr}, ReadHeaderTimeout: caddy.Duration(10 * time.Second), IdleTimeout: caddy.Duration(30 * time.Second), MaxHeaderBytes: 1024 * 10, Routes: RouteList{route}, AutoHTTPS: &AutoHTTPSConfig{DisableRedir: true}, } if accessLog { server.Logs = new(ServerLogConfig) } // save server httpApp.Servers[fmt.Sprintf("static%d", i)] = server } // finish building the config var false bool cfg := &caddy.Config{ Admin: &caddy.AdminConfig{ Disabled: true, Config: &caddy.ConfigSettings{ Persist: &false, }, }, AppsRaw: caddy.ModuleMap{ "http": caddyconfig.JSON(httpApp, nil), }, } if debug { cfg.Logging = &caddy.Logging{ Logs: map[string]*caddy.CustomLog{ "default": {Level: zap.DebugLevel.CapitalString()}, }, } } // run it! err = caddy.Run(cfg) if err != nil { return caddy.ExitCodeFailedStartup, err } // to print listener addresses, get the active HTTP app loadedHTTPApp, err := caddy.ActiveContext().App("http") if err != nil { return caddy.ExitCodeFailedStartup, err } // print each listener address for _, srv := range loadedHTTPApp.(*App).Servers { for _, ln := range srv.listeners { fmt.Printf("Server address: %s\n", ln.Addr()) } } select {} } // respondCmdHeaders holds the parsed values from repeated use of the --header flag. var respondCmdHeaders caddycmd.StringSlice // Interface guards var ( _ MiddlewareHandler = (*StaticResponse)(nil) _ caddyfile.Unmarshaler = (*StaticResponse)(nil) ) caddy-2.6.2/modules/caddyhttp/staticresp_test.go000066400000000000000000000033641435007237400220230ustar00rootroot00000000000000// 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 caddyhttp import ( "context" "io" "net/http" "net/http/httptest" "strconv" "testing" "github.com/caddyserver/caddy/v2" ) func TestStaticResponseHandler(t *testing.T) { r := fakeRequest() w := httptest.NewRecorder() s := StaticResponse{ StatusCode: WeakString(strconv.Itoa(http.StatusNotFound)), Headers: http.Header{ "X-Test": []string{"Testing"}, }, Body: "Text", Close: true, } err := s.ServeHTTP(w, r, nil) if err != nil { t.Errorf("did not expect an error, but got: %v", err) } resp := w.Result() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusNotFound { t.Errorf("expected status %d but got %d", http.StatusNotFound, resp.StatusCode) } if resp.Header.Get("X-Test") != "Testing" { t.Errorf("expected x-test header to be 'testing' but was '%s'", resp.Header.Get("X-Test")) } if string(respBody) != "Text" { t.Errorf("expected body to be 'test' but was '%s'", respBody) } } func fakeRequest() *http.Request { r, _ := http.NewRequest("GET", "/", nil) repl := caddy.NewReplacer() ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl) r = r.WithContext(ctx) return r } caddy-2.6.2/modules/caddyhttp/subroute.go000066400000000000000000000051071435007237400204500ustar00rootroot00000000000000// 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 caddyhttp import ( "fmt" "net/http" "github.com/caddyserver/caddy/v2" ) func init() { caddy.RegisterModule(Subroute{}) } // Subroute implements a handler that compiles and executes routes. // This is useful for a batch of routes that all inherit the same // matchers, or for multiple routes that should be treated as a // single route. // // You can also use subroutes to handle errors from its handlers. // First the primary routes will be executed, and if they return an // error, the errors routes will be executed; in that case, an error // is only returned to the entry point at the server if there is an // additional error returned from the errors routes. type Subroute struct { // The primary list of routes to compile and execute. Routes RouteList `json:"routes,omitempty"` // If the primary routes return an error, error handling // can be promoted to this configuration instead. Errors *HTTPErrorConfig `json:"errors,omitempty"` } // CaddyModule returns the Caddy module information. func (Subroute) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.subroute", New: func() caddy.Module { return new(Subroute) }, } } // Provision sets up subrouting. func (sr *Subroute) Provision(ctx caddy.Context) error { if sr.Routes != nil { err := sr.Routes.Provision(ctx) if err != nil { return fmt.Errorf("setting up subroutes: %v", err) } if sr.Errors != nil { err := sr.Errors.Routes.Provision(ctx) if err != nil { return fmt.Errorf("setting up error subroutes: %v", err) } } } return nil } func (sr *Subroute) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { subroute := sr.Routes.Compile(next) err := subroute.ServeHTTP(w, r) if err != nil && sr.Errors != nil { r = sr.Errors.WithError(r, err) errRoute := sr.Errors.Routes.Compile(next) return errRoute.ServeHTTP(w, r) } return err } // Interface guards var ( _ caddy.Provisioner = (*Subroute)(nil) _ MiddlewareHandler = (*Subroute)(nil) ) caddy-2.6.2/modules/caddyhttp/templates/000077500000000000000000000000001435007237400202445ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/templates/caddyfile.go000066400000000000000000000030351435007237400225200ustar00rootroot00000000000000// 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 templates import ( "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { httpcaddyfile.RegisterHandlerDirective("templates", parseCaddyfile) } // parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // templates [] { // mime // between // root // } // func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { t := new(Templates) for h.Next() { for h.NextBlock(0) { switch h.Val() { case "mime": t.MIMETypes = h.RemainingArgs() if len(t.MIMETypes) == 0 { return nil, h.ArgErr() } case "between": t.Delimiters = h.RemainingArgs() if len(t.Delimiters) != 2 { return nil, h.ArgErr() } case "root": if !h.Args(&t.FileRoot) { return nil, h.ArgErr() } } } } return t, nil } caddy-2.6.2/modules/caddyhttp/templates/frontmatter.go000066400000000000000000000055011435007237400231410ustar00rootroot00000000000000package templates import ( "encoding/json" "fmt" "strings" "unicode" "github.com/BurntSushi/toml" "gopkg.in/yaml.v3" ) func extractFrontMatter(input string) (map[string]any, string, error) { // get the bounds of the first non-empty line var firstLineStart, firstLineEnd int lineEmpty := true for i, b := range input { if b == '\n' { firstLineStart = firstLineEnd if firstLineStart > 0 { firstLineStart++ // skip newline character } firstLineEnd = i if !lineEmpty { break } continue } lineEmpty = lineEmpty && unicode.IsSpace(b) } firstLine := input[firstLineStart:firstLineEnd] // ensure residue windows carriage return byte is removed firstLine = strings.TrimSpace(firstLine) // see what kind of front matter there is, if any var closingFence []string var fmParser func([]byte) (map[string]any, error) for _, fmType := range supportedFrontMatterTypes { if firstLine == fmType.FenceOpen { closingFence = fmType.FenceClose fmParser = fmType.ParseFunc } } if fmParser == nil { // no recognized front matter; whole document is body return nil, input, nil } // find end of front matter var fmEndFence string fmEndFenceStart := -1 for _, fence := range closingFence { index := strings.Index(input[firstLineEnd:], "\n"+fence) if index >= 0 { fmEndFenceStart = index fmEndFence = fence break } } if fmEndFenceStart < 0 { return nil, "", fmt.Errorf("unterminated front matter") } fmEndFenceStart += firstLineEnd + 1 // add 1 to account for newline // extract and parse front matter frontMatter := input[firstLineEnd:fmEndFenceStart] fm, err := fmParser([]byte(frontMatter)) if err != nil { return nil, "", err } // the rest is the body body := input[fmEndFenceStart+len(fmEndFence):] return fm, body, nil } func yamlFrontMatter(input []byte) (map[string]any, error) { m := make(map[string]any) err := yaml.Unmarshal(input, &m) return m, err } func tomlFrontMatter(input []byte) (map[string]any, error) { m := make(map[string]any) err := toml.Unmarshal(input, &m) return m, err } func jsonFrontMatter(input []byte) (map[string]any, error) { input = append([]byte{'{'}, input...) input = append(input, '}') m := make(map[string]any) err := json.Unmarshal(input, &m) return m, err } type parsedMarkdownDoc struct { Meta map[string]any `json:"meta,omitempty"` Body string `json:"body,omitempty"` } type frontMatterType struct { FenceOpen string FenceClose []string ParseFunc func(input []byte) (map[string]any, error) } var supportedFrontMatterTypes = []frontMatterType{ { FenceOpen: "---", FenceClose: []string{"---", "..."}, ParseFunc: yamlFrontMatter, }, { FenceOpen: "+++", FenceClose: []string{"+++"}, ParseFunc: tomlFrontMatter, }, { FenceOpen: "{", FenceClose: []string{"}"}, ParseFunc: jsonFrontMatter, }, } caddy-2.6.2/modules/caddyhttp/templates/frontmatter_fuzz.go000066400000000000000000000014231435007237400242160ustar00rootroot00000000000000// 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. //go:build gofuzz package templates func FuzzExtractFrontMatter(data []byte) int { _, _, err := extractFrontMatter(string(data)) if err != nil { return 0 } return 1 } caddy-2.6.2/modules/caddyhttp/templates/templates.go000066400000000000000000000303101435007237400225660ustar00rootroot00000000000000// 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 templates import ( "bytes" "errors" "fmt" "net/http" "strconv" "strings" "text/template" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { caddy.RegisterModule(Templates{}) } // Templates is a middleware which executes response bodies as Go templates. // The syntax is documented in the Go standard library's // [text/template package](https://golang.org/pkg/text/template/). // // ⚠️ Template functions/actions are still experimental, so they are subject to change. // // Custom template functions can be registered by creating a plugin module under the `http.handlers.templates.functions.*` namespace that implements the `CustomFunctions` interface. // // [All Sprig functions](https://masterminds.github.io/sprig/) are supported. // // In addition to the standard functions and the Sprig library, Caddy adds // extra functions and data that are available to a template: // // ##### `.Args` // // A slice of arguments passed to this page/context, for example as the result of a `include`. // // ``` // {{index .Args 0}} // first argument // ``` // // ##### `.Cookie` // // Gets the value of a cookie by name. // // ``` // {{.Cookie "cookiename"}} // ``` // // ##### `env` // // Gets an environment variable. // // ``` // {{env "VAR_NAME"}} // ``` // // ##### `placeholder` // // Gets an [placeholder variable](/docs/conventions#placeholders). // The braces (`{}`) have to be omitted. // // ``` // {{placeholder "http.request.uri.path"}} // {{placeholder "http.error.status_code"}} // ``` // // ##### `.Host` // // Returns the hostname portion (no port) of the Host header of the HTTP request. // // ``` // {{.Host}} // ``` // // ##### `httpInclude` // // Includes the contents of another file by making a virtual HTTP request (also known as a sub-request). The URI path must exist on the same virtual server because the request does not use sockets; instead, the request is crafted in memory and the handler is invoked directly for increased efficiency. // // ``` // {{httpInclude "/foo/bar?q=val"}} // ``` // // ##### `import` // // Imports the contents of another file and adds any template definitions to the template stack. If there are no defitions, the filepath will be the defition name. Any {{ define }} blocks will be accessible by {{ template }} or {{ block }}. Imports must happen before the template or block action is called // // **filename.html** // ``` // {{ define "main" }} // content // {{ end }} // ``` // // **index.html** // ``` // {{ import "/path/to/filename.html" }} // {{ template "main" }} // ``` // // ##### `include` // // Includes the contents of another file and renders in-place. Optionally can pass key-value pairs as arguments to be accessed by the included file. // // ``` // {{include "path/to/file.html"}} // no arguments // {{include "path/to/file.html" "arg1" 2 "value 3"}} // with arguments // ``` // // ##### `listFiles` // // Returns a list of the files in the given directory, which is relative to the template context's file root. // // ``` // {{listFiles "/mydir"}} // ``` // // ##### `markdown` // // Renders the given Markdown text as HTML. This uses the // [Goldmark](https://github.com/yuin/goldmark) library, // which is CommonMark compliant. It also has these plugins // enabled: Github Flavored Markdown, Footnote and syntax // highlighting provided by [Chroma](https://github.com/alecthomas/chroma). // // ``` // {{markdown "My _markdown_ text"}} // ``` // // ##### `.RemoteIP` // // Returns the client's IP address. // // ``` // {{.RemoteIP}} // ``` // // ##### `.Req` // // Accesses the current HTTP request, which has various fields, including: // // - `.Method` - the method // - `.URL` - the URL, which in turn has component fields (Scheme, Host, Path, etc.) // - `.Header` - the header fields // - `.Host` - the Host or :authority header of the request // // ``` // {{.Req.Header.Get "User-Agent"}} // ``` // // ##### `.OriginalReq` // // Like .Req, except it accesses the original HTTP request before rewrites or other internal modifications. // // ##### `.RespHeader.Add` // // Adds a header field to the HTTP response. // // ``` // {{.RespHeader.Add "Field-Name" "val"}} // ``` // // ##### `.RespHeader.Del` // // Deletes a header field on the HTTP response. // // ``` // {{.RespHeader.Del "Field-Name"}} // ``` // // ##### `.RespHeader.Set` // // Sets a header field on the HTTP response, replacing any existing value. // // ``` // {{.RespHeader.Set "Field-Name" "val"}} // ``` // // ##### `httpError` // // Returns an error with the given status code to the HTTP handler chain. // // ``` // {{if not (fileExists $includedFile)}}{{httpError 404}}{{end}} // ``` // // ##### `splitFrontMatter` // // Splits front matter out from the body. Front matter is metadata that appears at the very beginning of a file or string. Front matter can be in YAML, TOML, or JSON formats: // // **TOML** front matter starts and ends with `+++`: // // ``` // +++ // template = "blog" // title = "Blog Homepage" // sitename = "A Caddy site" // +++ // ``` // // **YAML** is surrounded by `---`: // // ``` // --- // template: blog // title: Blog Homepage // sitename: A Caddy site // --- // ``` // // // **JSON** is simply `{` and `}`: // // ``` // { // "template": "blog", // "title": "Blog Homepage", // "sitename": "A Caddy site" // } // ``` // // The resulting front matter will be made available like so: // // - `.Meta` to access the metadata fields, for example: `{{$parsed.Meta.title}}` // - `.Body` to access the body after the front matter, for example: `{{markdown $parsed.Body}}` // // // ##### `stripHTML` // // Removes HTML from a string. // // ``` // {{stripHTML "Shows only text content"}} // ``` // // ##### `humanize` // // Transforms size and time inputs to a human readable format. // This uses the [go-humanize](https://github.com/dustin/go-humanize) library. // // The first argument must be a format type, and the last argument // is the input, or the input can be piped in. The supported format // types are: // - **size** which turns an integer amount of bytes into a string like `2.3 MB` // - **time** which turns a time string into a relative time string like `2 weeks ago` // // For the `time` format, the layout for parsing the input can be configured // by appending a colon `:` followed by the desired time layout. You can // find the documentation on time layouts [in Go's docs](https://pkg.go.dev/time#pkg-constants). // The default time layout is `RFC1123Z`, i.e. `Mon, 02 Jan 2006 15:04:05 -0700`. // // ``` // {{humanize "size" "2048000"}} // {{placeholder "http.response.header.Content-Length" | humanize "size"}} // {{humanize "time" "Fri, 05 May 2022 15:04:05 +0200"}} // {{humanize "time:2006-Jan-02" "2022-May-05"}} // ``` type Templates struct { // The root path from which to load files. Required if template functions // accessing the file system are used (such as include). Default is // `{http.vars.root}` if set, or current working directory otherwise. FileRoot string `json:"file_root,omitempty"` // The MIME types for which to render templates. It is important to use // this if the route matchers do not exclude images or other binary files. // Default is text/plain, text/markdown, and text/html. MIMETypes []string `json:"mime_types,omitempty"` // The template action delimiters. If set, must be precisely two elements: // the opening and closing delimiters. Default: `["{{", "}}"]` Delimiters []string `json:"delimiters,omitempty"` customFuncs []template.FuncMap } // Customfunctions is the interface for registering custom template functions. type CustomFunctions interface { // CustomTemplateFunctions should return the mapping from custom function names to implementations. CustomTemplateFunctions() template.FuncMap } // CaddyModule returns the Caddy module information. func (Templates) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.templates", New: func() caddy.Module { return new(Templates) }, } } // Provision provisions t. func (t *Templates) Provision(ctx caddy.Context) error { fnModInfos := caddy.GetModules("http.handlers.templates.functions") customFuncs := make([]template.FuncMap, 0, len(fnModInfos)) for _, modInfo := range fnModInfos { mod := modInfo.New() fnMod, ok := mod.(CustomFunctions) if !ok { return fmt.Errorf("module %q does not satisfy the CustomFunctions interface", modInfo.ID) } customFuncs = append(customFuncs, fnMod.CustomTemplateFunctions()) } t.customFuncs = customFuncs if t.MIMETypes == nil { t.MIMETypes = defaultMIMETypes } if t.FileRoot == "" { t.FileRoot = "{http.vars.root}" } return nil } // Validate ensures t has a valid configuration. func (t *Templates) Validate() error { if len(t.Delimiters) != 0 && len(t.Delimiters) != 2 { return fmt.Errorf("delimiters must consist of exactly two elements: opening and closing") } return nil } func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { buf := bufPool.Get().(*bytes.Buffer) buf.Reset() defer bufPool.Put(buf) // shouldBuf determines whether to execute templates on this response, // since generally we will not want to execute for images or CSS, etc. shouldBuf := func(status int, header http.Header) bool { ct := header.Get("Content-Type") for _, mt := range t.MIMETypes { if strings.Contains(ct, mt) { return true } } return false } rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuf) err := next.ServeHTTP(rec, r) if err != nil { return err } if !rec.Buffered() { return nil } err = t.executeTemplate(rec, r) if err != nil { return err } rec.Header().Set("Content-Length", strconv.Itoa(buf.Len())) rec.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content rec.Header().Del("Last-Modified") // useless for dynamic content since it's always changing // we don't know a way to quickly generate etag for dynamic content, // and weak etags still cause browsers to rely on it even after a // refresh, so disable them until we find a better way to do this rec.Header().Del("Etag") return rec.WriteResponse() } // executeTemplate executes the template contained in wb.buf and replaces it with the results. func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error { var fs http.FileSystem if t.FileRoot != "" { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) fs = http.Dir(repl.ReplaceAll(t.FileRoot, ".")) } ctx := &TemplateContext{ Root: fs, Req: r, RespHeader: WrappedHeader{rr.Header()}, config: t, CustomFuncs: t.customFuncs, } err := ctx.executeTemplateInBuffer(r.URL.Path, rr.Buffer()) if err != nil { // templates may return a custom HTTP error to be propagated to the client, // otherwise for any other error we assume the template is broken var handlerErr caddyhttp.HandlerError if errors.As(err, &handlerErr) { return handlerErr } return caddyhttp.Error(http.StatusInternalServerError, err) } return nil } // virtualResponseWriter is used in virtualized HTTP requests // that templates may execute. type virtualResponseWriter struct { status int header http.Header body *bytes.Buffer } func (vrw *virtualResponseWriter) Header() http.Header { return vrw.header } func (vrw *virtualResponseWriter) WriteHeader(statusCode int) { vrw.status = statusCode } func (vrw *virtualResponseWriter) Write(data []byte) (int, error) { return vrw.body.Write(data) } var defaultMIMETypes = []string{ "text/html", "text/plain", "text/markdown", } // Interface guards var ( _ caddy.Provisioner = (*Templates)(nil) _ caddy.Validator = (*Templates)(nil) _ caddyhttp.MiddlewareHandler = (*Templates)(nil) ) caddy-2.6.2/modules/caddyhttp/templates/tplcontext.go000066400000000000000000000313321435007237400230010ustar00rootroot00000000000000// 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 templates import ( "bytes" "fmt" "io" "net" "net/http" "os" "path" "strconv" "strings" "sync" "text/template" "time" "github.com/Masterminds/sprig/v3" "github.com/alecthomas/chroma/formatters/html" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/dustin/go-humanize" "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" gmhtml "github.com/yuin/goldmark/renderer/html" ) // TemplateContext is the TemplateContext with which HTTP templates are executed. type TemplateContext struct { Root http.FileSystem Req *http.Request Args []any // defined by arguments to funcInclude RespHeader WrappedHeader CustomFuncs []template.FuncMap // functions added by plugins config *Templates tpl *template.Template } // NewTemplate returns a new template intended to be evaluated with this // context, as it is initialized with configuration from this context. func (c *TemplateContext) NewTemplate(tplName string) *template.Template { c.tpl = template.New(tplName) // customize delimiters, if applicable if c.config != nil && len(c.config.Delimiters) == 2 { c.tpl.Delims(c.config.Delimiters[0], c.config.Delimiters[1]) } // add sprig library c.tpl.Funcs(sprigFuncMap) // add all custom functions for _, funcMap := range c.CustomFuncs { c.tpl.Funcs(funcMap) } // add our own library c.tpl.Funcs(template.FuncMap{ "include": c.funcInclude, "import": c.funcImport, "httpInclude": c.funcHTTPInclude, "stripHTML": c.funcStripHTML, "markdown": c.funcMarkdown, "splitFrontMatter": c.funcSplitFrontMatter, "listFiles": c.funcListFiles, "env": c.funcEnv, "placeholder": c.funcPlaceholder, "fileExists": c.funcFileExists, "httpError": c.funcHTTPError, "humanize": c.funcHumanize, }) return c.tpl } // OriginalReq returns the original, unmodified, un-rewritten request as // it originally came in over the wire. func (c TemplateContext) OriginalReq() http.Request { or, _ := c.Req.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request) return or } // funcInclude returns the contents of filename relative to the site root and renders it in place. // Note that included files are NOT escaped, so you should only include // trusted files. If it is not trusted, be sure to use escaping functions // in your template. func (c TemplateContext) funcInclude(filename string, args ...any) (string, error) { bodyBuf := bufPool.Get().(*bytes.Buffer) bodyBuf.Reset() defer bufPool.Put(bodyBuf) err := c.readFileToBuffer(filename, bodyBuf) if err != nil { return "", err } c.Args = args err = c.executeTemplateInBuffer(filename, bodyBuf) if err != nil { return "", err } return bodyBuf.String(), nil } // readFileToBuffer reads a file into a buffer func (c TemplateContext) readFileToBuffer(filename string, bodyBuf *bytes.Buffer) error { if c.Root == nil { return fmt.Errorf("root file system not specified") } file, err := c.Root.Open(filename) if err != nil { return err } defer file.Close() _, err = io.Copy(bodyBuf, file) if err != nil { return err } return nil } // funcHTTPInclude returns the body of a virtual (lightweight) request // to the given URI on the same server. Note that included bodies // are NOT escaped, so you should only include trusted resources. // If it is not trusted, be sure to use escaping functions yourself. func (c TemplateContext) funcHTTPInclude(uri string) (string, error) { // prevent virtual request loops by counting how many levels // deep we are; and if we get too deep, return an error recursionCount := 1 if numStr := c.Req.Header.Get(recursionPreventionHeader); numStr != "" { num, err := strconv.Atoi(numStr) if err != nil { return "", fmt.Errorf("parsing %s: %v", recursionPreventionHeader, err) } if num >= 3 { return "", fmt.Errorf("virtual request cycle") } recursionCount = num + 1 } buf := bufPool.Get().(*bytes.Buffer) buf.Reset() defer bufPool.Put(buf) virtReq, err := http.NewRequest("GET", uri, nil) if err != nil { return "", err } virtReq.Host = c.Req.Host virtReq.Header = c.Req.Header.Clone() virtReq.Header.Set("Accept-Encoding", "identity") // https://github.com/caddyserver/caddy/issues/4352 virtReq.Trailer = c.Req.Trailer.Clone() virtReq.Header.Set(recursionPreventionHeader, strconv.Itoa(recursionCount)) vrw := &virtualResponseWriter{body: buf, header: make(http.Header)} server := c.Req.Context().Value(caddyhttp.ServerCtxKey).(http.Handler) server.ServeHTTP(vrw, virtReq) if vrw.status >= 400 { return "", fmt.Errorf("http %d", vrw.status) } err = c.executeTemplateInBuffer(uri, buf) if err != nil { return "", err } return buf.String(), nil } // funcImport parses the filename into the current template stack. The imported // file will be rendered within the current template by calling {{ block }} or // {{ template }} from the standard template library. If the imported file has // no {{ define }} blocks, the name of the import will be the path func (c *TemplateContext) funcImport(filename string) (string, error) { bodyBuf := bufPool.Get().(*bytes.Buffer) bodyBuf.Reset() defer bufPool.Put(bodyBuf) err := c.readFileToBuffer(filename, bodyBuf) if err != nil { return "", err } _, err = c.tpl.Parse(bodyBuf.String()) if err != nil { return "", err } return "", nil } func (c *TemplateContext) executeTemplateInBuffer(tplName string, buf *bytes.Buffer) error { c.NewTemplate(tplName) _, err := c.tpl.Parse(buf.String()) if err != nil { return err } buf.Reset() // reuse buffer for output return c.tpl.Execute(buf, c) } func (c TemplateContext) funcPlaceholder(name string) string { repl := c.Req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) value, _ := repl.GetString(name) return value } func (TemplateContext) funcEnv(varName string) string { return os.Getenv(varName) } // Cookie gets the value of a cookie with name name. func (c TemplateContext) Cookie(name string) string { cookies := c.Req.Cookies() for _, cookie := range cookies { if cookie.Name == name { return cookie.Value } } return "" } // RemoteIP gets the IP address of the client making the request. func (c TemplateContext) RemoteIP() string { ip, _, err := net.SplitHostPort(c.Req.RemoteAddr) if err != nil { return c.Req.RemoteAddr } return ip } // Host returns the hostname portion of the Host header // from the HTTP request. func (c TemplateContext) Host() (string, error) { host, _, err := net.SplitHostPort(c.Req.Host) if err != nil { if !strings.Contains(c.Req.Host, ":") { // common with sites served on the default port 80 return c.Req.Host, nil } return "", err } return host, nil } // funcStripHTML returns s without HTML tags. It is fairly naive // but works with most valid HTML inputs. func (TemplateContext) funcStripHTML(s string) string { var buf bytes.Buffer var inTag, inQuotes bool var tagStart int for i, ch := range s { if inTag { if ch == '>' && !inQuotes { inTag = false } else if ch == '<' && !inQuotes { // false start buf.WriteString(s[tagStart:i]) tagStart = i } else if ch == '"' { inQuotes = !inQuotes } continue } if ch == '<' { inTag = true tagStart = i continue } buf.WriteRune(ch) } if inTag { // false start buf.WriteString(s[tagStart:]) } return buf.String() } // funcMarkdown renders the markdown body as HTML. The resulting // HTML is NOT escaped so that it can be rendered as HTML. func (TemplateContext) funcMarkdown(input any) (string, error) { inputStr := caddy.ToString(input) md := goldmark.New( goldmark.WithExtensions( extension.GFM, extension.Footnote, highlighting.NewHighlighting( highlighting.WithFormatOptions( html.WithClasses(true), ), ), ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), goldmark.WithRendererOptions( gmhtml.WithUnsafe(), // TODO: this is not awesome, maybe should be configurable? ), ) buf := bufPool.Get().(*bytes.Buffer) buf.Reset() defer bufPool.Put(buf) err := md.Convert([]byte(inputStr), buf) if err != nil { return "", err } return buf.String(), nil } // splitFrontMatter parses front matter out from the beginning of input, // and returns the separated key-value pairs and the body/content. input // must be a "stringy" value. func (TemplateContext) funcSplitFrontMatter(input any) (parsedMarkdownDoc, error) { meta, body, err := extractFrontMatter(caddy.ToString(input)) if err != nil { return parsedMarkdownDoc{}, err } return parsedMarkdownDoc{Meta: meta, Body: body}, nil } // funcListFiles reads and returns a slice of names from the given // directory relative to the root of c. func (c TemplateContext) funcListFiles(name string) ([]string, error) { if c.Root == nil { return nil, fmt.Errorf("root file system not specified") } dir, err := c.Root.Open(path.Clean(name)) if err != nil { return nil, err } defer dir.Close() stat, err := dir.Stat() if err != nil { return nil, err } if !stat.IsDir() { return nil, fmt.Errorf("%v is not a directory", name) } dirInfo, err := dir.Readdir(0) if err != nil { return nil, err } names := make([]string, len(dirInfo)) for i, fileInfo := range dirInfo { names[i] = fileInfo.Name() } return names, nil } // funcFileExists returns true if filename can be opened successfully. func (c TemplateContext) funcFileExists(filename string) (bool, error) { if c.Root == nil { return false, fmt.Errorf("root file system not specified") } file, err := c.Root.Open(filename) if err == nil { file.Close() return true, nil } return false, nil } // funcHTTPError returns a structured HTTP handler error. EXPERIMENTAL; SUBJECT TO CHANGE. // Example usage: `{{if not (fileExists $includeFile)}}{{httpError 404}}{{end}}` func (c TemplateContext) funcHTTPError(statusCode int) (bool, error) { return false, caddyhttp.Error(statusCode, nil) } // funcHumanize transforms size and time inputs to a human readable format. // // Size inputs are expected to be integers, and are formatted as a // byte size, such as "83 MB". // // Time inputs are parsed using the given layout (default layout is RFC1123Z) // and are formatted as a relative time, such as "2 weeks ago". // See https://pkg.go.dev/time#pkg-constants for time layout docs. func (c TemplateContext) funcHumanize(formatType, data string) (string, error) { // The format type can optionally be followed // by a colon to provide arguments for the format parts := strings.Split(formatType, ":") switch parts[0] { case "size": dataint, dataerr := strconv.ParseUint(data, 10, 64) if dataerr != nil { return "", fmt.Errorf("humanize: size cannot be parsed: %s", dataerr.Error()) } return humanize.Bytes(dataint), nil case "time": timelayout := time.RFC1123Z if len(parts) > 1 { timelayout = parts[1] } dataint, dataerr := time.Parse(timelayout, data) if dataerr != nil { return "", fmt.Errorf("humanize: time cannot be parsed: %s", dataerr.Error()) } return humanize.Time(dataint), nil } return "", fmt.Errorf("no know function was given") } // WrappedHeader wraps niladic functions so that they // can be used in templates. (Template functions must // return a value.) type WrappedHeader struct{ http.Header } // Add adds a header field value, appending val to // existing values for that field. It returns an // empty string. func (h WrappedHeader) Add(field, val string) string { h.Header.Add(field, val) return "" } // Set sets a header field value, overwriting any // other values for that field. It returns an // empty string. func (h WrappedHeader) Set(field, val string) string { h.Header.Set(field, val) return "" } // Del deletes a header field. It returns an empty string. func (h WrappedHeader) Del(field string) string { h.Header.Del(field) return "" } var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } // at time of writing, sprig.FuncMap() makes a copy, thus // involves iterating the whole map, so do it just once var sprigFuncMap = sprig.TxtFuncMap() const recursionPreventionHeader = "Caddy-Templates-Include" caddy-2.6.2/modules/caddyhttp/templates/tplcontext_test.go000066400000000000000000000424631435007237400240470ustar00rootroot00000000000000// 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 templates import ( "bytes" "context" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "reflect" "sort" "strings" "testing" "time" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) type handle struct { } func (h *handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Accept-Encoding") == "identity" { w.Write([]byte("good contents")) } else { w.Write([]byte("bad cause Accept-Encoding: " + r.Header.Get("Accept-Encoding"))) } } func TestHTTPInclude(t *testing.T) { tplContext := getContextOrFail(t) for i, test := range []struct { uri string handler *handle expect string }{ { uri: "https://example.com/foo/bar", handler: &handle{}, expect: "good contents", }, } { ctx := context.WithValue(tplContext.Req.Context(), caddyhttp.ServerCtxKey, test.handler) tplContext.Req = tplContext.Req.WithContext(ctx) tplContext.Req.Header.Add("Accept-Encoding", "gzip") result, err := tplContext.funcHTTPInclude(test.uri) if result != test.expect { t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expect, result) } if err != nil { t.Errorf("Test %d: got error: %v", i, result) } } } func TestMarkdown(t *testing.T) { tplContext := getContextOrFail(t) for i, test := range []struct { body string expect string }{ { body: "- str1\n- str2\n", expect: "
    \n
  • str1
  • \n
  • str2
  • \n
\n", }, } { result, err := tplContext.funcMarkdown(test.body) if result != test.expect { t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expect, result) } if err != nil { t.Errorf("Test %d: got error: %v", i, result) } } } func TestCookie(t *testing.T) { for i, test := range []struct { cookie *http.Cookie cookieName string expect string }{ { // happy path cookie: &http.Cookie{Name: "cookieName", Value: "cookieValue"}, cookieName: "cookieName", expect: "cookieValue", }, { // try to get a non-existing cookie cookie: &http.Cookie{Name: "cookieName", Value: "cookieValue"}, cookieName: "notExisting", expect: "", }, { // partial name match cookie: &http.Cookie{Name: "cookie", Value: "cookieValue"}, cookieName: "cook", expect: "", }, { // cookie with optional fields cookie: &http.Cookie{Name: "cookie", Value: "cookieValue", Path: "/path", Domain: "https://localhost", Expires: time.Now().Add(10 * time.Minute), MaxAge: 120}, cookieName: "cookie", expect: "cookieValue", }, } { tplContext := getContextOrFail(t) tplContext.Req.AddCookie(test.cookie) actual := tplContext.Cookie(test.cookieName) if actual != test.expect { t.Errorf("Test %d: Expected cookie value '%s' but got '%s' for cookie with name '%s'", i, test.expect, actual, test.cookieName) } } } func TestImport(t *testing.T) { for i, test := range []struct { fileContent string fileName string shouldErr bool expect string }{ { // file exists, template is defined fileContent: `{{ define "imported" }}text{{end}}`, fileName: "file1", shouldErr: false, expect: `"imported"`, }, { // file does not exit fileContent: "", fileName: "", shouldErr: true, }, } { tplContext := getContextOrFail(t) var absFilePath string // create files for test case if test.fileName != "" { absFilePath := filepath.Join(fmt.Sprintf("%s", tplContext.Root), test.fileName) if err := os.WriteFile(absFilePath, []byte(test.fileContent), os.ModePerm); err != nil { os.Remove(absFilePath) t.Fatalf("Test %d: Expected no error creating file, got: '%s'", i, err.Error()) } } // perform test tplContext.NewTemplate("parent") actual, err := tplContext.funcImport(test.fileName) templateWasDefined := strings.Contains(tplContext.tpl.DefinedTemplates(), test.expect) if err != nil { if !test.shouldErr { t.Errorf("Test %d: Expected no error, got: '%s'", i, err) } } else if test.shouldErr { t.Errorf("Test %d: Expected error but had none", i) } else if !templateWasDefined && actual != "" { // template should be defined, return value should be an empty string t.Errorf("Test %d: Expected template %s to be define but got %s", i, test.expect, tplContext.tpl.DefinedTemplates()) } if absFilePath != "" { if err := os.Remove(absFilePath); err != nil && !os.IsNotExist(err) { t.Fatalf("Test %d: Expected no error removing temporary test file, got: %v", i, err) } } } } func TestNestedInclude(t *testing.T) { for i, test := range []struct { child string childFile string parent string parentFile string shouldErr bool expect string child2 string child2File string }{ { // include in parent child: `{{ include "file1" }}`, childFile: "file0", parent: `{{ $content := "file2" }}{{ $p := include $content}}`, parentFile: "file1", shouldErr: false, expect: ``, child2: `This shouldn't show`, child2File: "file2", }, } { context := getContextOrFail(t) var absFilePath string var absFilePath0 string var absFilePath1 string var buf *bytes.Buffer var err error // create files and for test case if test.parentFile != "" { absFilePath = filepath.Join(fmt.Sprintf("%s", context.Root), test.parentFile) if err := ioutil.WriteFile(absFilePath, []byte(test.parent), os.ModePerm); err != nil { os.Remove(absFilePath) t.Fatalf("Test %d: Expected no error creating file, got: '%s'", i, err.Error()) } } if test.childFile != "" { absFilePath0 = filepath.Join(fmt.Sprintf("%s", context.Root), test.childFile) if err := ioutil.WriteFile(absFilePath0, []byte(test.child), os.ModePerm); err != nil { os.Remove(absFilePath0) t.Fatalf("Test %d: Expected no error creating file, got: '%s'", i, err.Error()) } } if test.child2File != "" { absFilePath1 = filepath.Join(fmt.Sprintf("%s", context.Root), test.child2File) if err := ioutil.WriteFile(absFilePath1, []byte(test.child2), os.ModePerm); err != nil { os.Remove(absFilePath0) t.Fatalf("Test %d: Expected no error creating file, got: '%s'", i, err.Error()) } } buf = bufPool.Get().(*bytes.Buffer) buf.Reset() defer bufPool.Put(buf) buf.WriteString(test.child) err = context.executeTemplateInBuffer(test.childFile, buf) if err != nil { if !test.shouldErr { t.Errorf("Test %d: Expected no error, got: '%s'", i, err) } } else if test.shouldErr { t.Errorf("Test %d: Expected error but had none", i) } else if buf.String() != test.expect { // t.Errorf("Test %d: Expected '%s' but got '%s'", i, test.expect, buf.String()) } if absFilePath != "" { if err := os.Remove(absFilePath); err != nil && !os.IsNotExist(err) { t.Fatalf("Test %d: Expected no error removing temporary test file, got: %v", i, err) } } if absFilePath0 != "" { if err := os.Remove(absFilePath0); err != nil && !os.IsNotExist(err) { t.Fatalf("Test %d: Expected no error removing temporary test file, got: %v", i, err) } } if absFilePath1 != "" { if err := os.Remove(absFilePath1); err != nil && !os.IsNotExist(err) { t.Fatalf("Test %d: Expected no error removing temporary test file, got: %v", i, err) } } } } func TestInclude(t *testing.T) { for i, test := range []struct { fileContent string fileName string shouldErr bool expect string args string }{ { // file exists, content is text only fileContent: "text", fileName: "file1", shouldErr: false, expect: "text", }, { // file exists, content is template fileContent: "{{ if . }}text{{ end }}", fileName: "file1", shouldErr: false, expect: "text", }, { // file does not exit fileContent: "", fileName: "", shouldErr: true, }, { // args fileContent: "{{ index .Args 0 }}", fileName: "file1", shouldErr: false, args: "text", expect: "text", }, { // args, reference arg out of range fileContent: "{{ index .Args 1 }}", fileName: "file1", shouldErr: true, args: "text", }, } { tplContext := getContextOrFail(t) var absFilePath string // create files for test case if test.fileName != "" { absFilePath := filepath.Join(fmt.Sprintf("%s", tplContext.Root), test.fileName) if err := os.WriteFile(absFilePath, []byte(test.fileContent), os.ModePerm); err != nil { os.Remove(absFilePath) t.Fatalf("Test %d: Expected no error creating file, got: '%s'", i, err.Error()) } } // perform test actual, err := tplContext.funcInclude(test.fileName, test.args) if err != nil { if !test.shouldErr { t.Errorf("Test %d: Expected no error, got: '%s'", i, err) } } else if test.shouldErr { t.Errorf("Test %d: Expected error but had none", i) } else if actual != test.expect { t.Errorf("Test %d: Expected %s but got %s", i, test.expect, actual) } if absFilePath != "" { if err := os.Remove(absFilePath); err != nil && !os.IsNotExist(err) { t.Fatalf("Test %d: Expected no error removing temporary test file, got: %v", i, err) } } } } func TestCookieMultipleCookies(t *testing.T) { tplContext := getContextOrFail(t) cookieNameBase, cookieValueBase := "cookieName", "cookieValue" for i := 0; i < 10; i++ { tplContext.Req.AddCookie(&http.Cookie{ Name: fmt.Sprintf("%s%d", cookieNameBase, i), Value: fmt.Sprintf("%s%d", cookieValueBase, i), }) } for i := 0; i < 10; i++ { expectedCookieVal := fmt.Sprintf("%s%d", cookieValueBase, i) actualCookieVal := tplContext.Cookie(fmt.Sprintf("%s%d", cookieNameBase, i)) if actualCookieVal != expectedCookieVal { t.Errorf("Expected cookie value %s, found %s", expectedCookieVal, actualCookieVal) } } } func TestIP(t *testing.T) { tplContext := getContextOrFail(t) for i, test := range []struct { inputRemoteAddr string expect string }{ {"1.1.1.1:1111", "1.1.1.1"}, {"1.1.1.1", "1.1.1.1"}, {"[::1]:11", "::1"}, {"[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]"}, {`[fe80:1::3%eth0]:44`, `fe80:1::3%eth0`}, } { tplContext.Req.RemoteAddr = test.inputRemoteAddr if actual := tplContext.RemoteIP(); actual != test.expect { t.Errorf("Test %d: Expected %s but got %s", i, test.expect, actual) } } } func TestStripHTML(t *testing.T) { tplContext := getContextOrFail(t) for i, test := range []struct { input string expect string }{ { // no tags input: `h1`, expect: `h1`, }, { // happy path input: `

h1

`, expect: `h1`, }, { // tag in quotes input: `">h1`, expect: `h1`, }, { // multiple tags input: `

h1

`, expect: `h1`, }, { // tags not closed input: `hi`, expect: ` 0 && !reflect.DeepEqual(test.fileNames, actual) { t.Errorf("Test %d: Expected files %v, got: %v", i, test.fileNames, actual) } } } if dirPath != "" { if err := os.RemoveAll(dirPath); err != nil && !os.IsNotExist(err) { t.Fatalf("Test %d: Expected no error removing temporary test directory, got: %v", i, err) } } } } func TestSplitFrontMatter(t *testing.T) { tplContext := getContextOrFail(t) for i, test := range []struct { input string expect string body string }{ { // yaml with windows newline input: "---\r\ntitle: Welcome\r\n---\r\n# Test\\r\\n", expect: `Welcome`, body: "\r\n# Test\\r\\n", }, { // yaml input: `--- title: Welcome --- ### Test`, expect: `Welcome`, body: "\n### Test", }, { // yaml with dots for closer input: `--- title: Welcome ... ### Test`, expect: `Welcome`, body: "\n### Test", }, { // yaml with non-fence '...' line after closing fence (i.e. first matching closing fence should be used) input: `--- title: Welcome --- ### Test ... yeah`, expect: `Welcome`, body: "\n### Test\n...\nyeah", }, { // toml input: `+++ title = "Welcome" +++ ### Test`, expect: `Welcome`, body: "\n### Test", }, { // json input: `{ "title": "Welcome" } ### Test`, expect: `Welcome`, body: "\n### Test", }, } { result, _ := tplContext.funcSplitFrontMatter(test.input) if result.Meta["title"] != test.expect { t.Errorf("Test %d: Expected %s, found %s. Input was SplitFrontMatter(%s)", i, test.expect, result.Meta["title"], test.input) } if result.Body != test.body { t.Errorf("Test %d: Expected body %s, found %s. Input was SplitFrontMatter(%s)", i, test.body, result.Body, test.input) } } } func TestHumanize(t *testing.T) { tplContext := getContextOrFail(t) for i, test := range []struct { format string inputData string expect string errorCase bool verifyErr func(actual_string, substring string) bool }{ { format: "size", inputData: "2048000", expect: "2.0 MB", errorCase: false, verifyErr: strings.Contains, }, { format: "time", inputData: "Fri, 05 May 2022 15:04:05 +0200", expect: "ago", errorCase: false, verifyErr: strings.HasSuffix, }, { format: "time:2006-Jan-02", inputData: "2022-May-05", expect: "ago", errorCase: false, verifyErr: strings.HasSuffix, }, { format: "time", inputData: "Fri, 05 May 2022 15:04:05 GMT+0200", expect: "error:", errorCase: true, verifyErr: strings.HasPrefix, }, } { if actual, err := tplContext.funcHumanize(test.format, test.inputData); !test.verifyErr(actual, test.expect) { if !test.errorCase { t.Errorf("Test %d: Expected '%s' but got '%s'", i, test.expect, actual) if err != nil { t.Errorf("Test %d: error: %s", i, err.Error()) } } } } } func getContextOrFail(t *testing.T) TemplateContext { tplContext, err := initTestContext() t.Cleanup(func() { os.RemoveAll(string(tplContext.Root.(http.Dir))) }) if err != nil { t.Fatalf("failed to prepare test context: %v", err) } return tplContext } func initTestContext() (TemplateContext, error) { body := bytes.NewBufferString("request body") request, err := http.NewRequest("GET", "https://example.com/foo/bar", body) if err != nil { return TemplateContext{}, err } tmpDir, err := os.MkdirTemp(os.TempDir(), "caddy") if err != nil { return TemplateContext{}, err } return TemplateContext{ Root: http.Dir(tmpDir), Req: request, RespHeader: WrappedHeader{make(http.Header)}, }, nil } caddy-2.6.2/modules/caddyhttp/tracing/000077500000000000000000000000001435007237400176755ustar00rootroot00000000000000caddy-2.6.2/modules/caddyhttp/tracing/module.go000066400000000000000000000064371435007237400215230ustar00rootroot00000000000000package tracing import ( "fmt" "net/http" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap" ) func init() { caddy.RegisterModule(Tracing{}) httpcaddyfile.RegisterHandlerDirective("tracing", parseCaddyfile) } // Tracing implements an HTTP handler that adds support for distributed tracing, // using OpenTelemetry. This module is responsible for the injection and // propagation of the trace context. Configure this module via environment // variables (see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md). // Some values can be overwritten in the configuration file. type Tracing struct { // SpanName is a span name. It should follow the naming guidelines here: // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#span SpanName string `json:"span"` // otel implements opentelemetry related logic. otel openTelemetryWrapper logger *zap.Logger } // CaddyModule returns the Caddy module information. func (Tracing) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.tracing", New: func() caddy.Module { return new(Tracing) }, } } // Provision implements caddy.Provisioner. func (ot *Tracing) Provision(ctx caddy.Context) error { ot.logger = ctx.Logger() var err error ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName) return err } // Cleanup implements caddy.CleanerUpper and closes any idle connections. It // calls Shutdown method for a trace provider https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#shutdown. func (ot *Tracing) Cleanup() error { if err := ot.otel.cleanup(ot.logger); err != nil { return fmt.Errorf("tracerProvider shutdown: %w", err) } return nil } // ServeHTTP implements caddyhttp.MiddlewareHandler. func (ot *Tracing) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { return ot.otel.ServeHTTP(w, r, next) } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: // // tracing { // [span ] // } func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { setParameter := func(d *caddyfile.Dispenser, val *string) error { if d.NextArg() { *val = d.Val() } else { return d.ArgErr() } if d.NextArg() { return d.ArgErr() } return nil } // paramsMap is a mapping between "string" parameter from the Caddyfile and its destination within the module paramsMap := map[string]*string{ "span": &ot.SpanName, } for d.Next() { args := d.RemainingArgs() if len(args) > 0 { return d.ArgErr() } for d.NextBlock(0) { if dst, ok := paramsMap[d.Val()]; ok { if err := setParameter(d, dst); err != nil { return err } } else { return d.ArgErr() } } } return nil } func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { var m Tracing err := m.UnmarshalCaddyfile(h.Dispenser) return &m, err } // Interface guards var ( _ caddy.Provisioner = (*Tracing)(nil) _ caddyhttp.MiddlewareHandler = (*Tracing)(nil) _ caddyfile.Unmarshaler = (*Tracing)(nil) ) caddy-2.6.2/modules/caddyhttp/tracing/module_test.go000066400000000000000000000103121435007237400225450ustar00rootroot00000000000000package tracing import ( "context" "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func TestTracing_UnmarshalCaddyfile(t *testing.T) { tests := []struct { name string spanName string d *caddyfile.Dispenser wantErr bool }{ { name: "Full config", spanName: "my-span", d: caddyfile.NewTestDispenser(` tracing { span my-span }`), wantErr: false, }, { name: "Only span name in the config", spanName: "my-span", d: caddyfile.NewTestDispenser(` tracing { span my-span }`), wantErr: false, }, { name: "Empty config", d: caddyfile.NewTestDispenser(` tracing { }`), wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ot := &Tracing{} if err := ot.UnmarshalCaddyfile(tt.d); (err != nil) != tt.wantErr { t.Errorf("UnmarshalCaddyfile() error = %v, wantErrType %v", err, tt.wantErr) } if ot.SpanName != tt.spanName { t.Errorf("UnmarshalCaddyfile() SpanName = %v, want SpanName %v", ot.SpanName, tt.spanName) } }) } } func TestTracing_UnmarshalCaddyfile_Error(t *testing.T) { tests := []struct { name string d *caddyfile.Dispenser wantErr bool }{ { name: "Unknown parameter", d: caddyfile.NewTestDispenser(` tracing { foo bar }`), wantErr: true, }, { name: "Missed argument", d: caddyfile.NewTestDispenser(` tracing { span }`), wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ot := &Tracing{} if err := ot.UnmarshalCaddyfile(tt.d); (err != nil) != tt.wantErr { t.Errorf("UnmarshalCaddyfile() error = %v, wantErrType %v", err, tt.wantErr) } }) } } func TestTracing_ServeHTTP_Propagation_Without_Initial_Headers(t *testing.T) { ot := &Tracing{ SpanName: "mySpan", } req := httptest.NewRequest("GET", "https://example.com/foo", nil) w := httptest.NewRecorder() var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error { traceparent := request.Header.Get("Traceparent") if traceparent == "" || strings.HasPrefix(traceparent, "00-00000000000000000000000000000000-0000000000000000") { t.Errorf("Invalid traceparent: %v", traceparent) } return nil } ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) defer cancel() if err := ot.Provision(ctx); err != nil { t.Errorf("Provision error: %v", err) t.FailNow() } if err := ot.ServeHTTP(w, req, handler); err != nil { t.Errorf("ServeHTTP error: %v", err) } } func TestTracing_ServeHTTP_Propagation_With_Initial_Headers(t *testing.T) { ot := &Tracing{ SpanName: "mySpan", } req := httptest.NewRequest("GET", "https://example.com/foo", nil) req.Header.Set("traceparent", "00-11111111111111111111111111111111-1111111111111111-01") w := httptest.NewRecorder() var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error { traceparent := request.Header.Get("Traceparent") if !strings.HasPrefix(traceparent, "00-11111111111111111111111111111111") { t.Errorf("Invalid traceparent: %v", traceparent) } return nil } ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) defer cancel() if err := ot.Provision(ctx); err != nil { t.Errorf("Provision error: %v", err) t.FailNow() } if err := ot.ServeHTTP(w, req, handler); err != nil { t.Errorf("ServeHTTP error: %v", err) } } func TestTracing_ServeHTTP_Next_Error(t *testing.T) { ot := &Tracing{ SpanName: "mySpan", } req := httptest.NewRequest("GET", "https://example.com/foo", nil) w := httptest.NewRecorder() expectErr := errors.New("test error") var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error { return expectErr } ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) defer cancel() if err := ot.Provision(ctx); err != nil { t.Errorf("Provision error: %v", err) t.FailNow() } if err := ot.ServeHTTP(w, req, handler); err == nil || !errors.Is(err, expectErr) { t.Errorf("expected error, got: %v", err) } } caddy-2.6.2/modules/caddyhttp/tracing/tracer.go000066400000000000000000000063571435007237400215170ustar00rootroot00000000000000package tracing import ( "context" "fmt" "net/http" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.7.0" "go.uber.org/zap" ) const ( webEngineName = "Caddy" defaultSpanName = "handler" nextCallCtxKey caddy.CtxKey = "nextCall" ) // nextCall store the next handler, and the error value return on calling it (if any) type nextCall struct { next caddyhttp.Handler err error } // openTelemetryWrapper is responsible for the tracing injection, extraction and propagation. type openTelemetryWrapper struct { propagators propagation.TextMapPropagator handler http.Handler spanName string } // newOpenTelemetryWrapper is responsible for the openTelemetryWrapper initialization using provided configuration. func newOpenTelemetryWrapper( ctx context.Context, spanName string, ) (openTelemetryWrapper, error) { if spanName == "" { spanName = defaultSpanName } ot := openTelemetryWrapper{ spanName: spanName, } version, _ := caddy.Version() res, err := ot.newResource(webEngineName, version) if err != nil { return ot, fmt.Errorf("creating resource error: %w", err) } traceExporter, err := otlptracegrpc.New(ctx) if err != nil { return ot, fmt.Errorf("creating trace exporter error: %w", err) } ot.propagators = propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) tracerProvider := globalTracerProvider.getTracerProvider( sdktrace.WithBatcher(traceExporter), sdktrace.WithResource(res), ) ot.handler = otelhttp.NewHandler(http.HandlerFunc(ot.serveHTTP), ot.spanName, otelhttp.WithTracerProvider(tracerProvider), otelhttp.WithPropagators(ot.propagators)) return ot, nil } // serveHTTP injects a tracing context and call the next handler. func (ot *openTelemetryWrapper) serveHTTP(w http.ResponseWriter, r *http.Request) { ot.propagators.Inject(r.Context(), propagation.HeaderCarrier(r.Header)) next := r.Context().Value(nextCallCtxKey).(*nextCall) next.err = next.next.ServeHTTP(w, r) } // ServeHTTP propagates call to the by wrapped by `otelhttp` next handler. func (ot *openTelemetryWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { n := &nextCall{ next: next, err: nil, } ot.handler.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), nextCallCtxKey, n))) return n.err } // cleanup flush all remaining data and shutdown a tracerProvider func (ot *openTelemetryWrapper) cleanup(logger *zap.Logger) error { return globalTracerProvider.cleanupTracerProvider(logger) } // newResource creates a resource that describe current handler instance and merge it with a default attributes value. func (ot *openTelemetryWrapper) newResource( webEngineName, webEngineVersion string, ) (*resource.Resource, error) { return resource.Merge(resource.Default(), resource.NewSchemaless( semconv.WebEngineNameKey.String(webEngineName), semconv.WebEngineVersionKey.String(webEngineVersion), )) } caddy-2.6.2/modules/caddyhttp/tracing/tracer_test.go000066400000000000000000000010101435007237400225330ustar00rootroot00000000000000package tracing import ( "context" "testing" "github.com/caddyserver/caddy/v2" ) func TestOpenTelemetryWrapper_newOpenTelemetryWrapper(t *testing.T) { ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) defer cancel() var otw openTelemetryWrapper var err error if otw, err = newOpenTelemetryWrapper(ctx, "", ); err != nil { t.Errorf("newOpenTelemetryWrapper() error = %v", err) t.FailNow() } if otw.propagators == nil { t.Errorf("Propagators should not be empty") } } caddy-2.6.2/modules/caddyhttp/tracing/tracerprovider.go000066400000000000000000000033601435007237400232610ustar00rootroot00000000000000package tracing import ( "context" "fmt" "sync" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.uber.org/zap" ) // globalTracerProvider stores global tracer provider and is responsible for graceful shutdown when nobody is using it. var globalTracerProvider = &tracerProvider{} type tracerProvider struct { mu sync.Mutex tracerProvider *sdktrace.TracerProvider tracerProvidersCounter int } // getTracerProvider create or return an existing global TracerProvider func (t *tracerProvider) getTracerProvider(opts ...sdktrace.TracerProviderOption) *sdktrace.TracerProvider { t.mu.Lock() defer t.mu.Unlock() t.tracerProvidersCounter++ if t.tracerProvider == nil { t.tracerProvider = sdktrace.NewTracerProvider( opts..., ) } return t.tracerProvider } // cleanupTracerProvider gracefully shutdown a TracerProvider func (t *tracerProvider) cleanupTracerProvider(logger *zap.Logger) error { t.mu.Lock() defer t.mu.Unlock() if t.tracerProvidersCounter > 0 { t.tracerProvidersCounter-- } if t.tracerProvidersCounter == 0 { if t.tracerProvider != nil { // tracerProvider.ForceFlush SHOULD be invoked according to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#forceflush if err := t.tracerProvider.ForceFlush(context.Background()); err != nil { logger.Error("forcing flush", zap.Error(err)) } // tracerProvider.Shutdown MUST be invoked according to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#shutdown if err := t.tracerProvider.Shutdown(context.Background()); err != nil { return fmt.Errorf("tracerProvider shutdown error: %w", err) } } t.tracerProvider = nil } return nil } caddy-2.6.2/modules/caddyhttp/tracing/tracerprovider_test.go000066400000000000000000000014521435007237400243200ustar00rootroot00000000000000package tracing import ( "testing" "go.uber.org/zap" ) func Test_tracersProvider_getTracerProvider(t *testing.T) { tp := tracerProvider{} tp.getTracerProvider() tp.getTracerProvider() if tp.tracerProvider == nil { t.Errorf("There should be tracer provider") } if tp.tracerProvidersCounter != 2 { t.Errorf("Tracer providers counter should equal to 2") } } func Test_tracersProvider_cleanupTracerProvider(t *testing.T) { tp := tracerProvider{} tp.getTracerProvider() tp.getTracerProvider() err := tp.cleanupTracerProvider(zap.NewNop()) if err != nil { t.Errorf("There should be no error: %v", err) } if tp.tracerProvider == nil { t.Errorf("There should be tracer provider") } if tp.tracerProvidersCounter != 1 { t.Errorf("Tracer providers counter should equal to 1") } } caddy-2.6.2/modules/caddyhttp/vars.go000066400000000000000000000210031435007237400175440ustar00rootroot00000000000000// 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 caddyhttp import ( "context" "fmt" "net/http" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { caddy.RegisterModule(VarsMiddleware{}) caddy.RegisterModule(VarsMatcher{}) caddy.RegisterModule(MatchVarsRE{}) } // VarsMiddleware is an HTTP middleware which sets variables to // have values that can be used in the HTTP request handler // chain. The primary way to access variables is with placeholders, // which have the form: `{http.vars.variable_name}`, or with // the `vars` and `vars_regexp` request matchers. // // The key is the variable name, and the value is the value of the // variable. Both the name and value may use or contain placeholders. type VarsMiddleware map[string]any // CaddyModule returns the Caddy module information. func (VarsMiddleware) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.vars", New: func() caddy.Module { return new(VarsMiddleware) }, } } func (m VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { vars := r.Context().Value(VarsCtxKey).(map[string]any) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) for k, v := range m { keyExpanded := repl.ReplaceAll(k, "") if valStr, ok := v.(string); ok { v = repl.ReplaceAll(valStr, "") } vars[keyExpanded] = v } return next.ServeHTTP(w, r) } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. Syntax: // // vars [ ] { // // ... // } func (m *VarsMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(VarsMiddleware) } nextVar := func(headerLine bool) error { if headerLine { // header line is optional if !d.NextArg() { return nil } } varName := d.Val() if !d.NextArg() { return d.ArgErr() } varValue := d.ScalarVal() (*m)[varName] = varValue if d.NextArg() { return d.ArgErr() } return nil } for d.Next() { if err := nextVar(true); err != nil { return err } for nesting := d.Nesting(); d.NextBlock(nesting); { if err := nextVar(false); err != nil { return err } } } return nil } // VarsMatcher is an HTTP request matcher which can match // requests based on variables in the context or placeholder // values. The key is the placeholder or name of the variable, // and the values are possible values the variable can be in // order to match (logical OR'ed). // // If the key is surrounded by `{ }` it is assumed to be a // placeholder. Otherwise, it will be considered a variable // name. // // Placeholders in the keys are not expanded, but // placeholders in the values are. type VarsMatcher map[string][]string // CaddyModule returns the Caddy module information. func (VarsMatcher) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.vars", New: func() caddy.Module { return new(VarsMatcher) }, } } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *VarsMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(map[string][]string) } for d.Next() { var field string if !d.Args(&field) { return d.Errf("malformed vars matcher: expected field name") } vals := d.RemainingArgs() if len(vals) == 0 { return d.Errf("malformed vars matcher: expected at least one value to match against") } (*m)[field] = append((*m)[field], vals...) if d.NextBlock(0) { return d.Err("malformed vars matcher: blocks are not supported") } } return nil } // Match matches a request based on variables in the context, // or placeholders if the key is not a variable. func (m VarsMatcher) Match(r *http.Request) bool { if len(m) == 0 { return true } vars := r.Context().Value(VarsCtxKey).(map[string]any) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) for key, vals := range m { var varValue any if strings.HasPrefix(key, "{") && strings.HasSuffix(key, "}") && strings.Count(key, "{") == 1 { varValue, _ = repl.Get(strings.Trim(key, "{}")) } else { varValue = vars[key] } // see if any of the values given in the matcher match the actual value for _, v := range vals { matcherValExpanded := repl.ReplaceAll(v, "") var varStr string switch vv := varValue.(type) { case string: varStr = vv case fmt.Stringer: varStr = vv.String() case error: varStr = vv.Error() default: varStr = fmt.Sprintf("%v", vv) } if varStr == matcherValExpanded { return true } } } return false } // MatchVarsRE matches the value of the context variables by a given regular expression. // // Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}` // where `name` is the regular expression's name, and `capture_group` is either // the named or positional capture group from the expression itself. If no name // is given, then the placeholder omits the name: `{http.regexp.capture_group}` // (potentially leading to collisions). type MatchVarsRE map[string]*MatchRegexp // CaddyModule returns the Caddy module information. func (MatchVarsRE) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.vars_regexp", New: func() caddy.Module { return new(MatchVarsRE) }, } } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchVarsRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(map[string]*MatchRegexp) } for d.Next() { var first, second, third string if !d.Args(&first, &second) { return d.ArgErr() } var name, field, val string if d.Args(&third) { name = first field = second val = third } else { field = first val = second } (*m)[field] = &MatchRegexp{Pattern: val, Name: name} if d.NextBlock(0) { return d.Err("malformed vars_regexp matcher: blocks are not supported") } } return nil } // Provision compiles m's regular expressions. func (m MatchVarsRE) Provision(ctx caddy.Context) error { for _, rm := range m { err := rm.Provision(ctx) if err != nil { return err } } return nil } // Match returns true if r matches m. func (m MatchVarsRE) Match(r *http.Request) bool { vars := r.Context().Value(VarsCtxKey).(map[string]any) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) for k, rm := range m { var varStr string switch vv := vars[k].(type) { case string: varStr = vv case fmt.Stringer: varStr = vv.String() case error: varStr = vv.Error() default: varStr = fmt.Sprintf("%v", vv) } valExpanded := repl.ReplaceAll(varStr, "") if match := rm.Match(valExpanded, repl); match { return match } replacedVal := repl.ReplaceAll(k, "") if match := rm.Match(replacedVal, repl); match { return match } } return false } // Validate validates m's regular expressions. func (m MatchVarsRE) Validate() error { for _, rm := range m { err := rm.Validate() if err != nil { return err } } return nil } // GetVar gets a value out of the context's variable table by key. // If the key does not exist, the return value will be nil. func GetVar(ctx context.Context, key string) any { varMap, ok := ctx.Value(VarsCtxKey).(map[string]any) if !ok { return nil } return varMap[key] } // SetVar sets a value in the context's variable table with // the given key. It overwrites any previous value with the // same key. // // If the value is nil (note: non-nil interface with nil // underlying value does not count) and the key exists in // the table, the key+value will be deleted from the table. func SetVar(ctx context.Context, key string, value any) { varMap, ok := ctx.Value(VarsCtxKey).(map[string]any) if !ok { return } if value == nil { if _, ok := varMap[key]; ok { delete(varMap, key) return } } varMap[key] = value } // Interface guards var ( _ MiddlewareHandler = (*VarsMiddleware)(nil) _ caddyfile.Unmarshaler = (*VarsMiddleware)(nil) _ RequestMatcher = (*VarsMatcher)(nil) _ caddyfile.Unmarshaler = (*VarsMatcher)(nil) ) caddy-2.6.2/modules/caddypki/000077500000000000000000000000001435007237400160525ustar00rootroot00000000000000caddy-2.6.2/modules/caddypki/acmeserver/000077500000000000000000000000001435007237400202065ustar00rootroot00000000000000caddy-2.6.2/modules/caddypki/acmeserver/acmeserver.go000066400000000000000000000151631435007237400226770ustar00rootroot00000000000000// 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 acmeserver import ( "fmt" "net/http" "os" "path/filepath" "regexp" "strings" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/go-chi/chi" "github.com/smallstep/certificates/acme" acmeAPI "github.com/smallstep/certificates/acme/api" acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" "go.uber.org/zap" ) func init() { caddy.RegisterModule(Handler{}) } // Handler is an ACME server handler. type Handler struct { // The ID of the CA to use for signing. This refers to // the ID given to the CA in the `pki` app. If omitted, // the default ID is "local". CA string `json:"ca,omitempty"` // The hostname or IP address by which ACME clients // will access the server. This is used to populate // the ACME directory endpoint. If not set, the Host // header of the request will be used. // COMPATIBILITY NOTE / TODO: This property may go away in the // future. Do not rely on this property long-term; check release notes. Host string `json:"host,omitempty"` // The path prefix under which to serve all ACME // endpoints. All other requests will not be served // by this handler and will be passed through to // the next one. Default: "/acme/". // COMPATIBILITY NOTE / TODO: This property may go away in the // future, as it is currently only required due to // limitations in the underlying library. Do not rely // on this property long-term; check release notes. PathPrefix string `json:"path_prefix,omitempty"` // If true, the CA's root will be the issuer instead of // the intermediate. This is NOT recommended and should // only be used when devices/clients do not properly // validate certificate chains. EXPERIMENTAL: Might be // changed or removed in the future. SignWithRoot bool `json:"sign_with_root,omitempty"` acmeEndpoints http.Handler logger *zap.Logger } // CaddyModule returns the Caddy module information. func (Handler) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.acme_server", New: func() caddy.Module { return new(Handler) }, } } // Provision sets up the ACME server handler. func (ash *Handler) Provision(ctx caddy.Context) error { ash.logger = ctx.Logger() // set some defaults if ash.CA == "" { ash.CA = caddypki.DefaultCAID } if ash.PathPrefix == "" { ash.PathPrefix = defaultPathPrefix } // get a reference to the configured CA appModule, err := ctx.App("pki") if err != nil { return err } pkiApp := appModule.(*caddypki.PKI) ca, err := pkiApp.GetCA(ctx, ash.CA) if err != nil { return err } database, err := ash.openDatabase() if err != nil { return err } authorityConfig := caddypki.AuthorityConfig{ SignWithRoot: ash.SignWithRoot, AuthConfig: &authority.AuthConfig{ Provisioners: provisioner.List{ &provisioner.ACME{ Name: ash.CA, Type: provisioner.TypeACME.String(), Claims: &provisioner.Claims{ MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour * 365}, DefaultTLSDur: &provisioner.Duration{Duration: 12 * time.Hour}, }, }, }, }, DB: database, } auth, err := ca.NewAuthority(authorityConfig) if err != nil { return err } var acmeDB acme.DB if authorityConfig.DB != nil { acmeDB, err = acmeNoSQL.New(auth.GetDatabase().(nosql.DB)) if err != nil { return fmt.Errorf("configuring ACME DB: %v", err) } } // create the router for the ACME endpoints acmeRouterHandler := acmeAPI.NewHandler(acmeAPI.HandlerOptions{ CA: auth, DB: acmeDB, // stores all the server state DNS: ash.Host, // used for directory links Prefix: strings.Trim(ash.PathPrefix, "/"), // used for directory links }) // extract its http.Handler so we can use it directly r := chi.NewRouter() r.Route(ash.PathPrefix, func(r chi.Router) { acmeRouterHandler.Route(r) }) ash.acmeEndpoints = r return nil } func (ash Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { if strings.HasPrefix(r.URL.Path, ash.PathPrefix) { ash.acmeEndpoints.ServeHTTP(w, r) return nil } return next.ServeHTTP(w, r) } func (ash Handler) getDatabaseKey() string { key := ash.CA key = strings.ToLower(key) key = strings.TrimSpace(key) return keyCleaner.ReplaceAllLiteralString(key, "") } // Cleanup implements caddy.CleanerUpper and closes any idle databases. func (ash Handler) Cleanup() error { key := ash.getDatabaseKey() deleted, err := databasePool.Delete(key) if deleted { ash.logger.Debug("unloading unused CA database", zap.String("db_key", key)) } if err != nil { ash.logger.Error("closing CA database", zap.String("db_key", key), zap.Error(err)) } return err } func (ash Handler) openDatabase() (*db.AuthDB, error) { key := ash.getDatabaseKey() database, loaded, err := databasePool.LoadOrNew(key, func() (caddy.Destructor, error) { dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server", key) dbPath := filepath.Join(dbFolder, "db") err := os.MkdirAll(dbFolder, 0755) if err != nil { return nil, fmt.Errorf("making folder for CA database: %v", err) } dbConfig := &db.Config{ Type: "bbolt", DataSource: dbPath, } database, err := db.New(dbConfig) return databaseCloser{&database}, err }) if loaded { ash.logger.Debug("loaded preexisting CA database", zap.String("db_key", key)) } return database.(databaseCloser).DB, err } const defaultPathPrefix = "/acme/" var keyCleaner = regexp.MustCompile(`[^\w.-_]`) var databasePool = caddy.NewUsagePool() type databaseCloser struct { DB *db.AuthDB } func (closer databaseCloser) Destruct() error { return (*closer.DB).Shutdown() } // Interface guards var ( _ caddyhttp.MiddlewareHandler = (*Handler)(nil) _ caddy.Provisioner = (*Handler)(nil) ) caddy-2.6.2/modules/caddypki/acmeserver/caddyfile.go000066400000000000000000000032571435007237400224700ustar00rootroot00000000000000// 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 acmeserver import ( "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddypki" ) func init() { httpcaddyfile.RegisterDirective("acme_server", parseACMEServer) } // parseACMEServer sets up an ACME server handler from Caddyfile tokens. // // acme_server [] { // ca // } // func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { if !h.Next() { return nil, h.ArgErr() } matcherSet, err := h.ExtractMatcherSet() if err != nil { return nil, err } var acmeServer Handler var ca *caddypki.CA for h.Next() { if h.NextArg() { return nil, h.ArgErr() } for h.NextBlock(0) { switch h.Val() { case "ca": if !h.AllArgs(&acmeServer.CA) { return nil, h.ArgErr() } if ca == nil { ca = new(caddypki.CA) } ca.ID = acmeServer.CA } } } configVals := h.NewRoute(matcherSet, acmeServer) if ca == nil { return configVals, nil } return append(configVals, httpcaddyfile.ConfigValue{ Class: "pki.ca", Value: ca, }), nil } caddy-2.6.2/modules/caddypki/adminapi.go000066400000000000000000000162171435007237400201720ustar00rootroot00000000000000// Copyright 2020 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 caddypki import ( "encoding/json" "fmt" "net/http" "strings" "github.com/caddyserver/caddy/v2" "go.uber.org/zap" ) func init() { caddy.RegisterModule(adminAPI{}) } // adminAPI is a module that serves PKI endpoints to retrieve // information about the CAs being managed by Caddy. type adminAPI struct { ctx caddy.Context log *zap.Logger pkiApp *PKI } // CaddyModule returns the Caddy module information. func (adminAPI) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "admin.api.pki", New: func() caddy.Module { return new(adminAPI) }, } } // Provision sets up the adminAPI module. func (a *adminAPI) Provision(ctx caddy.Context) error { a.ctx = ctx a.log = ctx.Logger(a) // TODO: passing in 'a' is a hack until the admin API is officially extensible (see #5032) // First check if the PKI app was configured, because // a.ctx.App() has the side effect of instantiating // and provisioning an app even if it wasn't configured. pkiAppConfigured := a.ctx.AppIsConfigured("pki") if !pkiAppConfigured { return nil } // Load the PKI app, so we can query it for information. appModule, err := a.ctx.App("pki") if err != nil { return err } a.pkiApp = appModule.(*PKI) return nil } // Routes returns the admin routes for the PKI app. func (a *adminAPI) Routes() []caddy.AdminRoute { return []caddy.AdminRoute{ { Pattern: adminPKIEndpointBase, Handler: caddy.AdminHandlerFunc(a.handleAPIEndpoints), }, } } // handleAPIEndpoints routes API requests within adminPKIEndpointBase. func (a *adminAPI) handleAPIEndpoints(w http.ResponseWriter, r *http.Request) error { uri := strings.TrimPrefix(r.URL.Path, "/pki/") parts := strings.Split(uri, "/") switch { case len(parts) == 2 && parts[0] == "ca" && parts[1] != "": return a.handleCAInfo(w, r) case len(parts) == 3 && parts[0] == "ca" && parts[1] != "" && parts[2] == "certificates": return a.handleCACerts(w, r) } return caddy.APIError{ HTTPStatus: http.StatusNotFound, Err: fmt.Errorf("resource not found: %v", r.URL.Path), } } // handleCAInfo returns information about a particular // CA by its ID. If the CA ID is the default, then the CA will be // provisioned if it has not already been. Other CA IDs will return an // error if they have not been previously provisioned. func (a *adminAPI) handleCAInfo(w http.ResponseWriter, r *http.Request) error { if r.Method != http.MethodGet { return caddy.APIError{ HTTPStatus: http.StatusMethodNotAllowed, Err: fmt.Errorf("method not allowed: %v", r.Method), } } ca, err := a.getCAFromAPIRequestPath(r) if err != nil { return err } rootCert, interCert, err := rootAndIntermediatePEM(ca) if err != nil { return caddy.APIError{ HTTPStatus: http.StatusInternalServerError, Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err), } } repl := ca.newReplacer() response := caInfo{ ID: ca.ID, Name: ca.Name, RootCN: repl.ReplaceAll(ca.RootCommonName, ""), IntermediateCN: repl.ReplaceAll(ca.IntermediateCommonName, ""), RootCert: string(rootCert), IntermediateCert: string(interCert), } encoded, err := json.Marshal(response) if err != nil { return caddy.APIError{ HTTPStatus: http.StatusInternalServerError, Err: err, } } w.Header().Set("Content-Type", "application/json") _, _ = w.Write(encoded) return nil } // handleCACerts returns the certificate chain for a particular // CA by its ID. If the CA ID is the default, then the CA will be // provisioned if it has not already been. Other CA IDs will return an // error if they have not been previously provisioned. func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error { if r.Method != http.MethodGet { return caddy.APIError{ HTTPStatus: http.StatusMethodNotAllowed, Err: fmt.Errorf("method not allowed: %v", r.Method), } } ca, err := a.getCAFromAPIRequestPath(r) if err != nil { return err } rootCert, interCert, err := rootAndIntermediatePEM(ca) if err != nil { return caddy.APIError{ HTTPStatus: http.StatusInternalServerError, Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err), } } w.Header().Set("Content-Type", "application/pem-certificate-chain") _, err = w.Write(interCert) if err == nil { _, _ = w.Write(rootCert) } return nil } func (a *adminAPI) getCAFromAPIRequestPath(r *http.Request) (*CA, error) { // Grab the CA ID from the request path, it should be the 4th segment (/pki/ca/) id := strings.Split(r.URL.Path, "/")[3] if id == "" { return nil, caddy.APIError{ HTTPStatus: http.StatusBadRequest, Err: fmt.Errorf("missing CA in path"), } } // Find the CA by ID, if PKI is configured var ca *CA var ok bool if a.pkiApp != nil { ca, ok = a.pkiApp.CAs[id] } // If we didn't find the CA, and PKI is not configured // then we'll either error out if the CA ID is not the // default. If the CA ID is the default, then we'll // provision it, because the user probably aims to // change their config to enable PKI immediately after // if they actually requested the local CA ID. if !ok { if id != DefaultCAID { return nil, caddy.APIError{ HTTPStatus: http.StatusNotFound, Err: fmt.Errorf("no certificate authority configured with id: %s", id), } } // Provision the default CA, which generates and stores a root // certificate in storage, if one doesn't already exist. ca = new(CA) err := ca.Provision(a.ctx, id, a.log) if err != nil { return nil, caddy.APIError{ HTTPStatus: http.StatusInternalServerError, Err: fmt.Errorf("failed to provision CA %s, %w", id, err), } } } return ca, nil } func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) { root, err = pemEncodeCert(ca.RootCertificate().Raw) if err != nil { return } inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw) if err != nil { return } return } // caInfo is the response structure for the CA info API endpoint. type caInfo struct { ID string `json:"id"` Name string `json:"name"` RootCN string `json:"root_common_name"` IntermediateCN string `json:"intermediate_common_name"` RootCert string `json:"root_certificate"` IntermediateCert string `json:"intermediate_certificate"` } // adminPKIEndpointBase is the base admin endpoint under which all PKI admin endpoints exist. const adminPKIEndpointBase = "/pki/" // Interface guards var ( _ caddy.AdminRouter = (*adminAPI)(nil) _ caddy.Provisioner = (*adminAPI)(nil) ) caddy-2.6.2/modules/caddypki/ca.go000066400000000000000000000326471435007237400170000ustar00rootroot00000000000000// 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 caddypki import ( "crypto" "crypto/x509" "encoding/json" "errors" "fmt" "io/fs" "path" "sync" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/certmagic" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/db" "github.com/smallstep/truststore" "go.uber.org/zap" ) // CA describes a certificate authority, which consists of // root/signing certificates and various settings pertaining // to the issuance of certificates and trusting them. type CA struct { // The user-facing name of the certificate authority. Name string `json:"name,omitempty"` // The name to put in the CommonName field of the // root certificate. RootCommonName string `json:"root_common_name,omitempty"` // The name to put in the CommonName field of the // intermediate certificates. IntermediateCommonName string `json:"intermediate_common_name,omitempty"` // Whether Caddy will attempt to install the CA's root // into the system trust store, as well as into Java // and Mozilla Firefox trust stores. Default: true. InstallTrust *bool `json:"install_trust,omitempty"` // The root certificate to use; if null, one will be generated. Root *KeyPair `json:"root,omitempty"` // The intermediate (signing) certificate; if null, one will be generated. Intermediate *KeyPair `json:"intermediate,omitempty"` // Optionally configure a separate storage module associated with this // issuer, instead of using Caddy's global/default-configured storage. // This can be useful if you want to keep your signing keys in a // separate location from your leaf certificates. StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` // The unique config-facing ID of the certificate authority. // Since the ID is set in JSON config via object key, this // field is exported only for purposes of config generation // and module provisioning. ID string `json:"-"` storage certmagic.Storage root, inter *x509.Certificate interKey any // TODO: should we just store these as crypto.Signer? mu *sync.RWMutex rootCertPath string // mainly used for logging purposes if trusting log *zap.Logger ctx caddy.Context } // Provision sets up the CA. func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { ca.mu = new(sync.RWMutex) ca.log = log.Named("ca." + id) ca.ctx = ctx if id == "" { return fmt.Errorf("CA ID is required (use 'local' for the default CA)") } ca.mu.Lock() ca.ID = id ca.mu.Unlock() if ca.StorageRaw != nil { val, err := ctx.LoadModule(ca, "StorageRaw") if err != nil { return fmt.Errorf("loading storage module: %v", err) } cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() if err != nil { return fmt.Errorf("creating storage configuration: %v", err) } ca.storage = cmStorage } if ca.storage == nil { ca.storage = ctx.Storage() } if ca.Name == "" { ca.Name = defaultCAName } if ca.RootCommonName == "" { ca.RootCommonName = defaultRootCommonName } if ca.IntermediateCommonName == "" { ca.IntermediateCommonName = defaultIntermediateCommonName } // load the certs and key that will be used for signing var rootCert, interCert *x509.Certificate var rootKey, interKey any var err error if ca.Root != nil { if ca.Root.Format == "" || ca.Root.Format == "pem_file" { ca.rootCertPath = ca.Root.Certificate } rootCert, rootKey, err = ca.Root.Load() } else { ca.rootCertPath = "storage:" + ca.storageKeyRootCert() rootCert, rootKey, err = ca.loadOrGenRoot() } if err != nil { return err } if ca.Intermediate != nil { interCert, interKey, err = ca.Intermediate.Load() } else { interCert, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey) } if err != nil { return err } ca.mu.Lock() ca.root, ca.inter, ca.interKey = rootCert, interCert, interKey ca.mu.Unlock() return nil } // RootCertificate returns the CA's root certificate (public key). func (ca CA) RootCertificate() *x509.Certificate { ca.mu.RLock() defer ca.mu.RUnlock() return ca.root } // RootKey returns the CA's root private key. Since the root key is // not cached in memory long-term, it needs to be loaded from storage, // which could yield an error. func (ca CA) RootKey() (any, error) { _, rootKey, err := ca.loadOrGenRoot() return rootKey, err } // IntermediateCertificate returns the CA's intermediate // certificate (public key). func (ca CA) IntermediateCertificate() *x509.Certificate { ca.mu.RLock() defer ca.mu.RUnlock() return ca.inter } // IntermediateKey returns the CA's intermediate private key. func (ca CA) IntermediateKey() any { ca.mu.RLock() defer ca.mu.RUnlock() return ca.interKey } // NewAuthority returns a new Smallstep-powered signing authority for this CA. // Note that we receive *CA (a pointer) in this method to ensure the closure within it, which // executes at a later time, always has the only copy of the CA so it can access the latest, // renewed certificates since NewAuthority was called. See #4517 and #4669. func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authority, error) { // get the root certificate and the issuer cert+key rootCert := ca.RootCertificate() // set up the signer; cert/key which signs the leaf certs var signerOption authority.Option if authorityConfig.SignWithRoot { // if we're signing with root, we can just pass the // cert/key directly, since it's unlikely to expire // while Caddy is running (long lifetime) var issuerCert *x509.Certificate var issuerKey any issuerCert = rootCert var err error issuerKey, err = ca.RootKey() if err != nil { return nil, fmt.Errorf("loading signing key: %v", err) } signerOption = authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)) } else { // if we're signing with intermediate, we need to make // sure it's always fresh, because the intermediate may // renew while Caddy is running (medium lifetime) signerOption = authority.WithX509SignerFunc(func() ([]*x509.Certificate, crypto.Signer, error) { issuerCert := ca.IntermediateCertificate() issuerKey := ca.IntermediateKey().(crypto.Signer) ca.log.Debug("using intermediate signer", zap.String("serial", issuerCert.SerialNumber.String()), zap.String("not_before", issuerCert.NotBefore.String()), zap.String("not_after", issuerCert.NotAfter.String())) return []*x509.Certificate{issuerCert}, issuerKey, nil }) } opts := []authority.Option{ authority.WithConfig(&authority.Config{ AuthorityConfig: authorityConfig.AuthConfig, }), signerOption, authority.WithX509RootCerts(rootCert), } // Add a database if we have one if authorityConfig.DB != nil { opts = append(opts, authority.WithDatabase(*authorityConfig.DB)) } auth, err := authority.NewEmbedded(opts...) if err != nil { return nil, fmt.Errorf("initializing certificate authority: %v", err) } return auth, nil } func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey any, err error) { rootCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyRootCert()) if err != nil { if !errors.Is(err, fs.ErrNotExist) { return nil, nil, fmt.Errorf("loading root cert: %v", err) } // TODO: should we require that all or none of the assets are required before overwriting anything? rootCert, rootKey, err = ca.genRoot() if err != nil { return nil, nil, fmt.Errorf("generating root: %v", err) } } if rootCert == nil { rootCert, err = pemDecodeSingleCert(rootCertPEM) if err != nil { return nil, nil, fmt.Errorf("parsing root certificate PEM: %v", err) } } if rootKey == nil { rootKeyPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyRootKey()) if err != nil { return nil, nil, fmt.Errorf("loading root key: %v", err) } rootKey, err = certmagic.PEMDecodePrivateKey(rootKeyPEM) if err != nil { return nil, nil, fmt.Errorf("decoding root key: %v", err) } } return rootCert, rootKey, nil } func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey any, err error) { repl := ca.newReplacer() rootCert, rootKey, err = generateRoot(repl.ReplaceAll(ca.RootCommonName, "")) if err != nil { return nil, nil, fmt.Errorf("generating CA root: %v", err) } rootCertPEM, err := pemEncodeCert(rootCert.Raw) if err != nil { return nil, nil, fmt.Errorf("encoding root certificate: %v", err) } err = ca.storage.Store(ca.ctx, ca.storageKeyRootCert(), rootCertPEM) if err != nil { return nil, nil, fmt.Errorf("saving root certificate: %v", err) } rootKeyPEM, err := certmagic.PEMEncodePrivateKey(rootKey) if err != nil { return nil, nil, fmt.Errorf("encoding root key: %v", err) } err = ca.storage.Store(ca.ctx, ca.storageKeyRootKey(), rootKeyPEM) if err != nil { return nil, nil, fmt.Errorf("saving root key: %v", err) } return rootCert, rootKey, nil } func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.PrivateKey) (interCert *x509.Certificate, interKey crypto.PrivateKey, err error) { interCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyIntermediateCert()) if err != nil { if !errors.Is(err, fs.ErrNotExist) { return nil, nil, fmt.Errorf("loading intermediate cert: %v", err) } // TODO: should we require that all or none of the assets are required before overwriting anything? interCert, interKey, err = ca.genIntermediate(rootCert, rootKey) if err != nil { return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err) } } if interCert == nil { interCert, err = pemDecodeSingleCert(interCertPEM) if err != nil { return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err) } } if interKey == nil { interKeyPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyIntermediateKey()) if err != nil { return nil, nil, fmt.Errorf("loading intermediate key: %v", err) } interKey, err = certmagic.PEMDecodePrivateKey(interKeyPEM) if err != nil { return nil, nil, fmt.Errorf("decoding intermediate key: %v", err) } } return interCert, interKey, nil } func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.PrivateKey) (interCert *x509.Certificate, interKey crypto.PrivateKey, err error) { repl := ca.newReplacer() interCert, interKey, err = generateIntermediate(repl.ReplaceAll(ca.IntermediateCommonName, ""), rootCert, rootKey) if err != nil { return nil, nil, fmt.Errorf("generating CA intermediate: %v", err) } interCertPEM, err := pemEncodeCert(interCert.Raw) if err != nil { return nil, nil, fmt.Errorf("encoding intermediate certificate: %v", err) } err = ca.storage.Store(ca.ctx, ca.storageKeyIntermediateCert(), interCertPEM) if err != nil { return nil, nil, fmt.Errorf("saving intermediate certificate: %v", err) } interKeyPEM, err := certmagic.PEMEncodePrivateKey(interKey) if err != nil { return nil, nil, fmt.Errorf("encoding intermediate key: %v", err) } err = ca.storage.Store(ca.ctx, ca.storageKeyIntermediateKey(), interKeyPEM) if err != nil { return nil, nil, fmt.Errorf("saving intermediate key: %v", err) } return interCert, interKey, nil } func (ca CA) storageKeyCAPrefix() string { return path.Join("pki", "authorities", certmagic.StorageKeys.Safe(ca.ID)) } func (ca CA) storageKeyRootCert() string { return path.Join(ca.storageKeyCAPrefix(), "root.crt") } func (ca CA) storageKeyRootKey() string { return path.Join(ca.storageKeyCAPrefix(), "root.key") } func (ca CA) storageKeyIntermediateCert() string { return path.Join(ca.storageKeyCAPrefix(), "intermediate.crt") } func (ca CA) storageKeyIntermediateKey() string { return path.Join(ca.storageKeyCAPrefix(), "intermediate.key") } func (ca CA) newReplacer() *caddy.Replacer { repl := caddy.NewReplacer() repl.Set("pki.ca.name", ca.Name) return repl } // installRoot installs this CA's root certificate into the // local trust store(s) if it is not already trusted. The CA // must already be provisioned. func (ca CA) installRoot() error { // avoid password prompt if already trusted if trusted(ca.root) { ca.log.Info("root certificate is already trusted by system", zap.String("path", ca.rootCertPath)) return nil } ca.log.Warn("installing root certificate (you might be prompted for password)", zap.String("path", ca.rootCertPath)) return truststore.Install(ca.root, truststore.WithDebug(), truststore.WithFirefox(), truststore.WithJava(), ) } // AuthorityConfig is used to help a CA configure // the underlying signing authority. type AuthorityConfig struct { SignWithRoot bool // TODO: should we just embed the underlying authority.Config struct type? DB *db.AuthDB AuthConfig *authority.AuthConfig } const ( // DefaultCAID is the default CA ID. DefaultCAID = "local" defaultCAName = "Caddy Local Authority" defaultRootCommonName = "{pki.ca.name} - {time.now.year} ECC Root" defaultIntermediateCommonName = "{pki.ca.name} - ECC Intermediate" defaultRootLifetime = 24 * time.Hour * 30 * 12 * 10 defaultIntermediateLifetime = 24 * time.Hour * 7 ) caddy-2.6.2/modules/caddypki/certificates.go000066400000000000000000000033011435007237400210430ustar00rootroot00000000000000// 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 caddypki import ( "crypto" "crypto/x509" "time" "github.com/smallstep/cli/crypto/x509util" ) func generateRoot(commonName string) (rootCrt *x509.Certificate, privateKey any, err error) { rootProfile, err := x509util.NewRootProfile(commonName) if err != nil { return } rootProfile.Subject().NotAfter = time.Now().Add(defaultRootLifetime) // TODO: make configurable return newCert(rootProfile) } func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey crypto.PrivateKey) (cert *x509.Certificate, privateKey crypto.PrivateKey, err error) { interProfile, err := x509util.NewIntermediateProfile(commonName, rootCrt, rootKey) if err != nil { return } interProfile.Subject().NotAfter = time.Now().Add(defaultIntermediateLifetime) // TODO: make configurable return newCert(interProfile) } func newCert(profile x509util.Profile) (cert *x509.Certificate, privateKey crypto.PrivateKey, err error) { certBytes, err := profile.CreateCertificate() if err != nil { return } privateKey = profile.SubjectPrivateKey() cert, err = x509.ParseCertificate(certBytes) return } caddy-2.6.2/modules/caddypki/command.go000066400000000000000000000203041435007237400200160ustar00rootroot00000000000000// 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 caddypki import ( "crypto/x509" "encoding/json" "encoding/pem" "flag" "fmt" "net/http" "os" "path" "github.com/caddyserver/caddy/v2" caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/smallstep/truststore" ) func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "trust", Func: cmdTrust, Usage: "[--ca ] [--address ] [--config [--adapter ]]", Short: "Installs a CA certificate into local trust stores", Long: ` Adds a root certificate into the local trust stores. Caddy will attempt to install its root certificates into the local trust stores automatically when they are first generated, but it might fail if Caddy doesn't have the appropriate permissions to write to the trust store. This command is necessary to pre-install the certificates before using them, if the server process runs as an unprivileged user (such as via systemd). By default, this command installs the root certificate for Caddy's default CA (i.e. 'local'). You may specify the ID of another CA with the --ca flag. This command will attempt to connect to Caddy's admin API running at '` + caddy.DefaultAdminListen + `' to fetch the root certificate. You may explicitly specify the --address, or use the --config flag to load the admin address from your config, if not using the default.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("trust", flag.ExitOnError) fs.String("ca", "", "The ID of the CA to trust (defaults to 'local')") fs.String("address", "", "Address of the administration API listener (if --config is not used)") fs.String("config", "", "Configuration file (if --address is not used)") fs.String("adapter", "", "Name of config adapter to apply (if --config is used)") return fs }(), }) caddycmd.RegisterCommand(caddycmd.Command{ Name: "untrust", Func: cmdUntrust, Usage: "[--cert ] | [[--ca ] [--address ] [--config [--adapter ]]]", Short: "Untrusts a locally-trusted CA certificate", Long: ` Untrusts a root certificate from the local trust store(s). This command uninstalls trust; it does not necessarily delete the root certificate from trust stores entirely. Thus, repeatedly trusting and untrusting new certificates can fill up trust databases. This command does not delete or modify certificate files from Caddy's configured storage. This command can be used in one of two ways. Either by specifying which certificate to untrust by a direct path to the certificate file with the --cert flag, or by fetching the root certificate for the CA from the admin API (default behaviour). If the admin API is used, then the CA defaults to 'local'. You may specify the ID of another CA with the --ca flag. By default, this will attempt to connect to the Caddy's admin API running at '` + caddy.DefaultAdminListen + `' to fetch the root certificate. You may explicitly specify the --address, or use the --config flag to load the admin address from your config, if not using the default.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("untrust", flag.ExitOnError) fs.String("cert", "", "The path to the CA certificate to untrust") fs.String("ca", "", "The ID of the CA to untrust (defaults to 'local')") fs.String("address", "", "Address of the administration API listener (if --config is not used)") fs.String("config", "", "Configuration file (if --address is not used)") fs.String("adapter", "", "Name of config adapter to apply (if --config is used)") return fs }(), }) } func cmdTrust(fl caddycmd.Flags) (int, error) { caID := fl.String("ca") addrFlag := fl.String("address") configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") // Prepare the URI to the admin endpoint if caID == "" { caID = DefaultCAID } // Determine where we're sending the request to get the CA info adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } // Fetch the root cert from the admin API rootCert, err := rootCertFromAdmin(adminAddr, caID) if err != nil { return caddy.ExitCodeFailedStartup, err } // Set up the CA struct; we only need to fill in the root // because we're only using it to make use of the installRoot() // function. Also needs a logger for warnings, and a "cert path" // for the root cert; since we're loading from the API and we // don't know the actual storage path via this flow, we'll just // pass through the admin API address instead. ca := CA{ log: caddy.Log(), root: rootCert, rootCertPath: adminAddr + path.Join(adminPKIEndpointBase, "ca", caID), } // Install the cert! err = ca.installRoot() if err != nil { return caddy.ExitCodeFailedStartup, err } return caddy.ExitCodeSuccess, nil } func cmdUntrust(fl caddycmd.Flags) (int, error) { certFile := fl.String("cert") caID := fl.String("ca") addrFlag := fl.String("address") configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") if certFile != "" && (caID != "" || addrFlag != "" || configFlag != "") { return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments, cannot use --cert with other flags") } // If a file was specified, try to uninstall the cert matching that file if certFile != "" { // Sanity check, make sure cert file exists first _, err := os.Stat(certFile) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err) } // Uninstall the file! err = truststore.UninstallFile(certFile, truststore.WithDebug(), truststore.WithFirefox(), truststore.WithJava()) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err) } return caddy.ExitCodeSuccess, nil } // Prepare the URI to the admin endpoint if caID == "" { caID = DefaultCAID } // Determine where we're sending the request to get the CA info adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } // Fetch the root cert from the admin API rootCert, err := rootCertFromAdmin(adminAddr, caID) if err != nil { return caddy.ExitCodeFailedStartup, err } // Uninstall the cert! err = truststore.Uninstall(rootCert, truststore.WithDebug(), truststore.WithFirefox(), truststore.WithJava()) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err) } return caddy.ExitCodeSuccess, nil } // rootCertFromAdmin makes the API request to fetch the root certificate for the named CA via admin API. func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) { uri := path.Join(adminPKIEndpointBase, "ca", caID) // Make the request to fetch the CA info resp, err := caddycmd.AdminAPIRequest(adminAddr, http.MethodGet, uri, make(http.Header), nil) if err != nil { return nil, fmt.Errorf("requesting CA info: %v", err) } defer resp.Body.Close() // Decode the response caInfo := new(caInfo) err = json.NewDecoder(resp.Body).Decode(caInfo) if err != nil { return nil, fmt.Errorf("failed to decode JSON response: %v", err) } // Decode the root cert rootBlock, _ := pem.Decode([]byte(caInfo.RootCert)) if rootBlock == nil { return nil, fmt.Errorf("failed to decode root certificate: %v", err) } rootCert, err := x509.ParseCertificate(rootBlock.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse root certificate: %v", err) } return rootCert, nil } caddy-2.6.2/modules/caddypki/crypto.go000066400000000000000000000054141435007237400177250ustar00rootroot00000000000000// 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 caddypki import ( "bytes" "crypto" "crypto/x509" "encoding/pem" "fmt" "os" "github.com/caddyserver/certmagic" ) func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) { pemBlock, remaining := pem.Decode(pemDER) if pemBlock == nil { return nil, fmt.Errorf("no PEM block found") } if len(remaining) > 0 { return nil, fmt.Errorf("input contained more than a single PEM block") } if pemBlock.Type != "CERTIFICATE" { return nil, fmt.Errorf("expected PEM block type to be CERTIFICATE, but got '%s'", pemBlock.Type) } return x509.ParseCertificate(pemBlock.Bytes) } func pemEncodeCert(der []byte) ([]byte, error) { return pemEncode("CERTIFICATE", der) } func pemEncode(blockType string, b []byte) ([]byte, error) { var buf bytes.Buffer err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: b}) return buf.Bytes(), err } func trusted(cert *x509.Certificate) bool { chains, err := cert.Verify(x509.VerifyOptions{}) return len(chains) > 0 && err == nil } // KeyPair represents a public-private key pair, where the // public key is also called a certificate. type KeyPair struct { // The certificate. By default, this should be the path to // a PEM file unless format is something else. Certificate string `json:"certificate,omitempty"` // The private key. By default, this should be the path to // a PEM file unless format is something else. PrivateKey string `json:"private_key,omitempty"` // The format in which the certificate and private // key are provided. Default: pem_file Format string `json:"format,omitempty"` } // Load loads the certificate and key. func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) { switch kp.Format { case "", "pem_file": certData, err := os.ReadFile(kp.Certificate) if err != nil { return nil, nil, err } keyData, err := os.ReadFile(kp.PrivateKey) if err != nil { return nil, nil, err } cert, err := pemDecodeSingleCert(certData) if err != nil { return nil, nil, err } key, err := certmagic.PEMDecodePrivateKey(keyData) if err != nil { return nil, nil, err } return cert, key, nil default: return nil, nil, fmt.Errorf("unsupported format: %s", kp.Format) } } caddy-2.6.2/modules/caddypki/maintain.go000066400000000000000000000053421435007237400202050ustar00rootroot00000000000000// 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 caddypki import ( "crypto/x509" "fmt" "log" "runtime/debug" "time" "go.uber.org/zap" ) func (p *PKI) maintenance() { defer func() { if err := recover(); err != nil { log.Printf("[PANIC] PKI maintenance: %v\n%s", err, debug.Stack()) } }() ticker := time.NewTicker(10 * time.Minute) // TODO: make configurable defer ticker.Stop() for { select { case <-ticker.C: p.renewCerts() case <-p.ctx.Done(): return } } } func (p *PKI) renewCerts() { for _, ca := range p.CAs { err := p.renewCertsForCA(ca) if err != nil { p.log.Error("renewing intermediate certificates", zap.Error(err), zap.String("ca", ca.ID)) } } } func (p *PKI) renewCertsForCA(ca *CA) error { ca.mu.Lock() defer ca.mu.Unlock() log := p.log.With(zap.String("ca", ca.ID)) // only maintain the root if it's not manually provided in the config if ca.Root == nil { if needsRenewal(ca.root) { // TODO: implement root renewal (use same key) log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)", zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)), ) } } // only maintain the intermediate if it's not manually provided in the config if ca.Intermediate == nil { if needsRenewal(ca.inter) { log.Info("intermediate expires soon; renewing", zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)), ) rootCert, rootKey, err := ca.loadOrGenRoot() if err != nil { return fmt.Errorf("loading root key: %v", err) } interCert, interKey, err := ca.genIntermediate(rootCert, rootKey) if err != nil { return fmt.Errorf("generating new certificate: %v", err) } ca.inter, ca.interKey = interCert, interKey log.Info("renewed intermediate", zap.Time("new_expiration", ca.inter.NotAfter), ) } } return nil } func needsRenewal(cert *x509.Certificate) bool { lifetime := cert.NotAfter.Sub(cert.NotBefore) renewalWindow := time.Duration(float64(lifetime) * renewalWindowRatio) renewalWindowStart := cert.NotAfter.Add(-renewalWindow) return time.Now().After(renewalWindowStart) } const renewalWindowRatio = 0.2 // TODO: make configurable caddy-2.6.2/modules/caddypki/pki.go000066400000000000000000000101161435007237400171630ustar00rootroot00000000000000// 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 caddypki import ( "fmt" "github.com/caddyserver/caddy/v2" "go.uber.org/zap" ) func init() { caddy.RegisterModule(PKI{}) } // PKI provides Public Key Infrastructure facilities for Caddy. // // This app can define certificate authorities (CAs) which are capable // of signing certificates. Other modules can be configured to use // the CAs defined by this app for issuing certificates or getting // key information needed for establishing trust. type PKI struct { // The certificate authorities to manage. Each CA is keyed by an // ID that is used to uniquely identify it from other CAs. // At runtime, the GetCA() method should be used instead to ensure // the default CA is provisioned if it hadn't already been. // The default CA ID is "local". CAs map[string]*CA `json:"certificate_authorities,omitempty"` ctx caddy.Context log *zap.Logger } // CaddyModule returns the Caddy module information. func (PKI) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "pki", New: func() caddy.Module { return new(PKI) }, } } // Provision sets up the configuration for the PKI app. func (p *PKI) Provision(ctx caddy.Context) error { p.ctx = ctx p.log = ctx.Logger() for caID, ca := range p.CAs { err := ca.Provision(ctx, caID, p.log) if err != nil { return fmt.Errorf("provisioning CA '%s': %v", caID, err) } } // if this app is initialized at all, ensure there's at // least a default CA that can be used: the standard CA // which is used implicitly for signing local-use certs if len(p.CAs) == 0 { err := p.ProvisionDefaultCA(ctx) if err != nil { return fmt.Errorf("provisioning CA '%s': %v", DefaultCAID, err) } } return nil } // ProvisionDefaultCA sets up the default CA. func (p *PKI) ProvisionDefaultCA(ctx caddy.Context) error { if p.CAs == nil { p.CAs = make(map[string]*CA) } p.CAs[DefaultCAID] = new(CA) return p.CAs[DefaultCAID].Provision(ctx, DefaultCAID, p.log) } // Start starts the PKI app. func (p *PKI) Start() error { // install roots to trust store, if not disabled for _, ca := range p.CAs { if ca.InstallTrust != nil && !*ca.InstallTrust { ca.log.Info("root certificate trust store installation disabled; unconfigured clients may show warnings", zap.String("path", ca.rootCertPath)) continue } if err := ca.installRoot(); err != nil { // could be some system dependencies that are missing; // shouldn't totally prevent startup, but we should log it ca.log.Error("failed to install root certificate", zap.Error(err), zap.String("certificate_file", ca.rootCertPath)) } } // see if root/intermediates need renewal... p.renewCerts() // ...and keep them renewed go p.maintenance() return nil } // Stop stops the PKI app. func (p *PKI) Stop() error { return nil } // GetCA retrieves a CA by ID. If the ID is the default // CA ID, and it hasn't been provisioned yet, it will // be provisioned. func (p *PKI) GetCA(ctx caddy.Context, id string) (*CA, error) { ca, ok := p.CAs[id] if !ok { // for anything other than the default CA ID, error out if it wasn't configured if id != DefaultCAID { return nil, fmt.Errorf("no certificate authority configured with id: %s", id) } // for the default CA ID, provision it, because we want it to "just work" err := p.ProvisionDefaultCA(ctx) if err != nil { return nil, fmt.Errorf("failed to provision default CA: %s", err) } ca = p.CAs[id] } return ca, nil } // Interface guards var ( _ caddy.Provisioner = (*PKI)(nil) _ caddy.App = (*PKI)(nil) ) caddy-2.6.2/modules/caddytls/000077500000000000000000000000001435007237400160715ustar00rootroot00000000000000caddy-2.6.2/modules/caddytls/acmeissuer.go000066400000000000000000000437451435007237400205750ustar00rootroot00000000000000// 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 caddytls import ( "context" "crypto/x509" "errors" "fmt" "net/url" "os" "strconv" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/certmagic" "github.com/mholt/acmez" "github.com/mholt/acmez/acme" "go.uber.org/zap" ) func init() { caddy.RegisterModule(ACMEIssuer{}) } // ACMEIssuer manages certificates using the ACME protocol (RFC 8555). type ACMEIssuer struct { // The URL to the CA's ACME directory endpoint. Default: // https://acme-v02.api.letsencrypt.org/directory CA string `json:"ca,omitempty"` // The URL to the test CA's ACME directory endpoint. // This endpoint is only used during retries if there // is a failure using the primary CA. Default: // https://acme-staging-v02.api.letsencrypt.org/directory TestCA string `json:"test_ca,omitempty"` // Your email address, so the CA can contact you if necessary. // Not required, but strongly recommended to provide one so // you can be reached if there is a problem. Your email is // not sent to any Caddy mothership or used for any purpose // other than ACME transactions. Email string `json:"email,omitempty"` // If you have an existing account with the ACME server, put // the private key here in PEM format. The ACME client will // look up your account information with this key first before // trying to create a new one. You can use placeholders here, // for example if you have it in an environment variable. AccountKey string `json:"account_key,omitempty"` // If using an ACME CA that requires an external account // binding, specify the CA-provided credentials here. ExternalAccount *acme.EAB `json:"external_account,omitempty"` // Time to wait before timing out an ACME operation. // Default: 0 (no timeout) ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"` // Configures the various ACME challenge types. Challenges *ChallengesConfig `json:"challenges,omitempty"` // An array of files of CA certificates to accept when connecting to the // ACME CA. Generally, you should only use this if the ACME CA endpoint // is internal or for development/testing purposes. TrustedRootsPEMFiles []string `json:"trusted_roots_pem_files,omitempty"` // Preferences for selecting alternate certificate chains, if offered // by the CA. By default, the first offered chain will be selected. // If configured, the chains may be sorted and the first matching chain // will be selected. PreferredChains *ChainPreference `json:"preferred_chains,omitempty"` rootPool *x509.CertPool logger *zap.Logger template certmagic.ACMEIssuer // set at Provision magic *certmagic.Config // set at PreCheck issuer *certmagic.ACMEIssuer // set at PreCheck; result of template + magic } // CaddyModule returns the Caddy module information. func (ACMEIssuer) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.issuance.acme", New: func() caddy.Module { return new(ACMEIssuer) }, } } // Provision sets up iss. func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { iss.logger = ctx.Logger() repl := caddy.NewReplacer() // expand email address, if non-empty if iss.Email != "" { email, err := repl.ReplaceOrErr(iss.Email, true, true) if err != nil { return fmt.Errorf("expanding email address '%s': %v", iss.Email, err) } iss.Email = email } // expand account key, if non-empty if iss.AccountKey != "" { accountKey, err := repl.ReplaceOrErr(iss.AccountKey, true, true) if err != nil { return fmt.Errorf("expanding account key PEM '%s': %v", iss.AccountKey, err) } iss.AccountKey = accountKey } // DNS providers if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.ProviderRaw != nil { val, err := ctx.LoadModule(iss.Challenges.DNS, "ProviderRaw") if err != nil { return fmt.Errorf("loading DNS provider module: %v", err) } if deprecatedProvider, ok := val.(acmez.Solver); ok { // TODO: For a temporary amount of time, we are allowing the use of DNS // providers from go-acme/lego since there are so many providers implemented // using that API -- they are adapted as an all-in-one Caddy module in this // repository: https://github.com/caddy-dns/lego-deprecated - the module is a // acmez.Solver type, so we use it directly. The user must set environment // variables to configure it. Remove this shim once a sufficient number of // DNS providers are implemented for the libdns APIs instead. iss.Challenges.DNS.solver = deprecatedProvider } else { iss.Challenges.DNS.solver = &certmagic.DNS01Solver{ DNSProvider: val.(certmagic.ACMEDNSProvider), TTL: time.Duration(iss.Challenges.DNS.TTL), PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay), PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout), Resolvers: iss.Challenges.DNS.Resolvers, OverrideDomain: iss.Challenges.DNS.OverrideDomain, } } } // add any custom CAs to trust store if len(iss.TrustedRootsPEMFiles) > 0 { iss.rootPool = x509.NewCertPool() for _, pemFile := range iss.TrustedRootsPEMFiles { pemData, err := os.ReadFile(pemFile) if err != nil { return fmt.Errorf("loading trusted root CA's PEM file: %s: %v", pemFile, err) } if !iss.rootPool.AppendCertsFromPEM(pemData) { return fmt.Errorf("unable to add %s to trust pool: %v", pemFile, err) } } } var err error iss.template, err = iss.makeIssuerTemplate() if err != nil { return err } return nil } func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) { template := certmagic.ACMEIssuer{ CA: iss.CA, TestCA: iss.TestCA, Email: iss.Email, AccountKeyPEM: iss.AccountKey, CertObtainTimeout: time.Duration(iss.ACMETimeout), TrustedRoots: iss.rootPool, ExternalAccount: iss.ExternalAccount, Logger: iss.logger, } if iss.Challenges != nil { if iss.Challenges.HTTP != nil { template.DisableHTTPChallenge = iss.Challenges.HTTP.Disabled template.AltHTTPPort = iss.Challenges.HTTP.AlternatePort } if iss.Challenges.TLSALPN != nil { template.DisableTLSALPNChallenge = iss.Challenges.TLSALPN.Disabled template.AltTLSALPNPort = iss.Challenges.TLSALPN.AlternatePort } if iss.Challenges.DNS != nil { template.DNS01Solver = iss.Challenges.DNS.solver } template.ListenHost = iss.Challenges.BindHost } if iss.PreferredChains != nil { template.PreferredChains = certmagic.ChainPreference{ Smallest: iss.PreferredChains.Smallest, AnyCommonName: iss.PreferredChains.AnyCommonName, RootCommonName: iss.PreferredChains.RootCommonName, } } return template, nil } // SetConfig sets the associated certmagic config for this issuer. // This is required because ACME needs values from the config in // order to solve the challenges during issuance. This implements // the ConfigSetter interface. func (iss *ACMEIssuer) SetConfig(cfg *certmagic.Config) { iss.magic = cfg iss.issuer = certmagic.NewACMEIssuer(cfg, iss.template) } // PreCheck implements the certmagic.PreChecker interface. func (iss *ACMEIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error { return iss.issuer.PreCheck(ctx, names, interactive) } // Issue obtains a certificate for the given csr. func (iss *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { return iss.issuer.Issue(ctx, csr) } // IssuerKey returns the unique issuer key for the configured CA endpoint. func (iss *ACMEIssuer) IssuerKey() string { return iss.issuer.IssuerKey() } // Revoke revokes the given certificate. func (iss *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error { return iss.issuer.Revoke(ctx, cert, reason) } // GetACMEIssuer returns iss. This is useful when other types embed ACMEIssuer, because // type-asserting them to *ACMEIssuer will fail, but type-asserting them to an interface // with only this method will succeed, and will still allow the embedded ACMEIssuer // to be accessed and manipulated. func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss } // UnmarshalCaddyfile deserializes Caddyfile tokens into iss. // // ... acme [] { // dir // test_dir // email // timeout // disable_http_challenge // disable_tlsalpn_challenge // alt_http_port // alt_tlsalpn_port // eab // trusted_roots // dns [] // propagation_delay // propagation_timeout // resolvers // dns_challenge_override_domain // preferred_chains [smallest] { // root_common_name // any_common_name // } // } func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { iss.CA = d.Val() if d.NextArg() { return d.ArgErr() } } for nesting := d.Nesting(); d.NextBlock(nesting); { switch d.Val() { case "dir": if iss.CA != "" { return d.Errf("directory is already specified: %s", iss.CA) } if !d.AllArgs(&iss.CA) { return d.ArgErr() } case "test_dir": if !d.AllArgs(&iss.TestCA) { return d.ArgErr() } case "email": if !d.AllArgs(&iss.Email) { return d.ArgErr() } case "timeout": var timeoutStr string if !d.AllArgs(&timeoutStr) { return d.ArgErr() } timeout, err := caddy.ParseDuration(timeoutStr) if err != nil { return d.Errf("invalid timeout duration %s: %v", timeoutStr, err) } iss.ACMETimeout = caddy.Duration(timeout) case "disable_http_challenge": if d.NextArg() { return d.ArgErr() } if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.HTTP == nil { iss.Challenges.HTTP = new(HTTPChallengeConfig) } iss.Challenges.HTTP.Disabled = true case "disable_tlsalpn_challenge": if d.NextArg() { return d.ArgErr() } if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.TLSALPN == nil { iss.Challenges.TLSALPN = new(TLSALPNChallengeConfig) } iss.Challenges.TLSALPN.Disabled = true case "alt_http_port": if !d.NextArg() { return d.ArgErr() } port, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("invalid port %s: %v", d.Val(), err) } if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.HTTP == nil { iss.Challenges.HTTP = new(HTTPChallengeConfig) } iss.Challenges.HTTP.AlternatePort = port case "alt_tlsalpn_port": if !d.NextArg() { return d.ArgErr() } port, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("invalid port %s: %v", d.Val(), err) } if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.TLSALPN == nil { iss.Challenges.TLSALPN = new(TLSALPNChallengeConfig) } iss.Challenges.TLSALPN.AlternatePort = port case "eab": iss.ExternalAccount = new(acme.EAB) if !d.AllArgs(&iss.ExternalAccount.KeyID, &iss.ExternalAccount.MACKey) { return d.ArgErr() } case "trusted_roots": iss.TrustedRootsPEMFiles = d.RemainingArgs() case "dns": if !d.NextArg() { return d.ArgErr() } provName := d.Val() if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.DNS == nil { iss.Challenges.DNS = new(DNSChallengeConfig) } unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName) if err != nil { return err } iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil) case "propagation_delay": if !d.NextArg() { return d.ArgErr() } delayStr := d.Val() delay, err := caddy.ParseDuration(delayStr) if err != nil { return d.Errf("invalid propagation_delay duration %s: %v", delayStr, err) } if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.DNS == nil { iss.Challenges.DNS = new(DNSChallengeConfig) } iss.Challenges.DNS.PropagationDelay = caddy.Duration(delay) case "propagation_timeout": if !d.NextArg() { return d.ArgErr() } timeoutStr := d.Val() var timeout time.Duration if timeoutStr == "-1" { timeout = time.Duration(-1) } else { var err error timeout, err = caddy.ParseDuration(timeoutStr) if err != nil { return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err) } } if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.DNS == nil { iss.Challenges.DNS = new(DNSChallengeConfig) } iss.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout) case "resolvers": if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.DNS == nil { iss.Challenges.DNS = new(DNSChallengeConfig) } iss.Challenges.DNS.Resolvers = d.RemainingArgs() if len(iss.Challenges.DNS.Resolvers) == 0 { return d.ArgErr() } case "dns_challenge_override_domain": arg := d.RemainingArgs() if len(arg) != 1 { return d.ArgErr() } if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.DNS == nil { iss.Challenges.DNS = new(DNSChallengeConfig) } iss.Challenges.DNS.OverrideDomain = arg[0] case "preferred_chains": chainPref, err := ParseCaddyfilePreferredChainsOptions(d) if err != nil { return err } iss.PreferredChains = chainPref default: return d.Errf("unrecognized ACME issuer property: %s", d.Val()) } } } return nil } // onDemandAskRequest makes a request to the ask URL // to see if a certificate can be obtained for name. // The certificate request should be denied if this // returns an error. func onDemandAskRequest(ask string, name string) error { askURL, err := url.Parse(ask) if err != nil { return fmt.Errorf("parsing ask URL: %v", err) } qs := askURL.Query() qs.Set("domain", name) askURL.RawQuery = qs.Encode() resp, err := onDemandAskClient.Get(askURL.String()) if err != nil { return fmt.Errorf("error checking %v to determine if certificate for hostname '%s' should be allowed: %v", ask, name, err) } resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { return fmt.Errorf("%s: %w %s - non-2xx status code %d", name, errAskDenied, ask, resp.StatusCode) } return nil } func ParseCaddyfilePreferredChainsOptions(d *caddyfile.Dispenser) (*ChainPreference, error) { chainPref := new(ChainPreference) if d.NextArg() { smallestOpt := d.Val() if smallestOpt == "smallest" { trueBool := true chainPref.Smallest = &trueBool if d.NextArg() { // Only one argument allowed return nil, d.ArgErr() } if d.NextBlock(d.Nesting()) { // Don't allow other options when smallest == true return nil, d.Err("No more options are accepted when using the 'smallest' option") } } else { // Smallest option should always be 'smallest' or unset return nil, d.Errf("Invalid argument '%s'", smallestOpt) } } for nesting := d.Nesting(); d.NextBlock(nesting); { switch d.Val() { case "root_common_name": rootCommonNameOpt := d.RemainingArgs() chainPref.RootCommonName = rootCommonNameOpt if rootCommonNameOpt == nil { return nil, d.ArgErr() } if chainPref.AnyCommonName != nil { return nil, d.Err("Can't set root_common_name when any_common_name is already set") } case "any_common_name": anyCommonNameOpt := d.RemainingArgs() chainPref.AnyCommonName = anyCommonNameOpt if anyCommonNameOpt == nil { return nil, d.ArgErr() } if chainPref.RootCommonName != nil { return nil, d.Err("Can't set any_common_name when root_common_name is already set") } default: return nil, d.Errf("Received unrecognized parameter '%s'", d.Val()) } } if chainPref.Smallest == nil && chainPref.RootCommonName == nil && chainPref.AnyCommonName == nil { return nil, d.Err("No options for preferred_chains received") } return chainPref, nil } // ChainPreference describes the client's preferred certificate chain, // useful if the CA offers alternate chains. The first matching chain // will be selected. type ChainPreference struct { // Prefer chains with the fewest number of bytes. Smallest *bool `json:"smallest,omitempty"` // Select first chain having a root with one of // these common names. RootCommonName []string `json:"root_common_name,omitempty"` // Select first chain that has any issuer with one // of these common names. AnyCommonName []string `json:"any_common_name,omitempty"` } // errAskDenied is an error that should be wrapped or returned when the // configured "ask" endpoint does not allow a certificate to be issued, // to distinguish that from other errors such as connection failure. var errAskDenied = errors.New("certificate not allowed by ask endpoint") // Interface guards var ( _ certmagic.PreChecker = (*ACMEIssuer)(nil) _ certmagic.Issuer = (*ACMEIssuer)(nil) _ certmagic.Revoker = (*ACMEIssuer)(nil) _ caddy.Provisioner = (*ACMEIssuer)(nil) _ ConfigSetter = (*ACMEIssuer)(nil) _ caddyfile.Unmarshaler = (*ACMEIssuer)(nil) ) caddy-2.6.2/modules/caddytls/automation.go000066400000000000000000000406371435007237400206120ustar00rootroot00000000000000// 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 caddytls import ( "encoding/json" "errors" "fmt" "net/http" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/certmagic" "github.com/mholt/acmez" "go.uber.org/zap" ) // AutomationConfig governs the automated management of TLS certificates. type AutomationConfig struct { // The list of automation policies. The first policy matching // a certificate or subject name will be applied. Policies []*AutomationPolicy `json:"policies,omitempty"` // On-Demand TLS defers certificate operations to the // moment they are needed, e.g. during a TLS handshake. // Useful when you don't know all the hostnames at // config-time, or when you are not in control of the // domain names you are managing certificates for. // In 2015, Caddy became the first web server to // implement this experimental technology. // // Note that this field does not enable on-demand TLS; // it only configures it for when it is used. To enable // it, create an automation policy with `on_demand`. OnDemand *OnDemandConfig `json:"on_demand,omitempty"` // Caddy staples OCSP (and caches the response) for all // qualifying certificates by default. This setting // changes how often it scans responses for freshness, // and updates them if they are getting stale. Default: 1h OCSPCheckInterval caddy.Duration `json:"ocsp_interval,omitempty"` // Every so often, Caddy will scan all loaded, managed // certificates for expiration. This setting changes how // frequently the scan for expiring certificates is // performed. Default: 10m RenewCheckInterval caddy.Duration `json:"renew_interval,omitempty"` // How often to scan storage units for old or expired // assets and remove them. These scans exert lots of // reads (and list operations) on the storage module, so // choose a longer interval for large deployments. // Default: 24h // // Storage will always be cleaned when the process first // starts. Then, a new cleaning will be started this // duration after the previous cleaning started if the // previous cleaning finished in less than half the time // of this interval (otherwise next start will be skipped). StorageCleanInterval caddy.Duration `json:"storage_clean_interval,omitempty"` defaultPublicAutomationPolicy *AutomationPolicy defaultInternalAutomationPolicy *AutomationPolicy // only initialized if necessary } // AutomationPolicy designates the policy for automating the // management (obtaining, renewal, and revocation) of managed // TLS certificates. // // An AutomationPolicy value is not valid until it has been // provisioned; use the `AddAutomationPolicy()` method on the // TLS app to properly provision a new policy. type AutomationPolicy struct { // Which subjects (hostnames or IP addresses) this policy applies to. Subjects []string `json:"subjects,omitempty"` // The modules that may issue certificates. Default: internal if all // subjects do not qualify for public certificates; othewise acme and // zerossl. IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"` // Modules that can get a custom certificate to use for any // given TLS handshake at handshake-time. Custom certificates // can be useful if another entity is managing certificates // and Caddy need only get it and serve it. // // TODO: This is an EXPERIMENTAL feature. It is subject to change or removal. ManagersRaw []json.RawMessage `json:"get_certificate,omitempty" caddy:"namespace=tls.get_certificate inline_key=via"` // If true, certificates will be requested with MustStaple. Not all // CAs support this, and there are potentially serious consequences // of enabling this feature without proper threat modeling. MustStaple bool `json:"must_staple,omitempty"` // How long before a certificate's expiration to try renewing it, // as a function of its total lifetime. As a general and conservative // rule, it is a good idea to renew a certificate when it has about // 1/3 of its total lifetime remaining. This utilizes the majority // of the certificate's lifetime while still saving time to // troubleshoot problems. However, for extremely short-lived certs, // you may wish to increase the ratio to ~1/2. RenewalWindowRatio float64 `json:"renewal_window_ratio,omitempty"` // The type of key to generate for certificates. // Supported values: `ed25519`, `p256`, `p384`, `rsa2048`, `rsa4096`. KeyType string `json:"key_type,omitempty"` // Optionally configure a separate storage module associated with this // manager, instead of using Caddy's global/default-configured storage. StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` // If true, certificates will be managed "on demand"; that is, during // TLS handshakes or when needed, as opposed to at startup or config // load. This enables On-Demand TLS for this policy. OnDemand bool `json:"on_demand,omitempty"` // Disables OCSP stapling. Disabling OCSP stapling puts clients at // greater risk, reduces their privacy, and usually lowers client // performance. It is NOT recommended to disable this unless you // are able to justify the costs. // EXPERIMENTAL. Subject to change. DisableOCSPStapling bool `json:"disable_ocsp_stapling,omitempty"` // Overrides the URLs of OCSP responders embedded in certificates. // Each key is a OCSP server URL to override, and its value is the // replacement. An empty value will disable querying of that server. // EXPERIMENTAL. Subject to change. OCSPOverrides map[string]string `json:"ocsp_overrides,omitempty"` // Issuers and Managers store the decoded issuer and manager modules; // they are only used to populate an underlying certmagic.Config's // fields during provisioning so that the modules can survive a // re-provisioning. Issuers []certmagic.Issuer `json:"-"` Managers []certmagic.Manager `json:"-"` magic *certmagic.Config storage certmagic.Storage } // Provision sets up ap and builds its underlying CertMagic config. func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { // policy-specific storage implementation if ap.StorageRaw != nil { val, err := tlsApp.ctx.LoadModule(ap, "StorageRaw") if err != nil { return fmt.Errorf("loading TLS storage module: %v", err) } cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() if err != nil { return fmt.Errorf("creating TLS storage configuration: %v", err) } ap.storage = cmStorage } // on-demand TLS var ond *certmagic.OnDemandConfig if ap.OnDemand { ond = &certmagic.OnDemandConfig{ DecisionFunc: func(name string) error { // if an "ask" endpoint was defined, consult it first if tlsApp.Automation != nil && tlsApp.Automation.OnDemand != nil && tlsApp.Automation.OnDemand.Ask != "" { err := onDemandAskRequest(tlsApp.Automation.OnDemand.Ask, name) if err != nil { // distinguish true errors from denials, because it's important to log actual errors if !errors.Is(err, errAskDenied) { tlsApp.logger.Error("request to 'ask' endpoint failed", zap.Error(err), zap.String("endpoint", tlsApp.Automation.OnDemand.Ask), zap.String("domain", name)) } return err } } // check the rate limiter last because // doing so makes a reservation if !onDemandRateLimiter.Allow() { return fmt.Errorf("on-demand rate limit exceeded") } return nil }, } } // we don't store loaded modules directly in the certmagic config since // policy provisioning may happen more than once (during auto-HTTPS) and // loading a module clears its config bytes; thus, load the module and // store them on the policy before putting it on the config // load and provision any cert manager modules if ap.ManagersRaw != nil { vals, err := tlsApp.ctx.LoadModule(ap, "ManagersRaw") if err != nil { return fmt.Errorf("loading external certificate manager modules: %v", err) } for _, getCertVal := range vals.([]any) { ap.Managers = append(ap.Managers, getCertVal.(certmagic.Manager)) } } // load and provision any explicitly-configured issuer modules if ap.IssuersRaw != nil { val, err := tlsApp.ctx.LoadModule(ap, "IssuersRaw") if err != nil { return fmt.Errorf("loading TLS automation management module: %s", err) } for _, issVal := range val.([]any) { ap.Issuers = append(ap.Issuers, issVal.(certmagic.Issuer)) } } issuers := ap.Issuers if len(issuers) == 0 { var err error issuers, err = DefaultIssuersProvisioned(tlsApp.ctx) if err != nil { return err } } keyType := ap.KeyType if keyType != "" { var err error keyType, err = caddy.NewReplacer().ReplaceOrErr(ap.KeyType, true, true) if err != nil { return fmt.Errorf("invalid key type %s: %s", ap.KeyType, err) } if _, ok := supportedCertKeyTypes[keyType]; !ok { return fmt.Errorf("unrecognized key type: %s", keyType) } } keySource := certmagic.StandardKeyGenerator{ KeyType: supportedCertKeyTypes[keyType], } storage := ap.storage if storage == nil { storage = tlsApp.ctx.Storage() } template := certmagic.Config{ MustStaple: ap.MustStaple, RenewalWindowRatio: ap.RenewalWindowRatio, KeySource: keySource, OnEvent: tlsApp.onEvent, OnDemand: ond, OCSP: certmagic.OCSPConfig{ DisableStapling: ap.DisableOCSPStapling, ResponderOverrides: ap.OCSPOverrides, }, Storage: storage, Issuers: issuers, Managers: ap.Managers, Logger: tlsApp.logger, } ap.magic = certmagic.New(tlsApp.certCache, template) // sometimes issuers may need the parent certmagic.Config in // order to function properly (for example, ACMEIssuer needs // access to the correct storage and cache so it can solve // ACME challenges -- it's an annoying, inelegant circular // dependency that I don't know how to resolve nicely!) for _, issuer := range ap.magic.Issuers { if annoying, ok := issuer.(ConfigSetter); ok { annoying.SetConfig(ap.magic) } } return nil } // DefaultIssuers returns empty Issuers (not provisioned) to be used as defaults. // This function is experimental and has no compatibility promises. func DefaultIssuers() []certmagic.Issuer { return []certmagic.Issuer{ new(ACMEIssuer), &ZeroSSLIssuer{ACMEIssuer: new(ACMEIssuer)}, } } // DefaultIssuersProvisioned returns empty but provisioned default Issuers from // DefaultIssuers(). This function is experimental and has no compatibility promises. func DefaultIssuersProvisioned(ctx caddy.Context) ([]certmagic.Issuer, error) { issuers := DefaultIssuers() for i, iss := range issuers { if prov, ok := iss.(caddy.Provisioner); ok { err := prov.Provision(ctx) if err != nil { return nil, fmt.Errorf("provisioning default issuer %d: %T: %v", i, iss, err) } } } return issuers, nil } // ChallengesConfig configures the ACME challenges. type ChallengesConfig struct { // HTTP configures the ACME HTTP challenge. This // challenge is enabled and used automatically // and by default. HTTP *HTTPChallengeConfig `json:"http,omitempty"` // TLSALPN configures the ACME TLS-ALPN challenge. // This challenge is enabled and used automatically // and by default. TLSALPN *TLSALPNChallengeConfig `json:"tls-alpn,omitempty"` // Configures the ACME DNS challenge. Because this // challenge typically requires credentials for // interfacing with a DNS provider, this challenge is // not enabled by default. This is the only challenge // type which does not require a direct connection // to Caddy from an external server. // // NOTE: DNS providers are currently being upgraded, // and this API is subject to change, but should be // stabilized soon. DNS *DNSChallengeConfig `json:"dns,omitempty"` // Optionally customize the host to which a listener // is bound if required for solving a challenge. BindHost string `json:"bind_host,omitempty"` } // HTTPChallengeConfig configures the ACME HTTP challenge. type HTTPChallengeConfig struct { // If true, the HTTP challenge will be disabled. Disabled bool `json:"disabled,omitempty"` // An alternate port on which to service this // challenge. Note that the HTTP challenge port is // hard-coded into the spec and cannot be changed, // so you would have to forward packets from the // standard HTTP challenge port to this one. AlternatePort int `json:"alternate_port,omitempty"` } // TLSALPNChallengeConfig configures the ACME TLS-ALPN challenge. type TLSALPNChallengeConfig struct { // If true, the TLS-ALPN challenge will be disabled. Disabled bool `json:"disabled,omitempty"` // An alternate port on which to service this // challenge. Note that the TLS-ALPN challenge port // is hard-coded into the spec and cannot be changed, // so you would have to forward packets from the // standard TLS-ALPN challenge port to this one. AlternatePort int `json:"alternate_port,omitempty"` } // DNSChallengeConfig configures the ACME DNS challenge. // // NOTE: This API is still experimental and is subject to change. type DNSChallengeConfig struct { // The DNS provider module to use which will manage // the DNS records relevant to the ACME challenge. ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"` // The TTL of the TXT record used for the DNS challenge. TTL caddy.Duration `json:"ttl,omitempty"` // How long to wait before starting propagation checks. // Default: 0 (no wait). PropagationDelay caddy.Duration `json:"propagation_delay,omitempty"` // Maximum time to wait for temporary DNS record to appear. // Set to -1 to disable propagation checks. // Default: 2 minutes. PropagationTimeout caddy.Duration `json:"propagation_timeout,omitempty"` // Custom DNS resolvers to prefer over system/built-in defaults. // Often necessary to configure when using split-horizon DNS. Resolvers []string `json:"resolvers,omitempty"` // Override the domain to use for the DNS challenge. This // is to delegate the challenge to a different domain, // e.g. one that updates faster or one with a provider API. OverrideDomain string `json:"override_domain,omitempty"` solver acmez.Solver } // OnDemandConfig configures on-demand TLS, for obtaining // needed certificates at handshake-time. Because this // feature can easily be abused, you should use this to // establish rate limits and/or an internal endpoint that // Caddy can "ask" if it should be allowed to manage // certificates for a given hostname. type OnDemandConfig struct { // An optional rate limit to throttle the // issuance of certificates from handshakes. RateLimit *RateLimit `json:"rate_limit,omitempty"` // If Caddy needs to obtain or renew a certificate // during a TLS handshake, it will perform a quick // HTTP request to this URL to check if it should be // allowed to try to get a certificate for the name // in the "domain" query string parameter, like so: // `?domain=example.com`. The endpoint must return a // 200 OK status if a certificate is allowed; // anything else will cause it to be denied. // Redirects are not followed. Ask string `json:"ask,omitempty"` } // RateLimit specifies an interval with optional burst size. type RateLimit struct { // A duration value. A certificate may be obtained 'burst' // times during this interval. Interval caddy.Duration `json:"interval,omitempty"` // How many times during an interval a certificate can be obtained. Burst int `json:"burst,omitempty"` } // ConfigSetter is implemented by certmagic.Issuers that // need access to a parent certmagic.Config as part of // their provisioning phase. For example, the ACMEIssuer // requires a config so it can access storage and the // cache to solve ACME challenges. type ConfigSetter interface { SetConfig(cfg *certmagic.Config) } // These perpetual values are used for on-demand TLS. var ( onDemandRateLimiter = certmagic.NewRateLimiter(0, 0) onDemandAskClient = &http.Client{ Timeout: 10 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return fmt.Errorf("following http redirects is not allowed") }, } ) caddy-2.6.2/modules/caddytls/certmanagers.go000066400000000000000000000134101435007237400210720ustar00rootroot00000000000000package caddytls import ( "context" "crypto/tls" "fmt" "io" "net/http" "net/url" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/certmagic" "github.com/tailscale/tscert" "go.uber.org/zap" ) func init() { caddy.RegisterModule(Tailscale{}) caddy.RegisterModule(HTTPCertGetter{}) } // Tailscale is a module that can get certificates from the local Tailscale process. type Tailscale struct { // If true, this module will operate in "best-effort" mode and // ignore "soft" errors; i.e. try Tailscale, and if it doesn't connect // or return a certificate, oh well. Failure to connect to Tailscale // results in a no-op instead of an error. Intended for the use case // where this module is added implicitly for convenience, even if // Tailscale isn't necessarily running. Optional bool `json:"optional,omitempty"` logger *zap.Logger } // CaddyModule returns the Caddy module information. func (Tailscale) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.get_certificate.tailscale", New: func() caddy.Module { return new(Tailscale) }, } } func (ts *Tailscale) Provision(ctx caddy.Context) error { ts.logger = ctx.Logger() return nil } func (ts Tailscale) GetCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (*tls.Certificate, error) { canGetCert, err := ts.canHazCertificate(ctx, hello) if err == nil && !canGetCert { return nil, nil // pass-thru: Tailscale can't offer a cert for this name } if err != nil { ts.logger.Warn("could not get status; will try to get certificate anyway", zap.Error(err)) } return tscert.GetCertificate(hello) } // canHazCertificate returns true if Tailscale reports it can get a certificate for the given ClientHello. func (ts Tailscale) canHazCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (bool, error) { if ts.Optional && !strings.HasSuffix(strings.ToLower(hello.ServerName), tailscaleDomainAliasEnding) { return false, nil } status, err := tscert.GetStatus(ctx) if err != nil { if ts.Optional { // ignore error if we don't expect/require it to work anyway, but log it for debugging ts.logger.Debug("error getting tailscale status", zap.Error(err), zap.String("server_name", hello.ServerName)) return false, nil } return false, err } for _, domain := range status.CertDomains { if certmagic.MatchWildcard(hello.ServerName, domain) { return true, nil } } return false, nil } // UnmarshalCaddyfile deserializes Caddyfile tokens into ts. // // ... tailscale func (Tailscale) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { return d.ArgErr() } } return nil } // tailscaleDomainAliasEnding is the ending for all Tailscale custom domains. const tailscaleDomainAliasEnding = ".ts.net" // HTTPCertGetter can get a certificate via HTTP(S) request. type HTTPCertGetter struct { // The URL from which to download the certificate. Required. // // The URL will be augmented with query string parameters taken // from the TLS handshake: // // - server_name: The SNI value // - signature_schemes: Comma-separated list of hex IDs of signatures // - cipher_suites: Comma-separated list of hex IDs of cipher suites // // To be valid, the response must be HTTP 200 with a PEM body // consisting of blocks for the certificate chain and the private // key. URL string `json:"url,omitempty"` ctx context.Context } // CaddyModule returns the Caddy module information. func (hcg HTTPCertGetter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.get_certificate.http", New: func() caddy.Module { return new(HTTPCertGetter) }, } } func (hcg *HTTPCertGetter) Provision(ctx caddy.Context) error { hcg.ctx = ctx if hcg.URL == "" { return fmt.Errorf("URL is required") } return nil } func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (*tls.Certificate, error) { sigs := make([]string, len(hello.SignatureSchemes)) for i, sig := range hello.SignatureSchemes { sigs[i] = fmt.Sprintf("%x", uint16(sig)) // you won't believe what %x uses if the val is a Stringer } suites := make([]string, len(hello.CipherSuites)) for i, cs := range hello.CipherSuites { suites[i] = fmt.Sprintf("%x", cs) } parsed, err := url.Parse(hcg.URL) if err != nil { return nil, err } qs := parsed.Query() qs.Set("server_name", hello.ServerName) qs.Set("signature_schemes", strings.Join(sigs, ",")) qs.Set("cipher_suites", strings.Join(suites, ",")) parsed.RawQuery = qs.Encode() req, err := http.NewRequestWithContext(hcg.ctx, http.MethodGet, parsed.String(), nil) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("got HTTP %d", resp.StatusCode) } bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response body: %v", err) } cert, err := tlsCertFromCertAndKeyPEMBundle(bodyBytes) if err != nil { return nil, err } return &cert, nil } // UnmarshalCaddyfile deserializes Caddyfile tokens into ts. // // ... http func (hcg *HTTPCertGetter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if !d.NextArg() { return d.ArgErr() } hcg.URL = d.Val() if d.NextArg() { return d.ArgErr() } for nesting := d.Nesting(); d.NextBlock(nesting); { return d.Err("block not allowed here") } } return nil } // Interface guards var ( _ certmagic.Manager = (*Tailscale)(nil) _ caddy.Provisioner = (*Tailscale)(nil) _ caddyfile.Unmarshaler = (*Tailscale)(nil) _ certmagic.Manager = (*HTTPCertGetter)(nil) _ caddy.Provisioner = (*HTTPCertGetter)(nil) _ caddyfile.Unmarshaler = (*HTTPCertGetter)(nil) ) caddy-2.6.2/modules/caddytls/certselection.go000066400000000000000000000077421435007237400212750ustar00rootroot00000000000000// 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 caddytls import ( "crypto/tls" "crypto/x509" "encoding/json" "fmt" "math/big" "github.com/caddyserver/certmagic" ) // CustomCertSelectionPolicy represents a policy for selecting the certificate // used to complete a handshake when there may be multiple options. All fields // specified must match the candidate certificate for it to be chosen. // This was needed to solve https://github.com/caddyserver/caddy/issues/2588. type CustomCertSelectionPolicy struct { // The certificate must have one of these serial numbers. SerialNumber []bigInt `json:"serial_number,omitempty"` // The certificate must have one of these organization names. SubjectOrganization []string `json:"subject_organization,omitempty"` // The certificate must use this public key algorithm. PublicKeyAlgorithm PublicKeyAlgorithm `json:"public_key_algorithm,omitempty"` // The certificate must have at least one of the tags in the list. AnyTag []string `json:"any_tag,omitempty"` // The certificate must have all of the tags in the list. AllTags []string `json:"all_tags,omitempty"` } // SelectCertificate implements certmagic.CertificateSelector. It // only chooses a certificate that at least meets the criteria in // p. It then chooses the first non-expired certificate that is // compatible with the client. If none are valid, it chooses the // first viable candidate anyway. func (p CustomCertSelectionPolicy) SelectCertificate(hello *tls.ClientHelloInfo, choices []certmagic.Certificate) (certmagic.Certificate, error) { viable := make([]certmagic.Certificate, 0, len(choices)) nextChoice: for _, cert := range choices { if len(p.SerialNumber) > 0 { var found bool for _, sn := range p.SerialNumber { if cert.Leaf.SerialNumber.Cmp(&sn.Int) == 0 { found = true break } } if !found { continue } } if len(p.SubjectOrganization) > 0 { var found bool for _, subjOrg := range p.SubjectOrganization { for _, org := range cert.Leaf.Subject.Organization { if subjOrg == org { found = true break } } } if !found { continue } } if p.PublicKeyAlgorithm != PublicKeyAlgorithm(x509.UnknownPublicKeyAlgorithm) && PublicKeyAlgorithm(cert.Leaf.PublicKeyAlgorithm) != p.PublicKeyAlgorithm { continue } if len(p.AnyTag) > 0 { var found bool for _, tag := range p.AnyTag { if cert.HasTag(tag) { found = true break } } if !found { continue } } if len(p.AllTags) > 0 { for _, tag := range p.AllTags { if !cert.HasTag(tag) { continue nextChoice } } } // this certificate at least meets the policy's requirements, // but we still have to check expiration and compatibility viable = append(viable, cert) } if len(viable) == 0 { return certmagic.Certificate{}, fmt.Errorf("no certificates matched custom selection policy") } return certmagic.DefaultCertificateSelector(hello, viable) } // bigInt is a big.Int type that interops with JSON encodings as a string. type bigInt struct{ big.Int } func (bi bigInt) MarshalJSON() ([]byte, error) { return json.Marshal(bi.String()) } func (bi *bigInt) UnmarshalJSON(p []byte) error { if string(p) == "null" { return nil } var stringRep string err := json.Unmarshal(p, &stringRep) if err != nil { return err } _, ok := bi.SetString(stringRep, 10) if !ok { return fmt.Errorf("not a valid big integer: %s", p) } return nil } caddy-2.6.2/modules/caddytls/connpolicy.go000066400000000000000000000506011435007237400205770ustar00rootroot00000000000000// 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 caddytls import ( "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" "fmt" "io" "os" "path/filepath" "strings" "github.com/caddyserver/caddy/v2" "github.com/mholt/acmez" "go.uber.org/zap" ) func init() { caddy.RegisterModule(LeafCertClientAuth{}) } // ConnectionPolicies govern the establishment of TLS connections. It is // an ordered group of connection policies; the first matching policy will // be used to configure TLS connections at handshake-time. type ConnectionPolicies []*ConnectionPolicy // Provision sets up each connection policy. It should be called // during the Validate() phase, after the TLS app (if any) is // already set up. func (cp ConnectionPolicies) Provision(ctx caddy.Context) error { for i, pol := range cp { // matchers mods, err := ctx.LoadModule(pol, "MatchersRaw") if err != nil { return fmt.Errorf("loading handshake matchers: %v", err) } for _, modIface := range mods.(map[string]any) { cp[i].matchers = append(cp[i].matchers, modIface.(ConnectionMatcher)) } // enable HTTP/2 by default if len(pol.ALPN) == 0 { pol.ALPN = append(pol.ALPN, defaultALPN...) } // pre-build standard TLS config so we don't have to at handshake-time err = pol.buildStandardTLSConfig(ctx) if err != nil { return fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err) } if pol.ClientAuthentication != nil && len(pol.ClientAuthentication.VerifiersRaw) > 0 { clientCertValidations, err := ctx.LoadModule(pol.ClientAuthentication, "VerifiersRaw") if err != nil { return fmt.Errorf("loading client cert verifiers: %v", err) } for _, validator := range clientCertValidations.([]any) { cp[i].ClientAuthentication.verifiers = append(cp[i].ClientAuthentication.verifiers, validator.(ClientCertificateVerifier)) } } } return nil } // TLSConfig returns a standard-lib-compatible TLS configuration which // selects the first matching policy based on the ClientHello. func (cp ConnectionPolicies) TLSConfig(_ caddy.Context) *tls.Config { // using ServerName to match policies is extremely common, especially in configs // with lots and lots of different policies; we can fast-track those by indexing // them by SNI, so we don't have to iterate potentially thousands of policies // (TODO: this map does not account for wildcards, see if this is a problem in practice? look for reports of high connection latency with wildcard certs but low latency for non-wildcards in multi-thousand-cert deployments) indexedBySNI := make(map[string]ConnectionPolicies) if len(cp) > 30 { for _, p := range cp { for _, m := range p.matchers { if sni, ok := m.(MatchServerName); ok { for _, sniName := range sni { indexedBySNI[sniName] = append(indexedBySNI[sniName], p) } } } } } return &tls.Config{ MinVersion: tls.VersionTLS12, GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { // filter policies by SNI first, if possible, to speed things up // when there may be lots of policies possiblePolicies := cp if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok { possiblePolicies = indexedPolicies } policyLoop: for _, pol := range possiblePolicies { for _, matcher := range pol.matchers { if !matcher.Match(hello) { continue policyLoop } } return pol.TLSConfig, nil } return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) }, } } // ConnectionPolicy specifies the logic for handling a TLS handshake. // An empty policy is valid; safe and sensible defaults will be used. type ConnectionPolicy struct { // How to match this policy with a TLS ClientHello. If // this policy is the first to match, it will be used. MatchersRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=tls.handshake_match"` // How to choose a certificate if more than one matched // the given ServerName (SNI) value. CertSelection *CustomCertSelectionPolicy `json:"certificate_selection,omitempty"` // The list of cipher suites to support. Caddy's // defaults are modern and secure. CipherSuites []string `json:"cipher_suites,omitempty"` // The list of elliptic curves to support. Caddy's // defaults are modern and secure. Curves []string `json:"curves,omitempty"` // Protocols to use for Application-Layer Protocol // Negotiation (ALPN) during the handshake. ALPN []string `json:"alpn,omitempty"` // Minimum TLS protocol version to allow. Default: `tls1.2` ProtocolMin string `json:"protocol_min,omitempty"` // Maximum TLS protocol version to allow. Default: `tls1.3` ProtocolMax string `json:"protocol_max,omitempty"` // Enables and configures TLS client authentication. ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"` // DefaultSNI becomes the ServerName in a ClientHello if there // is no policy configured for the empty SNI value. DefaultSNI string `json:"default_sni,omitempty"` // Also known as "SSLKEYLOGFILE", TLS secrets will be written to // this file in NSS key log format which can then be parsed by // Wireshark and other tools. This is INSECURE as it allows other // programs or tools to decrypt TLS connections. However, this // capability can be useful for debugging and troubleshooting. // **ENABLING THIS LOG COMPROMISES SECURITY!** // // This feature is EXPERIMENTAL and subject to change or removal. InsecureSecretsLog string `json:"insecure_secrets_log,omitempty"` // TLSConfig is the fully-formed, standard lib TLS config // used to serve TLS connections. Provision all // ConnectionPolicies to populate this. It is exported only // so it can be minimally adjusted after provisioning // if necessary (like to adjust NextProtos to disable HTTP/2), // and may be unexported in the future. TLSConfig *tls.Config `json:"-"` matchers []ConnectionMatcher } func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { tlsAppIface, err := ctx.App("tls") if err != nil { return fmt.Errorf("getting tls app: %v", err) } tlsApp := tlsAppIface.(*TLS) // fill in some "easy" default values, but for other values // (such as slices), we should ensure that they start empty // so the user-provided config can fill them in; then we will // fill in a default config at the end if they are still unset cfg := &tls.Config{ NextProtos: p.ALPN, GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { // TODO: I don't love how this works: we pre-build certmagic configs // so that handshakes are faster. Unfortunately, certmagic configs are // comprised of settings from both a TLS connection policy and a TLS // automation policy. The only two fields (as of March 2020; v2 beta 17) // of a certmagic config that come from the TLS connection policy are // CertSelection and DefaultServerName, so an automation policy is what // builds the base certmagic config. Since the pre-built config is // shared, I don't think we can change any of its fields per-handshake, // hence the awkward shallow copy (dereference) here and the subsequent // changing of some of its fields. I'm worried this dereference allocates // more at handshake-time, but I don't know how to practically pre-build // a certmagic config for each combination of conn policy + automation policy... cfg := *tlsApp.getConfigForName(hello.ServerName) if p.CertSelection != nil { // you would think we could just set this whether or not // p.CertSelection is nil, but that leads to panics if // it is, because cfg.CertSelection is an interface, // so it will have a non-nil value even if the actual // value underlying it is nil (sigh) cfg.CertSelection = p.CertSelection } cfg.DefaultServerName = p.DefaultSNI return cfg.GetCertificate(hello) }, MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, } // session tickets support if tlsApp.SessionTickets != nil { cfg.SessionTicketsDisabled = tlsApp.SessionTickets.Disabled // session ticket key rotation tlsApp.SessionTickets.register(cfg) ctx.OnCancel(func() { // do cleanup when the context is canceled because, // though unlikely, it is possible that a context // needing a TLS server config could exist for less // than the lifetime of the whole app tlsApp.SessionTickets.unregister(cfg) }) } // TODO: Clean up session ticket active locks in storage if app (or process) is being closed! // add all the cipher suites in order, without duplicates cipherSuitesAdded := make(map[uint16]struct{}) for _, csName := range p.CipherSuites { csID := CipherSuiteID(csName) if csID == 0 { return fmt.Errorf("unsupported cipher suite: %s", csName) } if _, ok := cipherSuitesAdded[csID]; !ok { cipherSuitesAdded[csID] = struct{}{} cfg.CipherSuites = append(cfg.CipherSuites, csID) } } // add all the curve preferences in order, without duplicates curvesAdded := make(map[tls.CurveID]struct{}) for _, curveName := range p.Curves { curveID := SupportedCurves[curveName] if _, ok := curvesAdded[curveID]; !ok { curvesAdded[curveID] = struct{}{} cfg.CurvePreferences = append(cfg.CurvePreferences, curveID) } } // ensure ALPN includes the ACME TLS-ALPN protocol var alpnFound bool for _, a := range p.ALPN { if a == acmez.ACMETLS1Protocol { alpnFound = true break } } if !alpnFound { cfg.NextProtos = append(cfg.NextProtos, acmez.ACMETLS1Protocol) } // min and max protocol versions if (p.ProtocolMin != "" && p.ProtocolMax != "") && p.ProtocolMin > p.ProtocolMax { return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax) } if p.ProtocolMin != "" { cfg.MinVersion = SupportedProtocols[p.ProtocolMin] } if p.ProtocolMax != "" { cfg.MaxVersion = SupportedProtocols[p.ProtocolMax] } // client authentication if p.ClientAuthentication != nil { err := p.ClientAuthentication.ConfigureTLSConfig(cfg) if err != nil { return fmt.Errorf("configuring TLS client authentication: %v", err) } } if p.InsecureSecretsLog != "" { filename, err := caddy.NewReplacer().ReplaceOrErr(p.InsecureSecretsLog, true, true) if err != nil { return err } filename, err = filepath.Abs(filename) if err != nil { return err } logFile, _, err := secretsLogPool.LoadOrNew(filename, func() (caddy.Destructor, error) { w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) return destructableWriter{w}, err }) if err != nil { return err } ctx.OnCancel(func() { _, _ = secretsLogPool.Delete(filename) }) cfg.KeyLogWriter = logFile.(io.Writer) tlsApp.logger.Warn("TLS SECURITY COMPROMISED: secrets logging is enabled!", zap.String("log_filename", filename)) } setDefaultTLSParams(cfg) p.TLSConfig = cfg return nil } // SettingsEmpty returns true if p's settings (fields // except the matchers) are all empty/unset. func (p ConnectionPolicy) SettingsEmpty() bool { return p.CertSelection == nil && p.CipherSuites == nil && p.Curves == nil && p.ALPN == nil && p.ProtocolMin == "" && p.ProtocolMax == "" && p.ClientAuthentication == nil && p.DefaultSNI == "" && p.InsecureSecretsLog == "" } // ClientAuthentication configures TLS client auth. type ClientAuthentication struct { // A list of base64 DER-encoded CA certificates // against which to validate client certificates. // Client certs which are not signed by any of // these CAs will be rejected. TrustedCACerts []string `json:"trusted_ca_certs,omitempty"` // TrustedCACertPEMFiles is a list of PEM file names // from which to load certificates of trusted CAs. // Client certificates which are not signed by any of // these CA certificates will be rejected. TrustedCACertPEMFiles []string `json:"trusted_ca_certs_pem_files,omitempty"` // DEPRECATED: This field is deprecated and will be removed in // a future version. Please use the `validators` field instead // with the tls.client_auth.leaf module instead. // // A list of base64 DER-encoded client leaf certs // to accept. If this list is not empty, client certs // which are not in this list will be rejected. TrustedLeafCerts []string `json:"trusted_leaf_certs,omitempty"` // Client certificate verification modules. These can perform // custom client authentication checks, such as ensuring the // certificate is not revoked. VerifiersRaw []json.RawMessage `json:"verifiers,omitempty" caddy:"namespace=tls.client_auth inline_key=verifier"` verifiers []ClientCertificateVerifier // The mode for authenticating the client. Allowed values are: // // Mode | Description // -----|--------------- // `request` | Ask clients for a certificate, but allow even if there isn't one; do not verify it // `require` | Require clients to present a certificate, but do not verify it // `verify_if_given` | Ask clients for a certificate; allow even if there isn't one, but verify it if there is // `require_and_verify` | Require clients to present a valid certificate that is verified // // The default mode is `require_and_verify` if any // TrustedCACerts or TrustedCACertPEMFiles or TrustedLeafCerts // are provided; otherwise, the default mode is `require`. Mode string `json:"mode,omitempty"` existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error } // Active returns true if clientauth has an actionable configuration. func (clientauth ClientAuthentication) Active() bool { return len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0 || len(clientauth.TrustedLeafCerts) > 0 || // TODO: DEPRECATED len(clientauth.VerifiersRaw) > 0 || len(clientauth.Mode) > 0 } // ConfigureTLSConfig sets up cfg to enforce clientauth's configuration. func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) error { // if there's no actionable client auth, simply disable it if !clientauth.Active() { cfg.ClientAuth = tls.NoClientCert return nil } // enforce desired mode of client authentication if len(clientauth.Mode) > 0 { switch clientauth.Mode { case "request": cfg.ClientAuth = tls.RequestClientCert case "require": cfg.ClientAuth = tls.RequireAnyClientCert case "verify_if_given": cfg.ClientAuth = tls.VerifyClientCertIfGiven case "require_and_verify": cfg.ClientAuth = tls.RequireAndVerifyClientCert default: return fmt.Errorf("client auth mode not recognized: %s", clientauth.Mode) } } else { // otherwise, set a safe default mode if len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0 || len(clientauth.TrustedLeafCerts) > 0 { cfg.ClientAuth = tls.RequireAndVerifyClientCert } else { cfg.ClientAuth = tls.RequireAnyClientCert } } // enforce CA verification by adding CA certs to the ClientCAs pool if len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0 { caPool := x509.NewCertPool() for _, clientCAString := range clientauth.TrustedCACerts { clientCA, err := decodeBase64DERCert(clientCAString) if err != nil { return fmt.Errorf("parsing certificate: %v", err) } caPool.AddCert(clientCA) } for _, pemFile := range clientauth.TrustedCACertPEMFiles { pemContents, err := os.ReadFile(pemFile) if err != nil { return fmt.Errorf("reading %s: %v", pemFile, err) } caPool.AppendCertsFromPEM(pemContents) } cfg.ClientCAs = caPool } // TODO: DEPRECATED: Only here for backwards compatibility. // If leaf cert is specified, enforce by adding a client auth module if len(clientauth.TrustedLeafCerts) > 0 { caddy.Log().Named("tls.connection_policy").Warn("trusted_leaf_certs is deprecated; use leaf verifier module instead") var trustedLeafCerts []*x509.Certificate for _, clientCertString := range clientauth.TrustedLeafCerts { clientCert, err := decodeBase64DERCert(clientCertString) if err != nil { return fmt.Errorf("parsing certificate: %v", err) } trustedLeafCerts = append(trustedLeafCerts, clientCert) } clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{TrustedLeafCerts: trustedLeafCerts}) } // if a custom verification function already exists, wrap it clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate return nil } // verifyPeerCertificate is for use as a tls.Config.VerifyPeerCertificate // callback to do custom client certificate verification. It is intended // for installation only by clientauth.ConfigureTLSConfig(). func (clientauth *ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { // first use any pre-existing custom verification function if clientauth.existingVerifyPeerCert != nil { err := clientauth.existingVerifyPeerCert(rawCerts, verifiedChains) if err != nil { return err } } for _, verifier := range clientauth.verifiers { err := verifier.VerifyClientCertificate(rawCerts, verifiedChains) if err != nil { return err } } return 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) } // setDefaultTLSParams sets the default TLS cipher suites, protocol versions, // and server preferences of cfg if they are not already set; it does not // overwrite values, only fills in missing values. func setDefaultTLSParams(cfg *tls.Config) { if len(cfg.CipherSuites) == 0 { cfg.CipherSuites = getOptimalDefaultCipherSuites() } // Not a cipher suite, but still important for mitigating protocol downgrade attacks // (prepend since having it at end breaks http2 due to non-h2-approved suites before it) cfg.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, cfg.CipherSuites...) if len(cfg.CurvePreferences) == 0 { cfg.CurvePreferences = defaultCurves } if cfg.MinVersion == 0 { cfg.MinVersion = tls.VersionTLS12 } if cfg.MaxVersion == 0 { cfg.MaxVersion = tls.VersionTLS13 } } // LeafCertClientAuth verifies the client's leaf certificate. type LeafCertClientAuth struct { TrustedLeafCerts []*x509.Certificate } // CaddyModule returns the Caddy module information. func (LeafCertClientAuth) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.client_auth.leaf", New: func() caddy.Module { return new(LeafCertClientAuth) }, } } func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error { if len(rawCerts) == 0 { return fmt.Errorf("no client certificate provided") } remoteLeafCert, err := x509.ParseCertificate(rawCerts[0]) if err != nil { return fmt.Errorf("can't parse the given certificate: %s", err.Error()) } for _, trustedLeafCert := range l.TrustedLeafCerts { if remoteLeafCert.Equal(trustedLeafCert) { return nil } } return fmt.Errorf("client leaf certificate failed validation") } // PublicKeyAlgorithm is a JSON-unmarshalable wrapper type. type PublicKeyAlgorithm x509.PublicKeyAlgorithm // UnmarshalJSON satisfies json.Unmarshaler. func (a *PublicKeyAlgorithm) UnmarshalJSON(b []byte) error { algoStr := strings.ToLower(strings.Trim(string(b), `"`)) algo, ok := publicKeyAlgorithms[algoStr] if !ok { return fmt.Errorf("unrecognized public key algorithm: %s (expected one of %v)", algoStr, publicKeyAlgorithms) } *a = PublicKeyAlgorithm(algo) return nil } // ConnectionMatcher is a type which matches TLS handshakes. type ConnectionMatcher interface { Match(*tls.ClientHelloInfo) bool } // ClientCertificateVerifier is a type which verifies client certificates. // It is called during verifyPeerCertificate in the TLS handshake. type ClientCertificateVerifier interface { VerifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error } var defaultALPN = []string{"h2", "http/1.1"} type destructableWriter struct{ *os.File } func (d destructableWriter) Destruct() error { return d.Close() } var secretsLogPool = caddy.NewUsagePool() caddy-2.6.2/modules/caddytls/distributedstek/000077500000000000000000000000001435007237400213025ustar00rootroot00000000000000caddy-2.6.2/modules/caddytls/distributedstek/distributedstek.go000066400000000000000000000162171435007237400250510ustar00rootroot00000000000000// 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 distributedstek provides TLS session ticket ephemeral // keys (STEKs) in a distributed fashion by utilizing configured // storage for locking and key sharing. This allows a cluster of // machines to optimally resume TLS sessions in a load-balanced // environment without any hassle. This is similar to what // Twitter does, but without needing to rely on SSH, as it is // built into the web server this way: // https://blog.twitter.com/engineering/en_us/a/2013/forward-secrecy-at-twitter.html package distributedstek import ( "bytes" "encoding/gob" "encoding/json" "errors" "fmt" "io/fs" "log" "runtime/debug" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/certmagic" ) func init() { caddy.RegisterModule(Provider{}) } // Provider implements a distributed STEK provider. This // module will obtain STEKs from a storage module instead // of generating STEKs internally. This allows STEKs to be // coordinated, improving TLS session resumption in a cluster. type Provider struct { // The storage module wherein to store and obtain session // ticket keys. If unset, Caddy's default/global-configured // storage module will be used. Storage json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` storage certmagic.Storage stekConfig *caddytls.SessionTicketService timer *time.Timer ctx caddy.Context } // CaddyModule returns the Caddy module information. func (Provider) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.stek.distributed", New: func() caddy.Module { return new(Provider) }, } } // Provision provisions s. func (s *Provider) Provision(ctx caddy.Context) error { s.ctx = ctx // unpack the storage module to use, if different from the default if s.Storage != nil { val, err := ctx.LoadModule(s, "Storage") if err != nil { return fmt.Errorf("loading TLS storage module: %s", err) } cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() if err != nil { return fmt.Errorf("creating TLS storage configuration: %v", err) } s.storage = cmStorage } // otherwise, use default storage if s.storage == nil { s.storage = ctx.Storage() } return nil } // Initialize sets the configuration for s and returns the starting keys. func (s *Provider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) { // keep a reference to the config; we'll need it when rotating keys s.stekConfig = config dstek, err := s.getSTEK() if err != nil { return nil, err } // create timer for the remaining time on the interval; // this timer is cleaned up only when rotate() returns s.timer = time.NewTimer(time.Until(dstek.NextRotation)) return dstek.Keys, nil } // Next returns a channel which transmits the latest session ticket keys. func (s *Provider) Next(doneChan <-chan struct{}) <-chan [][32]byte { keysChan := make(chan [][32]byte) go s.rotate(doneChan, keysChan) return keysChan } func (s *Provider) loadSTEK() (distributedSTEK, error) { var sg distributedSTEK gobBytes, err := s.storage.Load(s.ctx, stekFileName) if err != nil { return sg, err // don't wrap, in case error is certmagic.ErrNotExist } dec := gob.NewDecoder(bytes.NewReader(gobBytes)) err = dec.Decode(&sg) if err != nil { return sg, fmt.Errorf("STEK gob corrupted: %v", err) } return sg, nil } func (s *Provider) storeSTEK(dstek distributedSTEK) error { var buf bytes.Buffer err := gob.NewEncoder(&buf).Encode(dstek) if err != nil { return fmt.Errorf("encoding STEK gob: %v", err) } err = s.storage.Store(s.ctx, stekFileName, buf.Bytes()) if err != nil { return fmt.Errorf("storing STEK gob: %v", err) } return nil } // getSTEK locks and loads the current STEK from storage. If none // currently exists, a new STEK is created and persisted. If the // current STEK is outdated (NextRotation time is in the past), // then it is rotated and persisted. The resulting STEK is returned. func (s *Provider) getSTEK() (distributedSTEK, error) { err := s.storage.Lock(s.ctx, stekLockName) if err != nil { return distributedSTEK{}, fmt.Errorf("failed to acquire storage lock: %v", err) } //nolint:errcheck defer s.storage.Unlock(s.ctx, stekLockName) // load the current STEKs from storage dstek, err := s.loadSTEK() if errors.Is(err, fs.ErrNotExist) { // if there is none, then make some right away dstek, err = s.rotateKeys(dstek) if err != nil { return dstek, fmt.Errorf("creating new STEK: %v", err) } } else if err != nil { // some other error, that's a problem return dstek, fmt.Errorf("loading STEK: %v", err) } else if time.Now().After(dstek.NextRotation) { // if current STEKs are outdated, rotate them dstek, err = s.rotateKeys(dstek) if err != nil { return dstek, fmt.Errorf("rotating keys: %v", err) } } return dstek, nil } // rotateKeys rotates the keys of oldSTEK and returns the new distributedSTEK // with updated keys and timestamps. It stores the returned STEK in storage, // so this function must only be called in a storage-provided lock. func (s *Provider) rotateKeys(oldSTEK distributedSTEK) (distributedSTEK, error) { var newSTEK distributedSTEK var err error newSTEK.Keys, err = s.stekConfig.RotateSTEKs(oldSTEK.Keys) if err != nil { return newSTEK, err } now := time.Now() newSTEK.LastRotation = now newSTEK.NextRotation = now.Add(time.Duration(s.stekConfig.RotationInterval)) err = s.storeSTEK(newSTEK) if err != nil { return newSTEK, err } return newSTEK, nil } // rotate rotates keys on a regular basis, sending each updated set of // keys down keysChan, until doneChan is closed. func (s *Provider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) { defer func() { if err := recover(); err != nil { log.Printf("[PANIC] distributed STEK rotation: %v\n%s", err, debug.Stack()) } }() for { select { case <-s.timer.C: dstek, err := s.getSTEK() if err != nil { // TODO: improve this handling log.Printf("[ERROR] Loading STEK: %v", err) continue } // send the updated keys to the service keysChan <- dstek.Keys // timer channel is already drained, so reset directly (see godoc) s.timer.Reset(time.Until(dstek.NextRotation)) case <-doneChan: // again, see godocs for why timer is stopped this way if !s.timer.Stop() { <-s.timer.C } return } } } type distributedSTEK struct { Keys [][32]byte LastRotation, NextRotation time.Time } const ( stekLockName = "stek_check" stekFileName = "stek/stek.bin" ) // Interface guard var _ caddytls.STEKProvider = (*Provider)(nil) caddy-2.6.2/modules/caddytls/fileloader.go000066400000000000000000000047571435007237400205430ustar00rootroot00000000000000// 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 caddytls import ( "crypto/tls" "fmt" "os" "github.com/caddyserver/caddy/v2" ) func init() { caddy.RegisterModule(FileLoader{}) } // FileLoader loads certificates and their associated keys from disk. type FileLoader []CertKeyFilePair // CaddyModule returns the Caddy module information. func (FileLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.certificates.load_files", New: func() caddy.Module { return new(FileLoader) }, } } // CertKeyFilePair pairs certificate and key file names along with their // encoding format so that they can be loaded from disk. type CertKeyFilePair struct { // Path to the certificate (public key) file. Certificate string `json:"certificate"` // Path to the private key file. Key string `json:"key"` // The format of the cert and key. Can be "pem". Default: "pem" Format string `json:"format,omitempty"` // Arbitrary values to associate with this certificate. // Can be useful when you want to select a particular // certificate when there may be multiple valid candidates. Tags []string `json:"tags,omitempty"` } // LoadCertificates returns the certificates to be loaded by fl. func (fl FileLoader) LoadCertificates() ([]Certificate, error) { certs := make([]Certificate, 0, len(fl)) for _, pair := range fl { certData, err := os.ReadFile(pair.Certificate) if err != nil { return nil, err } keyData, err := os.ReadFile(pair.Key) if err != nil { return nil, err } var cert tls.Certificate switch pair.Format { case "": fallthrough case "pem": cert, err = tls.X509KeyPair(certData, keyData) default: return nil, fmt.Errorf("unrecognized certificate/key encoding format: %s", pair.Format) } if err != nil { return nil, err } certs = append(certs, Certificate{Certificate: cert, Tags: pair.Tags}) } return certs, nil } // Interface guard var _ CertificateLoader = (FileLoader)(nil) caddy-2.6.2/modules/caddytls/folderloader.go000066400000000000000000000104401435007237400210610ustar00rootroot00000000000000// 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 caddytls import ( "bytes" "crypto/tls" "encoding/pem" "fmt" "os" "path/filepath" "strings" "github.com/caddyserver/caddy/v2" ) func init() { caddy.RegisterModule(FolderLoader{}) } // FolderLoader loads certificates and their associated keys from disk // by recursively walking the specified directories, looking for PEM // files which contain both a certificate and a key. type FolderLoader []string // CaddyModule returns the Caddy module information. func (FolderLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.certificates.load_folders", New: func() caddy.Module { return new(FolderLoader) }, } } // LoadCertificates loads all the certificates+keys in the directories // listed in fl from all files ending with .pem. This method of loading // certificates expects the certificate and key to be bundled into the // same file. func (fl FolderLoader) LoadCertificates() ([]Certificate, error) { var certs []Certificate for _, dir := range fl { err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error { if err != nil { return fmt.Errorf("unable to traverse into path: %s", fpath) } if info.IsDir() { return nil } if !strings.HasSuffix(strings.ToLower(info.Name()), ".pem") { return nil } bundle, err := os.ReadFile(fpath) if err != nil { return err } cert, err := tlsCertFromCertAndKeyPEMBundle(bundle) if err != nil { return fmt.Errorf("%s: %w", fpath, err) } certs = append(certs, Certificate{Certificate: cert}) return nil }) if err != nil { return nil, err } } return certs, nil } func tlsCertFromCertAndKeyPEMBundle(bundle []byte) (tls.Certificate, error) { certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer) var foundKey bool // use only the first key in the file for { // Decode next block so we can see what type it is var derBlock *pem.Block derBlock, bundle = pem.Decode(bundle) if derBlock == nil { break } if derBlock.Type == "CERTIFICATE" { // Re-encode certificate as PEM, appending to certificate chain if err := pem.Encode(certBuilder, derBlock); err != nil { return tls.Certificate{}, err } } else if derBlock.Type == "EC PARAMETERS" { // EC keys generated from openssl can be composed of two blocks: // parameters and key (parameter block should come first) if !foundKey { // Encode parameters if err := pem.Encode(keyBuilder, derBlock); err != nil { return tls.Certificate{}, err } // Key must immediately follow derBlock, bundle = pem.Decode(bundle) if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" { return tls.Certificate{}, fmt.Errorf("expected elliptic private key to immediately follow EC parameters") } if err := pem.Encode(keyBuilder, derBlock); err != nil { return tls.Certificate{}, err } foundKey = true } } else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") { // RSA key if !foundKey { if err := pem.Encode(keyBuilder, derBlock); err != nil { return tls.Certificate{}, err } foundKey = true } } else { return tls.Certificate{}, fmt.Errorf("unrecognized PEM block type: %s", derBlock.Type) } } certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes() if len(certPEMBytes) == 0 { return tls.Certificate{}, fmt.Errorf("failed to parse PEM data") } if len(keyPEMBytes) == 0 { return tls.Certificate{}, fmt.Errorf("no private key block found") } cert, err := tls.X509KeyPair(certPEMBytes, keyPEMBytes) if err != nil { return tls.Certificate{}, fmt.Errorf("making X509 key pair: %v", err) } return cert, nil } var _ CertificateLoader = (FolderLoader)(nil) caddy-2.6.2/modules/caddytls/internalissuer.go000066400000000000000000000125371435007237400214770ustar00rootroot00000000000000// 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 caddytls import ( "bytes" "context" "crypto/x509" "encoding/pem" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/certmagic" "github.com/smallstep/certificates/authority/provisioner" "go.uber.org/zap" ) func init() { caddy.RegisterModule(InternalIssuer{}) } // InternalIssuer is a certificate issuer that generates // certificates internally using a locally-configured // CA which can be customized using the `pki` app. type InternalIssuer struct { // The ID of the CA to use for signing. The default // CA ID is "local". The CA can be configured with the // `pki` app. CA string `json:"ca,omitempty"` // The validity period of certificates. Lifetime caddy.Duration `json:"lifetime,omitempty"` // If true, the root will be the issuer instead of // the intermediate. This is NOT recommended and should // only be used when devices/clients do not properly // validate certificate chains. SignWithRoot bool `json:"sign_with_root,omitempty"` ca *caddypki.CA logger *zap.Logger } // CaddyModule returns the Caddy module information. func (InternalIssuer) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.issuance.internal", New: func() caddy.Module { return new(InternalIssuer) }, } } // Provision sets up the issuer. func (iss *InternalIssuer) Provision(ctx caddy.Context) error { iss.logger = ctx.Logger() // set some defaults if iss.CA == "" { iss.CA = caddypki.DefaultCAID } // get a reference to the configured CA appModule, err := ctx.App("pki") if err != nil { return err } pkiApp := appModule.(*caddypki.PKI) ca, err := pkiApp.GetCA(ctx, iss.CA) if err != nil { return err } iss.ca = ca // set any other default values if iss.Lifetime == 0 { iss.Lifetime = caddy.Duration(defaultInternalCertLifetime) } return nil } // IssuerKey returns the unique issuer key for the // confgured CA endpoint. func (iss InternalIssuer) IssuerKey() string { return iss.ca.ID } // Issue issues a certificate to satisfy the CSR. func (iss InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { // prepare the signing authority authCfg := caddypki.AuthorityConfig{ SignWithRoot: iss.SignWithRoot, } auth, err := iss.ca.NewAuthority(authCfg) if err != nil { return nil, err } // get the cert (public key) that will be used for signing var issuerCert *x509.Certificate if iss.SignWithRoot { issuerCert = iss.ca.RootCertificate() } else { issuerCert = iss.ca.IntermediateCertificate() } // ensure issued certificate does not expire later than its issuer lifetime := time.Duration(iss.Lifetime) if time.Now().Add(lifetime).After(issuerCert.NotAfter) { lifetime = time.Until(issuerCert.NotAfter) iss.logger.Warn("cert lifetime would exceed issuer NotAfter, clamping lifetime", zap.Duration("orig_lifetime", time.Duration(iss.Lifetime)), zap.Duration("lifetime", lifetime), zap.Time("not_after", issuerCert.NotAfter), ) } certChain, err := auth.Sign(csr, provisioner.SignOptions{}, customCertLifetime(caddy.Duration(lifetime))) if err != nil { return nil, err } var buf bytes.Buffer for _, cert := range certChain { err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) if err != nil { return nil, err } } return &certmagic.IssuedCertificate{ Certificate: buf.Bytes(), }, nil } // UnmarshalCaddyfile deserializes Caddyfile tokens into iss. // // ... internal { // ca // lifetime // sign_with_root // } func (iss *InternalIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { for d.NextBlock(0) { switch d.Val() { case "ca": if !d.AllArgs(&iss.CA) { return d.ArgErr() } case "lifetime": if !d.NextArg() { return d.ArgErr() } dur, err := caddy.ParseDuration(d.Val()) if err != nil { return err } iss.Lifetime = caddy.Duration(dur) case "sign_with_root": if d.NextArg() { return d.ArgErr() } iss.SignWithRoot = true } } } return nil } // customCertLifetime allows us to customize certificates that are issued // by Smallstep libs, particularly the NotBefore & NotAfter dates. type customCertLifetime time.Duration func (d customCertLifetime) Modify(cert *x509.Certificate, _ provisioner.SignOptions) error { cert.NotBefore = time.Now() cert.NotAfter = cert.NotBefore.Add(time.Duration(d)) return nil } const defaultInternalCertLifetime = 12 * time.Hour // Interface guards var ( _ caddy.Provisioner = (*InternalIssuer)(nil) _ certmagic.Issuer = (*InternalIssuer)(nil) _ provisioner.CertificateModifier = (*customCertLifetime)(nil) ) caddy-2.6.2/modules/caddytls/matchers.go000066400000000000000000000100041435007237400202210ustar00rootroot00000000000000// 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 caddytls import ( "crypto/tls" "fmt" "net" "net/netip" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) func init() { caddy.RegisterModule(MatchServerName{}) caddy.RegisterModule(MatchRemoteIP{}) } // MatchServerName matches based on SNI. Names in // this list may use left-most-label wildcards, // similar to wildcard certificates. type MatchServerName []string // CaddyModule returns the Caddy module information. func (MatchServerName) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.handshake_match.sni", New: func() caddy.Module { return new(MatchServerName) }, } } // Match matches hello based on SNI. func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { for _, name := range m { if certmagic.MatchWildcard(hello.ServerName, name) { return true } } return false } // MatchRemoteIP matches based on the remote IP of the // connection. Specific IPs or CIDR ranges can be specified. // // Note that IPs can sometimes be spoofed, so do not rely // on this as a replacement for actual authentication. type MatchRemoteIP struct { // The IPs or CIDR ranges to match. Ranges []string `json:"ranges,omitempty"` // The IPs or CIDR ranges to *NOT* match. NotRanges []string `json:"not_ranges,omitempty"` cidrs []netip.Prefix notCidrs []netip.Prefix logger *zap.Logger } // CaddyModule returns the Caddy module information. func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.handshake_match.remote_ip", New: func() caddy.Module { return new(MatchRemoteIP) }, } } // Provision parses m's IP ranges, either from IP or CIDR expressions. func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { m.logger = ctx.Logger() for _, str := range m.Ranges { cidrs, err := m.parseIPRange(str) if err != nil { return err } m.cidrs = append(m.cidrs, cidrs...) } for _, str := range m.NotRanges { cidrs, err := m.parseIPRange(str) if err != nil { return err } m.notCidrs = append(m.notCidrs, cidrs...) } return nil } // Match matches hello based on the connection's remote IP. func (m MatchRemoteIP) Match(hello *tls.ClientHelloInfo) bool { remoteAddr := hello.Conn.RemoteAddr().String() ipStr, _, err := net.SplitHostPort(remoteAddr) if err != nil { ipStr = remoteAddr // weird; maybe no port? } ipAddr, err := netip.ParseAddr(ipStr) if err != nil { m.logger.Error("invalid client IP addresss", zap.String("ip", ipStr)) return false } return (len(m.cidrs) == 0 || m.matches(ipAddr, m.cidrs)) && (len(m.notCidrs) == 0 || !m.matches(ipAddr, m.notCidrs)) } func (MatchRemoteIP) parseIPRange(str string) ([]netip.Prefix, error) { var cidrs []netip.Prefix if strings.Contains(str, "/") { ipNet, err := netip.ParsePrefix(str) if err != nil { return nil, fmt.Errorf("parsing CIDR expression: %v", err) } cidrs = append(cidrs, ipNet) } else { ipAddr, err := netip.ParseAddr(str) if err != nil { return nil, fmt.Errorf("invalid IP address: '%s': %v", str, err) } ip := netip.PrefixFrom(ipAddr, ipAddr.BitLen()) cidrs = append(cidrs, ip) } return cidrs, nil } func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool { for _, ipRange := range ranges { if ipRange.Contains(ip) { return true } } return false } // Interface guards var ( _ ConnectionMatcher = (*MatchServerName)(nil) _ ConnectionMatcher = (*MatchRemoteIP)(nil) ) caddy-2.6.2/modules/caddytls/matchers_test.go000066400000000000000000000076211435007237400212730ustar00rootroot00000000000000// 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 caddytls import ( "context" "crypto/tls" "net" "testing" "github.com/caddyserver/caddy/v2" ) func TestServerNameMatcher(t *testing.T) { for i, tc := range []struct { names []string input string expect bool }{ { names: []string{"example.com"}, input: "example.com", expect: true, }, { names: []string{"example.com"}, input: "foo.com", expect: false, }, { names: []string{"example.com"}, input: "", expect: false, }, { names: []string{}, input: "", expect: false, }, { names: []string{"foo", "example.com"}, input: "example.com", expect: true, }, { names: []string{"foo", "example.com"}, input: "sub.example.com", expect: false, }, { names: []string{"foo", "example.com"}, input: "foo.com", expect: false, }, { names: []string{"*.example.com"}, input: "example.com", expect: false, }, { names: []string{"*.example.com"}, input: "sub.example.com", expect: true, }, { names: []string{"*.example.com", "*.sub.example.com"}, input: "sub2.sub.example.com", expect: true, }, } { chi := &tls.ClientHelloInfo{ServerName: tc.input} actual := MatchServerName(tc.names).Match(chi) if actual != tc.expect { t.Errorf("Test %d: Expected %t but got %t (input=%s match=%v)", i, tc.expect, actual, tc.input, tc.names) } } } func TestRemoteIPMatcher(t *testing.T) { ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) defer cancel() for i, tc := range []struct { ranges []string notRanges []string input string expect bool }{ { ranges: []string{"127.0.0.1"}, input: "127.0.0.1:12345", expect: true, }, { ranges: []string{"127.0.0.1"}, input: "127.0.0.2:12345", expect: false, }, { ranges: []string{"127.0.0.1/16"}, input: "127.0.1.23:12345", expect: true, }, { ranges: []string{"127.0.0.1", "192.168.1.105"}, input: "192.168.1.105:12345", expect: true, }, { notRanges: []string{"127.0.0.1"}, input: "127.0.0.1:12345", expect: false, }, { notRanges: []string{"127.0.0.2"}, input: "127.0.0.1:12345", expect: true, }, { ranges: []string{"127.0.0.1"}, notRanges: []string{"127.0.0.2"}, input: "127.0.0.1:12345", expect: true, }, { ranges: []string{"127.0.0.2"}, notRanges: []string{"127.0.0.2"}, input: "127.0.0.2:12345", expect: false, }, { ranges: []string{"127.0.0.2"}, notRanges: []string{"127.0.0.2"}, input: "127.0.0.3:12345", expect: false, }, } { matcher := MatchRemoteIP{Ranges: tc.ranges, NotRanges: tc.notRanges} err := matcher.Provision(ctx) if err != nil { t.Fatalf("Test %d: Provision failed: %v", i, err) } addr := testAddr(tc.input) chi := &tls.ClientHelloInfo{Conn: testConn{addr: addr}} actual := matcher.Match(chi) if actual != tc.expect { t.Errorf("Test %d: Expected %t but got %t (input=%s ranges=%v notRanges=%v)", i, tc.expect, actual, tc.input, tc.ranges, tc.notRanges) } } } type testConn struct { *net.TCPConn addr testAddr } func (tc testConn) RemoteAddr() net.Addr { return tc.addr } type testAddr string func (testAddr) Network() string { return "tcp" } func (ta testAddr) String() string { return string(ta) } caddy-2.6.2/modules/caddytls/pemloader.go000066400000000000000000000041661435007237400203770ustar00rootroot00000000000000// 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 caddytls import ( "crypto/tls" "fmt" "github.com/caddyserver/caddy/v2" ) func init() { caddy.RegisterModule(PEMLoader{}) } // PEMLoader loads certificates and their associated keys by // decoding their PEM blocks directly. This has the advantage // of not needing to store them on disk at all. type PEMLoader []CertKeyPEMPair // CaddyModule returns the Caddy module information. func (PEMLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.certificates.load_pem", New: func() caddy.Module { return new(PEMLoader) }, } } // CertKeyPEMPair pairs certificate and key PEM blocks. type CertKeyPEMPair struct { // The certificate (public key) in PEM format. CertificatePEM string `json:"certificate"` // The private key in PEM format. KeyPEM string `json:"key"` // Arbitrary values to associate with this certificate. // Can be useful when you want to select a particular // certificate when there may be multiple valid candidates. Tags []string `json:"tags,omitempty"` } // LoadCertificates returns the certificates contained in pl. func (pl PEMLoader) LoadCertificates() ([]Certificate, error) { certs := make([]Certificate, 0, len(pl)) for i, pair := range pl { cert, err := tls.X509KeyPair([]byte(pair.CertificatePEM), []byte(pair.KeyPEM)) if err != nil { return nil, fmt.Errorf("PEM pair %d: %v", i, err) } certs = append(certs, Certificate{ Certificate: cert, Tags: pair.Tags, }) } return certs, nil } // Interface guard var _ CertificateLoader = (PEMLoader)(nil) caddy-2.6.2/modules/caddytls/sessiontickets.go000066400000000000000000000164061435007237400215010ustar00rootroot00000000000000// 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 caddytls import ( "crypto/rand" "crypto/tls" "encoding/json" "fmt" "io" "log" "runtime/debug" "sync" "time" "github.com/caddyserver/caddy/v2" ) // SessionTicketService configures and manages TLS session tickets. type SessionTicketService struct { // KeySource is the method by which Caddy produces or obtains // TLS session ticket keys (STEKs). By default, Caddy generates // them internally using a secure pseudorandom source. KeySource json.RawMessage `json:"key_source,omitempty" caddy:"namespace=tls.stek inline_key=provider"` // How often Caddy rotates STEKs. Default: 12h. RotationInterval caddy.Duration `json:"rotation_interval,omitempty"` // The maximum number of keys to keep in rotation. Default: 4. MaxKeys int `json:"max_keys,omitempty"` // Disables STEK rotation. DisableRotation bool `json:"disable_rotation,omitempty"` // Disables TLS session resumption by tickets. Disabled bool `json:"disabled,omitempty"` keySource STEKProvider configs map[*tls.Config]struct{} stopChan chan struct{} currentKeys [][32]byte mu *sync.Mutex } func (s *SessionTicketService) provision(ctx caddy.Context) error { s.configs = make(map[*tls.Config]struct{}) s.mu = new(sync.Mutex) // establish sane defaults if s.RotationInterval == 0 { s.RotationInterval = caddy.Duration(defaultSTEKRotationInterval) } if s.MaxKeys <= 0 { s.MaxKeys = defaultMaxSTEKs } if s.KeySource == nil { s.KeySource = json.RawMessage(`{"provider":"standard"}`) } // load the STEK module, which will provide keys val, err := ctx.LoadModule(s, "KeySource") if err != nil { return fmt.Errorf("loading TLS session ticket ephemeral keys provider module: %s", err) } s.keySource = val.(STEKProvider) // if session tickets or just rotation are // disabled, no need to start service if s.Disabled || s.DisableRotation { return nil } // start the STEK module; this ensures we have // a starting key before any config needs one return s.start() } // start loads the starting STEKs and spawns a goroutine // which loops to rotate the STEKs, which continues until // stop() is called. If start() was already called, this // is a no-op. func (s *SessionTicketService) start() error { if s.stopChan != nil { return nil } s.stopChan = make(chan struct{}) // initializing the key source gives us our // initial key(s) to start with; if successful, // we need to be sure to call Next() so that // the key source can know when it is done initialKeys, err := s.keySource.Initialize(s) if err != nil { return fmt.Errorf("setting STEK module configuration: %v", err) } s.mu.Lock() s.currentKeys = initialKeys s.mu.Unlock() // keep the keys rotated go s.stayUpdated() return nil } // stayUpdated is a blocking function which rotates // the keys whenever new ones are sent. It reads // from keysChan until s.stop() is called. func (s *SessionTicketService) stayUpdated() { defer func() { if err := recover(); err != nil { log.Printf("[PANIC] session ticket service: %v\n%s", err, debug.Stack()) } }() // this call is essential when Initialize() // returns without error, because the stop // channel is the only way the key source // will know when to clean up keysChan := s.keySource.Next(s.stopChan) for { select { case newKeys := <-keysChan: s.mu.Lock() s.currentKeys = newKeys configs := s.configs s.mu.Unlock() for cfg := range configs { cfg.SetSessionTicketKeys(newKeys) } case <-s.stopChan: return } } } // stop terminates the key rotation goroutine. func (s *SessionTicketService) stop() { if s.stopChan != nil { close(s.stopChan) } } // register sets the session ticket keys on cfg // and keeps them updated. Any values registered // must be unregistered, or they will not be // garbage-collected. s.start() must have been // called first. If session tickets are disabled // or if ticket key rotation is disabled, this // function is a no-op. func (s *SessionTicketService) register(cfg *tls.Config) { if s.Disabled || s.DisableRotation { return } s.mu.Lock() cfg.SetSessionTicketKeys(s.currentKeys) s.configs[cfg] = struct{}{} s.mu.Unlock() } // unregister stops session key management on cfg and // removes the internal stored reference to cfg. If // session tickets are disabled or if ticket key rotation // is disabled, this function is a no-op. func (s *SessionTicketService) unregister(cfg *tls.Config) { if s.Disabled || s.DisableRotation { return } s.mu.Lock() delete(s.configs, cfg) s.mu.Unlock() } // RotateSTEKs rotates the keys in keys by producing a new key and eliding // the oldest one. The new slice of keys is returned. func (s SessionTicketService) RotateSTEKs(keys [][32]byte) ([][32]byte, error) { // produce a new key newKey, err := s.generateSTEK() if err != nil { return nil, fmt.Errorf("generating STEK: %v", err) } // we need to prepend this new key to the list of // keys so that it is preferred, but we need to be // careful that we do not grow the slice larger // than MaxKeys, otherwise we'll be storing one // more key in memory than we expect; so be sure // that the slice does not grow beyond the limit // even for a brief period of time, since there's // no guarantee when that extra allocation will // be overwritten; this is why we first trim the // length to one less the max, THEN prepend the // new key if len(keys) >= s.MaxKeys { keys[len(keys)-1] = [32]byte{} // zero-out memory of oldest key keys = keys[:s.MaxKeys-1] // trim length of slice } keys = append([][32]byte{newKey}, keys...) // prepend new key return keys, nil } // generateSTEK generates key material suitable for use as a // session ticket ephemeral key. func (s *SessionTicketService) generateSTEK() ([32]byte, error) { var newTicketKey [32]byte _, err := io.ReadFull(rand.Reader, newTicketKey[:]) return newTicketKey, err } // STEKProvider is a type that can provide session ticket ephemeral // keys (STEKs). type STEKProvider interface { // Initialize provides the STEK configuration to the STEK // module so that it can obtain and manage keys accordingly. // It returns the initial key(s) to use. Implementations can // rely on Next() being called if Initialize() returns // without error, so that it may know when it is done. Initialize(config *SessionTicketService) ([][32]byte, error) // Next returns the channel through which the next session // ticket keys will be transmitted until doneChan is closed. // Keys should be sent on keysChan as they are updated. // When doneChan is closed, any resources allocated in // Initialize() must be cleaned up. Next(doneChan <-chan struct{}) (keysChan <-chan [][32]byte) } const ( defaultSTEKRotationInterval = 12 * time.Hour defaultMaxSTEKs = 4 ) caddy-2.6.2/modules/caddytls/standardstek/000077500000000000000000000000001435007237400205605ustar00rootroot00000000000000caddy-2.6.2/modules/caddytls/standardstek/stek.go000066400000000000000000000074141435007237400220630ustar00rootroot00000000000000// 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 standardstek import ( "log" "runtime/debug" "sync" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" ) func init() { caddy.RegisterModule(standardSTEKProvider{}) } type standardSTEKProvider struct { stekConfig *caddytls.SessionTicketService timer *time.Timer } // CaddyModule returns the Caddy module information. func (standardSTEKProvider) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.stek.standard", New: func() caddy.Module { return new(standardSTEKProvider) }, } } // Initialize sets the configuration for s and returns the starting keys. func (s *standardSTEKProvider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) { // keep a reference to the config; we'll need it when rotating keys s.stekConfig = config itvl := time.Duration(s.stekConfig.RotationInterval) mutex.Lock() defer mutex.Unlock() // if this is our first rotation or we are overdue // for one, perform a rotation immediately; otherwise, // we assume that the keys are non-empty and fresh since := time.Since(lastRotation) if lastRotation.IsZero() || since > itvl { var err error keys, err = s.stekConfig.RotateSTEKs(keys) if err != nil { return nil, err } since = 0 // since this is overdue or is the first rotation, use full interval lastRotation = time.Now() } // create timer for the remaining time on the interval; // this timer is cleaned up only when Next() returns s.timer = time.NewTimer(itvl - since) return keys, nil } // Next returns a channel which transmits the latest session ticket keys. func (s *standardSTEKProvider) Next(doneChan <-chan struct{}) <-chan [][32]byte { keysChan := make(chan [][32]byte) go s.rotate(doneChan, keysChan) return keysChan } // rotate rotates keys on a regular basis, sending each updated set of // keys down keysChan, until doneChan is closed. func (s *standardSTEKProvider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) { defer func() { if err := recover(); err != nil { log.Printf("[PANIC] standard STEK rotation: %v\n%s", err, debug.Stack()) } }() for { select { case now := <-s.timer.C: // copy the slice header to avoid races mutex.RLock() keysCopy := keys mutex.RUnlock() // generate a new key, rotating old ones var err error keysCopy, err = s.stekConfig.RotateSTEKs(keysCopy) if err != nil { // TODO: improve this handling log.Printf("[ERROR] Generating STEK: %v", err) continue } // replace keys slice with updated value and // record the timestamp of rotation mutex.Lock() keys = keysCopy lastRotation = now mutex.Unlock() // send the updated keys to the service keysChan <- keysCopy // timer channel is already drained, so reset directly (see godoc) s.timer.Reset(time.Duration(s.stekConfig.RotationInterval)) case <-doneChan: // again, see godocs for why timer is stopped this way if !s.timer.Stop() { <-s.timer.C } return } } } var ( lastRotation time.Time keys [][32]byte mutex sync.RWMutex // protects keys and lastRotation ) // Interface guard var _ caddytls.STEKProvider = (*standardSTEKProvider)(nil) caddy-2.6.2/modules/caddytls/storageloader.go000066400000000000000000000047461435007237400212660ustar00rootroot00000000000000// 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 caddytls import ( "crypto/tls" "fmt" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/certmagic" ) func init() { caddy.RegisterModule(StorageLoader{}) } // StorageLoader loads certificates and their associated keys // from the globally configured storage module. type StorageLoader struct { // A list of pairs of certificate and key file names along with their // encoding format so that they can be loaded from storage. Pairs []CertKeyFilePair `json:"pairs,omitempty"` // Reference to the globally configured storage module. storage certmagic.Storage ctx caddy.Context } // CaddyModule returns the Caddy module information. func (StorageLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.certificates.load_storage", New: func() caddy.Module { return new(StorageLoader) }, } } // Provision loads the storage module for sl. func (sl *StorageLoader) Provision(ctx caddy.Context) error { sl.storage = ctx.Storage() sl.ctx = ctx return nil } // LoadCertificates returns the certificates to be loaded by sl. func (sl StorageLoader) LoadCertificates() ([]Certificate, error) { certs := make([]Certificate, 0, len(sl.Pairs)) for _, pair := range sl.Pairs { certData, err := sl.storage.Load(sl.ctx, pair.Certificate) if err != nil { return nil, err } keyData, err := sl.storage.Load(sl.ctx, pair.Key) if err != nil { return nil, err } var cert tls.Certificate switch pair.Format { case "": fallthrough case "pem": cert, err = tls.X509KeyPair(certData, keyData) default: return nil, fmt.Errorf("unrecognized certificate/key encoding format: %s", pair.Format) } if err != nil { return nil, err } certs = append(certs, Certificate{Certificate: cert, Tags: pair.Tags}) } return certs, nil } // Interface guard var ( _ CertificateLoader = (*StorageLoader)(nil) _ caddy.Provisioner = (*StorageLoader)(nil) ) caddy-2.6.2/modules/caddytls/tls.go000066400000000000000000000471311435007237400172300ustar00rootroot00000000000000// 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 caddytls import ( "context" "crypto/tls" "encoding/json" "fmt" "log" "net/http" "runtime/debug" "sync" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) func init() { caddy.RegisterModule(TLS{}) caddy.RegisterModule(AutomateLoader{}) } // TLS provides TLS facilities including certificate // loading and management, client auth, and more. type TLS struct { // Caches certificates in memory for quick use during // TLS handshakes. Each key is the name of a certificate // loader module. All loaded certificates get pooled // into the same cache and may be used to complete TLS // handshakes for the relevant server names (SNI). // Certificates loaded manually (anything other than // "automate") are not automatically managed and will // have to be refreshed manually before they expire. CertificatesRaw caddy.ModuleMap `json:"certificates,omitempty" caddy:"namespace=tls.certificates"` // Configures certificate automation. Automation *AutomationConfig `json:"automation,omitempty"` // Configures session ticket ephemeral keys (STEKs). SessionTickets *SessionTicketService `json:"session_tickets,omitempty"` // Configures the in-memory certificate cache. Cache *CertCacheOptions `json:"cache,omitempty"` // Disables OCSP stapling for manually-managed certificates only. // To configure OCSP stapling for automated certificates, use an // automation policy instead. // // Disabling OCSP stapling puts clients at greater risk, reduces their // privacy, and usually lowers client performance. It is NOT recommended // to disable this unless you are able to justify the costs. // EXPERIMENTAL. Subject to change. DisableOCSPStapling bool `json:"disable_ocsp_stapling,omitempty"` certificateLoaders []CertificateLoader automateNames []string certCache *certmagic.Cache ctx caddy.Context storageCleanTicker *time.Ticker storageCleanStop chan struct{} logger *zap.Logger events *caddyevents.App } // CaddyModule returns the Caddy module information. func (TLS) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls", New: func() caddy.Module { return new(TLS) }, } } // Provision sets up the configuration for the TLS app. func (t *TLS) Provision(ctx caddy.Context) error { eventsAppIface, err := ctx.App("events") if err != nil { return fmt.Errorf("getting events app: %v", err) } t.events = eventsAppIface.(*caddyevents.App) t.ctx = ctx t.logger = ctx.Logger() repl := caddy.NewReplacer() // set up a new certificate cache; this (re)loads all certificates cacheOpts := certmagic.CacheOptions{ GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { return t.getConfigForName(cert.Names[0]), nil }, Logger: t.logger.Named("cache"), } if t.Automation != nil { cacheOpts.OCSPCheckInterval = time.Duration(t.Automation.OCSPCheckInterval) cacheOpts.RenewCheckInterval = time.Duration(t.Automation.RenewCheckInterval) } if t.Cache != nil { cacheOpts.Capacity = t.Cache.Capacity } if cacheOpts.Capacity <= 0 { cacheOpts.Capacity = 10000 } t.certCache = certmagic.NewCache(cacheOpts) // certificate loaders val, err := ctx.LoadModule(t, "CertificatesRaw") if err != nil { return fmt.Errorf("loading certificate loader modules: %s", err) } for modName, modIface := range val.(map[string]any) { if modName == "automate" { // special case; these will be loaded in later using our automation facilities, // which we want to avoid doing during provisioning if automateNames, ok := modIface.(*AutomateLoader); ok && automateNames != nil { t.automateNames = []string(*automateNames) } else { return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface) } continue } t.certificateLoaders = append(t.certificateLoaders, modIface.(CertificateLoader)) } // automation/management policies if t.Automation == nil { t.Automation = new(AutomationConfig) } t.Automation.defaultPublicAutomationPolicy = new(AutomationPolicy) err = t.Automation.defaultPublicAutomationPolicy.Provision(t) if err != nil { return fmt.Errorf("provisioning default public automation policy: %v", err) } for _, n := range t.automateNames { // if any names specified by the "automate" loader do not qualify for a public // certificate, we should initialize a default internal automation policy // (but we don't want to do this unnecessarily, since it may prompt for password!) if certmagic.SubjectQualifiesForPublicCert(n) { continue } t.Automation.defaultInternalAutomationPolicy = &AutomationPolicy{ IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)}, } err = t.Automation.defaultInternalAutomationPolicy.Provision(t) if err != nil { return fmt.Errorf("provisioning default internal automation policy: %v", err) } break } for i, ap := range t.Automation.Policies { err := ap.Provision(t) if err != nil { return fmt.Errorf("provisioning automation policy %d: %v", i, err) } } // session ticket ephemeral keys (STEK) service and provider if t.SessionTickets != nil { err := t.SessionTickets.provision(ctx) if err != nil { return fmt.Errorf("provisioning session tickets configuration: %v", err) } } // on-demand rate limiting if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil { onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst) onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval)) } else { // remove any existing rate limiter onDemandRateLimiter.SetMaxEvents(0) onDemandRateLimiter.SetWindow(0) } // run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036) if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.Ask != "" { t.Automation.OnDemand.Ask, err = repl.ReplaceOrErr(t.Automation.OnDemand.Ask, true, true) if err != nil { return fmt.Errorf("preparing 'ask' endpoint: %v", err) } } // load manual/static (unmanaged) certificates - we do this in // provision so that other apps (such as http) can know which // certificates have been manually loaded, and also so that // commands like validate can be a better test magic := certmagic.New(t.certCache, certmagic.Config{ Storage: ctx.Storage(), Logger: t.logger, OnEvent: t.onEvent, OCSP: certmagic.OCSPConfig{ DisableStapling: t.DisableOCSPStapling, }, }) for _, loader := range t.certificateLoaders { certs, err := loader.LoadCertificates() if err != nil { return fmt.Errorf("loading certificates: %v", err) } for _, cert := range certs { err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags) if err != nil { return fmt.Errorf("caching unmanaged certificate: %v", err) } } } return nil } // Validate validates t's configuration. func (t *TLS) Validate() error { if t.Automation != nil { // ensure that host aren't repeated; since only the first // automation policy is used, repeating a host in the lists // isn't useful and is probably a mistake; same for two // catch-all/default policies var hasDefault bool hostSet := make(map[string]int) for i, ap := range t.Automation.Policies { if len(ap.Subjects) == 0 { if hasDefault { return fmt.Errorf("automation policy %d is the second policy that acts as default/catch-all, but will never be used", i) } hasDefault = true } for _, h := range ap.Subjects { if first, ok := hostSet[h]; ok { return fmt.Errorf("automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)", i, h, first) } hostSet[h] = i } } } if t.Cache != nil { if t.Cache.Capacity < 0 { return fmt.Errorf("cache capacity must be >= 0") } } return nil } // Start activates the TLS module. func (t *TLS) Start() error { // warn if on-demand TLS is enabled but no restrictions are in place if t.Automation.OnDemand == nil || (t.Automation.OnDemand.Ask == "" && t.Automation.OnDemand.RateLimit == nil) { for _, ap := range t.Automation.Policies { if ap.OnDemand { t.logger.Warn("YOUR SERVER MAY BE VULNERABLE TO ABUSE: on-demand TLS is enabled, but no protections are in place", zap.String("docs", "https://caddyserver.com/docs/automatic-https#on-demand-tls")) break } } } // now that we are running, and all manual certificates have // been loaded, time to load the automated/managed certificates err := t.Manage(t.automateNames) if err != nil { return fmt.Errorf("automate: managing %v: %v", t.automateNames, err) } t.keepStorageClean() return nil } // Stop stops the TLS module and cleans up any allocations. func (t *TLS) Stop() error { // stop the storage cleaner goroutine and ticker if t.storageCleanStop != nil { close(t.storageCleanStop) } if t.storageCleanTicker != nil { t.storageCleanTicker.Stop() } return nil } // Cleanup frees up resources allocated during Provision. func (t *TLS) Cleanup() error { // stop the certificate cache if t.certCache != nil { t.certCache.Stop() } // stop the session ticket rotation goroutine if t.SessionTickets != nil { t.SessionTickets.stop() } return nil } // Manage immediately begins managing names according to the // matching automation policy. func (t *TLS) Manage(names []string) error { // for a large number of names, we can be more memory-efficient // by making only one certmagic.Config for all the names that // use that config, rather than calling ManageAsync once for // every name; so first, bin names by AutomationPolicy policyToNames := make(map[*AutomationPolicy][]string) for _, name := range names { ap := t.getAutomationPolicyForName(name) policyToNames[ap] = append(policyToNames[ap], name) } // now that names are grouped by policy, we can simply make one // certmagic.Config for each (potentially large) group of names // and call ManageAsync just once for the whole batch for ap, names := range policyToNames { err := ap.magic.ManageAsync(t.ctx.Context, names) if err != nil { return fmt.Errorf("automate: manage %v: %v", names, err) } } return nil } // HandleHTTPChallenge ensures that the HTTP challenge is handled for the // certificate named by r.Host, if it is an HTTP challenge request. It // requires that the automation policy for r.Host has an issuer of type // *certmagic.ACMEManager, or one that is ACME-enabled (GetACMEIssuer()). func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { // no-op if it's not an ACME challenge request if !certmagic.LooksLikeHTTPChallenge(r) { return false } // try all the issuers until we find the one that initiated the challenge ap := t.getAutomationPolicyForName(r.Host) type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer } for _, iss := range ap.magic.Issuers { if am, ok := iss.(acmeCapable); ok { iss := am.GetACMEIssuer() if iss.issuer.HandleHTTPChallenge(w, r) { return true } } } // it's possible another server in this process initiated the challenge; // users have requested that Caddy only handle HTTP challenges it initiated, // so that users can proxy the others through to their backends; but we // might not have an automation policy for all identifiers that are trying // to get certificates (e.g. the admin endpoint), so we do this manual check if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok { return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge) } return false } // AddAutomationPolicy provisions and adds ap to the list of the app's // automation policies. If an existing automation policy exists that has // fewer hosts in its list than ap does, ap will be inserted before that // other policy (this helps ensure that ap will be prioritized/chosen // over, say, a catch-all policy). func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error { if t.Automation == nil { t.Automation = new(AutomationConfig) } err := ap.Provision(t) if err != nil { return err } // sort new automation policies just before any other which is a superset // of this one; if we find an existing policy that covers every subject in // ap but less specifically (e.g. a catch-all policy, or one with wildcards // or with fewer subjects), insert ap just before it, otherwise ap would // never be used because the first matching policy is more general for i, existing := range t.Automation.Policies { // first see if existing is superset of ap for all names var otherIsSuperset bool outer: for _, thisSubj := range ap.Subjects { for _, otherSubj := range existing.Subjects { if certmagic.MatchWildcard(thisSubj, otherSubj) { otherIsSuperset = true break outer } } } // if existing AP is a superset or if it contains fewer names (i.e. is // more general), then new AP is more specific, so insert before it if otherIsSuperset || len(existing.Subjects) < len(ap.Subjects) { t.Automation.Policies = append(t.Automation.Policies[:i], append([]*AutomationPolicy{ap}, t.Automation.Policies[i:]...)...) return nil } } // otherwise just append the new one t.Automation.Policies = append(t.Automation.Policies, ap) return nil } func (t *TLS) getConfigForName(name string) *certmagic.Config { ap := t.getAutomationPolicyForName(name) return ap.magic } // getAutomationPolicyForName returns the first matching automation policy // for the given subject name. If no matching policy can be found, the // default policy is used, depending on whether the name qualifies for a // public certificate or not. func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy { for _, ap := range t.Automation.Policies { if len(ap.Subjects) == 0 { return ap // no host filter is an automatic match } for _, h := range ap.Subjects { if certmagic.MatchWildcard(name, h) { return ap } } } if certmagic.SubjectQualifiesForPublicCert(name) || t.Automation.defaultInternalAutomationPolicy == nil { return t.Automation.defaultPublicAutomationPolicy } return t.Automation.defaultInternalAutomationPolicy } // AllMatchingCertificates returns the list of all certificates in // the cache which could be used to satisfy the given SAN. func (t *TLS) AllMatchingCertificates(san string) []certmagic.Certificate { return t.certCache.AllMatchingCertificates(san) } // keepStorageClean starts a goroutine that immediately cleans up all // known storage units if it was not recently done, and then runs the // operation at every tick from t.storageCleanTicker. func (t *TLS) keepStorageClean() { t.storageCleanTicker = time.NewTicker(t.storageCleanInterval()) t.storageCleanStop = make(chan struct{}) go func() { defer func() { if err := recover(); err != nil { log.Printf("[PANIC] storage cleaner: %v\n%s", err, debug.Stack()) } }() t.cleanStorageUnits() for { select { case <-t.storageCleanStop: return case <-t.storageCleanTicker.C: t.cleanStorageUnits() } } }() } func (t *TLS) cleanStorageUnits() { storageCleanMu.Lock() defer storageCleanMu.Unlock() // If storage was cleaned recently, don't do it again for now. Although the ticker // calling this function drops missed ticks for us, config reloads discard the old // ticker and replace it with a new one, possibly invoking a cleaning to happen again // too soon. (We divide the interval by 2 because the actual cleaning takes non-zero // time, and we don't want to skip cleanings if we don't have to; whereas if a cleaning // took most of the interval, we'd probably want to skip the next one so we aren't // constantly cleaning. This allows cleanings to take up to half the interval's // duration before we decide to skip the next one.) if !storageClean.IsZero() && time.Since(storageClean) < t.storageCleanInterval()/2 { return } options := certmagic.CleanStorageOptions{ OCSPStaples: true, ExpiredCerts: true, ExpiredCertGracePeriod: 24 * time.Hour * 14, } // avoid cleaning same storage more than once per cleaning cycle storagesCleaned := make(map[string]struct{}) // start with the default/global storage storage := t.ctx.Storage() storageStr := fmt.Sprintf("%v", storage) t.logger.Info("cleaning storage unit", zap.String("description", storageStr)) certmagic.CleanStorage(t.ctx, storage, options) storagesCleaned[storageStr] = struct{}{} // then clean each storage defined in ACME automation policies if t.Automation != nil { for _, ap := range t.Automation.Policies { if ap.storage == nil { continue } storageStr := fmt.Sprintf("%v", ap.storage) if _, ok := storagesCleaned[storageStr]; ok { continue } t.logger.Info("cleaning storage unit", zap.String("description", storageStr)) certmagic.CleanStorage(t.ctx, ap.storage, options) storagesCleaned[storageStr] = struct{}{} } } // remember last time storage was finished cleaning storageClean = time.Now() t.logger.Info("finished cleaning storage units") } func (t *TLS) storageCleanInterval() time.Duration { if t.Automation != nil && t.Automation.StorageCleanInterval > 0 { return time.Duration(t.Automation.StorageCleanInterval) } return defaultStorageCleanInterval } // onEvent translates CertMagic events into Caddy events then dispatches them. func (t *TLS) onEvent(ctx context.Context, eventName string, data map[string]any) error { evt := t.events.Emit(t.ctx, eventName, data) return evt.Aborted } // CertificateLoader is a type that can load certificates. // Certificates can optionally be associated with tags. type CertificateLoader interface { LoadCertificates() ([]Certificate, error) } // Certificate is a TLS certificate, optionally // associated with arbitrary tags. type Certificate struct { tls.Certificate Tags []string } // AutomateLoader will automatically manage certificates for the names in the // list, including obtaining and renewing certificates. Automated certificates // are managed according to their matching automation policy, configured // elsewhere in this app. // // Technically, this is a no-op certificate loader module that is treated as // a special case: it uses this app's automation features to load certificates // for the list of hostnames, rather than loading certificates manually. type AutomateLoader []string // CaddyModule returns the Caddy module information. func (AutomateLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.certificates.automate", New: func() caddy.Module { return new(AutomateLoader) }, } } // CertCacheOptions configures the certificate cache. type CertCacheOptions struct { // Maximum number of certificates to allow in the // cache. If reached, certificates will be randomly // evicted to make room for new ones. Default: 10,000 Capacity int `json:"capacity,omitempty"` } // Variables related to storage cleaning. var ( defaultStorageCleanInterval = 24 * time.Hour storageClean time.Time storageCleanMu sync.Mutex ) // Interface guards var ( _ caddy.App = (*TLS)(nil) _ caddy.Provisioner = (*TLS)(nil) _ caddy.Validator = (*TLS)(nil) _ caddy.CleanerUpper = (*TLS)(nil) ) caddy-2.6.2/modules/caddytls/values.go000066400000000000000000000112471435007237400177240ustar00rootroot00000000000000// 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 caddytls import ( "crypto/tls" "crypto/x509" "fmt" "github.com/caddyserver/certmagic" "github.com/klauspost/cpuid/v2" ) // CipherSuiteNameSupported returns true if name is // a supported cipher suite. func CipherSuiteNameSupported(name string) bool { return CipherSuiteID(name) != 0 } // CipherSuiteID returns the ID of the cipher suite associated with // the given name, or 0 if the name is not recognized/supported. func CipherSuiteID(name string) uint16 { for _, cs := range SupportedCipherSuites() { if cs.Name == name { return cs.ID } } return 0 } // SupportedCipherSuites returns a list of all the cipher suites // Caddy supports. The list is NOT ordered by security preference. func SupportedCipherSuites() []*tls.CipherSuite { return tls.CipherSuites() } // defaultCipherSuites is the ordered list of all the cipher // suites we want to support by default, assuming AES-NI // (hardware acceleration for AES). var defaultCipherSuitesWithAESNI = []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, } // defaultCipherSuites is the ordered list of all the cipher // suites we want to support by default, assuming lack of // AES-NI (NO hardware acceleration for AES). var defaultCipherSuitesWithoutAESNI = []uint16{ tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, } // getOptimalDefaultCipherSuites returns an appropriate cipher // suite to use depending on the hardware support for AES. // // See https://github.com/caddyserver/caddy/issues/1674 func getOptimalDefaultCipherSuites() []uint16 { if cpuid.CPU.Supports(cpuid.AESNI) { return defaultCipherSuitesWithAESNI } return defaultCipherSuitesWithoutAESNI } // SupportedCurves is the unordered map of supported curves. // https://golang.org/pkg/crypto/tls/#CurveID var SupportedCurves = map[string]tls.CurveID{ "x25519": tls.X25519, "secp256r1": tls.CurveP256, "secp384r1": tls.CurveP384, "secp521r1": tls.CurveP521, } // supportedCertKeyTypes is all the key types that are supported // for certificates that are obtained through ACME. var supportedCertKeyTypes = map[string]certmagic.KeyType{ "rsa2048": certmagic.RSA2048, "rsa4096": certmagic.RSA4096, "p256": certmagic.P256, "p384": certmagic.P384, "ed25519": certmagic.ED25519, } // defaultCurves is the list of only the curves we want to use // by default, in descending order of preference. // // This list should only include curves which are fast by design // (e.g. X25519) and those for which an optimized assembly // implementation exists (e.g. P256). The latter ones can be // found here: // https://github.com/golang/go/tree/master/src/crypto/elliptic var defaultCurves = []tls.CurveID{ tls.X25519, tls.CurveP256, } // SupportedProtocols is a map of supported protocols. var SupportedProtocols = map[string]uint16{ "tls1.2": tls.VersionTLS12, "tls1.3": tls.VersionTLS13, } // unsupportedProtocols is a map of unsupported protocols. // Used for logging only, not enforcement. var unsupportedProtocols = map[string]uint16{ //nolint:staticcheck "ssl3.0": tls.VersionSSL30, "tls1.0": tls.VersionTLS10, "tls1.1": tls.VersionTLS11, } // publicKeyAlgorithms is the map of supported public key algorithms. var publicKeyAlgorithms = map[string]x509.PublicKeyAlgorithm{ "rsa": x509.RSA, "dsa": x509.DSA, "ecdsa": x509.ECDSA, } // ProtocolName returns the standard name for the passed protocol version ID // (e.g. "TLS1.3") or a fallback representation of the ID value if the version // is not supported. func ProtocolName(id uint16) string { for k, v := range SupportedProtocols { if v == id { return k } } for k, v := range unsupportedProtocols { if v == id { return k } } return fmt.Sprintf("0x%04x", id) } caddy-2.6.2/modules/caddytls/zerosslissuer.go000066400000000000000000000174611435007237400213650ustar00rootroot00000000000000// 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 caddytls import ( "context" "crypto/x509" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "sync" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/certmagic" "github.com/mholt/acmez/acme" "go.uber.org/zap" ) func init() { caddy.RegisterModule(new(ZeroSSLIssuer)) } // ZeroSSLIssuer makes an ACME issuer for getting certificates // from ZeroSSL by automatically generating EAB credentials. // Please be sure to set a valid email address in your config // so you can access/manage your domains in your ZeroSSL account. // // This issuer is only needed for automatic generation of EAB // credentials. If manually configuring/reusing EAB credentials, // the standard ACMEIssuer may be used if desired. type ZeroSSLIssuer struct { *ACMEIssuer // The API key (or "access key") for using the ZeroSSL API. // This is optional, but can be used if you have an API key // already and don't want to supply your email address. APIKey string `json:"api_key,omitempty"` mu sync.Mutex logger *zap.Logger } // CaddyModule returns the Caddy module information. func (*ZeroSSLIssuer) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "tls.issuance.zerossl", New: func() caddy.Module { return new(ZeroSSLIssuer) }, } } // Provision sets up iss. func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error { iss.logger = ctx.Logger() if iss.ACMEIssuer == nil { iss.ACMEIssuer = new(ACMEIssuer) } if iss.ACMEIssuer.CA == "" { iss.ACMEIssuer.CA = certmagic.ZeroSSLProductionCA } return iss.ACMEIssuer.Provision(ctx) } // newAccountCallback generates EAB if not already provided. It also sets a valid default contact on the account if not set. func (iss *ZeroSSLIssuer) newAccountCallback(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) { if acmeIss.ExternalAccount != nil { return acct, nil } var err error acmeIss.ExternalAccount, acct, err = iss.generateEABCredentials(ctx, acct) return acct, err } // generateEABCredentials generates EAB credentials using the API key if provided, // otherwise using the primary contact email on the issuer. If an email is not set // on the issuer, a default generic email is used. func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, error) { var endpoint string var body io.Reader // there are two ways to generate EAB credentials: authenticated with // their API key, or unauthenticated with their email address if iss.APIKey != "" { apiKey := caddy.NewReplacer().ReplaceAll(iss.APIKey, "") if apiKey == "" { return nil, acct, fmt.Errorf("missing API key: '%v'", iss.APIKey) } qs := url.Values{"access_key": []string{apiKey}} endpoint = fmt.Sprintf("%s/eab-credentials?%s", zerosslAPIBase, qs.Encode()) } else { email := iss.Email if email == "" { iss.logger.Warn("missing email address for ZeroSSL; it is strongly recommended to set one for next time") email = "caddy@zerossl.com" // special email address that preserves backwards-compat, but which black-holes dashboard features, oh well } if len(acct.Contact) == 0 { // we borrow the email from config or the default email, so ensure it's saved with the account acct.Contact = []string{"mailto:" + email} } endpoint = zerosslAPIBase + "/eab-credentials-email" form := url.Values{"email": []string{email}} body = strings.NewReader(form.Encode()) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) if err != nil { return nil, acct, fmt.Errorf("forming request: %v", err) } if body != nil { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } req.Header.Set("User-Agent", certmagic.UserAgent) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err) } defer resp.Body.Close() var result struct { Success bool `json:"success"` Error struct { Code int `json:"code"` Type string `json:"type"` } `json:"error"` EABKID string `json:"eab_kid"` EABHMACKey string `json:"eab_hmac_key"` } err = json.NewDecoder(resp.Body).Decode(&result) if err != nil { return nil, acct, fmt.Errorf("decoding API response: %v", err) } if result.Error.Code != 0 { return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d: %s (code %d)", resp.StatusCode, result.Error.Type, result.Error.Code) } if resp.StatusCode != http.StatusOK { return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode) } iss.logger.Info("generated EAB credentials", zap.String("key_id", result.EABKID)) return &acme.EAB{ KeyID: result.EABKID, MACKey: result.EABHMACKey, }, acct, nil } // initialize modifies the template for the underlying ACMEIssuer // values by setting the CA endpoint to the ZeroSSL directory and // setting the NewAccountFunc callback to one which allows us to // generate EAB credentials only if a new account is being made. // Since it modifies the stored template, its effect should only // be needed once, but it is fine to call it repeatedly. func (iss *ZeroSSLIssuer) initialize() { iss.mu.Lock() defer iss.mu.Unlock() if iss.ACMEIssuer.issuer.NewAccountFunc == nil { iss.ACMEIssuer.issuer.NewAccountFunc = iss.newAccountCallback } } // PreCheck implements the certmagic.PreChecker interface. func (iss *ZeroSSLIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error { iss.initialize() return iss.ACMEIssuer.PreCheck(ctx, names, interactive) } // Issue obtains a certificate for the given csr. func (iss *ZeroSSLIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { iss.initialize() return iss.ACMEIssuer.Issue(ctx, csr) } // IssuerKey returns the unique issuer key for the configured CA endpoint. func (iss *ZeroSSLIssuer) IssuerKey() string { iss.initialize() return iss.ACMEIssuer.IssuerKey() } // Revoke revokes the given certificate. func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error { iss.initialize() return iss.ACMEIssuer.Revoke(ctx, cert, reason) } // UnmarshalCaddyfile deserializes Caddyfile tokens into iss. // // ... zerossl [] { // ... // } // // Any of the subdirectives for the ACME issuer can be used in the block. func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { iss.APIKey = d.Val() if d.NextArg() { return d.ArgErr() } } if iss.ACMEIssuer == nil { iss.ACMEIssuer = new(ACMEIssuer) } err := iss.ACMEIssuer.UnmarshalCaddyfile(d.NewFromNextSegment()) if err != nil { return err } } return nil } const zerosslAPIBase = "https://api.zerossl.com/acme" // Interface guards var ( _ certmagic.PreChecker = (*ZeroSSLIssuer)(nil) _ certmagic.Issuer = (*ZeroSSLIssuer)(nil) _ certmagic.Revoker = (*ZeroSSLIssuer)(nil) _ caddy.Provisioner = (*ZeroSSLIssuer)(nil) _ ConfigSetter = (*ZeroSSLIssuer)(nil) // a type which properly embeds an ACMEIssuer should implement // this interface so it can be treated as an ACMEIssuer _ interface{ GetACMEIssuer() *ACMEIssuer } = (*ZeroSSLIssuer)(nil) ) caddy-2.6.2/modules/filestorage/000077500000000000000000000000001435007237400165665ustar00rootroot00000000000000caddy-2.6.2/modules/filestorage/filestorage.go000066400000000000000000000043661435007237400214320ustar00rootroot00000000000000// 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 filestorage import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/certmagic" ) func init() { caddy.RegisterModule(FileStorage{}) } // FileStorage is a certmagic.Storage wrapper for certmagic.FileStorage. type FileStorage struct { // The base path to the folder used for storage. Root string `json:"root,omitempty"` } // CaddyModule returns the Caddy module information. func (FileStorage) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.storage.file_system", New: func() caddy.Module { return new(FileStorage) }, } } // CertMagicStorage converts s to a certmagic.Storage instance. func (s FileStorage) CertMagicStorage() (certmagic.Storage, error) { return &certmagic.FileStorage{Path: s.Root}, nil } // UnmarshalCaddyfile sets up the storage module from Caddyfile tokens. func (s *FileStorage) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !d.Next() { return d.Err("expected tokens") } if d.NextArg() { s.Root = d.Val() } if d.NextArg() { return d.ArgErr() } for nesting := d.Nesting(); d.NextBlock(nesting); { switch d.Val() { case "root": if !d.NextArg() { return d.ArgErr() } if s.Root != "" { return d.Err("root already set") } s.Root = d.Val() if d.NextArg() { return d.ArgErr() } default: return d.Errf("unrecognized parameter '%s'", d.Val()) } } if s.Root == "" { return d.Err("missing root path (to use default, omit storage config entirely)") } return nil } // Interface guards var ( _ caddy.StorageConverter = (*FileStorage)(nil) _ caddyfile.Unmarshaler = (*FileStorage)(nil) ) caddy-2.6.2/modules/logging/000077500000000000000000000000001435007237400157105ustar00rootroot00000000000000caddy-2.6.2/modules/logging/encoders.go000066400000000000000000000172351435007237400200510ustar00rootroot00000000000000// 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 logging import ( "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "go.uber.org/zap" "go.uber.org/zap/buffer" "go.uber.org/zap/zapcore" ) func init() { caddy.RegisterModule(ConsoleEncoder{}) caddy.RegisterModule(JSONEncoder{}) } // ConsoleEncoder encodes log entries that are mostly human-readable. type ConsoleEncoder struct { zapcore.Encoder `json:"-"` LogEncoderConfig } // CaddyModule returns the Caddy module information. func (ConsoleEncoder) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.console", New: func() caddy.Module { return new(ConsoleEncoder) }, } } // Provision sets up the encoder. func (ce *ConsoleEncoder) Provision(_ caddy.Context) error { if ce.LevelFormat == "" { ce.LevelFormat = "color" } if ce.TimeFormat == "" { ce.TimeFormat = "wall_milli" } ce.Encoder = zapcore.NewConsoleEncoder(ce.ZapcoreEncoderConfig()) return nil } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: // // console { // // } // // See the godoc on the LogEncoderConfig type for the syntax of // subdirectives that are common to most/all encoders. func (ce *ConsoleEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { return d.ArgErr() } err := ce.LogEncoderConfig.UnmarshalCaddyfile(d) if err != nil { return err } } return nil } // JSONEncoder encodes entries as JSON. type JSONEncoder struct { zapcore.Encoder `json:"-"` LogEncoderConfig } // CaddyModule returns the Caddy module information. func (JSONEncoder) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.json", New: func() caddy.Module { return new(JSONEncoder) }, } } // Provision sets up the encoder. func (je *JSONEncoder) Provision(_ caddy.Context) error { je.Encoder = zapcore.NewJSONEncoder(je.ZapcoreEncoderConfig()) return nil } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: // // json { // // } // // See the godoc on the LogEncoderConfig type for the syntax of // subdirectives that are common to most/all encoders. func (je *JSONEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { return d.ArgErr() } err := je.LogEncoderConfig.UnmarshalCaddyfile(d) if err != nil { return err } } return nil } // LogEncoderConfig holds configuration common to most encoders. type LogEncoderConfig struct { MessageKey *string `json:"message_key,omitempty"` LevelKey *string `json:"level_key,omitempty"` TimeKey *string `json:"time_key,omitempty"` NameKey *string `json:"name_key,omitempty"` CallerKey *string `json:"caller_key,omitempty"` StacktraceKey *string `json:"stacktrace_key,omitempty"` LineEnding *string `json:"line_ending,omitempty"` TimeFormat string `json:"time_format,omitempty"` TimeLocal bool `json:"time_local,omitempty"` DurationFormat string `json:"duration_format,omitempty"` LevelFormat string `json:"level_format,omitempty"` } // UnmarshalCaddyfile populates the struct from Caddyfile tokens. Syntax: // // { // message_key // level_key // time_key // name_key // caller_key // stacktrace_key // line_ending // time_format // time_local // duration_format // level_format // } func (lec *LogEncoderConfig) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for nesting := d.Nesting(); d.NextBlock(nesting); { subdir := d.Val() switch subdir { case "time_local": lec.TimeLocal = true if d.NextArg() { return d.ArgErr() } continue } var arg string if !d.AllArgs(&arg) { return d.ArgErr() } switch subdir { case "message_key": lec.MessageKey = &arg case "level_key": lec.LevelKey = &arg case "time_key": lec.TimeKey = &arg case "name_key": lec.NameKey = &arg case "caller_key": lec.CallerKey = &arg case "stacktrace_key": lec.StacktraceKey = &arg case "line_ending": lec.LineEnding = &arg case "time_format": lec.TimeFormat = arg case "duration_format": lec.DurationFormat = arg case "level_format": lec.LevelFormat = arg default: return d.Errf("unrecognized subdirective %s", subdir) } } return nil } // ZapcoreEncoderConfig returns the equivalent zapcore.EncoderConfig. // If lec is nil, zap.NewProductionEncoderConfig() is returned. func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig { cfg := zap.NewProductionEncoderConfig() if lec == nil { lec = new(LogEncoderConfig) } if lec.MessageKey != nil { cfg.MessageKey = *lec.MessageKey } if lec.LevelKey != nil { cfg.LevelKey = *lec.LevelKey } if lec.TimeKey != nil { cfg.TimeKey = *lec.TimeKey } if lec.NameKey != nil { cfg.NameKey = *lec.NameKey } if lec.CallerKey != nil { cfg.CallerKey = *lec.CallerKey } if lec.StacktraceKey != nil { cfg.StacktraceKey = *lec.StacktraceKey } if lec.LineEnding != nil { cfg.LineEnding = *lec.LineEnding } // time format var timeFormatter zapcore.TimeEncoder switch lec.TimeFormat { case "", "unix_seconds_float": timeFormatter = zapcore.EpochTimeEncoder case "unix_milli_float": timeFormatter = zapcore.EpochMillisTimeEncoder case "unix_nano": timeFormatter = zapcore.EpochNanosTimeEncoder case "iso8601": timeFormatter = zapcore.ISO8601TimeEncoder default: timeFormat := lec.TimeFormat switch lec.TimeFormat { case "rfc3339": timeFormat = time.RFC3339 case "rfc3339_nano": timeFormat = time.RFC3339Nano case "wall": timeFormat = "2006/01/02 15:04:05" case "wall_milli": timeFormat = "2006/01/02 15:04:05.000" case "wall_nano": timeFormat = "2006/01/02 15:04:05.000000000" case "common_log": timeFormat = "02/Jan/2006:15:04:05 -0700" } timeFormatter = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) { var time time.Time if lec.TimeLocal { time = ts.Local() } else { time = ts.UTC() } encoder.AppendString(time.Format(timeFormat)) } } cfg.EncodeTime = timeFormatter // duration format var durFormatter zapcore.DurationEncoder switch lec.DurationFormat { case "", "seconds": durFormatter = zapcore.SecondsDurationEncoder case "nano": durFormatter = zapcore.NanosDurationEncoder case "string": durFormatter = zapcore.StringDurationEncoder } cfg.EncodeDuration = durFormatter // level format var levelFormatter zapcore.LevelEncoder switch lec.LevelFormat { case "", "lower": levelFormatter = zapcore.LowercaseLevelEncoder case "upper": levelFormatter = zapcore.CapitalLevelEncoder case "color": levelFormatter = zapcore.CapitalColorLevelEncoder } cfg.EncodeLevel = levelFormatter return cfg } var bufferpool = buffer.NewPool() // Interface guards var ( _ zapcore.Encoder = (*ConsoleEncoder)(nil) _ zapcore.Encoder = (*JSONEncoder)(nil) _ caddyfile.Unmarshaler = (*ConsoleEncoder)(nil) _ caddyfile.Unmarshaler = (*JSONEncoder)(nil) ) caddy-2.6.2/modules/logging/filewriter.go000066400000000000000000000136231435007237400204200ustar00rootroot00000000000000// 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 logging import ( "fmt" "io" "math" "os" "path/filepath" "strconv" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/dustin/go-humanize" "gopkg.in/natefinch/lumberjack.v2" ) func init() { caddy.RegisterModule(FileWriter{}) } // FileWriter can write logs to files. By default, log files // are rotated ("rolled") when they get large, and old log // files get deleted, to ensure that the process does not // exhaust disk space. type FileWriter struct { // Filename is the name of the file to write. Filename string `json:"filename,omitempty"` // Roll toggles log rolling or rotation, which is // enabled by default. Roll *bool `json:"roll,omitempty"` // When a log file reaches approximately this size, // it will be rotated. RollSizeMB int `json:"roll_size_mb,omitempty"` // Whether to compress rolled files. Default: true RollCompress *bool `json:"roll_gzip,omitempty"` // Whether to use local timestamps in rolled filenames. // Default: false RollLocalTime bool `json:"roll_local_time,omitempty"` // The maximum number of rolled log files to keep. // Default: 10 RollKeep int `json:"roll_keep,omitempty"` // How many days to keep rolled log files. Default: 90 RollKeepDays int `json:"roll_keep_days,omitempty"` } // CaddyModule returns the Caddy module information. func (FileWriter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.writers.file", New: func() caddy.Module { return new(FileWriter) }, } } // Provision sets up the module func (fw *FileWriter) Provision(ctx caddy.Context) error { // Replace placeholder in filename repl := caddy.NewReplacer() filename, err := repl.ReplaceOrErr(fw.Filename, true, true) if err != nil { return fmt.Errorf("invalid filename for log file: %v", err) } fw.Filename = filename return nil } func (fw FileWriter) String() string { fpath, err := filepath.Abs(fw.Filename) if err == nil { return fpath } return fw.Filename } // WriterKey returns a unique key representing this fw. func (fw FileWriter) WriterKey() string { return "file:" + fw.Filename } // OpenWriter opens a new file writer. func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { // roll log files by default if fw.Roll == nil || *fw.Roll { if fw.RollSizeMB == 0 { fw.RollSizeMB = 100 } if fw.RollCompress == nil { compress := true fw.RollCompress = &compress } if fw.RollKeep == 0 { fw.RollKeep = 10 } if fw.RollKeepDays == 0 { fw.RollKeepDays = 90 } return &lumberjack.Logger{ Filename: fw.Filename, MaxSize: fw.RollSizeMB, MaxAge: fw.RollKeepDays, MaxBackups: fw.RollKeep, LocalTime: fw.RollLocalTime, Compress: *fw.RollCompress, }, nil } // otherwise just open a regular file return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: // // file { // roll_disabled // roll_size // roll_uncompressed // roll_local_time // roll_keep // roll_keep_for // } // // The roll_size value has megabyte resolution. // Fractional values are rounded up to the next whole megabyte (MiB). // // By default, compression is enabled, but can be turned off by setting // the roll_uncompressed option. // // The roll_keep_for duration has day resolution. // Fractional values are rounded up to the next whole number of days. // // If any of the roll_size, roll_keep, or roll_keep_for subdirectives are // omitted or set to a zero value, then Caddy's default value for that // subdirective is used. func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if !d.NextArg() { return d.ArgErr() } fw.Filename = d.Val() if d.NextArg() { return d.ArgErr() } for d.NextBlock(0) { switch d.Val() { case "roll_disabled": var f bool fw.Roll = &f if d.NextArg() { return d.ArgErr() } case "roll_size": var sizeStr string if !d.AllArgs(&sizeStr) { return d.ArgErr() } size, err := humanize.ParseBytes(sizeStr) if err != nil { return d.Errf("parsing size: %v", err) } fw.RollSizeMB = int(math.Ceil(float64(size) / humanize.MiByte)) case "roll_uncompressed": var f bool fw.RollCompress = &f if d.NextArg() { return d.ArgErr() } case "roll_local_time": fw.RollLocalTime = true if d.NextArg() { return d.ArgErr() } case "roll_keep": var keepStr string if !d.AllArgs(&keepStr) { return d.ArgErr() } keep, err := strconv.Atoi(keepStr) if err != nil { return d.Errf("parsing roll_keep number: %v", err) } fw.RollKeep = keep case "roll_keep_for": var keepForStr string if !d.AllArgs(&keepForStr) { return d.ArgErr() } keepFor, err := caddy.ParseDuration(keepForStr) if err != nil { return d.Errf("parsing roll_keep_for duration: %v", err) } if keepFor < 0 { return d.Errf("negative roll_keep_for duration: %v", keepFor) } fw.RollKeepDays = int(math.Ceil(keepFor.Hours() / 24)) } } } return nil } // Interface guards var ( _ caddy.Provisioner = (*FileWriter)(nil) _ caddy.WriterOpener = (*FileWriter)(nil) _ caddyfile.Unmarshaler = (*FileWriter)(nil) ) caddy-2.6.2/modules/logging/filterencoder.go000066400000000000000000000266761435007237400211050ustar00rootroot00000000000000// 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 logging import ( "encoding/json" "fmt" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "go.uber.org/zap" "go.uber.org/zap/buffer" "go.uber.org/zap/zapcore" ) func init() { caddy.RegisterModule(FilterEncoder{}) } // FilterEncoder can filter (manipulate) fields on // log entries before they are actually encoded by // an underlying encoder. type FilterEncoder struct { // The underlying encoder that actually // encodes the log entries. Required. WrappedRaw json.RawMessage `json:"wrap,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"` // A map of field names to their filters. Note that this // is not a module map; the keys are field names. // // Nested fields can be referenced by representing a // layer of nesting with `>`. In other words, for an // object like `{"a":{"b":0}}`, the inner field can // be referenced as `a>b`. // // The following fields are fundamental to the log and // cannot be filtered because they are added by the // underlying logging library as special cases: ts, // level, logger, and msg. FieldsRaw map[string]json.RawMessage `json:"fields,omitempty" caddy:"namespace=caddy.logging.encoders.filter inline_key=filter"` wrapped zapcore.Encoder Fields map[string]LogFieldFilter `json:"-"` // used to keep keys unique across nested objects keyPrefix string } // CaddyModule returns the Caddy module information. func (FilterEncoder) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.filter", New: func() caddy.Module { return new(FilterEncoder) }, } } // Provision sets up the encoder. func (fe *FilterEncoder) Provision(ctx caddy.Context) error { if fe.WrappedRaw == nil { return fmt.Errorf("missing \"wrap\" (must specify an underlying encoder)") } // set up wrapped encoder (required) val, err := ctx.LoadModule(fe, "WrappedRaw") if err != nil { return fmt.Errorf("loading fallback encoder module: %v", err) } fe.wrapped = val.(zapcore.Encoder) // set up each field filter if fe.Fields == nil { fe.Fields = make(map[string]LogFieldFilter) } vals, err := ctx.LoadModule(fe, "FieldsRaw") if err != nil { return fmt.Errorf("loading log filter modules: %v", err) } for fieldName, modIface := range vals.(map[string]any) { fe.Fields[fieldName] = modIface.(LogFieldFilter) } return nil } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: // // filter { // wrap // fields { // { // // } // } // } func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { for d.NextBlock(0) { switch d.Val() { case "wrap": if !d.NextArg() { return d.ArgErr() } moduleName := d.Val() moduleID := "caddy.logging.encoders." + moduleName unm, err := caddyfile.UnmarshalModule(d, moduleID) if err != nil { return err } enc, ok := unm.(zapcore.Encoder) if !ok { return d.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm) } fe.WrappedRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, nil) case "fields": for d.NextBlock(1) { field := d.Val() if !d.NextArg() { return d.ArgErr() } filterName := d.Val() moduleID := "caddy.logging.encoders.filter." + filterName unm, err := caddyfile.UnmarshalModule(d, moduleID) if err != nil { return err } filter, ok := unm.(LogFieldFilter) if !ok { return d.Errf("module %s (%T) is not a logging.LogFieldFilter", moduleID, unm) } if fe.FieldsRaw == nil { fe.FieldsRaw = make(map[string]json.RawMessage) } fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filter, "filter", filterName, nil) } default: return d.Errf("unrecognized subdirective %s", d.Val()) } } } return nil } // AddArray is part of the zapcore.ObjectEncoder interface. // Array elements do not get filtered. func (fe FilterEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { if filter, ok := fe.Fields[fe.keyPrefix+key]; ok { filter.Filter(zap.Array(key, marshaler)).AddTo(fe.wrapped) return nil } return fe.wrapped.AddArray(key, marshaler) } // AddObject is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { if fe.filtered(key, marshaler) { return nil } fe.keyPrefix += key + ">" return fe.wrapped.AddObject(key, logObjectMarshalerWrapper{ enc: fe, marsh: marshaler, }) } // AddBinary is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddBinary(key string, value []byte) { if !fe.filtered(key, value) { fe.wrapped.AddBinary(key, value) } } // AddByteString is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddByteString(key string, value []byte) { if !fe.filtered(key, value) { fe.wrapped.AddByteString(key, value) } } // AddBool is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddBool(key string, value bool) { if !fe.filtered(key, value) { fe.wrapped.AddBool(key, value) } } // AddComplex128 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddComplex128(key string, value complex128) { if !fe.filtered(key, value) { fe.wrapped.AddComplex128(key, value) } } // AddComplex64 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddComplex64(key string, value complex64) { if !fe.filtered(key, value) { fe.wrapped.AddComplex64(key, value) } } // AddDuration is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddDuration(key string, value time.Duration) { if !fe.filtered(key, value) { fe.wrapped.AddDuration(key, value) } } // AddFloat64 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddFloat64(key string, value float64) { if !fe.filtered(key, value) { fe.wrapped.AddFloat64(key, value) } } // AddFloat32 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddFloat32(key string, value float32) { if !fe.filtered(key, value) { fe.wrapped.AddFloat32(key, value) } } // AddInt is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddInt(key string, value int) { if !fe.filtered(key, value) { fe.wrapped.AddInt(key, value) } } // AddInt64 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddInt64(key string, value int64) { if !fe.filtered(key, value) { fe.wrapped.AddInt64(key, value) } } // AddInt32 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddInt32(key string, value int32) { if !fe.filtered(key, value) { fe.wrapped.AddInt32(key, value) } } // AddInt16 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddInt16(key string, value int16) { if !fe.filtered(key, value) { fe.wrapped.AddInt16(key, value) } } // AddInt8 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddInt8(key string, value int8) { if !fe.filtered(key, value) { fe.wrapped.AddInt8(key, value) } } // AddString is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddString(key, value string) { if !fe.filtered(key, value) { fe.wrapped.AddString(key, value) } } // AddTime is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddTime(key string, value time.Time) { if !fe.filtered(key, value) { fe.wrapped.AddTime(key, value) } } // AddUint is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddUint(key string, value uint) { if !fe.filtered(key, value) { fe.wrapped.AddUint(key, value) } } // AddUint64 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddUint64(key string, value uint64) { if !fe.filtered(key, value) { fe.wrapped.AddUint64(key, value) } } // AddUint32 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddUint32(key string, value uint32) { if !fe.filtered(key, value) { fe.wrapped.AddUint32(key, value) } } // AddUint16 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddUint16(key string, value uint16) { if !fe.filtered(key, value) { fe.wrapped.AddUint16(key, value) } } // AddUint8 is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddUint8(key string, value uint8) { if !fe.filtered(key, value) { fe.wrapped.AddUint8(key, value) } } // AddUintptr is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddUintptr(key string, value uintptr) { if !fe.filtered(key, value) { fe.wrapped.AddUintptr(key, value) } } // AddReflected is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) AddReflected(key string, value any) error { if !fe.filtered(key, value) { return fe.wrapped.AddReflected(key, value) } return nil } // OpenNamespace is part of the zapcore.ObjectEncoder interface. func (fe FilterEncoder) OpenNamespace(key string) { fe.wrapped.OpenNamespace(key) } // Clone is part of the zapcore.ObjectEncoder interface. // We don't use it as of Oct 2019 (v2 beta 7), I'm not // really sure what it'd be useful for in our case. func (fe FilterEncoder) Clone() zapcore.Encoder { return FilterEncoder{ Fields: fe.Fields, wrapped: fe.wrapped.Clone(), keyPrefix: fe.keyPrefix, } } // EncodeEntry partially implements the zapcore.Encoder interface. func (fe FilterEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { // without this clone and storing it to fe.wrapped, fields // from subsequent log entries get appended to previous // ones, and I'm not 100% sure why; see end of // https://github.com/uber-go/zap/issues/750 fe.wrapped = fe.wrapped.Clone() for _, field := range fields { field.AddTo(fe) } return fe.wrapped.EncodeEntry(ent, nil) } // filtered returns true if the field was filtered. // If true is returned, the field was filtered and // added to the underlying encoder (so do not do // that again). If false was returned, the field has // not yet been added to the underlying encoder. func (fe FilterEncoder) filtered(key string, value any) bool { filter, ok := fe.Fields[fe.keyPrefix+key] if !ok { return false } filter.Filter(zap.Any(key, value)).AddTo(fe.wrapped) return true } // logObjectMarshalerWrapper allows us to recursively // filter fields of objects as they get encoded. type logObjectMarshalerWrapper struct { enc FilterEncoder marsh zapcore.ObjectMarshaler } // MarshalLogObject implements the zapcore.ObjectMarshaler interface. func (mom logObjectMarshalerWrapper) MarshalLogObject(_ zapcore.ObjectEncoder) error { return mom.marsh.MarshalLogObject(mom.enc) } // Interface guards var ( _ zapcore.Encoder = (*FilterEncoder)(nil) _ zapcore.ObjectMarshaler = (*logObjectMarshalerWrapper)(nil) _ caddyfile.Unmarshaler = (*FilterEncoder)(nil) ) caddy-2.6.2/modules/logging/filters.go000066400000000000000000000375361435007237400177250ustar00rootroot00000000000000// 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 logging import ( "crypto/sha256" "errors" "fmt" "net" "net/http" "net/url" "regexp" "strconv" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap/zapcore" ) func init() { caddy.RegisterModule(DeleteFilter{}) caddy.RegisterModule(HashFilter{}) caddy.RegisterModule(ReplaceFilter{}) caddy.RegisterModule(IPMaskFilter{}) caddy.RegisterModule(QueryFilter{}) caddy.RegisterModule(CookieFilter{}) caddy.RegisterModule(RegexpFilter{}) caddy.RegisterModule(RenameFilter{}) } // LogFieldFilter can filter (or manipulate) // a field in a log entry. type LogFieldFilter interface { Filter(zapcore.Field) zapcore.Field } // DeleteFilter is a Caddy log field filter that // deletes the field. type DeleteFilter struct{} // CaddyModule returns the Caddy module information. func (DeleteFilter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.filter.delete", New: func() caddy.Module { return new(DeleteFilter) }, } } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (DeleteFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } // Filter filters the input field. func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field { in.Type = zapcore.SkipType return in } // hash returns the first 4 bytes of the SHA-256 hash of the given data as hexadecimal func hash(s string) string { return fmt.Sprintf("%.4x", sha256.Sum256([]byte(s))) } // HashFilter is a Caddy log field filter that // replaces the field with the initial 4 bytes // of the SHA-256 hash of the content. Operates // on string fields, or on arrays of strings // where each string is hashed. type HashFilter struct { } // CaddyModule returns the Caddy module information. func (HashFilter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.filter.hash", New: func() caddy.Module { return new(HashFilter) }, } } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } // Filter filters the input field with the replacement value. func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field { if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { for i, s := range array { array[i] = hash(s) } } else { in.String = hash(in.String) } return in } // ReplaceFilter is a Caddy log field filter that // replaces the field with the indicated string. type ReplaceFilter struct { Value string `json:"value,omitempty"` } // CaddyModule returns the Caddy module information. func (ReplaceFilter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.filter.replace", New: func() caddy.Module { return new(ReplaceFilter) }, } } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (f *ReplaceFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { f.Value = d.Val() } } return nil } // Filter filters the input field with the replacement value. func (f *ReplaceFilter) Filter(in zapcore.Field) zapcore.Field { in.Type = zapcore.StringType in.String = f.Value return in } // IPMaskFilter is a Caddy log field filter that // masks IP addresses in a string, or in an array // of strings. The string may be a comma separated // list of IP addresses, where all of the values // will be masked. type IPMaskFilter struct { // The IPv4 mask, as an subnet size CIDR. IPv4MaskRaw int `json:"ipv4_cidr,omitempty"` // The IPv6 mask, as an subnet size CIDR. IPv6MaskRaw int `json:"ipv6_cidr,omitempty"` v4Mask net.IPMask v6Mask net.IPMask } // CaddyModule returns the Caddy module information. func (IPMaskFilter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.filter.ip_mask", New: func() caddy.Module { return new(IPMaskFilter) }, } } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (m *IPMaskFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { for d.NextBlock(0) { switch d.Val() { case "ipv4": if !d.NextArg() { return d.ArgErr() } val, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("error parsing %s: %v", d.Val(), err) } m.IPv4MaskRaw = val case "ipv6": if !d.NextArg() { return d.ArgErr() } val, err := strconv.Atoi(d.Val()) if err != nil { return d.Errf("error parsing %s: %v", d.Val(), err) } m.IPv6MaskRaw = val default: return d.Errf("unrecognized subdirective %s", d.Val()) } } } return nil } // Provision parses m's IP masks, from integers. func (m *IPMaskFilter) Provision(ctx caddy.Context) error { parseRawToMask := func(rawField int, bitLen int) net.IPMask { if rawField == 0 { return nil } // we assume the int is a subnet size CIDR // e.g. "16" being equivalent to masking the last // two bytes of an ipv4 address, like "255.255.0.0" return net.CIDRMask(rawField, bitLen) } m.v4Mask = parseRawToMask(m.IPv4MaskRaw, 32) m.v6Mask = parseRawToMask(m.IPv6MaskRaw, 128) return nil } // Filter filters the input field. func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { for i, s := range array { array[i] = m.mask(s) } } else { in.String = m.mask(in.String) } return in } func (m IPMaskFilter) mask(s string) string { output := "" for _, value := range strings.Split(s, ",") { value = strings.TrimSpace(value) host, port, err := net.SplitHostPort(value) if err != nil { host = value // assume whole thing was IP address } ipAddr := net.ParseIP(host) if ipAddr == nil { output += value + ", " continue } mask := m.v4Mask if ipAddr.To4() == nil { mask = m.v6Mask } masked := ipAddr.Mask(mask) if port == "" { output += masked.String() + ", " continue } output += net.JoinHostPort(masked.String(), port) + ", " } return strings.TrimSuffix(output, ", ") } type filterAction string const ( // Replace value(s). replaceAction filterAction = "replace" // Hash value(s). hashAction filterAction = "hash" // Delete. deleteAction filterAction = "delete" ) func (a filterAction) IsValid() error { switch a { case replaceAction, deleteAction, hashAction: return nil } return errors.New("invalid action type") } type queryFilterAction struct { // `replace` to replace the value(s) associated with the parameter(s), `hash` to replace them with the 4 initial bytes of the SHA-256 of their content or `delete` to remove them entirely. Type filterAction `json:"type"` // The name of the query parameter. Parameter string `json:"parameter"` // The value to use as replacement if the action is `replace`. Value string `json:"value,omitempty"` } // QueryFilter is a Caddy log field filter that filters // query parameters from a URL. // // This filter updates the logged URL string to remove, replace or hash // query parameters containing sensitive data. For instance, it can be // used to redact any kind of secrets which were passed as query parameters, // such as OAuth access tokens, session IDs, magic link tokens, etc. type QueryFilter struct { // A list of actions to apply to the query parameters of the URL. Actions []queryFilterAction `json:"actions"` } // Validate checks that action types are correct. func (f *QueryFilter) Validate() error { for _, a := range f.Actions { if err := a.Type.IsValid(); err != nil { return err } } return nil } // CaddyModule returns the Caddy module information. func (QueryFilter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.filter.query", New: func() caddy.Module { return new(QueryFilter) }, } } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { for d.NextBlock(0) { qfa := queryFilterAction{} switch d.Val() { case "replace": if !d.NextArg() { return d.ArgErr() } qfa.Type = replaceAction qfa.Parameter = d.Val() if !d.NextArg() { return d.ArgErr() } qfa.Value = d.Val() case "hash": if !d.NextArg() { return d.ArgErr() } qfa.Type = hashAction qfa.Parameter = d.Val() case "delete": if !d.NextArg() { return d.ArgErr() } qfa.Type = deleteAction qfa.Parameter = d.Val() default: return d.Errf("unrecognized subdirective %s", d.Val()) } m.Actions = append(m.Actions, qfa) } } return nil } // Filter filters the input field. func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field { u, err := url.Parse(in.String) if err != nil { return in } q := u.Query() for _, a := range m.Actions { switch a.Type { case replaceAction: for i := range q[a.Parameter] { q[a.Parameter][i] = a.Value } case hashAction: for i := range q[a.Parameter] { q[a.Parameter][i] = hash(a.Value) } case deleteAction: q.Del(a.Parameter) } } u.RawQuery = q.Encode() in.String = u.String() return in } type cookieFilterAction struct { // `replace` to replace the value of the cookie, `hash` to replace it with the 4 initial bytes of the SHA-256 of its content or `delete` to remove it entirely. Type filterAction `json:"type"` // The name of the cookie. Name string `json:"name"` // The value to use as replacement if the action is `replace`. Value string `json:"value,omitempty"` } // CookieFilter is a Caddy log field filter that filters // cookies. // // This filter updates the logged HTTP header string // to remove, replace or hash cookies containing sensitive data. For instance, // it can be used to redact any kind of secrets, such as session IDs. // // If several actions are configured for the same cookie name, only the first // will be applied. type CookieFilter struct { // A list of actions to apply to the cookies. Actions []cookieFilterAction `json:"actions"` } // Validate checks that action types are correct. func (f *CookieFilter) Validate() error { for _, a := range f.Actions { if err := a.Type.IsValid(); err != nil { return err } } return nil } // CaddyModule returns the Caddy module information. func (CookieFilter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.filter.cookie", New: func() caddy.Module { return new(CookieFilter) }, } } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (m *CookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { for d.NextBlock(0) { cfa := cookieFilterAction{} switch d.Val() { case "replace": if !d.NextArg() { return d.ArgErr() } cfa.Type = replaceAction cfa.Name = d.Val() if !d.NextArg() { return d.ArgErr() } cfa.Value = d.Val() case "hash": if !d.NextArg() { return d.ArgErr() } cfa.Type = hashAction cfa.Name = d.Val() case "delete": if !d.NextArg() { return d.ArgErr() } cfa.Type = deleteAction cfa.Name = d.Val() default: return d.Errf("unrecognized subdirective %s", d.Val()) } m.Actions = append(m.Actions, cfa) } } return nil } // Filter filters the input field. func (m CookieFilter) Filter(in zapcore.Field) zapcore.Field { cookiesSlice, ok := in.Interface.(caddyhttp.LoggableStringArray) if !ok { return in } // using a dummy Request to make use of the Cookies() function to parse it originRequest := http.Request{Header: http.Header{"Cookie": cookiesSlice}} cookies := originRequest.Cookies() transformedRequest := http.Request{Header: make(http.Header)} OUTER: for _, c := range cookies { for _, a := range m.Actions { if c.Name != a.Name { continue } switch a.Type { case replaceAction: c.Value = a.Value transformedRequest.AddCookie(c) continue OUTER case hashAction: c.Value = hash(c.Value) transformedRequest.AddCookie(c) continue OUTER case deleteAction: continue OUTER } } transformedRequest.AddCookie(c) } in.Interface = caddyhttp.LoggableStringArray(transformedRequest.Header["Cookie"]) return in } // RegexpFilter is a Caddy log field filter that // replaces the field matching the provided regexp // with the indicated string. If the field is an // array of strings, each of them will have the // regexp replacement applied. type RegexpFilter struct { // The regular expression pattern defining what to replace. RawRegexp string `json:"regexp,omitempty"` // The value to use as replacement Value string `json:"value,omitempty"` regexp *regexp.Regexp } // CaddyModule returns the Caddy module information. func (RegexpFilter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.filter.regexp", New: func() caddy.Module { return new(RegexpFilter) }, } } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (f *RegexpFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { f.RawRegexp = d.Val() } if d.NextArg() { f.Value = d.Val() } } return nil } // Provision compiles m's regexp. func (m *RegexpFilter) Provision(ctx caddy.Context) error { r, err := regexp.Compile(m.RawRegexp) if err != nil { return err } m.regexp = r return nil } // Filter filters the input field with the replacement value if it matches the regexp. func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field { if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { for i, s := range array { array[i] = f.regexp.ReplaceAllString(s, f.Value) } } else { in.String = f.regexp.ReplaceAllString(in.String, f.Value) } return in } // RenameFilter is a Caddy log field filter that // renames the field's key with the indicated name. type RenameFilter struct { Name string `json:"name,omitempty"` } // CaddyModule returns the Caddy module information. func (RenameFilter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.encoders.filter.rename", New: func() caddy.Module { return new(RenameFilter) }, } } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (f *RenameFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if d.NextArg() { f.Name = d.Val() } } return nil } // Filter renames the input field with the replacement name. func (f *RenameFilter) Filter(in zapcore.Field) zapcore.Field { in.Key = f.Name return in } // Interface guards var ( _ LogFieldFilter = (*DeleteFilter)(nil) _ LogFieldFilter = (*HashFilter)(nil) _ LogFieldFilter = (*ReplaceFilter)(nil) _ LogFieldFilter = (*IPMaskFilter)(nil) _ LogFieldFilter = (*QueryFilter)(nil) _ LogFieldFilter = (*CookieFilter)(nil) _ LogFieldFilter = (*RegexpFilter)(nil) _ LogFieldFilter = (*RenameFilter)(nil) _ caddyfile.Unmarshaler = (*DeleteFilter)(nil) _ caddyfile.Unmarshaler = (*HashFilter)(nil) _ caddyfile.Unmarshaler = (*ReplaceFilter)(nil) _ caddyfile.Unmarshaler = (*IPMaskFilter)(nil) _ caddyfile.Unmarshaler = (*QueryFilter)(nil) _ caddyfile.Unmarshaler = (*CookieFilter)(nil) _ caddyfile.Unmarshaler = (*RegexpFilter)(nil) _ caddyfile.Unmarshaler = (*RenameFilter)(nil) _ caddy.Provisioner = (*IPMaskFilter)(nil) _ caddy.Provisioner = (*RegexpFilter)(nil) _ caddy.Validator = (*QueryFilter)(nil) ) caddy-2.6.2/modules/logging/filters_test.go000066400000000000000000000133001435007237400207430ustar00rootroot00000000000000package logging import ( "testing" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap/zapcore" ) func TestIPMaskSingleValue(t *testing.T) { f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32} f.Provision(caddy.Context{}) out := f.Filter(zapcore.Field{String: "255.255.255.255"}) if out.String != "255.255.0.0" { t.Fatalf("field has not been filtered: %s", out.String) } out = f.Filter(zapcore.Field{String: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"}) if out.String != "ffff:ffff::" { t.Fatalf("field has not been filtered: %s", out.String) } out = f.Filter(zapcore.Field{String: "not-an-ip"}) if out.String != "not-an-ip" { t.Fatalf("field has been filtered: %s", out.String) } } func TestIPMaskCommaValue(t *testing.T) { f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32} f.Provision(caddy.Context{}) out := f.Filter(zapcore.Field{String: "255.255.255.255, 244.244.244.244"}) if out.String != "255.255.0.0, 244.244.0.0" { t.Fatalf("field has not been filtered: %s", out.String) } out = f.Filter(zapcore.Field{String: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff, ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff"}) if out.String != "ffff:ffff::, ff00:ffff::" { t.Fatalf("field has not been filtered: %s", out.String) } out = f.Filter(zapcore.Field{String: "not-an-ip, 255.255.255.255"}) if out.String != "not-an-ip, 255.255.0.0" { t.Fatalf("field has not been filtered: %s", out.String) } } func TestIPMaskMultiValue(t *testing.T) { f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32} f.Provision(caddy.Context{}) out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{ "255.255.255.255", "244.244.244.244", }}) arr, ok := out.Interface.(caddyhttp.LoggableStringArray) if !ok { t.Fatalf("field is wrong type: %T", out.Integer) } if arr[0] != "255.255.0.0" { t.Fatalf("field entry 0 has not been filtered: %s", arr[0]) } if arr[1] != "244.244.0.0" { t.Fatalf("field entry 1 has not been filtered: %s", arr[1]) } out = f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff", }}) arr, ok = out.Interface.(caddyhttp.LoggableStringArray) if !ok { t.Fatalf("field is wrong type: %T", out.Integer) } if arr[0] != "ffff:ffff::" { t.Fatalf("field entry 0 has not been filtered: %s", arr[0]) } if arr[1] != "ff00:ffff::" { t.Fatalf("field entry 1 has not been filtered: %s", arr[1]) } } func TestQueryFilter(t *testing.T) { f := QueryFilter{[]queryFilterAction{ {replaceAction, "foo", "REDACTED"}, {replaceAction, "notexist", "REDACTED"}, {deleteAction, "bar", ""}, {deleteAction, "notexist", ""}, {hashAction, "hash", ""}, }} if f.Validate() != nil { t.Fatalf("the filter must be valid") } out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e&hash=hashed"}) if out.String != "/path?baz=e&foo=REDACTED&foo=REDACTED&hash=e3b0c442" { t.Fatalf("query parameters have not been filtered: %s", out.String) } } func TestValidateQueryFilter(t *testing.T) { f := QueryFilter{[]queryFilterAction{ {}, }} if f.Validate() == nil { t.Fatalf("empty action type must be invalid") } f = QueryFilter{[]queryFilterAction{ {Type: "foo"}, }} if f.Validate() == nil { t.Fatalf("unknown action type must be invalid") } } func TestCookieFilter(t *testing.T) { f := CookieFilter{[]cookieFilterAction{ {replaceAction, "foo", "REDACTED"}, {deleteAction, "bar", ""}, {hashAction, "hash", ""}, }} out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{ "foo=a; foo=b; bar=c; bar=d; baz=e; hash=hashed", }}) outval := out.Interface.(caddyhttp.LoggableStringArray) expected := caddyhttp.LoggableStringArray{ "foo=REDACTED; foo=REDACTED; baz=e; hash=1a06df82", } if outval[0] != expected[0] { t.Fatalf("cookies have not been filtered: %s", out.String) } } func TestValidateCookieFilter(t *testing.T) { f := CookieFilter{[]cookieFilterAction{ {}, }} if f.Validate() == nil { t.Fatalf("empty action type must be invalid") } f = CookieFilter{[]cookieFilterAction{ {Type: "foo"}, }} if f.Validate() == nil { t.Fatalf("unknown action type must be invalid") } } func TestRegexpFilterSingleValue(t *testing.T) { f := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"} f.Provision(caddy.Context{}) out := f.Filter(zapcore.Field{String: "foo-secret-bar"}) if out.String != "foo-REDACTED-bar" { t.Fatalf("field has not been filtered: %s", out.String) } } func TestRegexpFilterMultiValue(t *testing.T) { f := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"} f.Provision(caddy.Context{}) out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{"foo-secret-bar", "bar-secret-foo"}}) arr, ok := out.Interface.(caddyhttp.LoggableStringArray) if !ok { t.Fatalf("field is wrong type: %T", out.Integer) } if arr[0] != "foo-REDACTED-bar" { t.Fatalf("field entry 0 has not been filtered: %s", arr[0]) } if arr[1] != "bar-REDACTED-foo" { t.Fatalf("field entry 1 has not been filtered: %s", arr[1]) } } func TestHashFilterSingleValue(t *testing.T) { f := HashFilter{} out := f.Filter(zapcore.Field{String: "foo"}) if out.String != "2c26b46b" { t.Fatalf("field has not been filtered: %s", out.String) } } func TestHashFilterMultiValue(t *testing.T) { f := HashFilter{} out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{"foo", "bar"}}) arr, ok := out.Interface.(caddyhttp.LoggableStringArray) if !ok { t.Fatalf("field is wrong type: %T", out.Integer) } if arr[0] != "2c26b46b" { t.Fatalf("field entry 0 has not been filtered: %s", arr[0]) } if arr[1] != "fcde2b2e" { t.Fatalf("field entry 1 has not been filtered: %s", arr[1]) } } caddy-2.6.2/modules/logging/netwriter.go000066400000000000000000000122371435007237400202670ustar00rootroot00000000000000// 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 logging import ( "fmt" "io" "net" "os" "sync" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { caddy.RegisterModule(NetWriter{}) } // NetWriter implements a log writer that outputs to a network socket. If // the socket goes down, it will dump logs to stderr while it attempts to // reconnect. type NetWriter struct { // The address of the network socket to which to connect. Address string `json:"address,omitempty"` // The timeout to wait while connecting to the socket. DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` addr caddy.NetworkAddress } // CaddyModule returns the Caddy module information. func (NetWriter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.writers.net", New: func() caddy.Module { return new(NetWriter) }, } } // Provision sets up the module. func (nw *NetWriter) Provision(ctx caddy.Context) error { repl := caddy.NewReplacer() address, err := repl.ReplaceOrErr(nw.Address, true, true) if err != nil { return fmt.Errorf("invalid host in address: %v", err) } nw.addr, err = caddy.ParseNetworkAddress(address) if err != nil { return fmt.Errorf("parsing network address '%s': %v", address, err) } if nw.addr.PortRangeSize() != 1 { return fmt.Errorf("multiple ports not supported") } if nw.DialTimeout < 0 { return fmt.Errorf("timeout cannot be less than 0") } return nil } func (nw NetWriter) String() string { return nw.addr.String() } // WriterKey returns a unique key representing this nw. func (nw NetWriter) WriterKey() string { return nw.addr.String() } // OpenWriter opens a new network connection. func (nw NetWriter) OpenWriter() (io.WriteCloser, error) { reconn := &redialerConn{ nw: nw, timeout: time.Duration(nw.DialTimeout), } conn, err := reconn.dial() if err != nil { return nil, err } reconn.connMu.Lock() reconn.Conn = conn reconn.connMu.Unlock() return reconn, nil } // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // net
{ // dial_timeout // } // func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { if !d.NextArg() { return d.ArgErr() } nw.Address = d.Val() if d.NextArg() { return d.ArgErr() } for nesting := d.Nesting(); d.NextBlock(nesting); { switch d.Val() { case "dial_timeout": if !d.NextArg() { return d.ArgErr() } timeout, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("invalid duration: %s", d.Val()) } if d.NextArg() { return d.ArgErr() } nw.DialTimeout = caddy.Duration(timeout) } } } return nil } // redialerConn wraps an underlying Conn so that if any // writes fail, the connection is redialed and the write // is retried. type redialerConn struct { net.Conn connMu sync.RWMutex nw NetWriter timeout time.Duration lastRedial time.Time } // Write wraps the underlying Conn.Write method, but if that fails, // it will re-dial the connection anew and try writing again. func (reconn *redialerConn) Write(b []byte) (n int, err error) { reconn.connMu.RLock() conn := reconn.Conn reconn.connMu.RUnlock() if n, err = conn.Write(b); err == nil { return } // problem with the connection - lock it and try to fix it reconn.connMu.Lock() defer reconn.connMu.Unlock() // if multiple concurrent writes failed on the same broken conn, then // one of them might have already re-dialed by now; try writing again if n, err = reconn.Conn.Write(b); err == nil { return } // there's still a problem, so try to re-attempt dialing the socket // if some time has passed in which the issue could have potentially // been resolved - we don't want to block at every single log // emission (!) - see discussion in #4111 if time.Since(reconn.lastRedial) > 10*time.Second { reconn.lastRedial = time.Now() conn2, err2 := reconn.dial() if err2 != nil { // logger socket still offline; instead of discarding the log, dump it to stderr os.Stderr.Write(b) return } if n, err = conn2.Write(b); err == nil { reconn.Conn.Close() reconn.Conn = conn2 } } else { // last redial attempt was too recent; just dump to stderr for now os.Stderr.Write(b) } return } func (reconn *redialerConn) dial() (net.Conn, error) { return net.DialTimeout(reconn.nw.addr.Network, reconn.nw.addr.JoinHostPort(0), reconn.timeout) } // Interface guards var ( _ caddy.Provisioner = (*NetWriter)(nil) _ caddy.WriterOpener = (*NetWriter)(nil) _ caddyfile.Unmarshaler = (*NetWriter)(nil) ) caddy-2.6.2/modules/logging/nopencoder.go000066400000000000000000000104261435007237400203760ustar00rootroot00000000000000// 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 logging import ( "time" "go.uber.org/zap/buffer" "go.uber.org/zap/zapcore" ) // nopEncoder is a zapcore.Encoder that does nothing. type nopEncoder struct{} // AddArray is part of the zapcore.ObjectEncoder interface. // Array elements do not get filtered. func (nopEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { return nil } // AddObject is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { return nil } // AddBinary is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddBinary(key string, value []byte) {} // AddByteString is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddByteString(key string, value []byte) {} // AddBool is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddBool(key string, value bool) {} // AddComplex128 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddComplex128(key string, value complex128) {} // AddComplex64 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddComplex64(key string, value complex64) {} // AddDuration is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddDuration(key string, value time.Duration) {} // AddFloat64 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddFloat64(key string, value float64) {} // AddFloat32 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddFloat32(key string, value float32) {} // AddInt is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddInt(key string, value int) {} // AddInt64 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddInt64(key string, value int64) {} // AddInt32 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddInt32(key string, value int32) {} // AddInt16 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddInt16(key string, value int16) {} // AddInt8 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddInt8(key string, value int8) {} // AddString is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddString(key, value string) {} // AddTime is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddTime(key string, value time.Time) {} // AddUint is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddUint(key string, value uint) {} // AddUint64 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddUint64(key string, value uint64) {} // AddUint32 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddUint32(key string, value uint32) {} // AddUint16 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddUint16(key string, value uint16) {} // AddUint8 is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddUint8(key string, value uint8) {} // AddUintptr is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddUintptr(key string, value uintptr) {} // AddReflected is part of the zapcore.ObjectEncoder interface. func (nopEncoder) AddReflected(key string, value any) error { return nil } // OpenNamespace is part of the zapcore.ObjectEncoder interface. func (nopEncoder) OpenNamespace(key string) {} // Clone is part of the zapcore.ObjectEncoder interface. // We don't use it as of Oct 2019 (v2 beta 7), I'm not // really sure what it'd be useful for in our case. func (ne nopEncoder) Clone() zapcore.Encoder { return ne } // EncodeEntry partially implements the zapcore.Encoder interface. func (nopEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { return bufferpool.Get(), nil } // Interface guard var _ zapcore.Encoder = (*nopEncoder)(nil) caddy-2.6.2/modules/metrics/000077500000000000000000000000001435007237400157305ustar00rootroot00000000000000caddy-2.6.2/modules/metrics/adminmetrics.go000066400000000000000000000033241435007237400207400ustar00rootroot00000000000000// Copyright 2020 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 metrics import ( "net/http" "github.com/caddyserver/caddy/v2" ) func init() { caddy.RegisterModule(AdminMetrics{}) } // AdminMetrics is a module that serves a metrics endpoint so that any gathered // metrics can be exposed for scraping. This module is not configurable, and // is permanently mounted to the admin API endpoint at "/metrics". // See the Metrics module for a configurable endpoint that is usable if the // Admin API is disabled. type AdminMetrics struct{} // CaddyModule returns the Caddy module information. func (AdminMetrics) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "admin.api.metrics", New: func() caddy.Module { return new(AdminMetrics) }, } } // Routes returns a route for the /metrics endpoint. func (m *AdminMetrics) Routes() []caddy.AdminRoute { metricsHandler := createMetricsHandler(nil, false) h := caddy.AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error { metricsHandler.ServeHTTP(w, r) return nil }) return []caddy.AdminRoute{{Pattern: "/metrics", Handler: h}} } // Interface guards var ( _ caddy.AdminRouter = (*AdminMetrics)(nil) ) caddy-2.6.2/modules/metrics/metrics.go000066400000000000000000000067341435007237400177370ustar00rootroot00000000000000// Copyright 2020 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 metrics import ( "net/http" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) func init() { caddy.RegisterModule(Metrics{}) httpcaddyfile.RegisterHandlerDirective("metrics", parseCaddyfile) } // Metrics is a module that serves a /metrics endpoint so that any gathered // metrics can be exposed for scraping. This module is configurable by end-users // unlike AdminMetrics. type Metrics struct { metricsHandler http.Handler // Disable OpenMetrics negotiation, enabled by default. May be necessary if // the produced metrics cannot be parsed by the service scraping metrics. DisableOpenMetrics bool `json:"disable_openmetrics,omitempty"` } // CaddyModule returns the Caddy module information. func (Metrics) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.metrics", New: func() caddy.Module { return new(Metrics) }, } } type zapLogger struct { zl *zap.Logger } func (l *zapLogger) Println(v ...any) { l.zl.Sugar().Error(v...) } // Provision sets up m. func (m *Metrics) Provision(ctx caddy.Context) error { log := ctx.Logger() m.metricsHandler = createMetricsHandler(&zapLogger{log}, !m.DisableOpenMetrics) return nil } func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { var m Metrics err := m.UnmarshalCaddyfile(h.Dispenser) return m, err } // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // metrics [] { // disable_openmetrics // } func (m *Metrics) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { args := d.RemainingArgs() if len(args) > 0 { return d.ArgErr() } for d.NextBlock(0) { switch d.Val() { case "disable_openmetrics": m.DisableOpenMetrics = true default: return d.Errf("unrecognized subdirective %q", d.Val()) } } } return nil } func (m Metrics) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { m.metricsHandler.ServeHTTP(w, r) return nil } // Interface guards var ( _ caddy.Provisioner = (*Metrics)(nil) _ caddyhttp.MiddlewareHandler = (*Metrics)(nil) _ caddyfile.Unmarshaler = (*Metrics)(nil) ) func createMetricsHandler(logger promhttp.Logger, enableOpenMetrics bool) http.Handler { return promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{ // will only log errors if logger is non-nil ErrorLog: logger, // Allow OpenMetrics format to be negotiated - largely compatible, // except quantile/le label values always have a decimal. EnableOpenMetrics: enableOpenMetrics, }), ) } caddy-2.6.2/modules/metrics/metrics_test.go000066400000000000000000000017561435007237400207750ustar00rootroot00000000000000package metrics import ( "testing" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func TestMetricsUnmarshalCaddyfile(t *testing.T) { m := &Metrics{} d := caddyfile.NewTestDispenser(`metrics bogus`) err := m.UnmarshalCaddyfile(d) if err == nil { t.Errorf("expected error") } m = &Metrics{} d = caddyfile.NewTestDispenser(`metrics`) err = m.UnmarshalCaddyfile(d) if err != nil { t.Errorf("unexpected error: %v", err) } if m.DisableOpenMetrics { t.Errorf("DisableOpenMetrics should've been false: %v", m.DisableOpenMetrics) } m = &Metrics{} d = caddyfile.NewTestDispenser(`metrics { disable_openmetrics }`) err = m.UnmarshalCaddyfile(d) if err != nil { t.Errorf("unexpected error: %v", err) } if !m.DisableOpenMetrics { t.Errorf("DisableOpenMetrics should've been true: %v", m.DisableOpenMetrics) } m = &Metrics{} d = caddyfile.NewTestDispenser(`metrics { bogus }`) err = m.UnmarshalCaddyfile(d) if err == nil { t.Errorf("expected error: %v", err) } } caddy-2.6.2/modules/standard/000077500000000000000000000000001435007237400160625ustar00rootroot00000000000000caddy-2.6.2/modules/standard/imports.go000066400000000000000000000014131435007237400201050ustar00rootroot00000000000000package standard import ( // standard Caddy modules _ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" _ "github.com/caddyserver/caddy/v2/modules/caddyevents" _ "github.com/caddyserver/caddy/v2/modules/caddyevents/eventsconfig" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard" _ "github.com/caddyserver/caddy/v2/modules/caddypki" _ "github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver" _ "github.com/caddyserver/caddy/v2/modules/caddytls" _ "github.com/caddyserver/caddy/v2/modules/caddytls/distributedstek" _ "github.com/caddyserver/caddy/v2/modules/caddytls/standardstek" _ "github.com/caddyserver/caddy/v2/modules/filestorage" _ "github.com/caddyserver/caddy/v2/modules/logging" _ "github.com/caddyserver/caddy/v2/modules/metrics" ) caddy-2.6.2/modules_test.go000066400000000000000000000047111435007237400156530ustar00rootroot00000000000000// 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 ( "reflect" "testing" ) func TestGetModules(t *testing.T) { modulesMu.Lock() modules = map[string]ModuleInfo{ "a": {ID: "a"}, "a.b": {ID: "a.b"}, "a.b.c": {ID: "a.b.c"}, "a.b.cd": {ID: "a.b.cd"}, "a.c": {ID: "a.c"}, "a.d": {ID: "a.d"}, "b": {ID: "b"}, "b.a": {ID: "b.a"}, "b.b": {ID: "b.b"}, "b.a.c": {ID: "b.a.c"}, "c": {ID: "c"}, } modulesMu.Unlock() for i, tc := range []struct { input string expect []ModuleInfo }{ { input: "", expect: []ModuleInfo{ {ID: "a"}, {ID: "b"}, {ID: "c"}, }, }, { input: "a", expect: []ModuleInfo{ {ID: "a.b"}, {ID: "a.c"}, {ID: "a.d"}, }, }, { input: "a.b", expect: []ModuleInfo{ {ID: "a.b.c"}, {ID: "a.b.cd"}, }, }, { input: "a.b.c", }, { input: "b", expect: []ModuleInfo{ {ID: "b.a"}, {ID: "b.b"}, }, }, { input: "asdf", }, } { actual := GetModules(tc.input) if !reflect.DeepEqual(actual, tc.expect) { t.Errorf("Test %d: Expected %v but got %v", i, tc.expect, actual) } } } func TestModuleID(t *testing.T) { for i, tc := range []struct { input ModuleID expectNamespace string expectName string }{ { input: "foo", expectNamespace: "", expectName: "foo", }, { input: "foo.bar", expectNamespace: "foo", expectName: "bar", }, { input: "a.b.c", expectNamespace: "a.b", expectName: "c", }, } { actualNamespace := tc.input.Namespace() if actualNamespace != tc.expectNamespace { t.Errorf("Test %d: Expected namespace '%s' but got '%s'", i, tc.expectNamespace, actualNamespace) } actualName := tc.input.Name() if actualName != tc.expectName { t.Errorf("Test %d: Expected name '%s' but got '%s'", i, tc.expectName, actualName) } } } caddy-2.6.2/notify/000077500000000000000000000000001435007237400141225ustar00rootroot00000000000000caddy-2.6.2/notify/notify_linux.go000066400000000000000000000041431435007237400172020ustar00rootroot00000000000000// 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 notify provides facilities for notifying process managers // of state changes, mainly for when running as a system service. package notify import ( "fmt" "net" "os" "strings" ) // The documentation about this IPC protocol is available here: // https://www.freedesktop.org/software/systemd/man/sd_notify.html func sdNotify(payload string) error { if socketPath == "" { return nil } socketAddr := &net.UnixAddr{ Name: socketPath, Net: "unixgram", } conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) if err != nil { return err } defer conn.Close() _, err = conn.Write([]byte(payload)) return err } // Ready notifies systemd that caddy has finished its // initialization routines. func Ready() error { return sdNotify("READY=1") } // Reloading notifies systemd that caddy is reloading its config. func Reloading() error { return sdNotify("RELOADING=1") } // Stopping notifies systemd that caddy is stopping. func Stopping() error { return sdNotify("STOPPING=1") } // Status sends systemd an updated status message. func Status(msg string) error { return sdNotify("STATUS=" + msg) } // Error is like Status, but sends systemd an error message // instead, with an optional errno-style error number. func Error(err error, errno int) error { collapsedErr := strings.ReplaceAll(err.Error(), "\n", " ") msg := fmt.Sprintf("STATUS=%s", collapsedErr) if errno > 0 { msg += fmt.Sprintf("\nERRNO=%d", errno) } return sdNotify(msg) } var socketPath, _ = os.LookupEnv("NOTIFY_SOCKET") caddy-2.6.2/notify/notify_other.go000066400000000000000000000016031435007237400171620ustar00rootroot00000000000000// 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. //go:build !linux && !windows package notify func Ready() error { return nil } func Reloading() error { return nil } func Stopping() error { return nil } func Status(_ string) error { return nil } func Error(_ error, _ int) error { return nil } caddy-2.6.2/notify/notify_windows.go000066400000000000000000000026201435007237400175330ustar00rootroot00000000000000// 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 notify import "golang.org/x/sys/windows/svc" // globalStatus store windows service status, it can be // use to notify caddy status. var globalStatus chan<- svc.Status func SetGlobalStatus(status chan<- svc.Status) { globalStatus = status } func Ready() error { if globalStatus != nil { globalStatus <- svc.Status{ State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown, } } return nil } func Reloading() error { if globalStatus != nil { globalStatus <- svc.Status{State: svc.StartPending} } return nil } func Stopping() error { if globalStatus != nil { globalStatus <- svc.Status{State: svc.StopPending} } return nil } // TODO: not implemented func Status(_ string) error { return nil } // TODO: not implemented func Error(_ error, _ int) error { return nil } caddy-2.6.2/replacer.go000066400000000000000000000224261435007237400147440ustar00rootroot00000000000000// 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 ( "fmt" "os" "path/filepath" "runtime" "strconv" "strings" "time" ) // NewReplacer returns a new Replacer. func NewReplacer() *Replacer { rep := &Replacer{ static: make(map[string]any), } rep.providers = []ReplacerFunc{ globalDefaultReplacements, rep.fromStatic, } return rep } // NewEmptyReplacer returns a new Replacer, // without the global default replacements. func NewEmptyReplacer() *Replacer { rep := &Replacer{ static: make(map[string]any), } rep.providers = []ReplacerFunc{ rep.fromStatic, } return rep } // Replacer can replace values in strings. // A default/empty Replacer is not valid; // use NewReplacer to make one. type Replacer struct { providers []ReplacerFunc static map[string]any } // Map adds mapFunc to the list of value providers. // mapFunc will be executed only at replace-time. func (r *Replacer) Map(mapFunc ReplacerFunc) { r.providers = append(r.providers, mapFunc) } // Set sets a custom variable to a static value. func (r *Replacer) Set(variable string, value any) { r.static[variable] = value } // Get gets a value from the replacer. It returns // the value and whether the variable was known. func (r *Replacer) Get(variable string) (any, bool) { for _, mapFunc := range r.providers { if val, ok := mapFunc(variable); ok { return val, true } } return nil, false } // GetString is the same as Get, but coerces the value to a // string representation as efficiently as possible. func (r *Replacer) GetString(variable string) (string, bool) { s, found := r.Get(variable) return ToString(s), found } // Delete removes a variable with a static value // that was created using Set. func (r *Replacer) Delete(variable string) { delete(r.static, variable) } // fromStatic provides values from r.static. func (r *Replacer) fromStatic(key string) (any, bool) { val, ok := r.static[key] return val, ok } // ReplaceOrErr is like ReplaceAll, but any placeholders // that are empty or not recognized will cause an error to // be returned. func (r *Replacer) ReplaceOrErr(input string, errOnEmpty, errOnUnknown bool) (string, error) { return r.replace(input, "", false, errOnEmpty, errOnUnknown, nil) } // ReplaceKnown is like ReplaceAll but only replaces // placeholders that are known (recognized). Unrecognized // placeholders will remain in the output. func (r *Replacer) ReplaceKnown(input, empty string) string { out, _ := r.replace(input, empty, false, false, false, nil) return out } // ReplaceAll efficiently replaces placeholders in input with // their values. All placeholders are replaced in the output // whether they are recognized or not. Values that are empty // string will be substituted with empty. func (r *Replacer) ReplaceAll(input, empty string) string { out, _ := r.replace(input, empty, true, false, false, nil) return out } // ReplaceFunc is the same as ReplaceAll, but calls f for every // replacement to be made, in case f wants to change or inspect // the replacement. func (r *Replacer) ReplaceFunc(input string, f ReplacementFunc) (string, error) { return r.replace(input, "", true, false, false, f) } func (r *Replacer) replace(input, empty string, treatUnknownAsEmpty, errOnEmpty, errOnUnknown bool, f ReplacementFunc) (string, error) { if !strings.Contains(input, string(phOpen)) { return input, nil } var sb strings.Builder // it is reasonable to assume that the output // will be approximately as long as the input sb.Grow(len(input)) // iterate the input to find each placeholder var lastWriteCursor int // fail fast if too many placeholders are unclosed var unclosedCount int scan: for i := 0; i < len(input); i++ { // check for escaped braces if i > 0 && input[i-1] == phEscape && (input[i] == phClose || input[i] == phOpen) { sb.WriteString(input[lastWriteCursor : i-1]) lastWriteCursor = i continue } if input[i] != phOpen { continue } // our iterator is now on an unescaped open brace (start of placeholder) // too many unclosed placeholders in absolutely ridiculous input can be extremely slow (issue #4170) if unclosedCount > 100 { return "", fmt.Errorf("too many unclosed placeholders") } // find the end of the placeholder end := strings.Index(input[i:], string(phClose)) + i if end < i { unclosedCount++ continue } // if necessary look for the first closing brace that is not escaped for end > 0 && end < len(input)-1 && input[end-1] == phEscape { nextEnd := strings.Index(input[end+1:], string(phClose)) if nextEnd < 0 { unclosedCount++ continue scan } end += nextEnd + 1 } // write the substring from the last cursor to this point sb.WriteString(input[lastWriteCursor:i]) // trim opening bracket key := input[i+1 : end] // try to get a value for this key, handle empty values accordingly val, found := r.Get(key) if !found { // placeholder is unknown (unrecognized); handle accordingly if errOnUnknown { return "", fmt.Errorf("unrecognized placeholder %s%s%s", string(phOpen), key, string(phClose)) } else if !treatUnknownAsEmpty { // if treatUnknownAsEmpty is true, we'll handle an empty // val later; so only continue otherwise lastWriteCursor = i continue } } // apply any transformations if f != nil { var err error val, err = f(key, val) if err != nil { return "", err } } // convert val to a string as efficiently as possible valStr := ToString(val) // write the value; if it's empty, either return // an error or write a default value if valStr == "" { if errOnEmpty { return "", fmt.Errorf("evaluated placeholder %s%s%s is empty", string(phOpen), key, string(phClose)) } else if empty != "" { sb.WriteString(empty) } } else { sb.WriteString(valStr) } // advance cursor to end of placeholder i = end lastWriteCursor = i + 1 } // flush any unwritten remainder sb.WriteString(input[lastWriteCursor:]) return sb.String(), nil } // ToString returns val as a string, as efficiently as possible. // EXPERIMENTAL: may be changed or removed later. func ToString(val any) string { switch v := val.(type) { case nil: return "" case string: return v case fmt.Stringer: return v.String() case error: return v.Error() case byte: return string(v) case []byte: return string(v) case []rune: return string(v) case int: return strconv.Itoa(v) case int32: return strconv.Itoa(int(v)) case int64: return strconv.Itoa(int(v)) case uint: return strconv.Itoa(int(v)) case uint32: return strconv.Itoa(int(v)) case uint64: return strconv.Itoa(int(v)) case float32: return strconv.FormatFloat(float64(v), 'f', -1, 32) case float64: return strconv.FormatFloat(v, 'f', -1, 64) case bool: if v { return "true" } return "false" default: return fmt.Sprintf("%+v", v) } } // ReplacerFunc is a function that returns a replacement // for the given key along with true if the function is able // to service that key (even if the value is blank). If the // function does not recognize the key, false should be // returned. type ReplacerFunc func(key string) (any, bool) func globalDefaultReplacements(key string) (any, bool) { // check environment variable const envPrefix = "env." if strings.HasPrefix(key, envPrefix) { return os.Getenv(key[len(envPrefix):]), true } switch key { case "system.hostname": // OK if there is an error; just return empty string name, _ := os.Hostname() return name, true case "system.slash": return string(filepath.Separator), true case "system.os": return runtime.GOOS, true case "system.wd": // OK if there is an error; just return empty string wd, _ := os.Getwd() return wd, true case "system.arch": return runtime.GOARCH, true case "time.now": return nowFunc(), true case "time.now.common_log": return nowFunc().Format("02/Jan/2006:15:04:05 -0700"), true case "time.now.year": return strconv.Itoa(nowFunc().Year()), true case "time.now.unix": return strconv.FormatInt(nowFunc().Unix(), 10), true case "time.now.unix_ms": return strconv.FormatInt(nowFunc().UnixNano()/int64(time.Millisecond), 10), true } return nil, false } // ReplacementFunc is a function that is called when a // replacement is being performed. It receives the // variable (i.e. placeholder name) and the value that // will be the replacement, and returns the value that // will actually be the replacement, or an error. Note // that errors are sometimes ignored by replacers. type ReplacementFunc func(variable string, val any) (any, error) // nowFunc is a variable so tests can change it // in order to obtain a deterministic time. var nowFunc = time.Now // ReplacerCtxKey is the context key for a replacer. const ReplacerCtxKey CtxKey = "replacer" const phOpen, phClose, phEscape = '{', '}', '\\' caddy-2.6.2/replacer_fuzz.go000066400000000000000000000017711435007237400160220ustar00rootroot00000000000000// 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. //go:build gofuzz package caddy func FuzzReplacer(data []byte) (score int) { NewReplacer().ReplaceAll(string(data), "") NewReplacer().ReplaceAll(NewReplacer().ReplaceAll(string(data), ""), "") NewReplacer().ReplaceAll(NewReplacer().ReplaceAll(string(data), ""), NewReplacer().ReplaceAll(string(data), "")) NewReplacer().ReplaceAll(string(data[:len(data)/2]), string(data[len(data)/2:])) return 0 } caddy-2.6.2/replacer_test.go000066400000000000000000000227311435007237400160020ustar00rootroot00000000000000// 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 ( "fmt" "os" "path/filepath" "runtime" "testing" ) func TestReplacer(t *testing.T) { type testCase struct { input, expect, empty string } rep := testReplacer() // ReplaceAll for i, tc := range []testCase{ { input: "{", expect: "{", }, { input: `\{`, expect: `{`, }, { input: "foo{", expect: "foo{", }, { input: `foo\{`, expect: `foo{`, }, { input: "foo{bar", expect: "foo{bar", }, { input: `foo\{bar`, expect: `foo{bar`, }, { input: "foo{bar}", expect: "foo", }, { input: `foo\{bar\}`, expect: `foo{bar}`, }, { input: "}", expect: "}", }, { input: `\}`, expect: `\}`, }, { input: "{}", expect: "", }, { input: `\{\}`, expect: `{}`, }, { input: `{"json": "object"}`, expect: "", }, { input: `\{"json": "object"}`, expect: `{"json": "object"}`, }, { input: `\{"json": "object"\}`, expect: `{"json": "object"}`, }, { input: `\{"json": "object{bar}"\}`, expect: `{"json": "object"}`, }, { input: `\{"json": \{"nested": "object"\}\}`, expect: `{"json": {"nested": "object"}}`, }, { input: `\{"json": \{"nested": "{bar}"\}\}`, expect: `{"json": {"nested": ""}}`, }, { input: `pre \{"json": \{"nested": "{bar}"\}\}`, expect: `pre {"json": {"nested": ""}}`, }, { input: `\{"json": \{"nested": "{bar}"\}\} post`, expect: `{"json": {"nested": ""}} post`, }, { input: `pre \{"json": \{"nested": "{bar}"\}\} post`, expect: `pre {"json": {"nested": ""}} post`, }, { input: `{{`, expect: "{{", }, { input: `{{}`, expect: "", }, { input: `{"json": "object"\}`, expect: "", }, { input: `{unknown}`, empty: "-", expect: "-", }, { input: `back\slashes`, expect: `back\slashes`, }, { input: `double back\\slashes`, expect: `double back\\slashes`, }, { input: `placeholder {with \{ brace} in name`, expect: `placeholder in name`, }, { input: `placeholder {with \} brace} in name`, expect: `placeholder in name`, }, { input: `placeholder {with \} \} braces} in name`, expect: `placeholder in name`, }, { input: `\{'group':'default','max_age':3600,'endpoints':[\{'url':'https://some.domain.local/a/d/g'\}],'include_subdomains':true\}`, expect: `{'group':'default','max_age':3600,'endpoints':[{'url':'https://some.domain.local/a/d/g'}],'include_subdomains':true}`, }, { input: `{}{}{}{\\\\}\\\\`, expect: `{\\\}\\\\`, }, { input: string([]byte{0x26, 0x00, 0x83, 0x7B, 0x84, 0x07, 0x5C, 0x7D, 0x84}), expect: string([]byte{0x26, 0x00, 0x83, 0x7B, 0x84, 0x07, 0x7D, 0x84}), }, } { actual := rep.ReplaceAll(tc.input, tc.empty) if actual != tc.expect { t.Errorf("Test %d: '%s': expected '%s' but got '%s'", i, tc.input, tc.expect, actual) } } } func TestReplacerSet(t *testing.T) { rep := testReplacer() for _, tc := range []struct { variable string value any }{ { variable: "test1", value: "val1", }, { variable: "asdf", value: "123", }, { variable: "numbers", value: 123.456, }, { variable: "äöü", value: "öö_äü", }, { variable: "with space", value: "space value", }, { variable: "1", value: "test-123", }, { variable: "mySuper_IP", value: "1.2.3.4", }, { variable: "testEmpty", value: "", }, } { rep.Set(tc.variable, tc.value) // test if key is added if val, ok := rep.static[tc.variable]; ok { if val != tc.value { t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val) } } else { t.Errorf("Expected existing key '%s' found nothing", tc.variable) } } // test if all keys are still there (by length) length := len(rep.static) if len(rep.static) != 8 { t.Errorf("Expected length '%v' got '%v'", 7, length) } } func TestReplacerReplaceKnown(t *testing.T) { rep := Replacer{ providers: []ReplacerFunc{ // split our possible vars to two functions (to test if both functions are called) func(key string) (val any, ok bool) { switch key { case "test1": return "val1", true case "asdf": return "123", true case "äöü": return "öö_äü", true case "with space": return "space value", true default: return "NOOO", false } }, func(key string) (val any, ok bool) { switch key { case "1": return "test-123", true case "mySuper_IP": return "1.2.3.4", true case "testEmpty": return "", true default: return "NOOO", false } }, }, } for _, tc := range []struct { testInput string expected string }{ { // test vars without space testInput: "{test1}{asdf}{äöü}{1}{with space}{mySuper_IP}", expected: "val1123öö_äütest-123space value1.2.3.4", }, { // test vars with space testInput: "{test1} {asdf} {äöü} {1} {with space} {mySuper_IP} ", expected: "val1 123 öö_äü test-123 space value 1.2.3.4 ", }, { // test with empty val testInput: "{test1} {testEmpty} {asdf} {1} ", expected: "val1 EMPTY 123 test-123 ", }, { // test vars with not finished placeholders testInput: "{te{test1}{as{{df{1}", expected: "{teval1{as{{dftest-123", }, { // test with non existing vars testInput: "{test1} {nope} {1} ", expected: "val1 {nope} test-123 ", }, } { actual := rep.ReplaceKnown(tc.testInput, "EMPTY") // test if all are replaced as expected if actual != tc.expected { t.Errorf("Expected '%s' got '%s' for '%s'", tc.expected, actual, tc.testInput) } } } func TestReplacerDelete(t *testing.T) { rep := Replacer{ static: map[string]any{ "key1": "val1", "key2": "val2", "key3": "val3", "key4": "val4", }, } startLen := len(rep.static) toDel := []string{ "key2", "key4", } for _, key := range toDel { rep.Delete(key) // test if key is removed from static map if _, ok := rep.static[key]; ok { t.Errorf("Expected '%s' to be removed. It is still in static map.", key) } } // check if static slice is smaller expected := startLen - len(toDel) actual := len(rep.static) if len(rep.static) != expected { t.Errorf("Expected length '%v' got length '%v'", expected, actual) } } func TestReplacerMap(t *testing.T) { rep := testReplacer() for i, tc := range []ReplacerFunc{ func(key string) (val any, ok bool) { return "", false }, func(key string) (val any, ok bool) { return "", false }, } { rep.Map(tc) // test if function (which listens on specific key) is added by checking length if len(rep.providers) == i+1 { // check if the last function is the one we just added pTc := fmt.Sprintf("%p", tc) pRep := fmt.Sprintf("%p", rep.providers[i]) if pRep != pTc { t.Errorf("Expected func pointer '%s' got '%s'", pTc, pRep) } } else { t.Errorf("Expected providers length '%v' got length '%v'", i+1, len(rep.providers)) } } } func TestReplacerNew(t *testing.T) { rep := NewReplacer() if len(rep.providers) != 2 { t.Errorf("Expected providers length '%v' got length '%v'", 2, len(rep.providers)) } else { // test if default global replacements are added as the first provider hostname, _ := os.Hostname() wd, _ := os.Getwd() os.Setenv("CADDY_REPLACER_TEST", "envtest") defer os.Setenv("CADDY_REPLACER_TEST", "") for _, tc := range []struct { variable string value string }{ { variable: "system.hostname", value: hostname, }, { variable: "system.slash", value: string(filepath.Separator), }, { variable: "system.os", value: runtime.GOOS, }, { variable: "system.arch", value: runtime.GOARCH, }, { variable: "system.wd", value: wd, }, { variable: "env.CADDY_REPLACER_TEST", value: "envtest", }, } { if val, ok := rep.providers[0](tc.variable); ok { if val != tc.value { t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val) } } else { t.Errorf("Expected key '%s' to be recognized by first provider", tc.variable) } } } } func BenchmarkReplacer(b *testing.B) { type testCase struct { name, input, empty string } rep := testReplacer() rep.Set("str", "a string") rep.Set("int", 123.456) for _, bm := range []testCase{ { name: "no placeholder", input: `simple string`, }, { name: "string replacement", input: `str={str}`, }, { name: "int replacement", input: `int={int}`, }, { name: "placeholder", input: `{"json": "object"}`, }, { name: "escaped placeholder", input: `\{"json": \{"nested": "{bar}"\}\}`, }, } { b.Run(bm.name, func(b *testing.B) { for i := 0; i < b.N; i++ { rep.ReplaceAll(bm.input, bm.empty) } }) } } func testReplacer() Replacer { return Replacer{ providers: make([]ReplacerFunc, 0), static: make(map[string]any), } } caddy-2.6.2/service_windows.go000066400000000000000000000030371435007237400163560ustar00rootroot00000000000000// 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 ( "os" "path/filepath" "github.com/caddyserver/caddy/v2/notify" "golang.org/x/sys/windows/svc" ) func init() { isService, err := svc.IsWindowsService() if err != nil || !isService { return } // Windows services always start in the system32 directory, try to // switch into the directory where the caddy executable is. execPath, err := os.Executable() if err == nil { _ = os.Chdir(filepath.Dir(execPath)) } go func() { _ = svc.Run("", runner{}) }() } type runner struct{} func (runner) Execute(args []string, request <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { notify.SetGlobalStatus(status) status <- svc.Status{State: svc.StartPending} for { req := <-request switch req.Cmd { case svc.Interrogate: status <- req.CurrentStatus case svc.Stop, svc.Shutdown: status <- svc.Status{State: svc.StopPending} exitProcessFromSignal("SIGINT") return false, 0 } } } caddy-2.6.2/sigtrap.go000066400000000000000000000037611435007237400146210ustar00rootroot00000000000000// 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 ( "context" "os" "os/signal" "go.uber.org/zap" ) // TrapSignals create signal/interrupt handlers as best it can for the // current OS. This is a rather invasive function to call in a Go program // that captures signals already, so in that case it would be better to // implement these handlers yourself. func TrapSignals() { trapSignalsCrossPlatform() trapSignalsPosix() } // trapSignalsCrossPlatform captures SIGINT or interrupt (depending // on the OS), which initiates a graceful shutdown. A second SIGINT // or interrupt will forcefully exit the process immediately. func trapSignalsCrossPlatform() { go func() { shutdown := make(chan os.Signal, 1) signal.Notify(shutdown, os.Interrupt) for i := 0; true; i++ { <-shutdown if i > 0 { Log().Warn("force quit", zap.String("signal", "SIGINT")) os.Exit(ExitCodeForceQuit) } Log().Info("shutting down", zap.String("signal", "SIGINT")) go exitProcessFromSignal("SIGINT") } }() } // exitProcessFromSignal exits the process from a system signal. func exitProcessFromSignal(sigName string) { logger := Log().With(zap.String("signal", sigName)) exitProcess(context.TODO(), logger) } // Exit codes. Generally, you should NOT // automatically restart the process if the // exit code is ExitCodeFailedStartup (1). const ( ExitCodeSuccess = iota ExitCodeFailedStartup ExitCodeForceQuit ExitCodeFailedQuit ) caddy-2.6.2/sigtrap_nonposix.go000066400000000000000000000012711435007237400165500ustar00rootroot00000000000000// 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. //go:build windows || plan9 || nacl || js package caddy func trapSignalsPosix() {} caddy-2.6.2/sigtrap_posix.go000066400000000000000000000034331435007237400160370ustar00rootroot00000000000000// 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. //go:build !windows && !plan9 && !nacl && !js package caddy import ( "context" "os" "os/signal" "syscall" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) // trapSignalsPosix captures POSIX-only signals. func trapSignalsPosix() { go func() { sigchan := make(chan os.Signal, 1) signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2) for sig := range sigchan { switch sig { case syscall.SIGQUIT: Log().Info("quitting process immediately", zap.String("signal", "SIGQUIT")) certmagic.CleanUpOwnLocks(context.TODO(), Log()) // try to clean up locks anyway, it's important os.Exit(ExitCodeForceQuit) case syscall.SIGTERM: Log().Info("shutting down apps, then terminating", zap.String("signal", "SIGTERM")) exitProcessFromSignal("SIGTERM") case syscall.SIGUSR1: Log().Info("not implemented", zap.String("signal", "SIGUSR1")) case syscall.SIGUSR2: Log().Info("not implemented", zap.String("signal", "SIGUSR2")) case syscall.SIGHUP: // ignore; this signal is sometimes sent outside of the user's control Log().Info("not implemented", zap.String("signal", "SIGHUP")) } } }() } caddy-2.6.2/storage.go000066400000000000000000000117631435007237400146150ustar00rootroot00000000000000// 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 ( "os" "path/filepath" "runtime" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) // StorageConverter is a type that can convert itself // to a valid, usable certmagic.Storage value. (The // value might be short-lived.) This interface allows // us to adapt any CertMagic storage implementation // into a consistent API for Caddy configuration. type StorageConverter interface { CertMagicStorage() (certmagic.Storage, error) } // HomeDir returns the best guess of the current user's home // directory from environment variables. If unknown, "." (the // current directory) is returned instead, except GOOS=android, // which returns "/sdcard". func HomeDir() string { home := homeDirUnsafe() if home == "" && runtime.GOOS == "android" { home = "/sdcard" } if home == "" { home = "." } return home } // homeDirUnsafe is a low-level function that returns // the user's home directory from environment // variables. Careful: if it cannot be determined, an // empty string is returned. If not accounting for // that case, use HomeDir() instead; otherwise you // may end up using the root of the file system. func homeDirUnsafe() string { home := os.Getenv("HOME") if home == "" && runtime.GOOS == "windows" { drive := os.Getenv("HOMEDRIVE") path := os.Getenv("HOMEPATH") home = drive + path if drive == "" || path == "" { home = os.Getenv("USERPROFILE") } } if home == "" && runtime.GOOS == "plan9" { home = os.Getenv("home") } return home } // AppConfigDir returns the directory where to store user's config. // // If XDG_CONFIG_HOME is set, it returns: $XDG_CONFIG_HOME/caddy. // Otherwise, os.UserConfigDir() is used; if successful, it appends // "Caddy" (Windows & Mac) or "caddy" (every other OS) to the path. // If it returns an error, the fallback path "./caddy" is returned. // // The config directory is not guaranteed to be different from // AppDataDir(). // // Unlike os.UserConfigDir(), this function prefers the // XDG_CONFIG_HOME env var on all platforms, not just Unix. // // Ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html func AppConfigDir() string { if basedir := os.Getenv("XDG_CONFIG_HOME"); basedir != "" { return filepath.Join(basedir, "caddy") } basedir, err := os.UserConfigDir() if err != nil { Log().Warn("unable to determine directory for user configuration; falling back to current directory", zap.Error(err)) return "./caddy" } subdir := "caddy" switch runtime.GOOS { case "windows", "darwin": subdir = "Caddy" } return filepath.Join(basedir, subdir) } // AppDataDir returns a directory path that is suitable for storing // application data on disk. It uses the environment for finding the // best place to store data, and appends a "caddy" or "Caddy" (depending // on OS and environment) subdirectory. // // For a base directory path: // If XDG_DATA_HOME is set, it returns: $XDG_DATA_HOME/caddy; otherwise, // on Windows it returns: %AppData%/Caddy, // on Mac: $HOME/Library/Application Support/Caddy, // on Plan9: $home/lib/caddy, // on Android: $HOME/caddy, // and on everything else: $HOME/.local/share/caddy. // // If a data directory cannot be determined, it returns "./caddy" // (this is not ideal, and the environment should be fixed). // // The data directory is not guaranteed to be different from AppConfigDir(). // // Ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html func AppDataDir() string { if basedir := os.Getenv("XDG_DATA_HOME"); basedir != "" { return filepath.Join(basedir, "caddy") } switch runtime.GOOS { case "windows": appData := os.Getenv("AppData") if appData != "" { return filepath.Join(appData, "Caddy") } case "darwin": home := homeDirUnsafe() if home != "" { return filepath.Join(home, "Library", "Application Support", "Caddy") } case "plan9": home := homeDirUnsafe() if home != "" { return filepath.Join(home, "lib", "caddy") } case "android": home := homeDirUnsafe() if home != "" { return filepath.Join(home, "caddy") } default: home := homeDirUnsafe() if home != "" { return filepath.Join(home, ".local", "share", "caddy") } } return "./caddy" } // ConfigAutosavePath is the default path to which the last config will be persisted. var ConfigAutosavePath = filepath.Join(AppConfigDir(), "autosave.json") // DefaultStorage is Caddy's default storage module. var DefaultStorage = &certmagic.FileStorage{Path: AppDataDir()} caddy-2.6.2/usagepool.go000066400000000000000000000150121435007237400151360ustar00rootroot00000000000000// 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 ( "fmt" "sync" "sync/atomic" ) // UsagePool is a thread-safe map that pools values // based on usage (reference counting). Values are // only inserted if they do not already exist. There // are two ways to add values to the pool: // // 1. LoadOrStore will increment usage and store the // value immediately if it does not already exist. // 2. LoadOrNew will atomically check for existence // and construct the value immediately if it does // not already exist, or increment the usage // otherwise, then store that value in the pool. // When the constructed value is finally deleted // from the pool (when its usage reaches 0), it // will be cleaned up by calling Destruct(). // // The use of LoadOrNew allows values to be created // and reused and finally cleaned up only once, even // though they may have many references throughout // their lifespan. This is helpful, for example, when // sharing thread-safe io.Writers that you only want // to open and close once. // // There is no way to overwrite existing keys in the // pool without first deleting it as many times as it // was stored. Deleting too many times will panic. // // The implementation does not use a sync.Pool because // UsagePool needs additional atomicity to run the // constructor functions when creating a new value when // LoadOrNew is used. (We could probably use sync.Pool // but we'd still have to layer our own additional locks // on top.) // // An empty UsagePool is NOT safe to use; always call // NewUsagePool() to make a new one. type UsagePool struct { sync.RWMutex pool map[any]*usagePoolVal } // NewUsagePool returns a new usage pool that is ready to use. func NewUsagePool() *UsagePool { return &UsagePool{ pool: make(map[any]*usagePoolVal), } } // LoadOrNew loads the value associated with key from the pool if it // already exists. If the key doesn't exist, it will call construct // to create a new value and then stores that in the pool. An error // is only returned if the constructor returns an error. The loaded // or constructed value is returned. The loaded return value is true // if the value already existed and was loaded, or false if it was // newly constructed. func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loaded bool, err error) { var upv *usagePoolVal up.Lock() upv, loaded = up.pool[key] if loaded { atomic.AddInt32(&upv.refs, 1) up.Unlock() upv.RLock() value = upv.value err = upv.err upv.RUnlock() } else { upv = &usagePoolVal{refs: 1} upv.Lock() up.pool[key] = upv up.Unlock() value, err = construct() if err == nil { upv.value = value } else { upv.err = err up.Lock() // this *should* be safe, I think, because we have a // write lock on upv, but we might also need to ensure // that upv.err is nil before doing this, since we // released the write lock on up during construct... // but then again it's also after midnight... delete(up.pool, key) up.Unlock() } upv.Unlock() } return } // LoadOrStore loads the value associated with key from the pool if it // already exists, or stores it if it does not exist. It returns the // value that was either loaded or stored, and true if the value already // existed and was func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) { var upv *usagePoolVal up.Lock() upv, loaded = up.pool[key] if loaded { atomic.AddInt32(&upv.refs, 1) up.Unlock() upv.Lock() if upv.err == nil { value = upv.value } else { upv.value = val upv.err = nil } upv.Unlock() } else { upv = &usagePoolVal{refs: 1, value: val} up.pool[key] = upv up.Unlock() value = val } return } // Range iterates the pool similarly to how sync.Map.Range() does: // it calls f for every key in the pool, and if f returns false, // iteration is stopped. Ranging does not affect usage counts. // // This method is somewhat naive and acquires a read lock on the // entire pool during iteration, so do your best to make f() really // fast, m'kay? func (up *UsagePool) Range(f func(key, value any) bool) { up.RLock() defer up.RUnlock() for key, upv := range up.pool { upv.RLock() if upv.err != nil { upv.RUnlock() continue } val := upv.value upv.RUnlock() if !f(key, val) { break } } } // Delete decrements the usage count for key and removes the // value from the underlying map if the usage is 0. It returns // true if the usage count reached 0 and the value was deleted. // It panics if the usage count drops below 0; always call // Delete precisely as many times as LoadOrStore. func (up *UsagePool) Delete(key any) (deleted bool, err error) { up.Lock() upv, ok := up.pool[key] if !ok { up.Unlock() return false, nil } refs := atomic.AddInt32(&upv.refs, -1) if refs == 0 { delete(up.pool, key) up.Unlock() upv.RLock() val := upv.value upv.RUnlock() if destructor, ok := val.(Destructor); ok { err = destructor.Destruct() } deleted = true } else { up.Unlock() if refs < 0 { panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)", upv.value, upv.refs)) } } return } // References returns the number of references (count of usages) to a // key in the pool, and true if the key exists, or false otherwise. func (up *UsagePool) References(key any) (int, bool) { up.RLock() upv, loaded := up.pool[key] up.RUnlock() if loaded { // I wonder if it'd be safer to read this value during // our lock on the UsagePool... guess we'll see... refs := atomic.LoadInt32(&upv.refs) return int(refs), true } return 0, false } // Constructor is a function that returns a new value // that can destruct itself when it is no longer needed. type Constructor func() (Destructor, error) // Destructor is a value that can clean itself up when // it is deallocated. type Destructor interface { Destruct() error } type usagePoolVal struct { refs int32 // accessed atomically; must be 64-bit aligned for 32-bit systems value any err error sync.RWMutex }