diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..a0717e4b --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05ec80f3..f194aa2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,16 +19,16 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] - go: [ '1.17', '1.18' ] + go: [ '1.18', '1.19' ] include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.17' - GO_SEMVER: '~1.17.9' - - go: '1.18' - GO_SEMVER: '~1.18.1' + GO_SEMVER: '~1.18.4' + + - go: '1.19' + GO_SEMVER: '~1.19.0' # Set some variables per OS, usable via ${{ matrix.VAR }} # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index f37dc854..653102c6 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -16,13 +16,13 @@ jobs: fail-fast: false matrix: goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd'] - go: [ '1.18' ] + go: [ '1.19' ] include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.18' - GO_SEMVER: '~1.18.1' + - go: '1.19' + GO_SEMVER: '~1.19.0' runs-on: ubuntu-latest continue-on-error: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0aae6196..d437d7bf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,17 +14,22 @@ jobs: # From https://github.com/golangci/golangci-lint-action golangci: name: lint - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '~1.17.9' + go-version: '~1.18.4' check-latest: true - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: - version: v1.44 + version: v1.47 + # Windows times out frequently after about 5m50s if we don't set a longer timeout. + args: --timeout 10m # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80241201..435086fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,15 +11,22 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - go: [ '1.18' ] + go: [ '1.19' ] include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.18' - GO_SEMVER: '~1.18.1' + - go: '1.19' + GO_SEMVER: '~1.19.0' runs-on: ${{ matrix.os }} + # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233 + # https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings + permissions: + id-token: write + # https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents + # "Releases" is part of `contents`, so it needs the `write` + contents: write steps: - name: Install Go @@ -99,7 +106,14 @@ jobs: key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go${{ matrix.go }}-release - + - name: Install Cosign + uses: sigstore/cosign-installer@main + - name: Cosign version + run: cosign version + - name: Install Syft + uses: anchore/sbom-action/download-syft@main + - name: Syft version + run: syft version # GoReleaser will take care of publishing those artifacts into the release - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 @@ -109,6 +123,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.vars.outputs.version_tag }} + COSIGN_EXPERIMENTAL: 1 # Only publish on non-special tags (e.g. non-beta) # We will continue to push to Gemfury for the foreseeable future, although diff --git a/.goreleaser.yml b/.goreleaser.yml index 0065d407..9369bc48 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,7 +14,11 @@ before: # run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation. - /bin/sh -c 'cd ./caddy-build && go mod tidy' - git clone --depth 1 https://github.com/caddyserver/dist caddy-dist + - mkdir -p caddy-dist/man - go mod download + - go run cmd/caddy/main.go manpage --directory ./caddy-dist/man + - gzip -r ./caddy-dist/man/ + - /bin/sh -c 'go run cmd/caddy/main.go completion bash > ./caddy-dist/scripts/bash-completion' builds: - env: @@ -58,9 +62,22 @@ builds: goarm: "5" flags: - -trimpath + - -mod=readonly ldflags: - -s -w - +signs: + - cmd: cosign + signature: "${artifact}.sig" + certificate: '{{ trimsuffix .Env.artifact ".tar.gz" }}.pem' + args: ["sign-blob", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"] + artifacts: all +sboms: + - artifacts: binary + # defaults to + # documents: + # - "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom" + cmd: syft + args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"] archives: - format_overrides: - goos: windows @@ -96,13 +113,16 @@ nfpms: - src: ./caddy-dist/welcome/index.html dst: /usr/share/caddy/index.html - - src: ./caddy-dist/scripts/completions/bash-completion + - src: ./caddy-dist/scripts/bash-completion dst: /etc/bash_completion.d/caddy - src: ./caddy-dist/config/Caddyfile dst: /etc/caddy/Caddyfile type: config + - src: ./caddy-dist/man/* + dst: /usr/share/man/man8/ + scripts: postinstall: ./caddy-dist/scripts/postinstall.sh preremove: ./caddy-dist/scripts/preremove.sh diff --git a/README.md b/README.md index 251e54a5..715eb135 100644 --- a/README.md +++ b/README.md @@ -57,25 +57,25 @@ - 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 tens of thousands of sites** ... and probably more -- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support +- **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, so much more to [discover](https://caddyserver.com/v2) +- So much more to [discover](https://caddyserver.com/v2) ## Install -The simplest, cross-platform way is to download from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH. +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. -For other install options, see https://caddyserver.com/docs/install. +See [our online documentation](https://caddyserver.com/docs/install) for other install instructions. ## Build from source Requirements: -- [Go 1.17 or newer](https://golang.org/dl/) +- [Go 1.18 or newer](https://golang.org/dl/) ### For development @@ -164,9 +164,9 @@ The docs are also open source. You can contribute to them here: https://github.c ## Getting help -- We **strongly recommend** that all professionals or companies using Caddy get a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed. +- 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! 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's also a great look for your company to your customers and potential customers! +- 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! diff --git a/admin.go b/admin.go index e37caf83..36cc2f82 100644 --- a/admin.go +++ b/admin.go @@ -21,7 +21,6 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" - "encoding/hex" "encoding/json" "errors" "expvar" @@ -41,7 +40,6 @@ import ( "sync" "time" - "github.com/caddyserver/caddy/v2/notify" "github.com/caddyserver/certmagic" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" @@ -341,17 +339,19 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL { // 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 + // 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 { + if oldAdminServer != nil && err == nil { go func(oldAdminServer *http.Server) { err := stopAdminServer(oldAdminServer) if err != nil { @@ -442,7 +442,7 @@ func manageIdentity(ctx Context, cfg *Config) error { if err != nil { return fmt.Errorf("loading identity issuer modules: %s", err) } - for _, issVal := range val.([]interface{}) { + for _, issVal := range val.([]any) { cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer)) } } @@ -901,6 +901,12 @@ func (h adminHandler) originAllowed(origin *url.URL) bool { // 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: @@ -919,7 +925,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error { // we could consider setting up a sync.Pool for the summed // hashes to reduce GC pressure. - w.Header().Set("ETag", r.URL.Path+" "+hex.EncodeToString(hash.Sum(nil))) + w.Header().Set("Etag", makeEtag(r.URL.Path, hash)) return nil @@ -988,9 +994,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error { id := parts[2] // map the ID to the expanded path - currentCfgMu.RLock() + currentCtxMu.RLock() expanded, ok := rawCfgIndex[id] - defer currentCfgMu.RUnlock() + defer currentCtxMu.RUnlock() if !ok { return APIError{ HTTPStatus: http.StatusNotFound, @@ -1013,10 +1019,6 @@ func handleStop(w http.ResponseWriter, r *http.Request) error { } } - if err := notify.NotifyStopping(); err != nil { - Log().Error("unable to notify stopping to service manager", zap.Error(err)) - } - exitProcess(context.Background(), Log().Named("admin.api")) return nil } @@ -1025,11 +1027,11 @@ func handleStop(w http.ResponseWriter, r *http.Request) error { // 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 currentCfgMu, depending on method (GET needs +// 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 interface{} + var val any // if there is a request body, decode it into the // variable that will be set in the config according @@ -1066,16 +1068,16 @@ func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error parts = parts[:len(parts)-1] } - var ptr interface{} = rawCfg + var ptr any = rawCfg traverseLoop: for i, part := range parts { switch v := ptr.(type) { - case map[string]interface{}: + 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].([]interface{}); ok && i == len(parts)-2 { + if arr, ok := v[part].([]any); ok && i == len(parts)-2 { var idx int if method != http.MethodPost { idxStr := parts[len(parts)-1] @@ -1097,7 +1099,7 @@ traverseLoop: } case http.MethodPost: if ellipses { - valArray, ok := val.([]interface{}) + valArray, ok := val.([]any) if !ok { return fmt.Errorf("final element is not an array") } @@ -1132,9 +1134,9 @@ traverseLoop: 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].([]interface{}); ok { + if arr, ok := v[part].([]any); ok { if ellipses { - valArray, ok := val.([]interface{}) + valArray, ok := val.([]any) if !ok { return fmt.Errorf("final element is not an array") } @@ -1165,12 +1167,12 @@ traverseLoop: // 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]interface{}) + v[part] = make(map[string]any) } ptr = v[part] } - case []interface{}: + case []any: partInt, err := strconv.Atoi(part) if err != nil { return fmt.Errorf("[/%s] invalid array index '%s': %v", @@ -1192,7 +1194,7 @@ traverseLoop: // 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]interface{} +// 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 { @@ -1324,7 +1326,7 @@ const ( ) var bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { return new(bytes.Buffer) }, } diff --git a/admin_test.go b/admin_test.go index 32f20c62..d53988b3 100644 --- a/admin_test.go +++ b/admin_test.go @@ -15,8 +15,8 @@ package caddy import ( - "encoding/hex" "encoding/json" + "fmt" "net/http" "reflect" "sync" @@ -115,7 +115,7 @@ func TestUnsyncedConfigAccess(t *testing.T) { } // decode the expected config so we can do a convenient DeepEqual - var expectedDecoded interface{} + var expectedDecoded any err = json.Unmarshal([]byte(tc.expect), &expectedDecoded) if err != nil { t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err) @@ -168,7 +168,7 @@ func TestETags(t *testing.T) { const key = "/" + rawConfigKey + "/apps/foo" // try update the config with the wrong etag - err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), "/"+rawConfigKey+" not_an_etag", false) + 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) } @@ -180,13 +180,13 @@ func TestETags(t *testing.T) { } // do the same update with the correct key - err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), key+" "+hex.EncodeToString(hash.Sum(nil)), false) + 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}`), key+" "+hex.EncodeToString(hash.Sum(nil)), false) + 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) } diff --git a/caddy.go b/caddy.go index 0c6dfcd0..26c149b9 100644 --- a/caddy.go +++ b/caddy.go @@ -102,20 +102,32 @@ func Run(cfg *Config) error { // if it is different from the current config or // forceReload is true. func Load(cfgJSON []byte, forceReload bool) error { - if err := notify.NotifyReloading(); err != nil { - Log().Error("unable to notify reloading to service manager", zap.Error(err)) + 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 := notify.NotifyReadiness(); err != nil { - Log().Error("unable to notify readiness to service manager", zap.Error(err)) + 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) + err = changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload) if errors.Is(err, errSameConfig) { err = nil // not really an error } + return err } @@ -127,7 +139,9 @@ func Load(cfgJSON []byte, forceReload bool) error { // 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. @@ -141,12 +155,20 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force return fmt.Errorf("method not allowed") } - currentCfgMu.Lock() - defer currentCfgMu.Unlock() + 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) + parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1]) if len(parts) != 2 { return APIError{ HTTPStatus: http.StatusBadRequest, @@ -209,7 +231,7 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force // 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 interface{} + var oldCfg any err2 := json.Unmarshal(rawCfgJSON, &oldCfg) if err2 != nil { err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2) @@ -234,18 +256,18 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force // readConfig traverses the current config to path // and writes its JSON encoding to out. func readConfig(path string, out io.Writer) error { - currentCfgMu.RLock() - defer currentCfgMu.RUnlock() + 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 currentCfgMu. -func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error { +// on currentCtxMu. +func indexConfigObjects(ptr any, configPath string, index map[string]string) error { switch val := ptr.(type) { - case map[string]interface{}: + case map[string]any: for k, v := range val { if k == idKey { switch idVal := v.(type) { @@ -264,7 +286,7 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str return err } } - case []interface{}: + case []any: // traverse each element of the array recursively for i := range val { err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index) @@ -282,7 +304,7 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str // 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 currentCfgMu is required! If +// 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 { @@ -311,17 +333,17 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { } // run the new config and start all its apps - err = run(newCfg, true) + ctx, err := run(newCfg, true) if err != nil { return err } - // swap old config with the new one - oldCfg := currentCfg - currentCfg = newCfg + // swap old context (including its config) with the new one + oldCtx := currentCtx + currentCtx = ctx // Stop, Cleanup each old app - unsyncedStop(oldCfg) + unsyncedStop(oldCtx) // autosave a non-nil config, if not disabled if allowPersist && @@ -365,7 +387,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { // 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) error { +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 @@ -396,8 +418,8 @@ func run(newCfg *Config, start bool) error { cancel() // also undo any other state changes we made - if currentCfg != nil { - certmagic.Default.Storage = currentCfg.storage + if currentCtx.cfg != nil { + certmagic.Default.Storage = currentCtx.cfg.storage } } }() @@ -409,14 +431,14 @@ func run(newCfg *Config, start bool) error { } err = newCfg.Logging.openLogs(ctx) if err != nil { - return err + return ctx, err } // start the admin endpoint (and stop any prior one) if start { err = replaceLocalAdminServer(newCfg) if err != nil { - return fmt.Errorf("starting caddy administration endpoint: %v", err) + return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err) } } @@ -445,7 +467,7 @@ func run(newCfg *Config, start bool) error { return nil }() if err != nil { - return err + return ctx, err } // Load and Provision each app and their submodules @@ -458,18 +480,18 @@ func run(newCfg *Config, start bool) error { return nil }() if err != nil { - return err + return ctx, err } if !start { - return nil + 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 err + return ctx, err } // Start @@ -494,12 +516,12 @@ func run(newCfg *Config, start bool) error { return nil }() if err != nil { - return err + 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 finishSettingUp(ctx, newCfg) + return ctx, finishSettingUp(ctx, newCfg) } // finishSettingUp should be run after all apps have successfully started. @@ -604,10 +626,10 @@ type ConfigLoader interface { // stop the others. Stop should only be called // if not replacing with a new config. func Stop() error { - currentCfgMu.Lock() - defer currentCfgMu.Unlock() - unsyncedStop(currentCfg) - currentCfg = nil + currentCtxMu.Lock() + defer currentCtxMu.Unlock() + unsyncedStop(currentCtx) + currentCtx = Context{} rawCfgJSON = nil rawCfgIndex = nil rawCfg[rawConfigKey] = nil @@ -620,13 +642,13 @@ func Stop() error { // it is logged and the function continues stopping // the next app. This function assumes all apps in // cfg were successfully started first. -func unsyncedStop(cfg *Config) { - if cfg == nil { +func unsyncedStop(ctx Context) { + if ctx.cfg == nil { return } // stop each app - for name, a := range cfg.apps { + for name, a := range ctx.cfg.apps { err := a.Stop() if err != nil { log.Printf("[ERROR] stop %s: %v", name, err) @@ -634,13 +656,13 @@ func unsyncedStop(cfg *Config) { } // clean up all modules - cfg.cancelFunc() + ctx.cfg.cancelFunc() } // Validate loads, provisions, and validates // cfg, but does not start running it. func Validate(cfg *Config) error { - err := run(cfg, false) + _, err := run(cfg, false) if err == nil { cfg.cancelFunc() // call Cleanup on all modules } @@ -654,6 +676,10 @@ func Validate(cfg *Config) error { // Errors are logged along the way, and an appropriate exit // code is emitted. func exitProcess(ctx context.Context, logger *zap.Logger) { + if err := notify.Stopping(); err != nil { + Log().Error("unable to notify service manager of stopping state", zap.Error(err)) + } + if logger == nil { logger = Log() } @@ -783,36 +809,106 @@ func InstanceID() (uuid.UUID, error) { return uuid.ParseBytes(uuidFileBytes) } -// GoModule returns the build info of this Caddy -// build from debug.BuildInfo (requires Go modules). -// If no version information is available, a non-nil -// value will still be returned, but with an -// unknown version. -func GoModule() *debug.Module { - var mod debug.Module - return goModule(&mod) -} - -// goModule holds the actual implementation of GoModule. -// Allocating debug.Module in GoModule() and passing a -// reference to goModule enables mid-stack inlining. -func goModule(mod *debug.Module) *debug.Module { - mod.Version = "unknown" +// 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)" becaise Go uses that, but for +// the simple form we change it to "unknown". +// +// 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 { - mod.Path = bi.Main.Path - // The recommended way to build Caddy involves - // creating a separate main module, which - // TODO: track related Go issue: https://github.com/golang/go/issues/29228 - // once that issue is fixed, we should just be able to use bi.Main... hopefully. + // find the Caddy module in the dependency list for _, dep := range bi.Deps { if dep.Path == ImportPath { - return dep + module = dep + break } } - return &bi.Main } - return mod + 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 simple == "" || simple == "(devel)" { + 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. @@ -820,18 +916,21 @@ type CtxKey string // This group of variables pertains to the current configuration. var ( - // currentCfgMu protects everything in this var block. - currentCfgMu sync.RWMutex + // currentCtxMu protects everything in this var block. + currentCtxMu sync.RWMutex - // currentCfg is the currently-running configuration. - currentCfg *Config + // 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]interface{}{ + rawCfg = map[string]any{ rawConfigKey: nil, } @@ -850,4 +949,5 @@ var ( 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" diff --git a/caddyconfig/caddyfile/adapter.go b/caddyconfig/caddyfile/adapter.go index 55d7253a..d5bb7c22 100644 --- a/caddyconfig/caddyfile/adapter.go +++ b/caddyconfig/caddyfile/adapter.go @@ -29,12 +29,12 @@ type Adapter struct { } // Adapt converts the Caddyfile config in body to Caddy JSON. -func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []caddyconfig.Warning, error) { +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]interface{}) + options = make(map[string]any) } filename, _ := options["filename"].(string) @@ -116,7 +116,7 @@ type ServerType interface { // (e.g. CLI flags) and creates a Caddy // config, along with any warnings or // an error. - Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error) + Setup([]ServerBlock, map[string]any) (*caddy.Config, []caddyconfig.Warning, error) } // UnmarshalModule instantiates a module with the given ID and invokes diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go index 8a78f433..91bd9a50 100644 --- a/caddyconfig/caddyfile/dispenser.go +++ b/caddyconfig/caddyfile/dispenser.go @@ -146,15 +146,15 @@ func (d *Dispenser) NextLine() bool { // // Proper use of this method looks like this: // -// for nesting := d.Nesting(); d.NextBlock(nesting); { -// } +// 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) { -// } +// for d.NextBlock(0) { +// } // // As with other token parsing logic, a loop over // NextBlock() should be contained within a loop over @@ -217,7 +217,7 @@ func (d *Dispenser) ValRaw() string { // 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() interface{} { +func (d *Dispenser) ScalarVal() any { if d.cursor < 0 || d.cursor >= len(d.tokens) { return nil } @@ -412,7 +412,7 @@ func (d *Dispenser) Err(msg string) error { } // Errf is like Err, but for formatted error messages -func (d *Dispenser) Errf(format string, args ...interface{}) error { +func (d *Dispenser) Errf(format string, args ...any) error { return d.WrapErr(fmt.Errorf(format, args...)) } diff --git a/caddyconfig/caddyfile/formatter_fuzz.go b/caddyconfig/caddyfile/formatter_fuzz.go index c07a2268..7c1fc643 100644 --- a/caddyconfig/caddyfile/formatter_fuzz.go +++ b/caddyconfig/caddyfile/formatter_fuzz.go @@ -13,7 +13,6 @@ // limitations under the License. //go:build gofuzz -// +build gofuzz package caddyfile diff --git a/caddyconfig/caddyfile/lexer.go b/caddyconfig/caddyfile/lexer.go index 4a23524d..5605a6aa 100644 --- a/caddyconfig/caddyfile/lexer.go +++ b/caddyconfig/caddyfile/lexer.go @@ -191,3 +191,7 @@ func Tokenize(input []byte, filename string) ([]Token, error) { } return tokens, nil } + +func (t Token) Quoted() bool { + return t.wasQuoted > 0 +} diff --git a/caddyconfig/caddyfile/lexer_fuzz.go b/caddyconfig/caddyfile/lexer_fuzz.go index 179fd87c..6f75694b 100644 --- a/caddyconfig/caddyfile/lexer_fuzz.go +++ b/caddyconfig/caddyfile/lexer_fuzz.go @@ -13,7 +13,6 @@ // limitations under the License. //go:build gofuzz -// +build gofuzz package caddyfile diff --git a/caddyconfig/configadapters.go b/caddyconfig/configadapters.go index ccac5f88..0ca3c3af 100644 --- a/caddyconfig/configadapters.go +++ b/caddyconfig/configadapters.go @@ -24,7 +24,7 @@ import ( // 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]interface{}) ([]byte, []Warning, error) + Adapt(body []byte, options map[string]any) ([]byte, []Warning, error) } // Warning represents a warning or notice related to conversion. @@ -48,7 +48,7 @@ func (w Warning) String() string { // 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 interface{}, warnings *[]Warning) json.RawMessage { +func JSON(val any, warnings *[]Warning) json.RawMessage { b, err := json.Marshal(val) if err != nil { if warnings != nil { @@ -64,9 +64,9 @@ func JSON(val interface{}, warnings *[]Warning) json.RawMessage { // 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]interface{} (i.e. it must be +// 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 interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage { +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 { @@ -77,7 +77,7 @@ func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]W } // then decode the object - var tmp map[string]interface{} + var tmp map[string]any err = json.Unmarshal(enc, &tmp) if err != nil { if warnings != nil { diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go index c7923e81..e7a7cdb0 100644 --- a/caddyconfig/httpcaddyfile/addresses.go +++ b/caddyconfig/httpcaddyfile/addresses.go @@ -17,6 +17,7 @@ package httpcaddyfile import ( "fmt" "net" + "net/netip" "reflect" "sort" "strconv" @@ -76,7 +77,7 @@ import ( // 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]interface{}) (map[string][]serverBlock, error) { + options map[string]any) (map[string][]serverBlock, error) { sbmap := make(map[string][]serverBlock) for i, sblock := range originalServerBlocks { @@ -183,8 +184,10 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se 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]interface{}) ([]string, error) { + options map[string]any) ([]string, error) { addr, err := ParseAddress(key) if err != nil { return nil, fmt.Errorf("parsing key: %v", err) @@ -232,12 +235,14 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str // use a map to prevent duplication listeners := make(map[string]struct{}) for _, host := range lnHosts { - addr, err := caddy.ParseNetworkAddress(host) - if err == nil && addr.IsUnixNetwork() { - listeners[host] = struct{}{} - } else { - listeners[host+":"+lnPort] = struct{}{} + // host can have network + host (e.g. "tcp6/localhost") but + // will/should not have port information because this usually + // comes from the bind directive, so we append the port + addr, err := caddy.ParseNetworkAddress(host + ":" + lnPort) + if err != nil { + return nil, fmt.Errorf("parsing network address: %v", err) } + listeners[addr.String()] = struct{}{} } // now turn map into list @@ -350,9 +355,9 @@ func (a Address) Normalize() Address { // ensure host is normalized if it's an IP address host := strings.TrimSpace(a.Host) - if ip := net.ParseIP(host); ip != nil { - if ipv6 := ip.To16(); ipv6 != nil && ipv6.DefaultMask() == nil { - host = ipv6.String() + if ip, err := netip.ParseAddr(host); err == nil { + if ip.Is6() && !ip.Is4() && !ip.Is4In6() { + host = ip.String() } } diff --git a/caddyconfig/httpcaddyfile/addresses_fuzz.go b/caddyconfig/httpcaddyfile/addresses_fuzz.go index be873ef1..364ff971 100644 --- a/caddyconfig/httpcaddyfile/addresses_fuzz.go +++ b/caddyconfig/httpcaddyfile/addresses_fuzz.go @@ -13,7 +13,6 @@ // limitations under the License. //go:build gofuzz -// +build gofuzz package httpcaddyfile diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 5c539e2a..cd233484 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -540,8 +540,13 @@ func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) { // parseRedir parses the redir directive. Syntax: // -// redir [] [] +// 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() @@ -558,6 +563,7 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { } var body string + var hdr http.Header switch code { case "permanent": code = "301" @@ -578,7 +584,7 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { ` safeTo := html.EscapeString(to) body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) - code = "302" + code = "200" // don't redirect non-browser clients default: // Allow placeholders for the code if strings.HasPrefix(code, "{") { @@ -601,9 +607,14 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { } } + // don't redirect non-browser clients + if code != "200" { + hdr = http.Header{"Location": []string{to}} + } + return caddyhttp.StaticResponse{ StatusCode: caddyhttp.WeakString(code), - Headers: http.Header{"Location": []string{to}}, + Headers: hdr, Body: body, }, nil } diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index 164b9126..e2113ebb 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -142,8 +142,8 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) { type Helper struct { *caddyfile.Dispenser // State stores intermediate variables during caddyfile adaptation. - State map[string]interface{} - options map[string]interface{} + State map[string]any + options map[string]any warnings *[]caddyconfig.Warning matcherDefs map[string]caddy.ModuleMap parentBlock caddyfile.ServerBlock @@ -151,7 +151,7 @@ type Helper struct { } // Option gets the option keyed by name. -func (h Helper) Option(name string) interface{} { +func (h Helper) Option(name string) any { return h.options[name] } @@ -175,7 +175,7 @@ func (h Helper) Caddyfiles() []string { } // JSON converts val into JSON. Any errors are added to warnings. -func (h Helper) JSON(val interface{}) json.RawMessage { +func (h Helper) JSON(val any) json.RawMessage { return caddyconfig.JSON(val, h.warnings) } @@ -375,7 +375,7 @@ type ConfigValue struct { // The value to be used when building the config. // Generally its type is associated with the // name of the Class. - Value interface{} + Value any directive string } @@ -406,7 +406,7 @@ func sortRoutes(routes []ConfigValue) { return false } - // decode the path matchers, if there is just one of them + // 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) @@ -415,38 +415,45 @@ func sortRoutes(routes []ConfigValue) { _ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM) } - // sort by longer path (more specific) first; missing path - // matchers or multi-matchers are treated as zero-length paths + // 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) > 0 { + if len(iPM) == 1 { iPathLen = len(iPM[0]) } - if len(jPM) > 0 { + if len(jPM) == 1 { jPathLen = len(jPM[0]) } // some directives involve setting values which can overwrite - // eachother, so it makes most sense to reverse the order so + // 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" { - // if both directives have no path matcher, use whichever one - // has no matcher first. - if iPathLen == 0 && jPathLen == 0 { - return len(iRoute.MatcherSetsRaw) == 0 && len(jRoute.MatcherSetsRaw) > 0 + // 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 } - // sort with the 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 { - // if both directives have no path matcher, use whichever one - // has any kind of matcher defined first. - if iPathLen == 0 && jPathLen == 0 { - return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0 + // 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 } - // sort with the 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 } }) } @@ -567,7 +574,7 @@ type ( // 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 interface{}) (interface{}, error) + UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal any) (any, error) ) var registeredDirectives = make(map[string]UnmarshalFunc) diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 5d782441..9cf386a4 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -53,27 +53,18 @@ type ServerType struct { // Setup makes a config from the tokens. func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock, - options map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error) { + options map[string]any) (*caddy.Config, []caddyconfig.Warning, error) { var warnings []caddyconfig.Warning gc := counter{new(int)} - state := make(map[string]interface{}) + state := make(map[string]any) - // load all the server blocks and associate them with a "pile" - // of config values; also prohibit duplicate keys because they - // can make a config confusing if more than one server block is - // chosen to handle a request - we actually will make each - // server block's route terminal so that only one will run - sbKeys := make(map[string]struct{}) + // load all the server blocks and associate them with a "pile" of config values originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks)) - for i, sblock := range 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) } - if _, ok := sbKeys[k]; ok { - return nil, warnings, fmt.Errorf("duplicate site address not allowed: '%s' in %v (site block %d, key %d)", k, sblock.Keys, i, j) - } - sbKeys[k] = struct{}{} } originalServerBlocks = append(originalServerBlocks, serverBlock{ block: sblock, @@ -100,14 +91,17 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock, search *regexp.Regexp replace string }{ - {regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"}, - {regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"}, {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 { @@ -199,10 +193,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock, // 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), - Servers: servers, + 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 @@ -322,14 +317,14 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock, // 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]interface{}) ([]serverBlock, error) { +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 interface{} + var val any var err error disp := caddyfile.NewDispenser(segment) @@ -399,7 +394,7 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options // to server blocks. Each pairing is essentially a server definition. func (st *ServerType) serversFromPairings( pairings []sbAddrAssociation, - options map[string]interface{}, + options map[string]any, warnings *[]caddyconfig.Warning, groupCounter counter, ) (map[string]*caddyhttp.Server, error) { @@ -420,6 +415,23 @@ func (st *ServerType) serversFromPairings( } 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, } @@ -717,7 +729,7 @@ func (st *ServerType) serversFromPairings( return servers, nil } -func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]interface{}) error { +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) @@ -943,7 +955,7 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList, 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 ordered, so it cannot be used here", val.directive) + return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here", val.directive) } } @@ -1191,6 +1203,7 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod 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 { @@ -1198,16 +1211,9 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M } matchers[definitionName] = make(caddy.ModuleMap) - // 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 { + // 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) @@ -1225,6 +1231,39 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M 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 @@ -1296,7 +1335,7 @@ func WasReplacedPlaceholderShorthand(token string) string { // tryInt tries to convert val to an integer. If it fails, // it downgrades the error to a warning and returns 0. -func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int { +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"}) @@ -1304,7 +1343,7 @@ func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int { return intVal } -func tryString(val interface{}, warnings *[]caddyconfig.Warning) string { +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"}) @@ -1312,7 +1351,7 @@ func tryString(val interface{}, warnings *[]caddyconfig.Warning) string { return stringVal } -func tryDuration(val interface{}, warnings *[]caddyconfig.Warning) caddy.Duration { +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"}) diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index ad52a7b0..c41e8bc8 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -31,11 +31,13 @@ func init() { 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) @@ -54,9 +56,9 @@ func init() { RegisterGlobalOption("preferred_chains", parseOptPreferredChains) } -func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil } +func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil } -func parseOptHTTPPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) { var httpPort int for d.Next() { var httpPortStr string @@ -72,7 +74,7 @@ func parseOptHTTPPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error return httpPort, nil } -func parseOptHTTPSPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) { var httpsPort int for d.Next() { var httpsPortStr string @@ -88,7 +90,7 @@ func parseOptHTTPSPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro return httpsPort, nil } -func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) { newOrder := directiveOrder for d.Next() { @@ -164,7 +166,7 @@ func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return newOrder, nil } -func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) { if !d.Next() { // consume option name return nil, d.ArgErr() } @@ -183,7 +185,7 @@ func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) return storage, nil } -func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) { if !d.Next() { // consume option name return nil, d.ArgErr() } @@ -197,7 +199,7 @@ func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error return caddy.Duration(dur), nil } -func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) { if !d.Next() { // consume option name return nil, d.ArgErr() } @@ -216,7 +218,7 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) return prov, nil } -func parseOptACMEEAB(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) { eab := new(acme.EAB) for d.Next() { if d.NextArg() { @@ -244,7 +246,7 @@ func parseOptACMEEAB(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) return eab, nil } -func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface{}, error) { +func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) { var issuers []certmagic.Issuer if existing != nil { issuers = existing.([]certmagic.Issuer) @@ -267,7 +269,7 @@ func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface return issuers, nil } -func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) { d.Next() // consume parameter name if !d.Next() { return "", d.ArgErr() @@ -279,7 +281,7 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, e return val, nil } -func parseOptStringList(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) { d.Next() // consume parameter name val := d.RemainingArgs() if len(val) == 0 { @@ -288,7 +290,7 @@ func parseOptStringList(d *caddyfile.Dispenser, _ interface{}) (interface{}, err return val, nil } -func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) { adminCfg := new(caddy.AdminConfig) for d.Next() { if d.NextArg() { @@ -324,7 +326,7 @@ func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return adminCfg, nil } -func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) { var ond *caddytls.OnDemandConfig for d.Next() { if d.NextArg() { @@ -384,7 +386,7 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error return ond, nil } -func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) { d.Next() // consume parameter name if !d.Next() { return "", d.ArgErr() @@ -399,11 +401,11 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro return val, nil } -func parseServerOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) { return unmarshalCaddyfileServerOptions(d) } -func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ any) (any, error) { d.Next() // consume option name var val string if !d.AllArgs(&val) { @@ -429,8 +431,7 @@ func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{ // // When the name argument is unspecified, this directive modifies the default // logger. -// -func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) { +func parseLogOptions(d *caddyfile.Dispenser, existingVal any) (any, error) { currentNames := make(map[string]struct{}) if existingVal != nil { innerVals, ok := existingVal.([]ConfigValue) @@ -465,7 +466,7 @@ func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface return configValues, nil } -func parseOptPreferredChains(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) { d.Next() return caddytls.ParseCaddyfilePreferredChainsOptions(d) } diff --git a/caddyconfig/httpcaddyfile/pkiapp.go b/caddyconfig/httpcaddyfile/pkiapp.go index 9feb4335..f8aba9ff 100644 --- a/caddyconfig/httpcaddyfile/pkiapp.go +++ b/caddyconfig/httpcaddyfile/pkiapp.go @@ -45,8 +45,7 @@ func init() { // } // // When the CA ID is unspecified, 'local' is assumed. -// -func parsePKIApp(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) { +func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) { pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)} for d.Next() { @@ -160,7 +159,7 @@ func parsePKIApp(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, func (st ServerType) buildPKIApp( pairings []sbAddrAssociation, - options map[string]interface{}, + options map[string]any, warnings []caddyconfig.Warning, ) (*caddypki.PKI, []caddyconfig.Warning, error) { diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go index 7a752524..f3e3d73f 100644 --- a/caddyconfig/httpcaddyfile/serveroptions.go +++ b/caddyconfig/httpcaddyfile/serveroptions.go @@ -38,14 +38,14 @@ type serverOptions struct { ReadHeaderTimeout caddy.Duration WriteTimeout caddy.Duration IdleTimeout caddy.Duration + KeepAliveInterval caddy.Duration MaxHeaderBytes int - AllowH2C bool - ExperimentalHTTP3 bool + Protocols []string StrictSNIHost *bool ShouldLogCredentials bool } -func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) { +func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { serverOpts := serverOptions{} for d.Next() { if d.NextArg() { @@ -123,6 +123,15 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error 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 @@ -141,22 +150,51 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error } 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 d.NextBlock(0) { + 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 + + // 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": - if d.NextArg() { - return nil, d.ArgErr() - } - serverOpts.AllowH2C = true + caddy.Log().Named("caddyfile").Warn("DEPRECATED: allow_h2c will be removed soon; use protocols option instead") - case "experimental_http3": if d.NextArg() { return nil, d.ArgErr() } - serverOpts.ExperimentalHTTP3 = true + 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()) } @@ -182,20 +220,9 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error // applyServerOptions sets the server options on the appropriate servers func applyServerOptions( servers map[string]*caddyhttp.Server, - options map[string]interface{}, + options map[string]any, warnings *[]caddyconfig.Warning, ) error { - // If experimental HTTP/3 is enabled, enable it on each server. - // We already know there won't be a conflict with serverOptions because - // we validated earlier that "experimental_http3" cannot be set at the same - // time as "servers" - if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 { - *warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"}) - for _, srv := range servers { - srv.ExperimentalHTTP3 = true - } - } - serverOpts, ok := options["servers"].([]serverOptions) if !ok { return nil @@ -228,9 +255,9 @@ func applyServerOptions( server.ReadHeaderTimeout = opts.ReadHeaderTimeout server.WriteTimeout = opts.WriteTimeout server.IdleTimeout = opts.IdleTimeout + server.KeepAliveInterval = opts.KeepAliveInterval server.MaxHeaderBytes = opts.MaxHeaderBytes - server.AllowH2C = opts.AllowH2C - server.ExperimentalHTTP3 = opts.ExperimentalHTTP3 + server.Protocols = opts.Protocols server.StrictSNIHost = opts.StrictSNIHost if opts.ShouldLogCredentials { if server.Logs == nil { diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 40ea47d0..947512aa 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -33,7 +33,7 @@ import ( func (st ServerType) buildTLSApp( pairings []sbAddrAssociation, - options map[string]interface{}, + options map[string]any, warnings []caddyconfig.Warning, ) (*caddytls.TLS, []caddyconfig.Warning, error) { @@ -307,6 +307,14 @@ func (st ServerType) buildTLSApp( 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 @@ -420,7 +428,7 @@ func (st ServerType) buildTLSApp( type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer } -func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) error { +func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) error { acmeWrapper, ok := issuer.(acmeCapable) if !ok { return nil @@ -467,7 +475,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf // 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]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, 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"] diff --git a/caddyconfig/load.go b/caddyconfig/load.go index 78aaba2b..9f5cda90 100644 --- a/caddyconfig/load.go +++ b/caddyconfig/load.go @@ -189,12 +189,11 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err } // adapter name should be suffix of MIME type - slashIdx := strings.Index(ct, "/") - if slashIdx < 0 { + _, adapterName, slashFound := strings.Cut(ct, "/") + if !slashFound { return nil, nil, fmt.Errorf("malformed Content-Type") } - adapterName := ct[slashIdx+1:] cfgAdapter := GetAdapter(adapterName) if cfgAdapter == nil { return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName) @@ -209,7 +208,7 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err } var bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { return new(bytes.Buffer) }, } diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 3be332ec..4fb33941 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -100,7 +100,7 @@ func (tc *Tester) InitServer(rawConfig string, configType string) { tc.t.Fail() } if err := tc.ensureConfigRunning(rawConfig, configType); err != nil { - tc.t.Logf("failed ensurng config is running: %s", err) + tc.t.Logf("failed ensuring config is running: %s", err) tc.t.Fail() } } @@ -186,7 +186,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil) } - var expected interface{} + var expected any err := json.Unmarshal(expectedBytes, &expected) if err != nil { return err @@ -196,7 +196,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error Timeout: Default.LoadRequestTimeout, } - fetchConfig := func(client *http.Client) interface{} { + fetchConfig := func(client *http.Client) any { resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) if err != nil { return nil @@ -206,7 +206,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error if err != nil { return nil } - var actual interface{} + var actual any err = json.Unmarshal(actualBytes, &actual) if err != nil { return nil @@ -371,7 +371,7 @@ func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, return false } - options := make(map[string]interface{}) + options := make(map[string]any) result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options) if err != nil { diff --git a/caddytest/integration/caddyfile_adapt/global_options.txt b/caddytest/integration/caddyfile_adapt/global_options.txt index 69e2d9d5..57831a44 100644 --- a/caddytest/integration/caddyfile_adapt/global_options.txt +++ b/caddytest/integration/caddyfile_adapt/global_options.txt @@ -3,6 +3,7 @@ http_port 8080 https_port 8443 grace_period 5s + shutdown_delay 10s default_sni localhost order root first storage file_system { @@ -45,6 +46,7 @@ "http_port": 8080, "https_port": 8443, "grace_period": 5000000000, + "shutdown_delay": 10000000000, "servers": { "srv0": { "listen": [ diff --git a/caddytest/integration/caddyfile_adapt/global_options_acme.txt b/caddytest/integration/caddyfile_adapt/global_options_acme.txt index 58ade308..1949d17a 100644 --- a/caddytest/integration/caddyfile_adapt/global_options_acme.txt +++ b/caddytest/integration/caddyfile_adapt/global_options_acme.txt @@ -22,6 +22,7 @@ } storage_clean_interval 7d renew_interval 1d + ocsp_interval 2d key_type ed25519 } @@ -83,6 +84,7 @@ }, "ask": "https://example.com" }, + "ocsp_interval": 172800000000000, "renew_interval": 86400000000000, "storage_clean_interval": 604800000000000 } diff --git a/caddytest/integration/caddyfile_adapt/global_server_options_single.txt b/caddytest/integration/caddyfile_adapt/global_server_options_single.txt index da3e6bc2..cfe6bfdf 100644 --- a/caddytest/integration/caddyfile_adapt/global_server_options_single.txt +++ b/caddytest/integration/caddyfile_adapt/global_server_options_single.txt @@ -12,11 +12,8 @@ } max_header_size 100MB log_credentials - protocol { - allow_h2c - experimental_http3 - strict_sni_host - } + strict_sni_host + protocols h1 h2 h2c h3 } } @@ -61,8 +58,12 @@ foo.com { "logs": { "should_log_credentials": true }, - "experimental_http3": true, - "allow_h2c": true + "protocols": [ + "h1", + "h2", + "h2c", + "h3" + ] } } } diff --git a/caddytest/integration/caddyfile_adapt/matcher_syntax.txt b/caddytest/integration/caddyfile_adapt/matcher_syntax.txt index a3e0a50e..fb3dfb66 100644 --- a/caddytest/integration/caddyfile_adapt/matcher_syntax.txt +++ b/caddytest/integration/caddyfile_adapt/matcher_syntax.txt @@ -19,27 +19,30 @@ @matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$` respond @matcher6 "from vars_regexp matcher without name" - @matcher7 { + @matcher7 `path('/foo*') && method('GET')` + respond @matcher7 "inline expression matcher shortcut" + + @matcher8 { header Foo bar header Foo foobar header Bar foo } - respond @matcher7 "header matcher merging values of the same field" + respond @matcher8 "header matcher merging values of the same field" - @matcher8 { + @matcher9 { query foo=bar foo=baz bar=foo query bar=baz } - respond @matcher8 "query matcher merging pairs with the same keys" + respond @matcher9 "query matcher merging pairs with the same keys" - @matcher9 { + @matcher10 { header !Foo header Bar foo } - respond @matcher9 "header matcher with null field matcher" + respond @matcher10 "header matcher with null field matcher" - @matcher10 remote_ip private_ranges - respond @matcher10 "remote_ip matcher with private ranges" + @matcher11 remote_ip private_ranges + respond @matcher11 "remote_ip matcher with private ranges" } ---------- { @@ -152,6 +155,19 @@ } ] }, + { + "match": [ + { + "expression": "path('/foo*') \u0026\u0026 method('GET')" + } + ], + "handle": [ + { + "body": "inline expression matcher shortcut", + "handler": "static_response" + } + ] + }, { "match": [ { diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_h2c_shorthand.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_h2c_shorthand.txt index 75ce9607..59394673 100644 --- a/caddytest/integration/caddyfile_adapt/reverse_proxy_h2c_shorthand.txt +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_h2c_shorthand.txt @@ -1,6 +1,8 @@ :8884 reverse_proxy h2c://localhost:8080 + +reverse_proxy unix+h2c//run/app.sock ---------- { "apps": { @@ -27,6 +29,21 @@ reverse_proxy h2c://localhost:8080 "dial": "localhost:8080" } ] + }, + { + "handler": "reverse_proxy", + "transport": { + "protocol": "http", + "versions": [ + "h2c", + "2" + ] + }, + "upstreams": [ + { + "dial": "unix//run/app.sock" + } + ] } ] } diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance.txt new file mode 100644 index 00000000..5885eec1 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance.txt @@ -0,0 +1,64 @@ +: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" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_options.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_options.txt index ea740f62..e05f1b90 100644 --- a/caddytest/integration/caddyfile_adapt/reverse_proxy_options.txt +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_options.txt @@ -24,8 +24,9 @@ https://example.com { max_conns_per_host 5 keepalive_idle_conns_per_host 2 keepalive_interval 30s - renegotiation freely - except_ports 8181 8182 + + tls_renegotiation freely + tls_except_ports 8181 8182 } } } diff --git a/caddytest/integration/caddyfile_test.go b/caddytest/integration/caddyfile_test.go index be85f4ab..27588833 100644 --- a/caddytest/integration/caddyfile_test.go +++ b/caddytest/integration/caddyfile_test.go @@ -68,7 +68,7 @@ func TestDuplicateHosts(t *testing.T) { } `, "caddyfile", - "duplicate site address not allowed") + "ambiguous site definition") } func TestReadCookie(t *testing.T) { diff --git a/caddytest/integration/map_test.go b/caddytest/integration/map_test.go index 7cd769e7..afed0c35 100644 --- a/caddytest/integration/map_test.go +++ b/caddytest/integration/map_test.go @@ -60,7 +60,7 @@ func TestMapRespondWithDefault(t *testing.T) { tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown") } -func TestMapAsJson(t *testing.T) { +func TestMapAsJSON(t *testing.T) { // arrange tester := caddytest.NewTester(t) tester.InitServer(` @@ -85,7 +85,7 @@ func TestMapAsJson(t *testing.T) { { "handler": "map", "source": "{http.request.method}", - "destinations": ["dest-name"], + "destinations": ["{dest-name}"], "defaults": ["unknown"], "mappings": [ { diff --git a/caddytest/integration/stream_test.go b/caddytest/integration/stream_test.go index d5edb793..cfd9d361 100644 --- a/caddytest/integration/stream_test.go +++ b/caddytest/integration/stream_test.go @@ -123,8 +123,8 @@ func TestH2ToH2CStream(t *testing.T) { // Disable any compression method from server. req.Header.Set("Accept-Encoding", "identity") - resp := tester.AssertResponseCode(req, 200) - if 200 != resp.StatusCode { + resp := tester.AssertResponseCode(req, http.StatusOK) + if resp.StatusCode != http.StatusOK { return } go func() { @@ -143,7 +143,6 @@ func TestH2ToH2CStream(t *testing.T) { if !strings.Contains(body, expectedBody) { t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body) } - return } func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server { @@ -335,8 +334,8 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { fmt.Fprint(w, expectedBody) w.Close() }() - resp := tester.AssertResponseCode(req, 200) - if 200 != resp.StatusCode { + resp := tester.AssertResponseCode(req, http.StatusOK) + if resp.StatusCode != http.StatusOK { return } @@ -351,7 +350,6 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { if body != expectedBody { t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body) } - return } func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server { diff --git a/cmd/cobra.go b/cmd/cobra.go new file mode 100644 index 00000000..d2a546ca --- /dev/null +++ b/cmd/cobra.go @@ -0,0 +1,120 @@ +package 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 +} diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index b18f5090..874cc6ff 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -29,7 +29,6 @@ import ( "os/exec" "runtime" "runtime/debug" - "sort" "strings" "github.com/aryann/difflib" @@ -280,7 +279,7 @@ func cmdStop(fl Flags) (int, error) { configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") - adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) + adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } @@ -310,7 +309,7 @@ func cmdReload(fl Flags) (int, error) { return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load") } - adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) + adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } @@ -331,30 +330,17 @@ func cmdReload(fl Flags) (int, error) { } func cmdVersion(_ Flags) (int, error) { - fmt.Println(CaddyVersion()) + _, full := caddy.Version() + fmt.Println(full) return caddy.ExitCodeSuccess, nil } -func cmdBuildInfo(fl Flags) (int, error) { +func cmdBuildInfo(_ Flags) (int, error) { bi, ok := debug.ReadBuildInfo() if !ok { return caddy.ExitCodeFailedStartup, fmt.Errorf("no build information") } - - fmt.Printf("go_version: %s\n", runtime.Version()) - fmt.Printf("go_os: %s\n", runtime.GOOS) - fmt.Printf("go_arch: %s\n", runtime.GOARCH) - fmt.Printf("path: %s\n", bi.Path) - fmt.Printf("main: %s %s %s\n", bi.Main.Path, bi.Main.Version, bi.Main.Sum) - fmt.Println("dependencies:") - - for _, goMod := range bi.Deps { - fmt.Printf("%s %s %s", goMod.Path, goMod.Version, goMod.Sum) - if goMod.Replace != nil { - fmt.Printf(" => %s %s %s", goMod.Replace.Path, goMod.Replace.Version, goMod.Replace.Sum) - } - fmt.Println() - } + fmt.Println(bi) return caddy.ExitCodeSuccess, nil } @@ -471,7 +457,7 @@ func cmdAdaptConfig(fl Flags) (int, error) { fmt.Errorf("reading input file: %v", err) } - opts := map[string]interface{}{"filename": adaptCmdInputFlag} + opts := map[string]any{"filename": adaptCmdInputFlag} adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts) if err != nil { @@ -593,70 +579,6 @@ func cmdFmt(fl Flags) (int, error) { return caddy.ExitCodeSuccess, nil } -func cmdHelp(fl Flags) (int, error) { - const fullDocs = `Full documentation is available at: -https://caddyserver.com/docs/command-line` - - args := fl.Args() - if len(args) == 0 { - s := `Caddy is an extensible server platform. - -usage: - caddy [] - -commands: -` - keys := make([]string, 0, len(commands)) - for k := range commands { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - cmd := commands[k] - short := strings.TrimSuffix(cmd.Short, ".") - s += fmt.Sprintf(" %-15s %s\n", cmd.Name, short) - } - - s += "\nUse 'caddy help ' for more information about a command.\n" - s += "\n" + fullDocs + "\n" - - fmt.Print(s) - - return caddy.ExitCodeSuccess, nil - } else if len(args) > 1 { - return caddy.ExitCodeFailedStartup, fmt.Errorf("can only give help with one command") - } - - subcommand, ok := commands[args[0]] - if !ok { - return caddy.ExitCodeFailedStartup, fmt.Errorf("unknown command: %s", args[0]) - } - - helpText := strings.TrimSpace(subcommand.Long) - if helpText == "" { - helpText = subcommand.Short - if !strings.HasSuffix(helpText, ".") { - helpText += "." - } - } - - result := fmt.Sprintf("%s\n\nusage:\n caddy %s %s\n", - helpText, - subcommand.Name, - strings.TrimSpace(subcommand.Usage), - ) - - if help := flagHelp(subcommand.Flags); help != "" { - result += fmt.Sprintf("\nflags:\n%s", help) - } - - result += "\n" + fullDocs + "\n" - - fmt.Print(result) - - 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 @@ -732,10 +654,11 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io // 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 `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, configFile, configAdapter string) (string, error) { +// 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 @@ -743,21 +666,29 @@ func DetermineAdminAPIAddress(address, configFile, configAdapter string) (string // Try to load the config from file if specified, with the given adapter name if configFile != "" { - // get the config in caddy's native format - config, loadedConfigFile, err := LoadConfig(configFile, configAdapter) - if err != nil { - return "", err - } - if loadedConfigFile == "" { - return "", fmt.Errorf("no config file to load") + 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") + } } - // get the address of the admin listener if set - if len(config) > 0 { + // 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(config, &tmpStruct) + err := json.Unmarshal(loadedConfig, &tmpStruct) if err != nil { return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err) } diff --git a/cmd/commands.go b/cmd/commands.go index 51960f34..e1e7a53e 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -16,7 +16,14 @@ 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, @@ -70,13 +77,6 @@ func Commands() map[string]Command { var commands = make(map[string]Command) func init() { - RegisterCommand(Command{ - Name: "help", - Func: cmdHelp, - Usage: "", - Short: "Shows help for a Caddy subcommand", - }) - RegisterCommand(Command{ Name: "start", Func: cmdStart, @@ -137,8 +137,8 @@ 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 is dangerous in production! Only use this option in a local -development environment.`, +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") @@ -200,6 +200,19 @@ config file; otherwise the default is assumed.`, 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{ @@ -226,6 +239,24 @@ config file; otherwise the default is assumed.`, 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{ @@ -346,16 +377,111 @@ EXPERIMENTAL: May be changed or removed. }(), }) + 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 +// - 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 @@ -378,7 +504,7 @@ func RegisterCommand(cmd Command) { if !commandNameRegex.MatchString(cmd.Name) { panic("invalid command name") } - commands[cmd.Name] = cmd + rootCmd.AddCommand(caddyCmdToCoral(cmd)) } var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`) diff --git a/cmd/main.go b/cmd/main.go index 498a8ae6..b91559ce 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -33,13 +33,14 @@ import ( "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 - goModule := caddy.GoModule() - cleanModVersion := strings.TrimPrefix(goModule.Version, "v") + version, _ := caddy.Version() + cleanModVersion := strings.TrimPrefix(version, "v") certmagic.UserAgent = "Caddy/" + cleanModVersion // by using Caddy, user indicates agreement to CA terms @@ -50,43 +51,14 @@ func init() { // Main implements the main function of the caddy command. // Call this if Caddy is to be the main() of your program. func Main() { - switch len(os.Args) { - case 0: + if len(os.Args) == 0 { fmt.Printf("[FATAL] no arguments provided by OS; args[0] must be command\n") os.Exit(caddy.ExitCodeFailedStartup) - case 1: - os.Args = append(os.Args, "help") } - subcommandName := os.Args[1] - subcommand, ok := commands[subcommandName] - if !ok { - if strings.HasPrefix(os.Args[1], "-") { - // user probably forgot to type the subcommand - fmt.Println("[ERROR] first argument must be a subcommand; see 'caddy help'") - } else { - fmt.Printf("[ERROR] '%s' is not a recognized subcommand; see 'caddy help'\n", os.Args[1]) - } - os.Exit(caddy.ExitCodeFailedStartup) + if err := rootCmd.Execute(); err != nil { + os.Exit(1) } - - fs := subcommand.Flags - if fs == nil { - fs = flag.NewFlagSet(subcommand.Name, flag.ExitOnError) - } - - err := fs.Parse(os.Args[2:]) - if err != nil { - fmt.Println(err) - os.Exit(caddy.ExitCodeFailedStartup) - } - - exitCode, err := subcommand.Func(Flags{fs}) - if err != nil { - fmt.Fprintf(os.Stderr, "%s: %v\n", subcommand.Name, err) - } - - os.Exit(exitCode) } // handlePingbackConn reads from conn and ensures it matches @@ -173,7 +145,7 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) { // adapt config if cfgAdapter != nil { - adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]interface{}{ + adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]any{ "filename": configFile, }) if err != nil { @@ -280,7 +252,7 @@ func watchConfigFile(filename, adapterName string) { // Flags wraps a FlagSet so that typed values // from flags can be easily retrieved. type Flags struct { - *flag.FlagSet + *pflag.FlagSet } // String returns the string representation of the @@ -326,22 +298,6 @@ func (f Flags) Duration(name string) time.Duration { return val } -// flagHelp returns the help text for fs. -func flagHelp(fs *flag.FlagSet) string { - if fs == nil { - return "" - } - - // temporarily redirect output - out := fs.Output() - defer fs.SetOutput(out) - - buf := new(bytes.Buffer) - fs.SetOutput(buf) - fs.PrintDefaults() - return buf.String() -} - func loadEnvFromFile(envFile string) error { file, err := os.Open(envFile) if err != nil { @@ -387,11 +343,11 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) { } // split line into key and value - fields := strings.SplitN(line, "=", 2) - if len(fields) != 2 { + 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 := fields[0], fields[1] + 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 ") @@ -408,11 +364,8 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) { } // remove any trailing comment after value - if commentStart := strings.Index(val, "#"); commentStart > 0 { - before := val[commentStart-1] - if before == '\t' || before == ' ' { - val = strings.TrimRight(val[:commentStart], " \t") - } + if commentStart, _, found := strings.Cut(val, "#"); found { + val = strings.TrimRight(commentStart, " \t") } // quoted value: support newlines @@ -441,11 +394,12 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) { } 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", CaddyVersion()) + 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) @@ -462,21 +416,15 @@ func printEnvironment() { } } -// CaddyVersion returns a detailed version string, if available. -func CaddyVersion() string { - goModule := caddy.GoModule() - ver := goModule.Version - if goModule.Sum != "" { - ver += " " + goModule.Sum - } - if goModule.Replace != nil { - ver += " => " + goModule.Replace.Path - if goModule.Replace.Version != "" { - ver += "@" + goModule.Replace.Version - } - if goModule.Replace.Sum != "" { - ver += " " + goModule.Replace.Sum - } - } - return ver +// 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) diff --git a/cmd/packagesfuncs.go b/cmd/packagesfuncs.go index ca15ea34..3aed0e8c 100644 --- a/cmd/packagesfuncs.go +++ b/cmd/packagesfuncs.go @@ -194,7 +194,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) { // 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 := interface{}(modInfo.New()) + iface := any(modInfo.New()) if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr { iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface() } diff --git a/cmd/removebinary.go b/cmd/removebinary.go index adef6b10..c74d2b2e 100644 --- a/cmd/removebinary.go +++ b/cmd/removebinary.go @@ -13,7 +13,6 @@ // limitations under the License. //go:build !windows -// +build !windows package caddycmd diff --git a/cmd/removebinary_windows.go b/cmd/removebinary_windows.go index 3d7ade5d..8cc271ad 100644 --- a/cmd/removebinary_windows.go +++ b/cmd/removebinary_windows.go @@ -31,6 +31,9 @@ import ( func removeCaddyBinary(path string) error { var sI syscall.StartupInfo var pI syscall.ProcessInformation - argv := syscall.StringToUTF16Ptr(filepath.Join(os.Getenv("windir"), "system32", "cmd.exe") + " /C del " + path) + 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) } diff --git a/context.go b/context.go index 2a6f5142..e850b73d 100644 --- a/context.go +++ b/context.go @@ -37,9 +37,10 @@ import ( // not actually need to do this). type Context struct { context.Context - moduleInstances map[string][]interface{} + moduleInstances map[string][]Module cfg *Config cleanupFuncs []func() + ancestry []Module } // NewContext provides a new context derived from the given @@ -51,7 +52,7 @@ type Context struct { // 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][]interface{}), cfg: ctx.cfg} + newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg} c, cancel := context.WithCancel(ctx.Context) wrappedCancel := func() { cancel() @@ -90,15 +91,15 @@ func (ctx *Context) OnCancel(f func()) { // 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 => interface{} -// []json.RawMessage => []interface{} -// [][]json.RawMessage => [][]interface{} -// map[string]json.RawMessage => map[string]interface{} -// []map[string]json.RawMessage => []map[string]interface{} +// 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" +// 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 @@ -115,20 +116,20 @@ func (ctx *Context) OnCancel(f func()) { // 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" +// 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 interface{} value(s) to the types that are useful to you +// 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 interface{}, fieldName string) (interface{}, error) { +func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error) { val := reflect.ValueOf(structPointer).Elem().FieldByName(fieldName) typ := val.Type() @@ -148,7 +149,7 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte } inlineModuleKey := opts["inline_key"] - var result interface{} + var result any switch val.Kind() { case reflect.Slice: @@ -170,7 +171,7 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte if inlineModuleKey == "" { panic("unable to determine module name without inline_key because type is not a ModuleMap") } - var all []interface{} + 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 { @@ -186,10 +187,10 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte if inlineModuleKey == "" { panic("unable to determine module name without inline_key because type is not a ModuleMap") } - var all [][]interface{} + var all [][]any for i := 0; i < val.Len(); i++ { innerVal := val.Index(i) - var allInner []interface{} + 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 { @@ -204,7 +205,7 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte } else if isModuleMapType(typ.Elem()) { // val is `[]map[string]json.RawMessage` - var all []map[string]interface{} + var all []map[string]any for i := 0; i < val.Len(); i++ { thisSet, err := ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val.Index(i)) if err != nil { @@ -232,10 +233,10 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte return result, nil } -// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]interface{}. +// 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]interface{}, error) { +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 == "" { @@ -253,8 +254,8 @@ func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, 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]interface{}, error) { - mods := make(map[string]interface{}) +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() @@ -268,10 +269,10 @@ func (ctx Context) loadModulesFromRegularMap(namespace, inlineModuleKey string, return mods, nil } -// loadModuleMap loads modules from a ModuleMap, i.e. map[string]interface{}, where the key is the +// 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]interface{}, error) { - all := make(map[string]interface{}) +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) @@ -299,19 +300,19 @@ func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[strin // 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) (interface{}, error) { +func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error) { modulesMu.RLock() - mod, ok := modules[id] + modInfo, ok := modules[id] modulesMu.RUnlock() if !ok { return nil, fmt.Errorf("unknown module: %s", id) } - if mod.New == nil { - return nil, fmt.Errorf("module '%s' has no constructor", mod.ID) + if modInfo.New == nil { + return nil, fmt.Errorf("module '%s' has no constructor", modInfo.ID) } - val := mod.New().(interface{}) + 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 @@ -327,7 +328,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ if len(rawMsg) > 0 { err := strictUnmarshalJSON(rawMsg, &val) if err != nil { - return nil, fmt.Errorf("decoding module config: %s: %v", mod, err) + return nil, fmt.Errorf("decoding module config: %s: %v", modInfo, err) } } @@ -340,6 +341,8 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ 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 { @@ -351,7 +354,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) } } - return nil, fmt.Errorf("provision %s: %v", mod, err) + return nil, fmt.Errorf("provision %s: %v", modInfo, err) } } @@ -365,7 +368,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) } } - return nil, fmt.Errorf("%s: invalid configuration: %v", mod, err) + return nil, fmt.Errorf("%s: invalid configuration: %v", modInfo, err) } } @@ -375,7 +378,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ } // loadModuleInline loads a module from a JSON raw message which decodes to -// a map[string]interface{}, where one of the object keys is moduleNameKey +// 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. @@ -385,7 +388,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ // 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) (interface{}, error) { +func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (any, error) { moduleName, raw, err := getModuleNameInline(moduleNameKey, raw) if err != nil { return nil, err @@ -407,7 +410,7 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json. // 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) (interface{}, error) { +func (ctx Context) App(name string) (any, error) { if app, ok := ctx.cfg.apps[name]; ok { return app, nil } @@ -439,8 +442,10 @@ func (ctx Context) Storage() certmagic.Storage { return ctx.cfg.storage } +// TODO: aw man, can I please change this? // Logger returns a logger that can be used by mod. func (ctx Context) Logger(mod Module) *zap.Logger { + // TODO: if mod is nil, use ctx.Module() instead... if ctx.cfg == nil { // often the case in tests; just use a dev logger l, err := zap.NewDevelopment() @@ -451,3 +456,34 @@ func (ctx Context) Logger(mod Module) *zap.Logger { } return ctx.cfg.Logging.Logger(mod) } + +// TODO: use this +// // Logger returns a logger that can be used by the current module. +// func (ctx Context) Log() *zap.Logger { +// 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 +// } +// return ctx.cfg.Logging.Logger(ctx.Module()) +// } + +// 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] +} diff --git a/context_test.go b/context_test.go index afa10dbd..27395612 100644 --- a/context_test.go +++ b/context_test.go @@ -71,13 +71,13 @@ func ExampleContext_LoadModule_array() { }, } - // since our input is []json.RawMessage, the output will be []interface{} + // 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.([]interface{}) { + for _, mod := range mods.([]any) { myStruct.guestModules = append(myStruct.guestModules, mod.(io.Writer)) } @@ -104,13 +104,13 @@ func ExampleContext_LoadModule_map() { }, } - // since our input is map[string]json.RawMessage, the output will be map[string]interface{} + // 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]interface{}) { + for modName, mod := range mods.(map[string]any) { myStruct.guestModules[modName] = mod.(io.Writer) } diff --git a/duration_fuzz.go b/duration_fuzz.go index aa01a46a..8a1f0c7c 100644 --- a/duration_fuzz.go +++ b/duration_fuzz.go @@ -13,7 +13,6 @@ // limitations under the License. //go:build gofuzz -// +build gofuzz package caddy diff --git a/go.mod b/go.mod index 814d1359..2b558310 100644 --- a/go.mod +++ b/go.mod @@ -1,43 +1,50 @@ module github.com/caddyserver/caddy/v2 -go 1.17 +go 1.18 require ( - github.com/BurntSushi/toml v1.1.0 + 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.16.1 + github.com/caddyserver/certmagic v0.17.1 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.11.4 + github.com/google/cel-go v0.12.4 github.com/google/uuid v1.3.0 - github.com/klauspost/compress v1.15.6 - github.com/klauspost/cpuid/v2 v2.0.13 - github.com/lucas-clemente/quic-go v0.28.0 - github.com/mholt/acmez v1.0.2 - github.com/prometheus/client_golang v1.12.1 - github.com/smallstep/certificates v0.19.0 - github.com/smallstep/cli v0.18.0 + github.com/klauspost/compress v1.15.9 + github.com/klauspost/cpuid/v2 v2.1.0 + github.com/lucas-clemente/quic-go v0.28.2-0.20220813150001-9957668d4301 + github.com/mholt/acmez v1.0.4 + github.com/prometheus/client_golang v1.12.2 + github.com/smallstep/certificates v0.21.0 + github.com/smallstep/cli v0.21.0 github.com/smallstep/nosql v0.4.0 - github.com/smallstep/truststore v0.11.0 + github.com/smallstep/truststore v0.12.0 + github.com/spf13/cobra v1.1.3 + github.com/spf13/pflag v1.0.5 github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 - github.com/yuin/goldmark v1.4.12 + github.com/yuin/goldmark v1.4.13 github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 - go.opentelemetry.io/otel v1.4.0 + 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.21.0 - golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 - golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e + 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-20220502173005-c8bf987b8c21 - google.golang.org/protobuf v1.28.0 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 @@ -48,19 +55,18 @@ require ( 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/cheekybits/genny v1.0.0 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.1 // 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.2 // 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.2 // 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 @@ -69,6 +75,7 @@ require ( 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 @@ -80,16 +87,14 @@ require ( 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-16 v0.1.5 // indirect - github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect - github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect + github.com/marten-seemann/qtls-go1-19 v0.1.0 // 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.46 // 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 @@ -100,7 +105,7 @@ require ( 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.0.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 @@ -112,21 +117,21 @@ require ( 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/internal/metric v0.27.0 // indirect - go.opentelemetry.io/otel/metric v0.27.0 // indirect - go.opentelemetry.io/otel/trace 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.0 // indirect - go.step.sm/crypto v0.16.1 // indirect - go.step.sm/linkedca v0.15.0 // indirect + go.step.sm/cli-utils v0.7.3 // indirect + go.step.sm/crypto v0.16.2 // indirect + go.step.sm/linkedca v0.16.1 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/mod v0.4.2 // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // 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.7 // indirect + golang.org/x/tools v0.1.10 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/grpc v1.46.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 diff --git a/go.sum b/go.sum index 086df08c..1044ece3 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,8 @@ -bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.31.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.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= 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.44.3/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= @@ -21,7 +15,6 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -57,40 +50,17 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+ 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/security v1.3.0/go.mod h1:pQsnLAXfMzuWVJdctBs8BV3tGd3Jr0SMYu6KK3QXYAs= -cloud.google.com/go/spanner v1.17.0/go.mod h1:+17t2ixFwRG4lWRwE+5kipDR9Ef07Jkmc8z0IbMDKUs= -cloud.google.com/go/spanner v1.18.0/go.mod h1:LvAjUXPeJRGNuGpikMULjhLj/t9cRvdc+fxRoLiugXA= -cloud.google.com/go/spanner v1.20.0/go.mod h1:ajR/W06cMHQu7nqQ4irRGplPNoWgejGJlEhlB8xBTKk= 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= -code.gitea.io/sdk/gitea v0.11.3/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY= -contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= -contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= -contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw= -contrib.go.opencensus.io/exporter/stackdriver v0.13.5/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= -contrib.go.opencensus.io/exporter/stackdriver v0.13.7/go.mod h1:huNtlWx75MwO7qMs0KrMxPZXzNNWebav1Sq/pm02JdQ= -contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= -contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= -dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= -dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= -dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 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/Azure/azure-amqp-common-go/v2 v2.1.0/go.mod h1:R8rea+gJRuJR6QxTir/XuEd+YuKoUiazDC/N96FiDEU= -github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= -github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v58.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= -github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= -github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= @@ -104,87 +74,54 @@ github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYX github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +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/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= 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 v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 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 v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 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/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= 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/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= -github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944/go.mod h1:sPML5WwI6oxLRLPuuqbtoOKhtmpVDCYtwsps+I+vjIY= 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/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 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/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= 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/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 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/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= -github.com/apache/beam v2.28.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o= -github.com/apache/beam v2.30.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o= 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/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ= -github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= -github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= -github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 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-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 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/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= -github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.19.45/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.25.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.37.0 h1:GzFnhOIsrGyQ69s7VgqtrG2BG8v7X7vwB3Xpbd/DBBk= github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= -github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= 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= @@ -193,34 +130,20 @@ 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/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw= -github.com/caddyserver/certmagic v0.16.1 h1:rdSnjcUVJojmL4M0efJ+yHXErrrijS4YYg3FuwRdJkI= -github.com/caddyserver/certmagic v0.16.1/go.mod h1:jKQ5n+ViHAr6DbPwEGLTSM2vDwTO6EvCKBblBRUvvuQ= -github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= +github.com/caddyserver/certmagic v0.17.1 h1:VrWANhQAj3brK7jAUKyN6XBHg56WsyorI/84Ilq1tCQ= +github.com/caddyserver/certmagic v0.17.1/go.mod h1:pSS2aZcdKlrTZrb2DKuRafckx20o5Fz1EdDKEB8KOQM= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= 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.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= 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/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= -github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= 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= @@ -243,11 +166,7 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH 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/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= -github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -255,28 +174,21 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.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-20181012123002-c6f51f82210d/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/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= 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 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= 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.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= 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= @@ -302,7 +214,6 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m 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/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 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= @@ -314,21 +225,15 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m 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/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE= github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= -github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +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/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= 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/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= @@ -337,21 +242,14 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo 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/fullstorydev/grpcurl v1.8.0/go.mod h1:Mn2jWbdMrQGJQ8UD62uNyMumT2acsZUCkZIqFxsQf1o= -github.com/fullstorydev/grpcurl v1.8.1/go.mod h1:3BWhvHZwNO7iLXaQlojdg5NA6SxUDePli4ecpK1N7gw= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 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-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 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-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 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= @@ -363,14 +261,13 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 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 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= 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-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk= -github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/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= @@ -380,19 +277,14 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me 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/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -400,7 +292,6 @@ github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4er 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/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 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= @@ -435,13 +326,8 @@ 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/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/cel-go v0.11.4 h1:wWOnKmLxALl3l9Av221MfIOWRiR01sDVljzg6LZ6Zn0= -github.com/google/cel-go v0.11.4/go.mod h1:Av7CU6r6X3YmcHR9GXqVDaEJYfEtSxl6wvIjUQTriCw= -github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/certificate-transparency-go v1.1.2-0.20210422104406-9f33727a7a18/go.mod h1:6CKh9dscIRoqc2kC6YUFICHZMT9NrClyPrRVFrdw1QQ= -github.com/google/certificate-transparency-go v1.1.2-0.20210512142713-bed466244fa6/go.mod h1:aF2dp7Dh81mY8Y/zpzyXps4fQW5zQbDu2CxfpJB6NkI= -github.com/google/certificate-transparency-go v1.1.2-0.20210623111010-a50f74f4ce95/go.mod h1:Qj+RD7dL44/KQVYkRk4wDVszkPOzxNcHmuX4HCMEqKg= +github.com/google/cel-go v0.12.4 h1:YINKfuHZ8n72tPOqSPZBwGiDpew2CJS48mdM5W8LZQU= +github.com/google/cel-go v0.12.4/go.mod h1:Av7CU6r6X3YmcHR9GXqVDaEJYfEtSxl6wvIjUQTriCw= 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= @@ -454,19 +340,11 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= -github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= -github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= +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/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/licenseclassifier v0.0.0-20210325184830-bb04aff29e72/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= @@ -479,29 +357,17 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg= -github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/google/trillian v1.3.14-0.20210409160123-c5ea3abd4a41/go.mod h1:1dPv0CUjNQVFEDuAUFhZql16pw/VlPgaX8qj+g5pVzQ= -github.com/google/trillian v1.3.14-0.20210511103300-67b5f349eefa/go.mod h1:s4jO3Ai4NSvxucdvqUHON0bCqJyoya32eNw6XJwsmNc= -github.com/google/trillian v1.3.14-0.20210622121126-870e0cdde059/go.mod h1:77nhQ5M0g7nqL2S6sjQWUyqQ90h0X26T8cr0pQqqxec= -github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww= -github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 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.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -509,29 +375,19 @@ github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= -github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ= -github.com/goreleaser/nfpm v1.2.1/go.mod h1:TtWrABZozuLOttX2uDlYyECfQX7x5XYkVxhjYcR6G9w= 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/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= 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.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= @@ -554,7 +410,6 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= @@ -583,27 +438,23 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw= github.com/hashicorp/vault/api/auth/approle v0.1.1/go.mod h1:mHOLgh//xDx4dpqXoq6tS8Ob0FoCFWLU2ibJ26Lfmag= +github.com/hashicorp/vault/api/auth/kubernetes v0.1.0/go.mod h1:Pdgk78uIs0mgDOLvc3a+h/vYIT9rznw2sz+ucuH9024= github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= -github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= -github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.9/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 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 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= @@ -624,7 +475,6 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W 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 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 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= @@ -652,23 +502,14 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f 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/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= -github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= -github.com/jhump/protoreflect v1.8.2/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jhump/protoreflect v1.9.0/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 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= @@ -680,22 +521,17 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm 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/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= 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/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 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.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY= -github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.0.13 h1:1XxvOiqXZ8SULZUKim/wncr3wZ38H4yCuVDvKdK9OGs= -github.com/klauspost/cpuid/v2 v2.0.13/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= +github.com/klauspost/cpuid/v2 v2.1.0/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= @@ -705,16 +541,12 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= 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.1.1/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= @@ -722,35 +554,27 @@ 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.28.0 h1:9eXVRgIkMQQyiyorz/dAaOYIx3TFzXsIFkNFz4cxuJM= -github.com/lucas-clemente/quic-go v0.28.0/go.mod h1:oGz5DKK41cJt5+773+BSO9BXDsREY4HLf7+0odGAPO0= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/lucas-clemente/quic-go v0.28.2-0.20220813150001-9957668d4301 h1:6W5uGdteJsB9b2T36wrc/nr48COmN9M0jeFx05NEsUQ= +github.com/lucas-clemente/quic-go v0.28.2-0.20220813150001-9957668d4301/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= 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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 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-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ= -github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= -github.com/marten-seemann/qtls-go1-17 v0.1.2 h1:JADBlm0LYiVbuSySCHeY863dNkcpMmDR7s0bLKJeYlQ= -github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= -github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 h1:7m/WlWcSROrcK5NxuXaxYD32BZqe/LEEnBrWcH/cOqQ= -github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= +github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU= +github.com/marten-seemann/qtls-go1-19 v0.1.0/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.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 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-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= 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= @@ -762,25 +586,18 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky 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/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= 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-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 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.2 h1:C8wsEBIUVi6e0DYoxqCcFuXtwc4AWXL/jgcDjF7mjVo= -github.com/mholt/acmez v1.0.2/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +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.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.46 h1:uzwpxRtSVxtcIZmz/4Uz6/Rn7G11DvsaslXoy5LxQio= -github.com/miekg/dns v1.1.46/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +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/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -801,7 +618,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.1/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= @@ -809,12 +625,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ 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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= 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/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= -github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= 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= @@ -823,10 +635,7 @@ github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi 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/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f/go.mod h1:nwPd6pDNId/Xi16qtKrFHrauSwMNuvk+zcjk89wrnlA= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= -github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -835,42 +644,29 @@ github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtb github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 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.10.3/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.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= 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.5.0/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/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= 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.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 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/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= 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/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 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= @@ -881,24 +677,19 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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 v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk= -github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 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 v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 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.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +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= @@ -906,7 +697,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: 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.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -914,83 +704,47 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= 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-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 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.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 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.2.0/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/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= -github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 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/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 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 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 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/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189/go.mod h1:UUwuHEJ9zkkPDxspIHOa59PUeSkGFljESGzbxntLmIg= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= 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/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= -github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= -github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= -github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= -github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= -github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= -github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= -github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= -github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= -github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= -github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= -github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= -github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= -github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.3.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.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= @@ -999,32 +753,19 @@ github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIp 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.18.0/go.mod h1:8eHwHNg/bRWvNZo9S0uWFVMkS+LSpDYxM4//EgBhkFM= -github.com/smallstep/certificates v0.19.0 h1:wW344Q/QpupjKKFKa4PqzEXfwgeq/54dkU/HNvGnwQQ= -github.com/smallstep/certificates v0.19.0/go.mod h1:nkG4c+1HLnCmIBAgZ/bKaBPAsN7ePtyzkDWzPSqcObQ= -github.com/smallstep/certinfo v1.5.2/go.mod h1:gA7HBbue0Wwr3kD60P2UtgTIFfMAOC66D3rzYhI0GZ4= -github.com/smallstep/cli v0.18.0 h1:BslbUHuMfj/LbVHxuZ4Hv1sL+vAHHidqia4JRoCBwXs= -github.com/smallstep/cli v0.18.0/go.mod h1:C8ZSfMm/pKdCHnN1C3Pc44bjIOkBbyuoyq6XjS/K9lI= -github.com/smallstep/nosql v0.3.9/go.mod h1:X2qkYpNcW3yjLUvhEHfgGfClpKbFPapewvx7zo4TOFs= +github.com/smallstep/certificates v0.21.0 h1:RdzGph8pwPZ3RJwnJ6T+HrnHJswJNhX8skoJ4Cc/lxM= +github.com/smallstep/certificates v0.21.0/go.mod h1:QlIuU3l25qxeWTo19VviZq/CKhRFtJt75RoTEUh9pbY= +github.com/smallstep/cli v0.21.0 h1:m7p1lQFfbBF7RI+Z49eaRBW3oI/5HmnwT8Pa8/xgGcE= +github.com/smallstep/cli v0.21.0/go.mod h1:o8hZZAjO4901b3iWmS4fsKasuD57hmkkJJBumYnspPo= 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.9.6/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM= -github.com/smallstep/truststore v0.11.0 h1:JUTkQ4oHr40jHTS/A2t0usEhteMWG+45CDD2iJA/dIk= -github.com/smallstep/truststore v0.11.0/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM= -github.com/smallstep/zcrypto v0.0.0-20210924233136-66c2600f6e71/go.mod h1:+F24VU3UCxfVFvvqgm5jNUFQOm/L6ed13ImwWGFgg/g= -github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJDI9ti3oEaFnvx1F4N8n3ZSw2YM1+sbEoxc4= +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/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= -github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/soheilhy/cmux v0.1.5-0.20210205191134-5ec6847320e5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 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= @@ -1035,17 +776,15 @@ 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.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= 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/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 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= @@ -1054,49 +793,31 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J 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 v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 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/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 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/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= -github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= -github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= -github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= -github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= -github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 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.4/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/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= -github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= -github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= -github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 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= @@ -1106,48 +827,20 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/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.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0= -github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/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= -github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= -github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= -github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= -github.com/zmap/zcertificate v0.0.0-20190521191901-30e388164f71/go.mod h1:gIZi1KPgkZNUQzPZXsZrNnUnxy05nTc0+tmlqvIkhRw= -github.com/zmap/zcrypto v0.0.0-20190329181646-dff83107394d/go.mod h1:ix3q2kpLy0ibAuFXlr7qOhPKwFRRSjuynGuTR8EUPCk= -github.com/zmap/zlint v0.0.0-20190516161541-9047d02cf65a/go.mod h1:xwLbce0UzBXp44sIAL1cii+hoK8j4AxRKlymZA2AIcY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 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.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0/go.mod h1:YPwSaBciV5G6Gpt435AasAG3ROetZsKNUzibRa/++oo= -go.etcd.io/etcd/etcdctl/v3 v3.5.0/go.mod h1:vGTfKdsh87RI7kA2JHFBEGxjQEYx+pi299wqEOdi34M= -go.etcd.io/etcd/etcdutl/v3 v3.5.0/go.mod h1:o98rKMCibbFAG8QS9KmvlYDGDShmmIbmRE8vSofzYNg= -go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0/go.mod h1:tV31atvwzcybuqejDoY3oaNRTtlD2l/Ot78Pc9w7DMY= -go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= -go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0/go.mod h1:FAwse6Zlm5v4tEWZaTjmNhe17Int4Oxbu7+2r0DiD3w= -go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= -go.etcd.io/etcd/server/v3 v3.5.0-alpha.0/go.mod h1:tsKetYpt980ZTpzl/gb+UOJj9RkIyCb1u4wjzMg90BQ= -go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= -go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0/go.mod h1:HnrHxjyCuZ8YDt8PYVyQQ5d1ZQfzJVEtQWllr5Vp/30= -go.etcd.io/etcd/tests/v3 v3.5.0/go.mod h1:f+mtZ1bE1YPvgKdOJV2BKy4JQW0nAFnQehgOE7+WyJE= -go.etcd.io/etcd/v3 v3.5.0-alpha.0/go.mod h1:JZ79d3LV6NUfPjUxXrpiFAYcjhT+06qqw+i28snx8To= -go.etcd.io/etcd/v3 v3.5.0/go.mod h1:FldM0/VzcxYWLvWx1sdA7ghKw7C3L2DvUTzGrcEtsC4= 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.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 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= @@ -1156,51 +849,37 @@ 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.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0= -go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 h1:SLme4Porm+UwX0DdHMxlwRt7FzPSE0sys81bet2o0pU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0/go.mod h1:tLYsuf2v8fZreBVwp9gVMhefZlLFZaUiNVSq8QxXRII= -go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel v1.4.0 h1:7ESuKPq6zpjRaY5nvVDGiuwK7VAJ8MwkKnmNJ9whNZ4= +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/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg= -go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +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/internal/metric v0.27.0 h1:9dAVGAfFiiEq5NVB9FUJ5et+btbDQAUIJehJ+ikyryk= -go.opentelemetry.io/otel/internal/metric v0.27.0/go.mod h1:n1CVxRqKqYZtqyTh9U/onvKapPGv7y/rpyOTI+LFNzw= -go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/metric v0.27.0 h1:HhJPsGhJoKRSegPQILFbODU56NS/L1UE4fS1sC5kIwQ= -go.opentelemetry.io/otel/metric v0.27.0/go.mod h1:raXDJ7uP2/Jc0nVZWQjJtzoyssOYWu/+pjZqRzfvZ7g= -go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= -go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +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/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= -go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= -go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.opentelemetry.io/otel/trace v1.4.0 h1:4OOUrPZdVFQkbzl/JSdvGCWIdw5ONXXxzHlaLlWppmo= 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.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds= go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= +go.step.sm/cli-utils v0.7.3 h1:IA12IaiXVCI18yOFVQuvMpyvjL8wuwUn1yO+KhAVAr0= +go.step.sm/cli-utils v0.7.3/go.mod h1:RJRwbBLqzs5nrepQLAV9FuT3fVpWz66tKzLIB7Izpfk= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= -go.step.sm/crypto v0.13.0/go.mod h1:5YzQ85BujYBu6NH18jw7nFjwuRnDch35nLzH0ES5sKg= -go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk= -go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= -go.step.sm/linkedca v0.7.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= -go.step.sm/linkedca v0.15.0 h1:lEkGRDY+u7FudGKt8yEo7nBy5OzceO9s3rl+/sZVL5M= -go.step.sm/linkedca v0.15.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= +go.step.sm/crypto v0.16.2 h1:Pr9aazTwWBBZNogUsOqhOrPSdwAa9pPs+lMB602lnDA= +go.step.sm/crypto v0.16.2/go.mod h1:1WkTOTY+fOX/RY4TnZREp6trQAsBHRQ7nu6QJBiNQF8= +go.step.sm/linkedca v0.16.1 h1:CdbMV5SjnlRsgeYTXaaZmQCkYIgJq8BOzpewri57M2k= +go.step.sm/linkedca v0.16.1/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= 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= @@ -1208,7 +887,6 @@ 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.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 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= @@ -1221,32 +899,18 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E 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.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= -go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= -gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI= -golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/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-20190313024323-a1f597ede03a/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-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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= @@ -1256,12 +920,11 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 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-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 h1:XdAboW3BNMv9ocSCOk/u1MFioZGzCNkiJZ19v9Oe3Ig= -golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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= @@ -1272,10 +935,10 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 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-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +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-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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= @@ -1298,40 +961,31 @@ 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.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 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-20180530234432-1e491301e022/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-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181108082009-03003ca0c849/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-20190301231341-16b79f2e4e95/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-20190313220215-9f648a60d977/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-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 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-20190619014844-b5b0513f8c1b/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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914/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= @@ -1339,7 +993,6 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL 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-20200421231249-e086a090c8fd/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= @@ -1351,55 +1004,44 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +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-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/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-20190402181905-9f3314589c9a/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-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210126194326-f9ce19ea3013/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 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-20190412183630-56d357773e84/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= @@ -1414,16 +1056,13 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h 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-20181029174526-d69651ed3497/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-20190221075227-b4e8571b14e0/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-20190316082340-a2f829d7f35f/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= @@ -1431,7 +1070,6 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w 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-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/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= @@ -1442,7 +1080,6 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191119060738-e882bf8e40c2/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= @@ -1458,7 +1095,6 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w 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-20200420163511-1957bb5e6d1f/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= @@ -1470,7 +1106,6 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/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-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1482,15 +1117,11 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/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-20210503080704-8803ae5d1324/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-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1512,11 +1143,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/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-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= 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= @@ -1535,20 +1168,16 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb 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/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/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-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20190422233926-fe54fb35175b/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= @@ -1556,19 +1185,15 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn 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-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= 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-20191010075000-0337d82405ff/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-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/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-20191118222007-07fc4c7f2b98/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= @@ -1586,7 +1211,6 @@ golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapK 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-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 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= @@ -1598,14 +1222,12 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc 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-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -1613,8 +1235,9 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/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.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +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= @@ -1624,17 +1247,11 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/windows v0.5.1/go.mod h1:EApyTk/ZNrkbZjurHL1nleDYnsPpJYBO7LZEBCyDAHk= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 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.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= 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.10.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= @@ -1649,12 +1266,9 @@ google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSr google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.37.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= -google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= @@ -1670,29 +1284,20 @@ google.golang.org/api v0.70.0 h1:67zQnAE0T2rB0A3CwLSas0K+SbVzSxP+zTLkQLexeiw= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= 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.3.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.2/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/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 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-20190508193815-b515fa19cec8/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-20190620144150-6af8c5fc6601/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= @@ -1709,7 +1314,6 @@ google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfG 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-20200423170343-7949de9c1215/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= @@ -1724,17 +1328,11 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210331142528-b7513248f0ba/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210427215850-f767ed18ee4d/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= @@ -1742,7 +1340,6 @@ google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxH google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= @@ -1760,11 +1357,10 @@ google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I= google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 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= @@ -1783,7 +1379,6 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji 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.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= @@ -1800,6 +1395,7 @@ google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= 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.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -1826,12 +1422,10 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/cheggaaa/pb.v1 v1.0.28/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/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= @@ -1839,9 +1433,6 @@ 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/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= -gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= 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= @@ -1859,9 +1450,9 @@ 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.0/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= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 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= @@ -1873,13 +1464,8 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 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= -pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= 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= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -software.sslmate.com/src/go-pkcs12 v0.0.0-20201103104416-57fc603b7f52/go.mod h1:/xvNRWUqm0+/ZMiF4EX00vrSCMsE4/NHb+Pt3freEeQ= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= -sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/listen.go b/listen.go new file mode 100644 index 00000000..2c4a0b28 --- /dev/null +++ b/listen.go @@ -0,0 +1,172 @@ +//go:build !linux + +package caddy + +import ( + "fmt" + "net" + "sync" + "sync/atomic" + "time" + + "go.uber.org/zap" +) + +func ListenTimeout(network, addr string, keepAlivePeriod time.Duration) (net.Listener, error) { + // check to see if plugin provides listener + if ln, err := getListenerFromPlugin(network, addr); err != nil || ln != nil { + return ln, err + } + + lnKey := listenerKey(network, addr) + + sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { + ln, err := net.Listen(network, addr) + if err != nil { + // https://github.com/caddyserver/caddy/pull/4534 + if isUnixNetwork(network) && isListenBindAddressAlreadyInUseError(err) { + return nil, fmt.Errorf("%w: this can happen if Caddy was forcefully killed", err) + } + return nil, err + } + return &sharedListener{Listener: ln, key: lnKey}, nil + }) + if err != nil { + return nil, err + } + + return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: keepAlivePeriod}, 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{}) + case *net.UnixListener: + 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) + case *net.UnixListener: + 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() +} diff --git a/listen_linux.go b/listen_linux.go new file mode 100644 index 00000000..b1220ce4 --- /dev/null +++ b/listen_linux.go @@ -0,0 +1,34 @@ +package caddy + +import ( + "context" + "net" + "syscall" + "time" + + "go.uber.org/zap" + "golang.org/x/sys/unix" +) + +// ListenTimeout is the same as Listen, but with a configurable keep-alive timeout duration. +func ListenTimeout(network, addr string, keepalivePeriod time.Duration) (net.Listener, error) { + // check to see if plugin provides listener + if ln, err := getListenerFromPlugin(network, addr); err != nil || ln != nil { + return ln, err + } + + config := &net.ListenConfig{Control: reusePort, KeepAlive: keepalivePeriod} + return config.Listen(context.Background(), network, addr) +} + +func reusePort(network, address string, conn syscall.RawConn) error { + 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)) + } + }) +} diff --git a/listeners.go b/listeners.go index f2d7e10b..6a23c61a 100644 --- a/listeners.go +++ b/listeners.go @@ -20,16 +20,16 @@ import ( "errors" "fmt" "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" ) // Listen is like net.Listen, except Caddy's listeners can overlap @@ -41,31 +41,30 @@ import ( // the socket have been finished. Always be sure to close listeners // when you are done with them, just like normal listeners. func Listen(network, addr string) (net.Listener, error) { - lnKey := network + "/" + addr + // a 0 timeout means Go uses its default + return ListenTimeout(network, addr, 0) +} - sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { - ln, err := net.Listen(network, addr) - if err != nil { - // https://github.com/caddyserver/caddy/pull/4534 - if isUnixNetwork(network) && isListenBindAddressAlreadyInUseError(err) { - return nil, fmt.Errorf("%w: this can happen if Caddy was forcefully killed", err) - } - return nil, err - } - return &sharedListener{Listener: ln, key: lnKey}, nil - }) - if err != nil { - return nil, err +// 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(network, addr string) (net.Listener, error) { + network = strings.TrimSpace(strings.ToLower(network)) + + // 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(network, addr) } - return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener)}, nil + return nil, nil } // ListenPacket returns a net.PacketConn suitable for use in a Caddy module. // It is like Listen except for PacketConns. // Always be sure to close the PacketConn when you are done. func ListenPacket(network, addr string) (net.PacketConn, error) { - lnKey := network + "/" + addr + lnKey := listenerKey(network, addr) sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { pc, err := net.ListenPacket(network, addr) @@ -88,88 +87,45 @@ func ListenPacket(network, addr string) (net.PacketConn, error) { // ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module. // Note that the context passed to Accept is currently ignored, so using // a context other than context.Background is meaningless. -func ListenQUIC(addr string, tlsConf *tls.Config) (quic.EarlyListener, error) { - lnKey := "quic/" + addr +// This API is EXPERIMENTAL and may change. +func ListenQUIC(addr string, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) { + lnKey := listenerKey("udp", addr) sharedEl, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { - el, err := quic.ListenAddrEarly(addr, http3.ConfigureTLSConfig(tlsConf), &quic.Config{}) + el, err := quic.ListenAddrEarly(addr, 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: el, key: lnKey}, nil }) + if err != nil { + return nil, err + } ctx, cancel := context.WithCancel(context.Background()) return &fakeCloseQuicListener{ sharedQuicListener: sharedEl.(*sharedQuicListener), - context: ctx, contextCancel: cancel, - }, err + context: ctx, + contextCancel: cancel, + }, 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 +// 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 } -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 { - 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 +func listenerKey(network, addr string) string { + return network + "/" + addr } type fakeCloseQuicListener struct { @@ -255,55 +211,6 @@ func (fcpc fakeClosePacketConn) SyscallConn() (syscall.RawConn, error) { return nil, fmt.Errorf("SyscallConn() not implemented for %T", fcpc.PacketConn) } -// 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{}) - case *net.UnixListener: - 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) - case *net.UnixListener: - 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() -} - // sharedQuicListener is like sharedListener, but for quic.EarlyListeners. type sharedQuicListener struct { quic.EarlyListener @@ -353,11 +260,25 @@ func (na NetworkAddress) JoinHostPort(offset uint) string { return net.JoinHostPort(na.Host, strconv.Itoa(int(na.StartPort+offset))) } +func (na NetworkAddress) Expand() []NetworkAddress { + size := na.PortRangeSize() + addrs := make([]NetworkAddress, size) + for portOffset := uint(0); portOffset < size; portOffset++ { + na2 := na + na2.StartPort, na2.EndPort = na.StartPort+portOffset, na.StartPort+portOffset + addrs[portOffset] = na2 + } + return addrs +} + // 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 } @@ -368,7 +289,7 @@ func (na NetworkAddress) isLoopback() bool { if na.Host == "localhost" { return true } - if ip := net.ParseIP(na.Host); ip != nil { + if ip, err := netip.ParseAddr(na.Host); err == nil { return ip.IsLoopback() } return false @@ -378,7 +299,7 @@ func (na NetworkAddress) isWildcardInterface() bool { if na.Host == "" { return true } - if ip := net.ParseIP(na.Host); ip != nil { + if ip, err := netip.ParseAddr(na.Host); err == nil { return ip.IsUnspecified() } return false @@ -391,10 +312,13 @@ func (na NetworkAddress) port() string { return fmt.Sprintf("%d-%d", na.StartPort, na.EndPort) } -// String reconstructs the address string to the form expected -// by ParseNetworkAddress(). If the address is a unix socket, -// any non-zero port will be dropped. +// 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()) } @@ -427,36 +351,38 @@ func isListenBindAddressAlreadyInUseError(err error) bool { func ParseNetworkAddress(addr string) (NetworkAddress, error) { var host, port string network, host, port, err := SplitNetworkAddress(addr) - if network == "" { - network = "tcp" - } if err != nil { return NetworkAddress{}, err } + if network == "" { + network = "tcp" + } if isUnixNetwork(network) { return NetworkAddress{ Network: network, Host: host, }, nil } - ports := strings.SplitN(port, "-", 2) - if len(ports) == 1 { - ports = append(ports, ports[0]) - } var start, end uint64 - start, err = strconv.ParseUint(ports[0], 10, 16) - if err != nil { - return NetworkAddress{}, fmt.Errorf("invalid start port: %v", err) - } - end, err = strconv.ParseUint(ports[1], 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) + 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, @@ -469,15 +395,29 @@ func ParseNetworkAddress(addr string) (NetworkAddress, error) { // 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) { - if idx := strings.Index(a, "/"); idx >= 0 { - network = strings.ToLower(strings.TrimSpace(a[:idx])) - a = a[idx+1:] + 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 } @@ -499,6 +439,35 @@ func JoinNetworkAddress(network, host, port string) string { return a } +// 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 +} + +// 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(network, addr string) (net.Listener, 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 diff --git a/listeners_fuzz.go b/listeners_fuzz.go index 95f1e327..02b65ab2 100644 --- a/listeners_fuzz.go +++ b/listeners_fuzz.go @@ -13,7 +13,6 @@ // limitations under the License. //go:build gofuzz -// +build gofuzz package caddy diff --git a/listeners_test.go b/listeners_test.go index b75e2dce..c5aa5273 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -32,9 +32,24 @@ func TestSplitNetworkAddress(t *testing.T) { expectErr: true, }, { - input: "foo", + input: "foo", + expectHost: "foo", + }, + { + input: ":", // empty host & empty port + }, + { + input: "::", expectErr: true, }, + { + input: "[::]", + expectHost: "::", + }, + { + input: ":1234", + expectPort: "1234", + }, { input: "foo:1234", expectHost: "foo", @@ -80,10 +95,10 @@ func TestSplitNetworkAddress(t *testing.T) { } { actualNetwork, actualHost, actualPort, err := SplitNetworkAddress(tc.input) if tc.expectErr && err == nil { - t.Errorf("Test %d: Expected error but got: %v", i, err) + 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) + 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) @@ -169,8 +184,17 @@ func TestParseNetworkAddress(t *testing.T) { expectErr: true, }, { - input: ":", - expectErr: true, + input: ":", + expectAddr: NetworkAddress{ + Network: "tcp", + }, + }, + { + input: "[::]", + expectAddr: NetworkAddress{ + Network: "tcp", + Host: "::", + }, }, { input: ":1234", @@ -307,3 +331,85 @@ func TestJoinHostPort(t *testing.T) { } } } + +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) + } + } +} diff --git a/modules.go b/modules.go index 0f4a563b..e83bc747 100644 --- a/modules.go +++ b/modules.go @@ -44,7 +44,7 @@ import ( // 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 -// interface{} to some other, more useful interface expected +// '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 @@ -172,7 +172,7 @@ func GetModule(name string) (ModuleInfo, error) { // 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 interface{}) string { +func GetModuleName(instance any) string { var name string if mod, ok := instance.(Module); ok { name = mod.CaddyModule().ID.Name() @@ -182,7 +182,7 @@ func GetModuleName(instance interface{}) string { // 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 interface{}) string { +func GetModuleID(instance any) string { var id string if mod, ok := instance.(Module); ok { id = string(mod.CaddyModule().ID) @@ -259,7 +259,7 @@ func Modules() []string { // 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]interface{} + var tmp map[string]any err := json.Unmarshal(raw, &tmp) if err != nil { return "", nil, err @@ -324,11 +324,11 @@ func ParseStructTag(tag string) (map[string]string, error) { if pair == "" { continue } - parts := strings.SplitN(pair, "=", 2) - if len(parts) != 2 { + before, after, isCut := strings.Cut(pair, "=") + if !isCut { return nil, fmt.Errorf("missing key in '%s' (pair %d)", pair, i) } - results[parts[0]] = parts[1] + results[before] = after } return results, nil } @@ -337,7 +337,7 @@ func ParseStructTag(tag string) (map[string]string, 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 interface{}) error { +func strictUnmarshalJSON(data []byte, v any) error { dec := json.NewDecoder(bytes.NewReader(data)) dec.DisallowUnknownFields() return dec.Decode(v) diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go new file mode 100644 index 00000000..3ae40e81 --- /dev/null +++ b/modules/caddyevents/app.go @@ -0,0 +1,373 @@ +// 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) + 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. +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{ + id: id, + ts: time.Now(), + name: eventName, + origin: ctx.Module(), + data: data, + } + + 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 := 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. +type Event struct { + id uuid.UUID + ts time.Time + name string + origin caddy.Module + data map[string]any + + // 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 +} + +// 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) +) diff --git a/modules/caddyevents/eventsconfig/caddyfile.go b/modules/caddyevents/eventsconfig/caddyfile.go new file mode 100644 index 00000000..9c3fae78 --- /dev/null +++ b/modules/caddyevents/eventsconfig/caddyfile.go @@ -0,0 +1,88 @@ +// 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 +} diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 4a8a3755..e48f8286 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -20,11 +20,12 @@ import ( "fmt" "net/http" "strconv" + "sync" "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddytls" - "github.com/lucas-clemente/quic-go/http3" "go.uber.org/zap" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" @@ -95,6 +96,8 @@ func init() { // `{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 @@ -107,18 +110,31 @@ type App struct { HTTPSPort int `json:"https_port,omitempty"` // GracePeriod is how long to wait for active connections when shutting - // down the server. Once the grace period is over, connections will - // be forcefully closed. + // 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"` - servers []*http.Server - h3servers []*http3.Server - ctx caddy.Context logger *zap.Logger tlsApp *caddytls.TLS @@ -146,6 +162,11 @@ func (app *App) Provision(ctx caddy.Context) error { app.ctx = ctx app.logger = ctx.Logger(app) + 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, @@ -160,14 +181,28 @@ func (app *App) Provision(ctx caddy.Context) error { for srvName, srv := range app.Servers { 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 @@ -179,8 +214,7 @@ func (app *App) Provision(ctx caddy.Context) error { // 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), - ) + zap.String("server_id", srvName)) trueBool := true srv.StrictSNIHost = &trueBool } @@ -189,8 +223,7 @@ func (app *App) Provision(ctx caddy.Context) error { 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) + return fmt.Errorf("server %s, listener %d: %v", srvName, i, err) } srv.Listen[i] = lnOut } @@ -202,7 +235,7 @@ func (app *App) Provision(ctx caddy.Context) error { return fmt.Errorf("loading listener wrapper modules: %v", err) } var hasTLSPlaceholder bool - for i, val := range vals.([]interface{}) { + for i, val := range vals.([]any) { if _, ok := val.(*tlsPlaceholderWrapper); ok { if i == 0 { // putting the tls placeholder wrapper first is nonsensical because @@ -298,7 +331,7 @@ func (app *App) Start() error { } for srvName, srv := range app.Servers { - s := &http.Server{ + srv.server = &http.Server{ ReadTimeout: time.Duration(srv.ReadTimeout), ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout), WriteTimeout: time.Duration(srv.WriteTimeout), @@ -308,12 +341,37 @@ func (app *App) Start() error { ErrorLog: serverLogger, } - // enable h2c if configured - if srv.AllowH2C { + // 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) + + // enable H2C if configured + if srv.protocol("h2c") { h2server := &http2.Server{ IdleTimeout: time.Duration(srv.IdleTimeout), } - s.Handler = h2c.NewHandler(srv, h2server) + srv.server.Handler = h2c.NewHandler(srv, h2server) } for _, lnAddr := range srv.Listen { @@ -321,10 +379,12 @@ func (app *App) Start() error { 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) - ln, err := caddy.Listen(listenAddr.Network, hostport) + ln, err := caddy.ListenTimeout(listenAddr.Network, hostport, time.Duration(srv.KeepAliveInterval)) if err != nil { return fmt.Errorf("%s: listening on %s: %v", listenAddr.Network, hostport, err) } @@ -342,32 +402,16 @@ func (app *App) Start() error { // 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 - tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx) + // create TLS listener - this enables and terminates TLS ln = tls.NewListener(ln, tlsCfg) - ///////// - // TODO: HTTP/3 support is experimental for now - if srv.ExperimentalHTTP3 { - app.logger.Info("enabling experimental HTTP/3 listener", - zap.String("addr", hostport), - ) - h3ln, err := caddy.ListenQUIC(hostport, tlsCfg) - if err != nil { - return fmt.Errorf("getting HTTP/3 QUIC listener: %v", err) + // enable HTTP/3 if configured + if srv.protocol("h3") { + app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport)) + if err := srv.serveHTTP3(hostport, tlsCfg); err != nil { + return err } - h3srv := &http3.Server{ - Addr: hostport, - Handler: srv, - TLSConfig: tlsCfg, - MaxHeaderBytes: srv.MaxHeaderBytes, - } - //nolint:errcheck - go h3srv.ServeListener(h3ln) - app.h3servers = append(app.h3servers, h3srv) - srv.h3server = h3srv } - ///////// } // finish wrapping listener where we left off before TLS @@ -386,15 +430,22 @@ func (app *App) Start() error { app.logger.Debug("starting server loop", zap.String("address", ln.Addr().String()), - zap.Bool("http3", srv.ExperimentalHTTP3), zap.Bool("tls", useTLS), - ) + zap.Bool("http3", srv.h3server != nil)) - //nolint:errcheck - go s.Serve(ln) - app.servers = append(app.servers, s) + 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 @@ -410,26 +461,65 @@ func (app *App) Start() error { // 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") } - for _, s := range app.servers { - err := s.Shutdown(ctx) - if err != nil { - return err + + // shut down servers + for _, server := range app.Servers { + if err := server.server.Shutdown(ctx); err != nil { + app.logger.Error("server shutdown", + zap.Error(err), + zap.Strings("addresses", server.Listen)) + } + + if server.h3server != nil { + // 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 _, s := range app.h3servers { - // TODO: CloseGracefully, once implemented upstream - // (see https://github.com/lucas-clemente/quic-go/issues/2103) - err := s.Close() - if err != nil { - return err - } - } return nil } diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 4cc5d2f2..3d476cb1 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -93,6 +93,9 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // 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 @@ -172,6 +175,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er 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) @@ -400,6 +410,7 @@ redirServersLoop: app.Servers["remaining_auto_https_redirects"] = &Server{ Listen: redirServerAddrsList, Routes: appendCatchAll(redirRoutes), + Logs: logCfg, } } diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go index 33be70df..eb6fd592 100644 --- a/modules/caddyhttp/caddyauth/basicauth.go +++ b/modules/caddyhttp/caddyauth/basicauth.go @@ -21,6 +21,7 @@ import ( "fmt" weakrand "math/rand" "net/http" + "strings" "sync" "time" @@ -94,10 +95,7 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error { // if supported, generate a fake password we can compare against if needed if hasher, ok := hba.Hash.(Hasher); ok { - hba.fakePassword, err = hasher.Hash([]byte("antitiming"), []byte("fakesalt")) - if err != nil { - return fmt.Errorf("generating anti-timing password hash: %v", err) - } + hba.fakePassword = hasher.FakeHash() } repl := caddy.NewReplacer() @@ -117,10 +115,19 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error { return fmt.Errorf("account %d: username and password are required", i) } - acct.password, err = base64.StdEncoding.DecodeString(acct.Password) - if err != nil { - return fmt.Errorf("base64-decoding password: %v", err) + // 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 { @@ -271,9 +278,11 @@ type Comparer interface { // 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. +// 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). diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go index 28b2e1b3..ae30a086 100644 --- a/modules/caddyhttp/caddyauth/caddyauth.go +++ b/modules/caddyhttp/caddyauth/caddyauth.go @@ -62,7 +62,7 @@ func (a *Authentication) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading authentication providers: %v", err) } - for modName, modIface := range mods.(map[string]interface{}) { + for modName, modIface := range mods.(map[string]any) { a.Providers[modName] = modIface.(Authenticator) } return nil diff --git a/modules/caddyhttp/caddyauth/command.go b/modules/caddyhttp/caddyauth/command.go index 597681b6..609de4e8 100644 --- a/modules/caddyhttp/caddyauth/command.go +++ b/modules/caddyhttp/caddyauth/command.go @@ -42,11 +42,13 @@ hash is written to stdout as a base64 string. Caddy is attached to a controlling tty, the plaintext will not be echoed. ---algorithm may be bcrypt or scrypt. If script, the default +--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) @@ -112,13 +114,16 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) { } 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) } @@ -126,9 +131,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) { return caddy.ExitCodeFailedStartup, err } - hashBase64 := base64.StdEncoding.EncodeToString(hash) - - fmt.Println(hashBase64) + fmt.Println(hashString) return 0, nil } diff --git a/modules/caddyhttp/caddyauth/hashes.go b/modules/caddyhttp/caddyauth/hashes.go index 63bfe1be..6505d187 100644 --- a/modules/caddyhttp/caddyauth/hashes.go +++ b/modules/caddyhttp/caddyauth/hashes.go @@ -16,6 +16,7 @@ package caddyauth import ( "crypto/subtle" + "encoding/base64" "github.com/caddyserver/caddy/v2" "golang.org/x/crypto/bcrypt" @@ -55,7 +56,16 @@ 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"` @@ -80,8 +90,9 @@ func (ScryptHash) CaddyModule() caddy.ModuleInfo { } // Provision sets up s. -func (s *ScryptHash) Provision(_ caddy.Context) error { +func (s *ScryptHash) Provision(ctx caddy.Context) error { s.SetDefaults() + ctx.Logger(s).Warn("use of 'scrypt' is deprecated, please use 'bcrypt' instead") return nil } @@ -123,6 +134,14 @@ 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 } diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 784b2b90..c9cc9e6d 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -20,6 +20,7 @@ import ( "io" "net" "net/http" + "path" "path/filepath" "strconv" "strings" @@ -244,6 +245,40 @@ func SanitizedPathJoin(root, reqPath string) string { 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 diff --git a/modules/caddyhttp/caddyhttp_test.go b/modules/caddyhttp/caddyhttp_test.go index 09011fe9..1bca4d60 100644 --- a/modules/caddyhttp/caddyhttp_test.go +++ b/modules/caddyhttp/caddyhttp_test.go @@ -92,3 +92,60 @@ func TestSanitizedPathJoin(t *testing.T) { } } } + +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) + } + } +} diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go index 4938cd5c..e7067c32 100644 --- a/modules/caddyhttp/celmatcher.go +++ b/modules/caddyhttp/celmatcher.go @@ -28,7 +28,6 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker/decls" "github.com/google/cel-go/common" "github.com/google/cel-go/common/operators" "github.com/google/cel-go/common/types" @@ -40,7 +39,6 @@ import ( "github.com/google/cel-go/parser" "go.uber.org/zap" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" - "google.golang.org/protobuf/proto" ) func init() { @@ -126,13 +124,12 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error { // create the CEL environment env, err := cel.NewEnv( - cel.Declarations( - decls.NewVar("request", httpRequestObjectType), - decls.NewFunction(placeholderFuncName, - decls.NewOverload(placeholderFuncName+"_httpRequest_string", - []*exprpb.Type{httpRequestObjectType, decls.String}, - decls.Any)), - ), + 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, @@ -149,20 +146,12 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error { // request matching is a boolean operation, so we don't really know // what to do if the expression returns a non-boolean type - if !proto.Equal(checked.ResultType(), decls.Bool) { - return fmt.Errorf("CEL request matcher expects return type of bool, not %s", checked.ResultType()) + 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), - cel.Functions( - &functions.Overload{ - Operator: placeholderFuncName, - Binary: m.caddyPlaceholderFunc, - }, - ), - ) + m.prg, err = env.Program(checked, cel.EvalOptions(cel.OptOptimize)) if err != nil { return fmt.Errorf("compiling CEL program: %s", err) } @@ -229,7 +218,7 @@ var httpRequestCELType = types.NewTypeValue("http.Request", traits.ReceiverType) // drops allocation costs for CEL expression evaluations by roughly half. type celHTTPRequest struct{ *http.Request } -func (cr celHTTPRequest) ResolveName(name string) (interface{}, bool) { +func (cr celHTTPRequest) ResolveName(name string) (any, bool) { if name == "request" { return cr, true } @@ -240,7 +229,7 @@ func (cr celHTTPRequest) Parent() interpreter.Activation { return nil } -func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { +func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (any, error) { return cr.Request, nil } func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val { @@ -252,8 +241,8 @@ func (cr celHTTPRequest) Equal(other ref.Val) ref.Val { } return types.ValOrErr(other, "%v is not comparable type", other) } -func (celHTTPRequest) Type() ref.Type { return httpRequestCELType } -func (cr celHTTPRequest) Value() interface{} { return cr } +func (celHTTPRequest) Type() ref.Type { return httpRequestCELType } +func (cr celHTTPRequest) Value() any { return cr } var pkixNameCELType = types.NewTypeValue("pkix.Name", traits.ReceiverType) @@ -261,7 +250,7 @@ var pkixNameCELType = types.NewTypeValue("pkix.Name", traits.ReceiverType) // methods to satisfy the ref.Val interface. type celPkixName struct{ *pkix.Name } -func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { +func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (any, error) { return pn.Name, nil } func (celPkixName) ConvertToType(typeVal ref.Type) ref.Val { @@ -273,13 +262,13 @@ func (pn celPkixName) Equal(other ref.Val) ref.Val { } return types.ValOrErr(other, "%v is not comparable type", other) } -func (celPkixName) Type() ref.Type { return pkixNameCELType } -func (pn celPkixName) Value() interface{} { return pn } +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 interface{}) ref.Val { +func (celTypeAdapter) NativeToValue(value any) ref.Val { switch v := value.(type) { case celHTTPRequest: return v @@ -321,62 +310,45 @@ type CELLibraryProducer interface { // 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 []*exprpb.Type, fac CELMatcherFactory) (cel.Library, error) { - requestType := decls.NewObjectType("http.Request") +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] - if isCELStringListType(matcherDataType) { + switch matcherDataType.String() { + case "list(string)": macro = parser.NewGlobalVarArgMacro(macroName, celMatcherStringListMacroExpander(funcName)) - } else if isCELStringType(matcherDataType) { + case cel.StringType.String(): macro = parser.NewGlobalMacro(macroName, 1, celMatcherStringMacroExpander(funcName)) - } else if isCELJSONType(matcherDataType) { + case CELTypeJSON.String(): macro = parser.NewGlobalMacro(macroName, 1, celMatcherJSONMacroExpander(funcName)) - } else { - return nil, fmt.Errorf("unsupported matcher data type: %s", cel.FormatType(matcherDataType)) + default: + return nil, fmt.Errorf("unsupported matcher data type: %s", matcherDataType) } case 2: - if isCELStringType(matcherDataTypes[0]) && isCELStringType(matcherDataTypes[1]) { + if matcherDataTypes[0] == cel.StringType && matcherDataTypes[1] == cel.StringType { macro = parser.NewGlobalMacro(macroName, 2, celMatcherStringListMacroExpander(funcName)) - matcherDataTypes = []*exprpb.Type{CelTypeListString} + matcherDataTypes = []*cel.Type{cel.ListType(cel.StringType)} } else { - return nil, fmt.Errorf( - "unsupported matcher data type: %s, %s", - cel.FormatType(matcherDataTypes[0]), cel.FormatType(matcherDataTypes[1]), - ) + return nil, fmt.Errorf("unsupported matcher data type: %s, %s", matcherDataTypes[0], matcherDataTypes[1]) } case 3: - if isCELStringType(matcherDataTypes[0]) && isCELStringType(matcherDataTypes[1]) && isCELStringType(matcherDataTypes[2]) { + if matcherDataTypes[0] == cel.StringType && matcherDataTypes[1] == cel.StringType && matcherDataTypes[2] == cel.StringType { macro = parser.NewGlobalMacro(macroName, 3, celMatcherStringListMacroExpander(funcName)) - matcherDataTypes = []*exprpb.Type{CelTypeListString} + matcherDataTypes = []*cel.Type{cel.ListType(cel.StringType)} } else { - return nil, fmt.Errorf( - "unsupported matcher data type: %s, %s, %s", - cel.FormatType(matcherDataTypes[0]), cel.FormatType(matcherDataTypes[1]), cel.FormatType(matcherDataTypes[2]), - ) + return nil, fmt.Errorf("unsupported matcher data type: %s, %s, %s", matcherDataTypes[0], matcherDataTypes[1], matcherDataTypes[2]) } } envOptions := []cel.EnvOption{ cel.Macros(macro), - cel.Declarations( - decls.NewFunction(funcName, - decls.NewOverload( - funcName, - append([]*exprpb.Type{requestType}, matcherDataTypes...), - decls.Bool, - ), - ), - ), + 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)), - cel.Functions( - &functions.Overload{ - Operator: funcName, - Binary: CELMatcherRuntimeFunction(funcName, fac), - }, - ), } return NewMatcherCELLibrary(envOptions, programOptions), nil } @@ -573,17 +545,17 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander { // 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]interface{}. +// 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]interface{}{}) + mapStrType := reflect.TypeOf(map[string]any{}) mapStrRaw, err := data.ConvertToNative(mapStrType) if err != nil { return nil, err } - mapStrIface := mapStrRaw.(map[string]interface{}) + mapStrIface := mapStrRaw.(map[string]any) mapStrListStr := make(map[string][]string, len(mapStrIface)) for k, v := range mapStrIface { switch val := v.(type) { @@ -610,25 +582,6 @@ func CELValueToMapStrList(data ref.Val) (map[string][]string, error) { return mapStrListStr, nil } -// isCELJSONType returns whether the type corresponds to JSON input. -func isCELJSONType(t *exprpb.Type) bool { - switch t.GetTypeKind().(type) { - case *exprpb.Type_MapType_: - mapType := t.GetMapType() - return isCELStringType(mapType.GetKeyType()) && mapType.GetValueType().GetDyn() != nil - } - return false -} - -// isCELStringType returns whether the type corresponds to a string. -func isCELStringType(t *exprpb.Type) bool { - switch t.GetTypeKind().(type) { - case *exprpb.Type_Primitive: - return t.GetPrimitive() == exprpb.Type_STRING - } - 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) @@ -681,15 +634,6 @@ func isCELConcatCall(e *exprpb.Expr) bool { return false } -// isCELStringListType returns whether the type corresponds to a list of strings. -func isCELStringListType(t *exprpb.Type) bool { - switch t.GetTypeKind().(type) { - case *exprpb.Type_ListType_: - return isCELStringType(t.GetListType().GetElemType()) - } - 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 { @@ -713,11 +657,10 @@ var ( placeholderRegexp = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`) placeholderExpansion = `caddyPlaceholder(request, "${1}")` - CelTypeListString = decls.NewListType(decls.String) - CelTypeJson = decls.NewMapType(decls.String, decls.Dyn) + CELTypeJSON = cel.MapType(cel.StringType, cel.DynType) ) -var httpRequestObjectType = decls.NewObjectType("http.Request") +var httpRequestObjectType = cel.ObjectType("http.Request") // The name of the CEL function which accesses Replacer values. const placeholderFuncName = "caddyPlaceholder" diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index 8b492050..aae72806 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -71,7 +71,7 @@ func (enc *Encode) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading encoder modules: %v", err) } - for modName, modIface := range mods.(map[string]interface{}) { + 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) @@ -142,7 +142,7 @@ func (enc *Encode) addEncoding(e Encoding) error { enc.writerPools = make(map[string]*sync.Pool) } enc.writerPools[ae] = &sync.Pool{ - New: func() interface{} { + New: func() any { return e.NewEncoder() }, } @@ -418,7 +418,7 @@ type Precompressed interface { } var bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { return new(bytes.Buffer) }, } diff --git a/modules/caddyhttp/encode/zstd/zstd.go b/modules/caddyhttp/encode/zstd/zstd.go index d2a638cb..3da9b138 100644 --- a/modules/caddyhttp/encode/zstd/zstd.go +++ b/modules/caddyhttp/encode/zstd/zstd.go @@ -45,7 +45,7 @@ func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // used in the Accept-Encoding request headers. func (Zstd) AcceptEncoding() string { return "zstd" } -// NewEncoder returns a new gzip writer. +// 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 diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go index f308281a..6a72e155 100644 --- a/modules/caddyhttp/fileserver/browse.go +++ b/modules/caddyhttp/fileserver/browse.go @@ -19,6 +19,8 @@ import ( _ "embed" "encoding/json" "fmt" + "io" + "io/fs" "net/http" "os" "path" @@ -67,9 +69,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, 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)) - origReq.URL.Path += "/" - http.Redirect(w, r, origReq.URL.String(), http.StatusMovedPermanently) - return nil + return redirect(w, r, origReq.URL.Path+"/") } } @@ -82,7 +82,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, 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(dir, root, path.Clean(r.URL.Path), repl) + listing, err := fsrv.loadDirectoryContents(dir.(fs.ReadDirFile), root, path.Clean(r.URL.Path), repl) switch { case os.IsPermission(err): return caddyhttp.Error(http.StatusForbidden, err) @@ -95,6 +95,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, fsrv.browseApplyQueryParams(w, r, &listing) buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() defer bufPool.Put(buf) acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) @@ -135,9 +136,9 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, return nil } -func (fsrv *FileServer) loadDirectoryContents(dir *os.File, root, urlPath string, repl *caddy.Replacer) (browseTemplateContext, error) { - files, err := dir.Readdir(-1) - if err != nil { +func (fsrv *FileServer) loadDirectoryContents(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 } @@ -203,25 +204,25 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.T return tpl, nil } -// isSymlink return true if f is a symbolic link -func isSymlink(f os.FileInfo) bool { - return f.Mode()&os.ModeSymlink != 0 -} - // isSymlinkTargetDir returns true if f's symbolic link target // is a directory. -func isSymlinkTargetDir(f os.FileInfo, root, urlPath string) bool { +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 := os.Stat(target) + 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. @@ -232,7 +233,7 @@ type templateContext struct { // bufPool is used to increase the efficiency of file listings. var bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { return new(bytes.Buffer) }, } diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go index 87156d48..cd24fc23 100644 --- a/modules/caddyhttp/fileserver/browsetplcontext.go +++ b/modules/caddyhttp/fileserver/browsetplcontext.go @@ -15,6 +15,7 @@ package fileserver import ( + "io/fs" "net/url" "os" "path" @@ -26,22 +27,31 @@ import ( "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(files []os.FileInfo, canGoUp bool, root, urlPath string, repl *caddy.Replacer) browseTemplateContext { +func (fsrv *FileServer) directoryListing(entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) browseTemplateContext { filesToHide := fsrv.transformHidePaths(repl) var dirCount, fileCount int fileInfos := []fileInfo{} - for _, f := range files { - name := f.Name() + for _, entry := range entries { + name := entry.Name() if fileHidden(name, filesToHide) { continue } - isDir := f.IsDir() || isSymlinkTargetDir(f, root, urlPath) + 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 { @@ -51,11 +61,11 @@ func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, root fileCount++ } - size := f.Size() - fileIsSymlink := isSymlink(f) + size := info.Size() + fileIsSymlink := isSymlink(info) if fileIsSymlink { - path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name())) - fileInfo, err := os.Stat(path) + path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, info.Name())) + fileInfo, err := fs.Stat(fsrv.fileSystem, path) if err == nil { size = fileInfo.Size() } @@ -73,8 +83,8 @@ func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, root Name: name, Size: size, URL: u.String(), - ModTime: f.ModTime().UTC(), - Mode: f.Mode(), + ModTime: info.ModTime().UTC(), + Mode: info.Mode(), }) } name, _ := url.PathUnescape(urlPath) diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go index 26415353..df56092b 100644 --- a/modules/caddyhttp/fileserver/caddyfile.go +++ b/modules/caddyhttp/fileserver/caddyfile.go @@ -15,11 +15,13 @@ 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" @@ -34,16 +36,16 @@ func init() { // parseCaddyfile parses the file_server directive. It enables the static file // server and configures it with this syntax: // -// file_server [] [browse] { -// root -// hide -// index -// browse [] -// precompressed -// status -// disable_canonical_uris -// } -// +// file_server [] [browse] { +// fs +// root +// hide +// index +// browse [] +// precompressed +// status +// disable_canonical_uris +// } func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { var fsrv FileServer @@ -62,6 +64,25 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) 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 { @@ -155,22 +176,23 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // 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 +// try_files { +// policy first_exist|smallest_size|largest_size|most_recently_modified +// } // // and is basically shorthand for: // -// @try_files { -// file { -// try_files -// } -// } -// rewrite @try_files {http.matchers.file.relative} +// @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} +// 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 @@ -185,6 +207,27 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) 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. @@ -193,7 +236,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) URI: "{http.matchers.file.relative}" + userQueryString, } matcherSet := caddy.ModuleMap{ - "file": h.JSON(MatchFile{TryFiles: try}), + "file": h.JSON(MatchFile{TryFiles: try, TryPolicy: tryPolicy}), } return h.NewRoute(matcherSet, handler) } diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go index 7b4ab110..902c5f8c 100644 --- a/modules/caddyhttp/fileserver/command.go +++ b/modules/caddyhttp/fileserver/command.go @@ -117,8 +117,14 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) { Servers: map[string]*caddyhttp.Server{"static": server}, } + var false bool cfg := &caddy.Config{ - Admin: &caddy.AdminConfig{Disabled: true}, + Admin: &caddy.AdminConfig{ + Disabled: true, + Config: &caddy.ConfigSettings{ + Persist: &false, + }, + }, AppsRaw: caddy.ModuleMap{ "http": caddyconfig.JSON(httpApp, nil), }, diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 4f3ffefa..87a15243 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -15,24 +15,26 @@ package fileserver import ( + "encoding/json" "fmt" + "io/fs" "net/http" "os" "path" + "path/filepath" + "runtime" "strconv" "strings" - "time" "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/checker/decls" "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/interpreter/functions" "github.com/google/cel-go/parser" + "go.uber.org/zap" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) @@ -55,7 +57,15 @@ func init() { // 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}` @@ -96,6 +106,8 @@ type MatchFile struct { // 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. @@ -108,12 +120,11 @@ func (MatchFile) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the matcher from Caddyfile tokens. Syntax: // -// file { -// root -// try_files -// try_policy first_exist|smallest_size|largest_size|most_recently_modified -// } -// +// 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()...) @@ -151,20 +162,10 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // expression matchers. // // Example: -// expression file({'root': '/srv', 'try_files': [{http.request.uri.path}, '/index.php'], 'try_policy': 'first_exist', 'split_path': ['.php']}) +// +// 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 := decls.NewObjectType("http.Request") - envOptions := []cel.EnvOption{ - cel.Macros(parser.NewGlobalVarArgMacro("file", celFileMatcherMacroExpander())), - cel.Declarations( - decls.NewFunction("file", - decls.NewOverload("file_request_map", - []*exprpb.Type{requestType, caddyhttp.CelTypeJson}, - decls.Bool, - ), - ), - ), - } + requestType := cel.ObjectType("http.Request") matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) { values, err := caddyhttp.CELValueToMapStrList(data) @@ -193,14 +194,16 @@ func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) { 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)), - cel.Functions( - &functions.Overload{ - Operator: "file_request_map", - Binary: caddyhttp.CELMatcherRuntimeFunction("file_request_map", matcherFactory), - }, - ), } return caddyhttp.NewMatcherCELLibrary(envOptions, programOptions), nil @@ -252,10 +255,25 @@ func celFileMatcherMacroExpander() parser.MacroExpander { } // Provision sets up m's defaults. -func (m *MatchFile) Provision(_ caddy.Context) error { +func (m *MatchFile) Provision(ctx caddy.Context) error { + m.logger = ctx.Logger(m) + + // 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 { @@ -281,10 +299,10 @@ func (m MatchFile) Validate() error { // 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 -// - http.matchers.file.absolute -// - http.matchers.file.type -// - http.matchers.file.remainder +// - 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) } @@ -294,23 +312,80 @@ func (m MatchFile) Match(r *http.Request) bool { func (m MatchFile) selectFile(r *http.Request) (matched bool) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - root := repl.ReplaceAll(m.Root, ".") + root := filepath.Clean(repl.ReplaceAll(m.Root, ".")) - // common preparation of the file into parts - prepareFilePath := func(file string) (suffix, fullpath, remainder string) { - suffix, remainder = m.firstSplit(path.Clean(repl.ReplaceAll(file, ""))) - if strings.HasSuffix(file, "/") { - suffix += "/" - } - fullpath = caddyhttp.SanitizedPathJoin(root, suffix) - return + type matchCandidate struct { + fullpath, relative, splitRemainder string } - // sets up the placeholders for the matched file - setPlaceholders := func(info os.FileInfo, rel string, abs string, remainder string) { - repl.Set("http.matchers.file.relative", rel) - repl.Set("http.matchers.file.absolute", abs) - repl.Set("http.matchers.file.remainder", remainder) + // 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() { @@ -319,76 +394,83 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { repl.Set("http.matchers.file.type", fileType) } + // match file according to the configured policy switch m.TryPolicy { case "", tryPolicyFirstExist: - for _, f := range m.TryFiles { - if err := parseErrorCode(f); err != nil { + for _, pattern := range m.TryFiles { + if err := parseErrorCode(pattern); err != nil { caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err) return } - suffix, fullpath, remainder := prepareFilePath(f) - if info, exists := strictFileExists(fullpath); exists { - setPlaceholders(info, suffix, fullpath, remainder) - return true + 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 largestFilename string - var largestSuffix string - var remainder string - var info os.FileInfo - for _, f := range m.TryFiles { - suffix, fullpath, splitRemainder := prepareFilePath(f) - info, err := os.Stat(fullpath) - if err == nil && info.Size() > largestSize { - largestSize = info.Size() - largestFilename = fullpath - largestSuffix = suffix - remainder = splitRemainder + 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 + } } } - setPlaceholders(info, largestSuffix, largestFilename, remainder) + if largestInfo == nil { + return false + } + setPlaceholders(largest, largestInfo) return true case tryPolicySmallestSize: var smallestSize int64 - var smallestFilename string - var smallestSuffix string - var remainder string - var info os.FileInfo - for _, f := range m.TryFiles { - suffix, fullpath, splitRemainder := prepareFilePath(f) - info, err := os.Stat(fullpath) - if err == nil && (smallestSize == 0 || info.Size() < smallestSize) { - smallestSize = info.Size() - smallestFilename = fullpath - smallestSuffix = suffix - remainder = splitRemainder + 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 + } } } - setPlaceholders(info, smallestSuffix, smallestFilename, remainder) + if smallestInfo == nil { + return false + } + setPlaceholders(smallest, smallestInfo) return true case tryPolicyMostRecentlyMod: - var recentDate time.Time - var recentFilename string - var recentSuffix string - var remainder string - var info os.FileInfo - for _, f := range m.TryFiles { - suffix, fullpath, splitRemainder := prepareFilePath(f) - info, err := os.Stat(fullpath) - if err == nil && - (recentDate.IsZero() || info.ModTime().After(recentDate)) { - recentDate = info.ModTime() - recentFilename = fullpath - recentSuffix = suffix - remainder = splitRemainder + 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 + } } } - setPlaceholders(info, recentSuffix, recentFilename, remainder) + if recentInfo == nil { + return false + } + setPlaceholders(recent, recentInfo) return true } @@ -415,8 +497,8 @@ func parseErrorCode(input string) error { // the file must also be a directory; if it does // NOT end in a forward slash, the file must NOT // be a directory. -func strictFileExists(file string) (os.FileInfo, bool) { - stat, err := os.Stat(file) +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 @@ -431,11 +513,11 @@ func strictFileExists(file string) (os.FileInfo, bool) { if strings.HasSuffix(file, separator) { // by convention, file paths ending // in a path separator must be a directory - return stat, stat.IsDir() + return info, info.IsDir() } // by convention, file paths NOT ending // in a path separator must NOT be a directory - return stat, !stat.IsDir() + return info, !info.IsDir() } // firstSplit returns the first result where the path @@ -572,6 +654,15 @@ func isCELStringListLiteral(e *exprpb.Expr) bool { 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" diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go index fd109e65..0f8c6bbc 100644 --- a/modules/caddyhttp/fileserver/matcher_test.go +++ b/modules/caddyhttp/fileserver/matcher_test.go @@ -28,7 +28,6 @@ import ( ) func TestFileMatcher(t *testing.T) { - // Windows doesn't like colons in files names isWindows := runtime.GOOS == "windows" if !isWindows { @@ -87,37 +86,38 @@ func TestFileMatcher(t *testing.T) { }, { path: "ملف.txt", // the path file name is not escaped - expectedPath: "ملف.txt", + expectedPath: "/ملف.txt", expectedType: "file", matched: true, }, { path: url.PathEscape("ملف.txt"), // singly-escaped path - expectedPath: "ملف.txt", + expectedPath: "/ملف.txt", expectedType: "file", matched: true, }, { path: url.PathEscape(url.PathEscape("ملف.txt")), // doubly-escaped path - expectedPath: "%D9%85%D9%84%D9%81.txt", + 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", + expectedPath: "/with:in-name.txt", expectedType: "file", matched: !isWindows, }, } { m := &MatchFile{ - Root: "./testdata", - TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, + fileSystem: osFS{}, + Root: "./testdata", + TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, } u, err := url.Parse(tc.path) if err != nil { - t.Fatalf("Test %d: parsing path: %v", i, err) + t.Errorf("Test %d: parsing path: %v", i, err) } req := &http.Request{URL: u} @@ -125,24 +125,24 @@ func TestFileMatcher(t *testing.T) { result := m.Match(req) if result != tc.matched { - t.Fatalf("Test %d: expected match=%t, got %t", i, tc.matched, result) + 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.Fatalf("Test %d: expected replacer value", i) + t.Errorf("Test %d: expected replacer value", i) } if !result { continue } if rel != tc.expectedPath { - t.Fatalf("Test %d: actual path: %v, expected: %v", i, 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.Fatalf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) + t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) } } } @@ -213,14 +213,15 @@ func TestPHPFileMatcher(t *testing.T) { }, } { m := &MatchFile{ - Root: "./testdata", - TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"}, - SplitPath: []string{".php"}, + 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.Fatalf("Test %d: parsing path: %v", i, err) + t.Errorf("Test %d: parsing path: %v", i, err) } req := &http.Request{URL: u} @@ -228,24 +229,24 @@ func TestPHPFileMatcher(t *testing.T) { result := m.Match(req) if result != tc.matched { - t.Fatalf("Test %d: expected match=%t, got %t", i, tc.matched, result) + 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.Fatalf("Test %d: expected replacer value", i) + t.Errorf("Test %d: expected replacer value", i) } if !result { continue } if rel != tc.expectedPath { - t.Fatalf("Test %d: actual path: %v, expected: %v", i, 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.Fatalf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) + t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) } } } diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 9a2bc6e2..0639d979 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -15,11 +15,14 @@ package fileserver import ( + "encoding/json" + "errors" "fmt" + "io" + "io/fs" weakrand "math/rand" "mime" "net/http" - "net/url" "os" "path" "path/filepath" @@ -39,10 +42,63 @@ func init() { caddy.RegisterModule(FileServer{}) } -// FileServer implements a static file server responder for Caddy. +// 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. + // 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 @@ -63,6 +119,7 @@ type FileServer struct { 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 @@ -95,8 +152,7 @@ type FileServer struct { // 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 + precompressors map[string]encode.Precompressed logger *zap.Logger } @@ -113,6 +169,18 @@ func (FileServer) CaddyModule() caddy.ModuleInfo { func (fsrv *FileServer) Provision(ctx caddy.Context) error { fsrv.logger = ctx.Logger(fsrv) + // 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}" } @@ -131,11 +199,12 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error { } } + // 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]interface{}) { + for modName, modIface := range mods.(map[string]any) { p, ok := modIface.(encode.Precompressed) if !ok { return fmt.Errorf("module %s is not precompressor", modName) @@ -166,16 +235,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c filesToHide := fsrv.transformHidePaths(repl) root := repl.ReplaceAll(fsrv.Root, ".") - // PathUnescape returns an error if the escapes aren't well-formed, - // meaning the count % matches the RFC. Return early if the escape is - // improper. - if _, err := url.PathUnescape(r.URL.Path); err != nil { - fsrv.logger.Debug("improper path escape", - zap.String("site_root", root), - zap.String("request_path", r.URL.Path), - zap.Error(err)) - return err - } + filename := caddyhttp.SanitizedPathJoin(root, r.URL.Path) fsrv.logger.Debug("sanitized path join", @@ -184,12 +244,12 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c zap.String("result", filename)) // get information about the file - info, err := os.Stat(filename) + info, err := fs.Stat(fsrv.fileSystem, filename) if err != nil { - err = mapDirOpenError(err, filename) - if os.IsNotExist(err) { + err = fsrv.mapDirOpenError(err, filename) + if errors.Is(err, fs.ErrNotExist) { return fsrv.notFound(w, r, next) - } else if os.IsPermission(err) { + } else if errors.Is(err, fs.ErrPermission) { return caddyhttp.Error(http.StatusForbidden, err) } return caddyhttp.Error(http.StatusInternalServerError, err) @@ -210,7 +270,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c continue } - indexInfo, err := os.Stat(indexPath) + indexInfo, err := fs.Stat(fsrv.fileSystem, indexPath) if err != nil { continue } @@ -280,7 +340,8 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c } } - var file *os.File + var file fs.File + var etag string // check for precompressed files for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) { @@ -289,7 +350,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c continue } compressedFilename := filename + precompress.Suffix() - compressedInfo, err := os.Stat(compressedFilename) + 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 @@ -301,12 +362,19 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c 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 } @@ -324,18 +392,18 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c 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", 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 - // TODO: If we want a Content-Type, consider writing a default of application/octet-stream - this is secure but violates spec + // 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) @@ -375,7 +443,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c // 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) + http.ServeContent(w, r, info.Name(), info.ModTime(), file.(io.ReadSeeker)) return nil } @@ -384,10 +452,10 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c // 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) (*os.File, error) { - file, err := os.Open(filename) +func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (fs.File, error) { + file, err := fsrv.fileSystem.Open(filename) if err != nil { - err = mapDirOpenError(err, filename) + 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) @@ -412,8 +480,8 @@ func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (*os.Fi // 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 mapDirOpenError(originalErr error, name string) error { - if os.IsNotExist(originalErr) || os.IsPermission(originalErr) { +func (fsrv *FileServer) mapDirOpenError(originalErr error, name string) error { + if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) { return originalErr } @@ -422,12 +490,12 @@ func mapDirOpenError(originalErr error, name string) error { if parts[i] == "" { continue } - fi, err := os.Stat(strings.Join(parts[:i+1], separator)) + fi, err := fs.Stat(fsrv.fileSystem, strings.Join(parts[:i+1], separator)) if err != nil { return originalErr } if !fi.IsDir() { - return os.ErrNotExist + return fs.ErrNotExist } } @@ -545,6 +613,21 @@ 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 ( @@ -556,4 +639,9 @@ const ( 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) ) diff --git a/modules/caddyhttp/fileserver/testdata/foodir/bar.txt b/modules/caddyhttp/fileserver/testdata/foodir/bar.txt new file mode 100644 index 00000000..df34bd20 --- /dev/null +++ b/modules/caddyhttp/fileserver/testdata/foodir/bar.txt @@ -0,0 +1 @@ +foodir/bar.txt \ No newline at end of file diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index f67df928..d5237236 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -194,27 +194,27 @@ type RespHeaderOps struct { func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { // add for fieldName, vals := range ops.Add { - fieldName = repl.ReplaceAll(fieldName, "") + fieldName = repl.ReplaceKnown(fieldName, "") for _, v := range vals { - hdr.Add(fieldName, repl.ReplaceAll(v, "")) + hdr.Add(fieldName, repl.ReplaceKnown(v, "")) } } // set for fieldName, vals := range ops.Set { - fieldName = repl.ReplaceAll(fieldName, "") + 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.ReplaceAll(vals[i], "")) + newVals = append(newVals, repl.ReplaceKnown(vals[i], "")) } hdr.Set(fieldName, strings.Join(newVals, ",")) } // delete for _, fieldName := range ops.Delete { - fieldName = strings.ToLower(repl.ReplaceAll(fieldName, "")) + fieldName = strings.ToLower(repl.ReplaceKnown(fieldName, "")) switch { case strings.HasPrefix(fieldName, "*") && strings.HasSuffix(fieldName, "*"): for existingField := range hdr { @@ -241,13 +241,13 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { // replace for fieldName, replacements := range ops.Replace { - fieldName = http.CanonicalHeaderKey(repl.ReplaceAll(fieldName, "")) + fieldName = http.CanonicalHeaderKey(repl.ReplaceKnown(fieldName, "")) // all fields... if fieldName == "*" { for _, r := range replacements { - search := repl.ReplaceAll(r.Search, "") - replace := repl.ReplaceAll(r.Replace, "") + search := repl.ReplaceKnown(r.Search, "") + replace := repl.ReplaceKnown(r.Replace, "") for fieldName, vals := range hdr { for i := range vals { if r.re != nil { @@ -263,8 +263,8 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { // ...or only with the named field for _, r := range replacements { - search := repl.ReplaceAll(r.Search, "") - replace := repl.ReplaceAll(r.Replace, "") + 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 { diff --git a/modules/caddyhttp/map/caddyfile.go b/modules/caddyhttp/map/caddyfile.go index f1ee4680..9cc7d8c6 100644 --- a/modules/caddyhttp/map/caddyfile.go +++ b/modules/caddyhttp/map/caddyfile.go @@ -27,10 +27,10 @@ func init() { // parseCaddyfile sets up the map handler from Caddyfile tokens. Syntax: // -// map [] { -// [~] -// default -// } +// map [] { +// [~] +// default +// } // // If the input value is prefixed with a tilde (~), then the input will be parsed as a // regular expression. @@ -76,9 +76,9 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) continue } - // every other line maps one input to one or more outputs + // every line maps an input value to one or more outputs in := h.Val() - var outs []interface{} + var outs []any for h.NextArg() { val := h.ScalarVal() if val == "-" { diff --git a/modules/caddyhttp/map/map.go b/modules/caddyhttp/map/map.go index 0a27aab8..d41806d4 100644 --- a/modules/caddyhttp/map/map.go +++ b/modules/caddyhttp/map/map.go @@ -62,6 +62,9 @@ func (Handler) CaddyModule() caddy.ModuleInfo { // 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, "{}") } @@ -106,6 +109,16 @@ func (h *Handler) Validate() error { } seen[input] = i + // prevent infinite recursion + for _, out := range m.Outputs { + for _, dest := range h.Destinations { + if strings.Contains(caddy.ToString(out), dest) || + strings.Contains(m.Input, dest) { + return fmt.Errorf("mapping %d requires value of {%s} to define value of {%s}: infinite recursion", i, dest, dest) + } + } + } + // ensure mappings have 1:1 output-to-destination correspondence nOut := len(m.Outputs) if nOut != nDest { @@ -119,7 +132,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt 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) (interface{}, bool) { + 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 { @@ -135,21 +148,22 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt 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, output.(string), input, matches) + result = m.re.ExpandString(result, outputStr, input, matches) return string(result), true } + + // otherwise simple string comparison if input == m.Input { - if outputStr, ok := output.(string); ok { - // NOTE: if the output has a placeholder that has the same key as the input, this is infinite recursion - return repl.ReplaceAll(outputStr, ""), true - } - return output, true + return repl.ReplaceAll(outputStr, ""), true } } @@ -187,7 +201,7 @@ type Mapping struct { // 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 []interface{} `json:"outputs,omitempty"` + Outputs []any `json:"outputs,omitempty"` re *regexp.Regexp } diff --git a/modules/caddyhttp/map/map_test.go b/modules/caddyhttp/map/map_test.go index 26d6e85e..fe233bf2 100644 --- a/modules/caddyhttp/map/map_test.go +++ b/modules/caddyhttp/map/map_test.go @@ -15,7 +15,7 @@ func TestHandler(t *testing.T) { for i, tc := range []struct { handler Handler reqURI string - expect map[string]interface{} + expect map[string]any }{ { reqURI: "/foo", @@ -25,11 +25,11 @@ func TestHandler(t *testing.T) { Mappings: []Mapping{ { Input: "/foo", - Outputs: []interface{}{"FOO"}, + Outputs: []any{"FOO"}, }, }, }, - expect: map[string]interface{}{ + expect: map[string]any{ "output": "FOO", }, }, @@ -41,11 +41,11 @@ func TestHandler(t *testing.T) { Mappings: []Mapping{ { InputRegexp: "(/abc)", - Outputs: []interface{}{"ABC"}, + Outputs: []any{"ABC"}, }, }, }, - expect: map[string]interface{}{ + expect: map[string]any{ "output": "ABC", }, }, @@ -57,11 +57,11 @@ func TestHandler(t *testing.T) { Mappings: []Mapping{ { InputRegexp: "(xyz)", - Outputs: []interface{}{"...${1}..."}, + Outputs: []any{"...${1}..."}, }, }, }, - expect: map[string]interface{}{ + expect: map[string]any{ "output": "...xyz...", }, }, @@ -74,11 +74,11 @@ func TestHandler(t *testing.T) { Mappings: []Mapping{ { InputRegexp: "(?i)(\\^|`|<|>|%|\\\\|\\{|\\}|\\|)", - Outputs: []interface{}{"3"}, + Outputs: []any{"3"}, }, }, }, - expect: map[string]interface{}{ + expect: map[string]any{ "output": "3", }, }, @@ -90,11 +90,11 @@ func TestHandler(t *testing.T) { Mappings: []Mapping{ { Input: "/foo", - Outputs: []interface{}{"{testvar}"}, + Outputs: []any{"{testvar}"}, }, }, }, - expect: map[string]interface{}{ + expect: map[string]any{ "output": "testing", }, }, diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 268b936b..a01f8f98 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -20,10 +20,10 @@ import ( "fmt" "net" "net/http" + "net/netip" "net/textproto" "net/url" "path" - "path/filepath" "reflect" "regexp" "sort" @@ -33,11 +33,9 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker/decls" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "go.uber.org/zap" - exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) type ( @@ -65,20 +63,51 @@ type ( // Duplicate entries will return an error. MatchHost []string - // MatchPath matches requests by the URI's path (case-insensitive). Path - // matches are exact, but wildcards may be used: + // 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, for a prefix match (`/prefix/*`) - // - At the beginning, for a suffix match (`*.suffix`) - // - On both sides, for a substring match (`*/contains/*`) + // - 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. + // 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 @@ -103,6 +132,9 @@ type ( // "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 @@ -140,7 +172,7 @@ type ( // cidrs and zones vars should aligned always in the same // length and indexes for matching later - cidrs []*net.IPNet + cidrs []*netip.Prefix zones []string logger *zap.Logger } @@ -302,12 +334,13 @@ outer: // expression matchers. // // Example: -// expression host('localhost') +// +// expression host('localhost') func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) { return CELMatcherImpl( "host", "host_match_request_list", - []*exprpb.Type{CelTypeListString}, + []*cel.Type{cel.ListType(cel.StringType)}, func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) strList, err := data.ConvertToNative(refStringList) @@ -341,6 +374,11 @@ func (MatchPath) CaddyModule() caddy.ModuleInfo { // 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 @@ -348,77 +386,108 @@ func (m MatchPath) Provision(_ caddy.Context) error { // Match returns true if r matches m. func (m MatchPath) Match(r *http.Request) bool { - // PathUnescape returns an error if the escapes aren't - // well-formed, meaning the count % matches the RFC. - // Return early if the escape is improper. - unescapedPath, err := url.PathUnescape(r.URL.Path) - if err != nil { - return false - } + // 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) - lowerPath := strings.ToLower(unescapedPath) - - // Clean the path, merges doubled slashes, etc. - // This ensures maliciously crafted requests can't bypass - // the path matcher. See #4407 - lowerPath = path.Clean(lowerPath) - - // see #2917; Windows ignores trailing dots and spaces + // 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 - lowerPath = strings.TrimRight(lowerPath, ". ") - - // Cleaning may remove the trailing slash, but we want to keep it - if lowerPath != "/" && strings.HasSuffix(r.URL.Path, "/") { - lowerPath = lowerPath + "/" - } + // being matched by *.php to be treated as PHP scripts. + reqPath = strings.TrimRight(reqPath, ". ") repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - for _, matchPath := range m { - matchPath = repl.ReplaceAll(matchPath, "") + 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 matchPath == "*" { + 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 len(matchPath) > 1 && - strings.HasPrefix(matchPath, "*") && - strings.HasSuffix(matchPath, "*") { - if strings.Contains(lowerPath, matchPath[1:len(matchPath)-1]) { + 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 } - // special case: first character is a wildcard, - // treat it as a fast suffix match - if strings.HasPrefix(matchPath, "*") { - if strings.HasSuffix(lowerPath, matchPath[1:]) { - return true + // 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 } - continue } - // special case: last character is a wildcard, - // treat it as a fast prefix match - if strings.HasSuffix(matchPath, "*") { - if strings.HasPrefix(lowerPath, matchPath[:len(matchPath)-1]) { - return true - } - continue - } - - // for everything else, try globular matching, which also - // is exact matching if there are no glob/wildcard chars; - // can ignore error here because we can't handle it anyway - matches, _ := filepath.Match(matchPath, lowerPath) + // 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 } @@ -426,11 +495,118 @@ func (m MatchPath) Match(r *http.Request) bool { 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') +// +// 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. @@ -438,7 +614,7 @@ func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) { // name of the function that the macro will be rewritten to call. "path_match_request_list", // internal data type of the MatchPath value. - []*exprpb.Type{CelTypeListString}, + []*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{}) @@ -476,23 +652,10 @@ func (MatchPathRE) CaddyModule() caddy.ModuleInfo { func (m MatchPathRE) Match(r *http.Request) bool { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - // PathUnescape returns an error if the escapes aren't - // well-formed, meaning the count % matches the RFC. - // Return early if the escape is improper. - unescapedPath, err := url.PathUnescape(r.URL.Path) - if err != nil { - return false - } - // Clean the path, merges doubled slashes, etc. // This ensures maliciously crafted requests can't bypass // the path matcher. See #4407 - cleanedPath := path.Clean(unescapedPath) - - // Cleaning may remove the trailing slash, but we want to keep it - if cleanedPath != "/" && strings.HasSuffix(r.URL.Path, "/") { - cleanedPath = cleanedPath + "/" - } + cleanedPath := cleanPath(r.URL.Path) return m.MatchRegexp.Match(cleanedPath, repl) } @@ -501,12 +664,13 @@ func (m MatchPathRE) Match(r *http.Request) bool { // expression matchers. // // Example: -// expression path_regexp('^/bar') +// +// expression path_regexp('^/bar') func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { unnamedPattern, err := CELMatcherImpl( "path_regexp", "path_regexp_request_string", - []*exprpb.Type{decls.String}, + []*cel.Type{cel.StringType}, func(data ref.Val) (RequestMatcher, error) { pattern := data.(types.String) matcher := MatchPathRE{MatchRegexp{Pattern: string(pattern)}} @@ -520,7 +684,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { namedPattern, err := CELMatcherImpl( "path_regexp", "path_regexp_request_string_string", - []*exprpb.Type{decls.String, decls.String}, + []*cel.Type{cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) params, err := data.ConvertToNative(refStringList) @@ -574,12 +738,13 @@ func (m MatchMethod) Match(r *http.Request) bool { // expression matchers. // // Example: -// expression method('PUT', 'POST') +// +// expression method('PUT', 'POST') func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) { return CELMatcherImpl( "method", "method_request_list", - []*exprpb.Type{CelTypeListString}, + []*cel.Type{cel.ListType(cel.StringType)}, func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) strList, err := data.ConvertToNative(refStringList) @@ -609,11 +774,11 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if query == "" { continue } - parts := strings.SplitN(query, "=", 2) - if len(parts) != 2 { + 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(parts[0], parts[1]) + url.Values(*m).Add(before, after) } if d.NextBlock(0) { return d.Err("malformed query matcher: blocks are not supported") @@ -625,9 +790,25 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // 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 := r.URL.Query()[param] + paramVal, found := parsed[param] if found { for _, v := range vals { v = repl.ReplaceAll(v, "") @@ -644,12 +825,13 @@ func (m MatchQuery) Match(r *http.Request) bool { // expression matchers. // // Example: -// expression query({'sort': 'asc'}) || query({'foo': ['*bar*', 'baz']}) +// +// expression query({'sort': 'asc'}) || query({'foo': ['*bar*', 'baz']}) func (MatchQuery) CELLibrary(_ caddy.Context) (cel.Library, error) { return CELMatcherImpl( "query", "query_matcher_request_map", - []*exprpb.Type{CelTypeJson}, + []*cel.Type{CELTypeJSON}, func(data ref.Val) (RequestMatcher, error) { mapStrListStr, err := CELValueToMapStrList(data) if err != nil { @@ -719,13 +901,14 @@ func (m MatchHeader) Match(r *http.Request) bool { // expression matchers. // // Example: -// expression header({'content-type': 'image/png'}) -// expression header({'foo': ['bar', 'baz']}) // match bar or baz +// +// 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", - []*exprpb.Type{CelTypeJson}, + []*cel.Type{CELTypeJSON}, func(data ref.Val) (RequestMatcher, error) { mapStrListStr, err := CELValueToMapStrList(data) if err != nil { @@ -822,6 +1005,12 @@ func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 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) { @@ -877,12 +1066,13 @@ func (m MatchHeaderRE) Validate() error { // expression matchers. // // Example: -// expression header_regexp('foo', 'Field', 'fo+') +// +// 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", - []*exprpb.Type{decls.String, decls.String}, + []*cel.Type{cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) params, err := data.ConvertToNative(refStringList) @@ -902,7 +1092,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { namedPattern, err := CELMatcherImpl( "header_regexp", "header_regexp_request_string_string_string", - []*exprpb.Type{decls.String, decls.String, decls.String}, + []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcher, error) { refStringList := reflect.TypeOf([]string{}) params, err := data.ConvertToNative(refStringList) @@ -941,6 +1131,22 @@ func (m MatchProtocol) Match(r *http.Request) bool { 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 } @@ -961,12 +1167,13 @@ func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // expression matchers. // // Example: -// expression protocol('https') +// +// expression protocol('https') func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) { return CELMatcherImpl( "protocol", "protocol_request_string", - []*exprpb.Type{decls.String}, + []*cel.Type{cel.StringType}, func(data ref.Val) (RequestMatcher, error) { protocolStr, ok := data.(types.String) if !ok { @@ -987,57 +1194,12 @@ func (MatchNot) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchNot) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - // first, unmarshal each matcher in the set from its tokens - type matcherPair struct { - raw caddy.ModuleMap - decoded MatcherSet - } for d.Next() { - var mp matcherPair - 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()...) + matcherSet, err := ParseCaddyfileNestedMatcherSet(d) + if err != nil { + return err } - for matcherName, tokens := range tokensByMatcherName { - mod, err := caddy.GetModule("http.matchers." + matcherName) - if err != nil { - return d.Errf("getting matcher module '%s': %v", matcherName, err) - } - unm, ok := mod.New().(caddyfile.Unmarshaler) - if !ok { - return d.Errf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) - } - err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens)) - if err != nil { - return err - } - rm, ok := unm.(RequestMatcher) - if !ok { - return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) - } - matcherMap[matcherName] = rm - mp.decoded = append(mp.decoded, rm) - } - - // we should now have a functional 'not' matcher, but we also - // need to be able to marshal as JSON, otherwise config - // adaptation will be missing the matchers! - mp.raw = make(caddy.ModuleMap) - for name, matcher := range matcherMap { - jsonBytes, err := json.Marshal(matcher) - if err != nil { - return fmt.Errorf("marshaling %T matcher: %v", matcher, err) - } - mp.raw[name] = jsonBytes - } - m.MatcherSetsRaw = append(m.MatcherSetsRaw, mp.raw) + m.MatcherSetsRaw = append(m.MatcherSetsRaw, matcherSet) } return nil } @@ -1060,7 +1222,7 @@ func (m *MatchNot) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading matcher sets: %v", err) } - for _, modMap := range matcherSets.([]map[string]interface{}) { + for _, modMap := range matcherSets.([]map[string]any) { var ms MatcherSet for _, modIface := range modMap { ms = append(ms, modIface.(RequestMatcher)) @@ -1125,7 +1287,8 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // expression matchers. // // Example: -// expression remote_ip('forwarded', '192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8') +// +// 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. @@ -1133,7 +1296,7 @@ func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) { // name of the function that the macro will be rewritten to call. "remote_ip_match_request_list", // internal data type of the MatchPath value. - []*exprpb.Type{CelTypeListString}, + []*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{}) @@ -1175,27 +1338,24 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { m.zones = append(m.zones, "") } if strings.Contains(str, "/") { - _, ipNet, err := net.ParseCIDR(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) + m.cidrs = append(m.cidrs, &ipNet) } else { - ip := net.ParseIP(str) - if ip == nil { - return fmt.Errorf("invalid IP address: %s", str) + ipAddr, err := netip.ParseAddr(str) + if err != nil { + return fmt.Errorf("invalid IP address: '%s': %v", str, err) } - mask := len(ip) * 8 - m.cidrs = append(m.cidrs, &net.IPNet{ - IP: ip, - Mask: net.CIDRMask(mask, mask), - }) + ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen()) + m.cidrs = append(m.cidrs, &ipNew) } } return nil } -func (m MatchRemoteIP) getClientIP(r *http.Request) (net.IP, string, error) { +func (m MatchRemoteIP) getClientIP(r *http.Request) (netip.Addr, string, error) { remote := r.RemoteAddr zoneID := "" if m.Forwarded { @@ -1214,11 +1374,11 @@ func (m MatchRemoteIP) getClientIP(r *http.Request) (net.IP, string, error) { ipStr = split[0] zoneID = split[1] } - ip := net.ParseIP(ipStr) - if ip == nil { - return nil, zoneID, fmt.Errorf("invalid client IP address: %s", ipStr) + ipAddr, err := netip.ParseAddr(ipStr) + if err != nil { + return netip.IPv4Unspecified(), "", err } - return ip, zoneID, nil + return ipAddr, zoneID, nil } // Match returns true if r matches m. @@ -1319,6 +1479,13 @@ func (mre *MatchRegexp) Match(input string, repl *caddy.Replacer) bool { // 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: @@ -1336,6 +1503,56 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 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+`) ) diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index f394921a..4d5538cd 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -158,9 +158,10 @@ func TestHostMatcher(t *testing.T) { func TestPathMatcher(t *testing.T) { for i, tc := range []struct { - match MatchPath - input string - expect bool + 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{}, @@ -252,6 +253,11 @@ func TestPathMatcher(t *testing.T) { input: "/FOOOO", expect: true, }, + { + match: MatchPath{"*.php"}, + input: "/foo/index.php. .", + expect: true, + }, { match: MatchPath{"/foo/bar.txt"}, input: "/foo/BAR.txt", @@ -263,10 +269,60 @@ func TestPathMatcher(t *testing.T) { expect: true, }, { - match: MatchPath{"/foo*"}, + 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", @@ -292,8 +348,79 @@ func TestPathMatcher(t *testing.T) { 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, + }, } { - req := &http.Request{URL: &url.URL{Path: tc.input}} + 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) @@ -387,6 +514,16 @@ func TestPathREMatcher(t *testing.T) { 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{}) @@ -401,7 +538,11 @@ func TestPathREMatcher(t *testing.T) { } // set up the fake request and its Replacer - req := &http.Request{URL: &url.URL{Path: tc.input}} + 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) @@ -807,7 +948,7 @@ func TestVarREMatcher(t *testing.T) { 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]interface{})) + ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any)) req = req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) diff --git a/modules/caddyhttp/push/handler.go b/modules/caddyhttp/push/handler.go index 75442be1..27652ef0 100644 --- a/modules/caddyhttp/push/handler.go +++ b/modules/caddyhttp/push/handler.go @@ -29,10 +29,24 @@ func init() { caddy.RegisterModule(Handler{}) } -// Handler is a middleware for manipulating the request body. +// 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 { - Resources []Resource `json:"resources,omitempty"` - Headers *HeaderConfig `json:"headers,omitempty"` + // The resources to push. + Resources []Resource `json:"resources,omitempty"` + + // Headers to modify for the push requests. + Headers *HeaderConfig `json:"headers,omitempty"` logger *zap.Logger } diff --git a/modules/caddyhttp/push/link.go b/modules/caddyhttp/push/link.go index 16b0e7d2..f7c1dd89 100644 --- a/modules/caddyhttp/push/link.go +++ b/modules/caddyhttp/push/link.go @@ -52,17 +52,16 @@ func parseLinkHeader(header string) []linkResource { l.uri = strings.TrimSpace(link[li+1 : ri]) for _, param := range strings.Split(strings.TrimSpace(link[ri+1:]), semicolon) { - parts := strings.SplitN(strings.TrimSpace(param), equal, 2) - key := strings.TrimSpace(parts[0]) + before, after, isCut := strings.Cut(strings.TrimSpace(param), equal) + key := strings.TrimSpace(before) if key == "" { continue } - if len(parts) == 1 { + if isCut { + l.params[key] = strings.TrimSpace(after) + } else { l.params[key] = key } - if len(parts) == 2 { - l.params[key] = strings.TrimSpace(parts[1]) - } } resources = append(resources, l) diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 2fa65171..e1546499 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -57,7 +57,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo SetVar(req.Context(), "start_time", time.Now()) SetVar(req.Context(), "uuid", new(requestID)) - httpVars := func(key string) (interface{}, bool) { + httpVars := func(key string) (any, bool) { if req != nil { // query string parameters if strings.HasPrefix(key, reqURIQueryReplPrefix) { @@ -143,6 +143,10 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo 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": @@ -169,7 +173,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo req.Body = io.NopCloser(buf) // replace real body with buffered data return buf.String(), true - // original request, before any internal changes + // original request, before any internal changes case "http.request.orig_method": or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request) return or.Method, true @@ -233,7 +237,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo // middleware variables if strings.HasPrefix(key, varsReplPrefix) { varName := key[len(varsReplPrefix):] - tbl := req.Context().Value(VarsCtxKey).(map[string]interface{}) + 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 @@ -252,13 +256,29 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } } + 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) (interface{}, bool) { +func getReqTLSReplacement(req *http.Request, key string) (any, bool) { if req == nil || req.TLS == nil { return nil, false } @@ -279,7 +299,7 @@ func getReqTLSReplacement(req *http.Request, key string) (interface{}, bool) { if strings.HasPrefix(field, "client.san.") { field = field[len("client.san."):] var fieldName string - var fieldValue interface{} + var fieldValue any switch { case strings.HasPrefix(field, "dns_names"): fieldName = "dns_names" @@ -383,7 +403,7 @@ func getReqTLSReplacement(req *http.Request, key string) (interface{}, bool) { } // marshalPublicKey returns the byte encoding of pubKey. -func marshalPublicKey(pubKey interface{}) ([]byte, error) { +func marshalPublicKey(pubKey any) ([]byte, error) { switch key := pubKey.(type) { case *rsa.PublicKey: return asn1.Marshal(key) diff --git a/modules/caddyhttp/replacer_test.go b/modules/caddyhttp/replacer_test.go index 5026ac84..18253d3f 100644 --- a/modules/caddyhttp/replacer_test.go +++ b/modules/caddyhttp/replacer_test.go @@ -27,7 +27,7 @@ import ( ) func TestHTTPVarReplacement(t *testing.T) { - req, _ := http.NewRequest("GET", "/", nil) + 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) @@ -72,114 +72,134 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV addHTTPVarsToReplacer(repl, req, res) for i, tc := range []struct { - input string + get string expect string }{ { - input: "{http.request.scheme}", + get: "http.request.scheme", expect: "https", }, { - input: "{http.request.host}", + get: "http.request.method", + expect: http.MethodGet, + }, + { + get: "http.request.host", expect: "example.com", }, { - input: "{http.request.port}", + get: "http.request.port", expect: "80", }, { - input: "{http.request.hostport}", + get: "http.request.hostport", expect: "example.com:80", }, { - input: "{http.request.remote.host}", + get: "http.request.remote.host", expect: "localhost", }, { - input: "{http.request.remote.port}", + get: "http.request.remote.port", expect: "1234", }, { - input: "{http.request.host.labels.0}", + get: "http.request.host.labels.0", expect: "com", }, { - input: "{http.request.host.labels.1}", + get: "http.request.host.labels.1", expect: "example", }, { - input: "{http.request.host.labels.2}", - expect: "", + get: "http.request.host.labels.2", + expect: "", }, { - input: "{http.request.tls.cipher_suite}", + 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", }, { - input: "{http.request.tls.proto}", + get: "http.request.tls.proto", expect: "h2", }, { - input: "{http.request.tls.proto_mutual}", + get: "http.request.tls.proto_mutual", expect: "true", }, { - input: "{http.request.tls.resumed}", + get: "http.request.tls.resumed", expect: "false", }, { - input: "{http.request.tls.server_name}", + get: "http.request.tls.server_name", expect: "foo.com", }, { - input: "{http.request.tls.version}", + get: "http.request.tls.version", expect: "tls1.3", }, { - input: "{http.request.tls.client.fingerprint}", + get: "http.request.tls.client.fingerprint", expect: "9f57b7b497cceacc5459b76ac1c3afedbc12b300e728071f55f84168ff0f7702", }, { - input: "{http.request.tls.client.issuer}", + get: "http.request.tls.client.issuer", expect: "CN=Caddy Test CA", }, { - input: "{http.request.tls.client.serial}", + get: "http.request.tls.client.serial", expect: "2", }, { - input: "{http.request.tls.client.subject}", + get: "http.request.tls.client.subject", expect: "CN=client.localdomain", }, { - input: "{http.request.tls.client.san.dns_names}", + get: "http.request.tls.client.san.dns_names", expect: "[localhost]", }, { - input: "{http.request.tls.client.san.dns_names.0}", + get: "http.request.tls.client.san.dns_names.0", expect: "localhost", }, { - input: "{http.request.tls.client.san.dns_names.1}", - expect: "", + get: "http.request.tls.client.san.dns_names.1", + expect: "", }, { - input: "{http.request.tls.client.san.ips}", + get: "http.request.tls.client.san.ips", expect: "[127.0.0.1]", }, { - input: "{http.request.tls.client.san.ips.0}", + get: "http.request.tls.client.san.ips.0", expect: "127.0.0.1", }, { - input: "{http.request.tls.client.certificate_pem}", + get: "http.request.tls.client.certificate_pem", expect: string(clientCert) + "\n", // returned value comes with a newline appended to it }, } { - actual := repl.ReplaceAll(tc.input, "") + 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 placeholder %s to be '%s' but got '%s'", - i, tc.input, tc.expect, actual) + t.Errorf("Test %d: Expected %s to be '%s' but got '%s'", + i, tc.get, tc.expect, actual) } } } diff --git a/modules/caddyhttp/responsewriter.go b/modules/caddyhttp/responsewriter.go index 0ffb9320..9820b41b 100644 --- a/modules/caddyhttp/responsewriter.go +++ b/modules/caddyhttp/responsewriter.go @@ -62,6 +62,16 @@ func (rww *ResponseWriterWrapper) Push(target string, opts *http.PushOptions) er 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 @@ -111,15 +121,15 @@ type responseRecorder struct { // // 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 +// 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. @@ -129,7 +139,7 @@ type responseRecorder struct { // 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() +// 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 @@ -138,9 +148,12 @@ type responseRecorder struct { // recorder's body buffer, but you might have your own body to write // instead): // -// w.WriteHeader(rec.Status()) -// io.Copy(w, rec.Buffer()) +// 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}, @@ -149,22 +162,29 @@ func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, 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 } - rr.statusCode = statusCode - 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()) + // 1xx responses aren't final; just informational + if statusCode < 100 || statusCode > 199 { + rr.statusCode = statusCode + 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 not buffered, immediately write header - if rr.stream { + // if informational or not buffered, immediately write header + if rr.stream || (100 <= statusCode && statusCode <= 199) { rr.ResponseWriterWrapper.WriteHeader(rr.statusCode) } } @@ -178,9 +198,26 @@ func (rr *responseRecorder) Write(data []byte) (int, error) { } else { n, err = rr.buf.Write(data) } - if err == nil { - rr.size += n + + 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 } @@ -241,4 +278,10 @@ type ShouldBufferFunc func(status int, header http.Header) bool 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) ) diff --git a/modules/caddyhttp/responsewriter_test.go b/modules/caddyhttp/responsewriter_test.go new file mode 100644 index 00000000..19139320 --- /dev/null +++ b/modules/caddyhttp/responsewriter_test.go @@ -0,0 +1,165 @@ +package 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") + } + } + }) + } +} diff --git a/modules/caddyhttp/reverseproxy/addresses.go b/modules/caddyhttp/reverseproxy/addresses.go index f15ed76d..4da47fbb 100644 --- a/modules/caddyhttp/reverseproxy/addresses.go +++ b/modules/caddyhttp/reverseproxy/addresses.go @@ -80,9 +80,9 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) { scheme, host, port = toURL.Scheme, toURL.Hostname(), toURL.Port() } else { // extract network manually, since caddy.ParseNetworkAddress() will always add one - if idx := strings.Index(upstreamAddr, "/"); idx >= 0 { - network = strings.ToLower(strings.TrimSpace(upstreamAddr[:idx])) - upstreamAddr = upstreamAddr[idx+1:] + 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) @@ -96,6 +96,12 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) { } } + // 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 != "" { diff --git a/modules/caddyhttp/reverseproxy/admin.go b/modules/caddyhttp/reverseproxy/admin.go index 771fa10c..f64d1ecf 100644 --- a/modules/caddyhttp/reverseproxy/admin.go +++ b/modules/caddyhttp/reverseproxy/admin.go @@ -76,7 +76,7 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er // Iterate over the upstream pool (needs to be fast) var rangeErr error - hosts.Range(func(key, val interface{}) bool { + hosts.Range(func(key, val any) bool { address, ok := key.(string) if !ok { rangeErr = caddy.APIError{ diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index b2bdf049..f746ee50 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -52,71 +52,73 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // -// reverse_proxy [] [] { -// # backends -// to -// dynamic [...] +// reverse_proxy [] [] { +// # backends +// to +// dynamic [...] // -// # load balancing -// lb_policy [] -// lb_try_duration -// lb_try_interval +// # 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 { -// [] -// } +// # 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 +// # passive health checking +// fail_duration +// max_fails +// unhealthy_status +// unhealthy_latency +// unhealthy_request_count // -// # streaming -// flush_interval -// buffer_requests -// buffer_responses -// max_buffer_size +// # streaming +// flush_interval +// buffer_requests +// buffer_responses +// max_buffer_size // -// # request manipulation -// trusted_proxies [private_ranges] -// header_up [+|-] [ []] -// header_down [+|-] [ []] -// method -// rewrite +// # request manipulation +// trusted_proxies [private_ranges] +// header_up [+|-] [ []] +// header_down [+|-] [ []] +// method +// rewrite // -// # round trip -// transport { -// ... -// } +// # round trip +// transport { +// ... +// } // -// # optionally intercept responses from upstream -// @name { -// status -// header [] -// } -// replace_status [] -// handle_response [] { -// +// # 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 -// } -// } -// } +// # 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 @@ -247,6 +249,19 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } 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() @@ -273,6 +288,16 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } 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() @@ -799,31 +824,32 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error // 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 -// keepalive [off|] -// keepalive_interval -// keepalive_idle_conns -// keepalive_idle_conns_per_host -// versions -// compression off -// max_conns_per_host -// max_idle_conns_per_host -// } -// +// 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) { @@ -848,6 +874,26 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } 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() @@ -907,6 +953,11 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 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) @@ -922,25 +973,6 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } - case "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": - if h.TLS == nil { - h.TLS = new(TLSConfig) - } - case "tls_insecure_skip_verify": if d.NextArg() { return d.ArgErr() @@ -982,6 +1014,29 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } 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() @@ -1063,15 +1118,6 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } h.MaxConnsPerHost = num - case "except_ports": - if h.TLS == nil { - h.TLS = new(TLSConfig) - } - h.TLS.ExceptPorts = d.RemainingArgs() - if len(h.TLS.ExceptPorts) == 0 { - return d.ArgErr() - } - default: return d.Errf("unrecognized subdirective %s", d.Val()) } @@ -1091,10 +1137,9 @@ func parseCopyResponseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHan // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // -// copy_response [] [] { -// status -// } -// +// copy_response [] [] { +// status +// } func (h *CopyResponseHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { args := d.RemainingArgs() @@ -1131,11 +1176,10 @@ func parseCopyResponseHeadersCaddyfile(h httpcaddyfile.Helper) (caddyhttp.Middle // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // -// copy_response_headers [] { -// include -// exclude -// } -// +// copy_response_headers [] { +// include +// exclude +// } func (h *CopyResponseHeadersHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { args := d.RemainingArgs() @@ -1161,16 +1205,15 @@ func (h *CopyResponseHeadersHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) // UnmarshalCaddyfile deserializes Caddyfile tokens into h. // -// dynamic srv [] { -// service -// proto -// name -// refresh -// resolvers -// dial_timeout -// dial_fallback_delay -// } -// +// 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() @@ -1260,15 +1303,14 @@ func (u *SRVUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // UnmarshalCaddyfile deserializes Caddyfile tokens into h. // -// dynamic a [ -// port -// refresh -// resolvers -// dial_timeout -// dial_fallback_delay -// } -// +// dynamic a [ +// port +// refresh +// resolvers +// dial_timeout +// dial_fallback_delay +// } func (u *AUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { args := d.RemainingArgs() @@ -1277,7 +1319,9 @@ func (u *AUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } if len(args) > 0 { u.Name = args[0] - u.Port = args[1] + if len(args) == 2 { + u.Port = args[1] + } } for d.NextBlock(0) { @@ -1348,6 +1392,35 @@ func (u *AUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 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 @@ -1356,4 +1429,5 @@ var ( _ caddyfile.Unmarshaler = (*HTTPTransport)(nil) _ caddyfile.Unmarshaler = (*SRVUpstreams)(nil) _ caddyfile.Unmarshaler = (*AUpstreams)(nil) + _ caddyfile.Unmarshaler = (*MultiUpstreams)(nil) ) diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go index 6153b6ec..fed1cd91 100644 --- a/modules/caddyhttp/reverseproxy/command.go +++ b/modules/caddyhttp/reverseproxy/command.go @@ -172,8 +172,13 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { appsRaw["tls"] = caddyconfig.JSON(tlsApp, nil) } + var false bool cfg := &caddy.Config{ - Admin: &caddy.AdminConfig{Disabled: true}, + Admin: &caddy.AdminConfig{Disabled: true, + Config: &caddy.ConfigSettings{ + Persist: &false, + }, + }, AppsRaw: appsRaw, } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go index 96b84f21..a9e6b220 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go @@ -35,16 +35,16 @@ func init() { // UnmarshalCaddyfile deserializes Caddyfile tokens into h. // -// transport fastcgi { -// root -// split -// env -// resolve_root_symlink -// dial_timeout -// read_timeout -// write_timeout -// } -// +// 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) { @@ -107,6 +107,12 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } 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()) } @@ -120,31 +126,31 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // 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 +// 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 +// # 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} +// # 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 -// } -// } +// # 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 @@ -154,7 +160,7 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // // If a matcher is specified by the user, for example: // -// php_fastcgi /subpath localhost:7777 +// 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 @@ -303,6 +309,14 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error 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 } } } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client.go b/modules/caddyhttp/reverseproxy/fastcgi/client.go index 07720539..ae36dd8b 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/client.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/client.go @@ -26,9 +26,6 @@ package fastcgi import ( "bufio" "bytes" - "context" - "encoding/binary" - "errors" "io" "mime/multipart" "net" @@ -40,8 +37,9 @@ import ( "path/filepath" "strconv" "strings" - "sync" "time" + + "go.uber.org/zap" ) // FCGIListenSockFileno describes listen socket file number. @@ -120,293 +118,84 @@ const ( maxPad = 255 ) -type header struct { - Version uint8 - Type uint8 - ID uint16 - ContentLength uint16 - PaddingLength uint8 - Reserved uint8 -} - // 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 -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) -} - -type record struct { - h header - rbuf []byte -} - -func (rec *record) read(r io.Reader) (buf []byte, err error) { - 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 - } - n := int(rec.h.ContentLength) + int(rec.h.PaddingLength) - if len(rec.rbuf) < n { - rec.rbuf = make([]byte, n) - } - if _, err = io.ReadFull(r, rec.rbuf[:n]); err != nil { - return - } - buf = rec.rbuf[:int(rec.h.ContentLength)] - - return -} - -// FCGIClient implements a FastCGI client, which is a standard for +// client implements a FastCGI client, which is a standard for // interfacing external applications with Web servers. -type FCGIClient struct { - mutex sync.Mutex - rwc io.ReadWriteCloser - h header - buf bytes.Buffer - stderr bytes.Buffer - keepAlive bool - reqID uint16 -} - -// DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer -// and a context. -// See func net.Dial for a description of the network and address parameters. -func DialWithDialerContext(ctx context.Context, network, address string, dialer net.Dialer) (fcgi *FCGIClient, err error) { - var conn net.Conn - conn, err = dialer.DialContext(ctx, network, address) - if err != nil { - return - } - - fcgi = &FCGIClient{ - rwc: conn, - keepAlive: false, - reqID: 1, - } - - return -} - -// DialContext is like Dial but passes ctx to dialer.Dial. -func DialContext(ctx context.Context, network, address string) (fcgi *FCGIClient, err error) { - // TODO: why not set timeout here? - return DialWithDialerContext(ctx, network, address, net.Dialer{}) -} - -// Dial connects to the fcgi responder at the specified network address, using default net.Dialer. -// See func net.Dial for a description of the network and address parameters. -func Dial(network, address string) (fcgi *FCGIClient, err error) { - return DialContext(context.Background(), network, address) -} - -// Close closes fcgi connection -func (c *FCGIClient) Close() { - c.rwc.Close() -} - -func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) { - c.mutex.Lock() - defer c.mutex.Unlock() - c.buf.Reset() - c.h.init(recType, c.reqID, len(content)) - if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil { - return err - } - if _, err := c.buf.Write(content); err != nil { - return err - } - if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil { - return err - } - _, err = c.rwc.Write(c.buf.Bytes()) - return err -} - -func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error { - b := [8]byte{byte(role >> 8), byte(role), flags} - return c.writeRecord(BeginRequest, b[:]) -} - -func (c *FCGIClient) writePairs(recType uint8, pairs map[string]string) error { - w := newWriter(c, recType) - b := make([]byte, 8) - nn := 0 - 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 { - w.Flush() - nn = 0 - } - nn += m - if _, err := w.Write(b[:n]); err != nil { - return err - } - if _, err := w.WriteString(k); err != nil { - return err - } - if _, err := w.WriteString(v); err != nil { - return err - } - } - w.Close() - return nil -} - -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 -} - -// bufWriter encapsulates bufio.Writer but also closes the underlying stream when -// Closed. -type bufWriter struct { - closer io.Closer - *bufio.Writer -} - -func (w *bufWriter) Close() error { - if err := w.Writer.Flush(); err != nil { - w.closer.Close() - return err - } - return w.closer.Close() -} - -func newWriter(c *FCGIClient, recType uint8) *bufWriter { - s := &streamWriter{c: c, recType: recType} - w := bufio.NewWriterSize(s, maxWrite) - return &bufWriter{s, w} -} - -// streamWriter abstracts out the separation of a stream into discrete records. -// It only writes maxWrite bytes at a time. -type streamWriter struct { - c *FCGIClient - recType uint8 -} - -func (w *streamWriter) Write(p []byte) (int, error) { - nn := 0 - for len(p) > 0 { - n := len(p) - if n > maxWrite { - n = maxWrite - } - if err := w.c.writeRecord(w.recType, p[:n]); err != nil { - return nn, err - } - nn += n - p = p[n:] - } - return nn, nil -} - -func (w *streamWriter) Close() error { - // send empty record to close the stream - return w.c.writeRecord(w.recType, nil) -} - -type streamReader struct { - c *FCGIClient - buf []byte -} - -func (w *streamReader) Read(p []byte) (n int, err error) { - - if len(p) > 0 { - if len(w.buf) == 0 { - - // filter outputs for error log - for { - rec := &record{} - var buf []byte - buf, err = rec.read(w.c.rwc) - if err != nil { - return - } - // standard error output - if rec.h.Type == Stderr { - w.c.stderr.Write(buf) - continue - } - w.buf = buf - break - } - } - - n = len(p) - if n > len(w.buf) { - n = len(w.buf) - } - copy(p, w.buf[:n]) - w.buf = w.buf[n:] - } - - return +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 *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) { - err = c.writeBeginRequest(uint16(Responder), 0) +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 } - err = c.writePairs(Params, p) + writer.recType = Params + err = writer.writePairs(p) if err != nil { return } - body := newWriter(c, Stdin) + writer.recType = Stdin if req != nil { - _, _ = io.Copy(body, req) + _, err = io.Copy(writer, req) + if err != nil { + return nil, err + } + } + err = writer.FlushStream() + if err != nil { + return nil, err } - body.Close() r = &streamReader{c: c} return } // clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer -// that closes FCGIClient connection. +// that closes the client connection. type clientCloser struct { - *FCGIClient + rwc net.Conn + r *streamReader io.Reader + + status int + logger *zap.Logger } -func (f clientCloser) Close() error { return f.rwc.Close() } +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 *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) { +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 @@ -424,13 +213,13 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res resp.Header = http.Header(mimeHeader) if resp.Header.Get("Status") != "" { - statusParts := strings.SplitN(resp.Header.Get("Status"), " ", 2) - resp.StatusCode, err = strconv.Atoi(statusParts[0]) + statusNumber, statusInfo, statusIsCut := strings.Cut(resp.Header.Get("Status"), " ") + resp.StatusCode, err = strconv.Atoi(statusNumber) if err != nil { return } - if len(statusParts) > 1 { - resp.Status = statusParts[1] + if statusIsCut { + resp.Status = statusInfo } } else { @@ -441,16 +230,27 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res resp.TransferEncoding = resp.Header["Transfer-Encoding"] resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) - if chunked(resp.TransferEncoding) { - resp.Body = clientCloser{c, httputil.NewChunkedReader(rb)} - } else { - resp.Body = clientCloser{c, io.NopCloser(rb)} + // 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 *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) { +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) @@ -459,7 +259,7 @@ func (c *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *ht } // Head issues a HEAD request to the fcgi responder. -func (c *FCGIClient) Head(p map[string]string) (resp *http.Response, err error) { +func (c *client) Head(p map[string]string) (resp *http.Response, err error) { p["REQUEST_METHOD"] = "HEAD" p["CONTENT_LENGTH"] = "0" @@ -468,7 +268,7 @@ func (c *FCGIClient) Head(p map[string]string) (resp *http.Response, err error) } // Options issues an OPTIONS request to the fcgi responder. -func (c *FCGIClient) Options(p map[string]string) (resp *http.Response, err error) { +func (c *client) Options(p map[string]string) (resp *http.Response, err error) { p["REQUEST_METHOD"] = "OPTIONS" p["CONTENT_LENGTH"] = "0" @@ -478,7 +278,7 @@ func (c *FCGIClient) Options(p map[string]string) (resp *http.Response, err erro // Post issues a POST request to the fcgi responder. with request body // in the format that bodyType specified -func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, body io.Reader, l int64) (resp *http.Response, err error) { +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) } @@ -501,7 +301,7 @@ func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, b // PostForm issues a POST to the fcgi responder, with form // as a string key to a list values (url.Values) -func (c *FCGIClient) PostForm(p map[string]string, data url.Values) (resp *http.Response, err error) { +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())) } @@ -509,7 +309,7 @@ func (c *FCGIClient) PostForm(p map[string]string, data url.Values) (resp *http. // 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 *FCGIClient) PostFile(p map[string]string, data url.Values, file map[string]string) (resp *http.Response, err error) { +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() @@ -550,18 +350,18 @@ func (c *FCGIClient) PostFile(p map[string]string, data url.Values, file map[str // 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 *FCGIClient) SetReadTimeout(t time.Duration) error { - if conn, ok := c.rwc.(net.Conn); ok && t != 0 { - return conn.SetReadDeadline(time.Now().Add(t)) +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 *FCGIClient) SetWriteTimeout(t time.Duration) error { - if conn, ok := c.rwc.(net.Conn); ok && t != 0 { - return conn.SetWriteDeadline(time.Now().Add(t)) +func (c *client) SetWriteTimeout(t time.Duration) error { + if t != 0 { + return c.rwc.SetWriteDeadline(time.Now().Add(t)) } return nil } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go index ef3474d4..78e5713a 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go @@ -118,12 +118,14 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { } func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) { - fcgi, err := Dial("tcp", ipPort) + 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 @@ -168,7 +170,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[ content, _ = io.ReadAll(resp.Body) log.Println("c: send data length ≈", length, string(content)) - fcgi.Close() + conn.Close() time.Sleep(1 * time.Second) if bytes.Contains(content, []byte("FAILED")) { diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index 28481337..6ff6ff4a 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -15,7 +15,6 @@ package fastcgi import ( - "context" "crypto/tls" "fmt" "net" @@ -34,6 +33,8 @@ import ( "github.com/caddyserver/caddy/v2" ) +var noopLogger = zap.NewNop() + func init() { caddy.RegisterModule(Transport{}) } @@ -74,6 +75,11 @@ type Transport struct { // 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 } @@ -94,10 +100,8 @@ func (t *Transport) Provision(ctx caddy.Context) error { t.Root = "{http.vars.root}" } - t.serverSoftware = "Caddy" - if mod := caddy.GoModule(); mod.Version != "" { - t.serverSoftware += "/" + mod.Version - } + version, _ := caddy.Version() + t.serverSoftware = "Caddy/" + version // Set a relatively short default dial timeout. // This is helpful to make load-balancer retries more speedy. @@ -110,6 +114,8 @@ func (t *Transport) Provision(ctx caddy.Context) error { // 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 @@ -122,13 +128,7 @@ func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { return nil, fmt.Errorf("building environment: %v", err) } - // TODO: doesn't dialer have a Timeout field? ctx := r.Context() - if t.DialTimeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, time.Duration(t.DialTimeout)) - defer cancel() - } // extract dial information from request (should have been embedded by the reverse proxy) network, address := "tcp", r.URL.Host @@ -137,24 +137,47 @@ func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { address = dialInfo.Address } - t.logger.Debug("roundtrip", - zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}), - zap.String("dial", address), - zap.Object("env", env), - ) + logCreds := server.Logs != nil && server.Logs.ShouldLogCredentials + loggableReq := caddyhttp.LoggableHTTPRequest{ + Request: r, + ShouldLogCredentials: logCreds, + } + loggableEnv := loggableEnv{vars: env, logCredentials: logCreds} - fcgiBackend, err := DialContext(ctx, network, address) + 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 { - // TODO: wrap in a special error type if the dial failed, so retries can happen if enabled return nil, fmt.Errorf("dialing backend: %v", err) } - // fcgiBackend gets closed when response body is closed (see clientCloser) + 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 := fcgiBackend.SetReadTimeout(time.Duration(t.ReadTimeout)); err != nil { + if err = client.SetReadTimeout(time.Duration(t.ReadTimeout)); err != nil { return nil, fmt.Errorf("setting read timeout: %v", err) } - if err := fcgiBackend.SetWriteTimeout(time.Duration(t.WriteTimeout)); err != nil { + if err = client.SetWriteTimeout(time.Duration(t.WriteTimeout)); err != nil { return nil, fmt.Errorf("setting write timeout: %v", err) } @@ -166,16 +189,19 @@ func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { var resp *http.Response switch r.Method { case http.MethodHead: - resp, err = fcgiBackend.Head(env) + resp, err = client.Head(env) case http.MethodGet: - resp, err = fcgiBackend.Get(env, r.Body, contentLength) + resp, err = client.Get(env, r.Body, contentLength) case http.MethodOptions: - resp, err = fcgiBackend.Options(env) + resp, err = client.Options(env) default: - resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength) + resp, err = client.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength) + } + if err != nil { + return nil, err } - return resp, err + return resp, nil } // buildEnv returns a set of CGI environment variables for the request. @@ -366,11 +392,22 @@ func (t Transport) splitPos(path string) int { return -1 } -// envVars is a simple type to allow for speeding up zap log encoding. type envVars map[string]string -func (env envVars) MarshalLogObject(enc zapcore.ObjectEncoder) error { - for k, v := range env { +// 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 @@ -389,7 +426,7 @@ var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") // Interface guards var ( - _ zapcore.ObjectMarshaler = (*envVars)(nil) + _ zapcore.ObjectMarshaler = (*loggableEnv)(nil) _ caddy.Provisioner = (*Transport)(nil) _ http.RoundTripper = (*Transport)(nil) diff --git a/modules/caddyhttp/reverseproxy/fastcgi/header.go b/modules/caddyhttp/reverseproxy/fastcgi/header.go new file mode 100644 index 00000000..59dce715 --- /dev/null +++ b/modules/caddyhttp/reverseproxy/fastcgi/header.go @@ -0,0 +1,32 @@ +// 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) +} diff --git a/notify/notify.go b/modules/caddyhttp/reverseproxy/fastcgi/pool.go similarity index 63% rename from notify/notify.go rename to modules/caddyhttp/reverseproxy/fastcgi/pool.go index bca80c1f..29017f11 100644 --- a/notify/notify.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/pool.go @@ -12,19 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package notify +package fastcgi -// NotifyReadiness notifies process manager of readiness. -func NotifyReadiness() error { - return notifyReadiness() -} +import ( + "bytes" + "sync" +) -// NotifyReloading notifies process manager of reloading. -func NotifyReloading() error { - return notifyReloading() -} - -// NotifyStopping notifies process manager of stopping. -func NotifyStopping() error { - return notifyStopping() +var bufPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/reader.go b/modules/caddyhttp/reverseproxy/fastcgi/reader.go new file mode 100644 index 00000000..3a8e91de --- /dev/null +++ b/modules/caddyhttp/reverseproxy/fastcgi/reader.go @@ -0,0 +1,44 @@ +// 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) +} diff --git a/modules/caddyhttp/reverseproxy/fastcgi/record.go b/modules/caddyhttp/reverseproxy/fastcgi/record.go new file mode 100644 index 00000000..46c1f17b --- /dev/null +++ b/modules/caddyhttp/reverseproxy/fastcgi/record.go @@ -0,0 +1,58 @@ +// 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 +} diff --git a/modules/caddyhttp/reverseproxy/fastcgi/writer.go b/modules/caddyhttp/reverseproxy/fastcgi/writer.go new file mode 100644 index 00000000..3af00d9a --- /dev/null +++ b/modules/caddyhttp/reverseproxy/fastcgi/writer.go @@ -0,0 +1,145 @@ +// 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() +} diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index 317b283e..cf22d261 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -269,7 +269,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre // 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]interface{}{ + ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{ dialInfoVarKey: dialInfo, }) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) @@ -284,6 +284,13 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre } } + 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 { @@ -291,7 +298,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre zap.String("host", hostAddr), zap.Error(err), ) - upstream.setHealthy(false) + markUnhealthy() return nil } var body io.Reader = resp.Body @@ -311,7 +318,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre zap.Int("status_code", resp.StatusCode), zap.String("host", hostAddr), ) - upstream.setHealthy(false) + markUnhealthy() return nil } } else if resp.StatusCode < 200 || resp.StatusCode >= 400 { @@ -319,7 +326,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre zap.Int("status_code", resp.StatusCode), zap.String("host", hostAddr), ) - upstream.setHealthy(false) + markUnhealthy() return nil } @@ -331,14 +338,14 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre zap.String("host", hostAddr), zap.Error(err), ) - upstream.setHealthy(false) + markUnhealthy() return nil } if !h.HealthChecks.Active.bodyRegexp.Match(bodyBytes) { h.HealthChecks.Active.logger.Info("response body failed expectations", zap.String("host", hostAddr), ) - upstream.setHealthy(false) + markUnhealthy() return nil } } @@ -346,6 +353,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre // 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 diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 94a09380..e9c7ddd0 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -30,6 +30,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" + "go.uber.org/zap" "golang.org/x/net/http2" ) @@ -88,6 +89,12 @@ type HTTPTransport struct { // 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 @@ -121,33 +128,11 @@ func (h *HTTPTransport) Provision(ctx caddy.Context) error { } h.Transport = rt - // 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 - DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) { - // TODO: no context, thus potentially wrong dial info - return net.Dial(network, addr) - }, - AllowHTTP: true, - } - if h.Compression != nil { - h2t.DisableCompression = !*h.Compression - } - h.h2cTransport = h2t - } - return nil } // NewTransport builds a standard-lib-compatible http.Transport value from h. -func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error) { +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{ @@ -187,22 +172,38 @@ func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error) } } + // 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(h), + } + } + + return conn, nil + } + rt := &http.Transport{ - 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} - } - return conn, nil - }, + DialContext: dialContext, MaxConnsPerHost: h.MaxConnsPerHost, ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout), ExpectContinueTimeout: time.Duration(h.ExpectContinueTimeout), @@ -214,7 +215,7 @@ func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error) if h.TLS != nil { rt.TLSHandshakeTimeout = time.Duration(h.TLS.HandshakeTimeout) var err error - rt.TLSClientConfig, err = h.TLS.MakeTLSClientConfig(ctx) + rt.TLSClientConfig, err = h.TLS.MakeTLSClientConfig(caddyCtx) if err != nil { return nil, fmt.Errorf("making TLS client config: %v", err) } @@ -240,6 +241,27 @@ func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error) } } + // 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 } @@ -281,7 +303,7 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) transport := h.replaceTLSServername(repl) - transport.setScheme(req) + 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 @@ -292,10 +314,13 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { return transport.Transport.RoundTrip(req) } -// setScheme ensures that the outbound request 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. -func (h *HTTPTransport) setScheme(req *http.Request) { +// +// 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 } @@ -507,6 +532,36 @@ type KeepAlive struct { 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 diff --git a/modules/caddyhttp/reverseproxy/metrics.go b/modules/caddyhttp/reverseproxy/metrics.go new file mode 100644 index 00000000..4272bc4e --- /dev/null +++ b/modules/caddyhttp/reverseproxy/metrics.go @@ -0,0 +1,81 @@ +package 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) + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index ce30a9df..907c08b9 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -23,9 +23,12 @@ import ( "io" "net" "net/http" + "net/http/httptrace" + "net/netip" "net/textproto" "net/url" "regexp" + "runtime" "strconv" "strings" "sync" @@ -33,6 +36,7 @@ import ( "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" @@ -40,7 +44,13 @@ import ( "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{}) } @@ -103,6 +113,11 @@ type Handler struct { // 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 @@ -169,7 +184,7 @@ type Handler struct { DynamicUpstreams UpstreamSource `json:"-"` // Holds the parsed CIDR ranges from TrustedProxies - trustedProxies []*net.IPNet + trustedProxies []netip.Prefix // Holds the named response matchers from the Caddyfile while adapting responseMatchers map[string]caddyhttp.ResponseMatcher @@ -177,8 +192,13 @@ type Handler struct { // 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. @@ -191,8 +211,15 @@ func (Handler) CaddyModule() caddy.ModuleInfo { // 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) + 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 { @@ -240,24 +267,18 @@ func (h *Handler) Provision(ctx caddy.Context) error { // parse trusted proxy CIDRs ahead of time for _, str := range h.TrustedProxies { if strings.Contains(str, "/") { - _, ipNet, err := net.ParseCIDR(str) + ipNet, err := netip.ParsePrefix(str) if err != nil { - return fmt.Errorf("parsing CIDR expression: %v", err) + return fmt.Errorf("parsing CIDR expression: '%s': %v", str, err) } h.trustedProxies = append(h.trustedProxies, ipNet) } else { - ip := net.ParseIP(str) - if ip == nil { - return fmt.Errorf("invalid IP address: %s", str) + ipAddr, err := netip.ParseAddr(str) + if err != nil { + return fmt.Errorf("invalid IP address: '%s': %v", str, err) } - if ipv4 := ip.To4(); ipv4 != nil { - ip = ipv4 - } - mask := len(ip) * 8 - h.trustedProxies = append(h.trustedProxies, &net.IPNet{ - IP: ip, - Mask: net.CIDRMask(mask, mask), - }) + ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen()) + h.trustedProxies = append(h.trustedProxies, ipNew) } } @@ -386,19 +407,40 @@ func (h *Handler) Provision(ctx caddy.Context) error { } } + upstreamHealthyUpdater := newMetricsUpstreamsHealthyUpdater(h) + upstreamHealthyUpdater.Init() + return nil } -// Cleanup cleans up the resources made by h during provisioning. +// Cleanup cleans up the resources made by h. func (h *Handler) Cleanup() error { - // TODO: Close keepalive connections on reload? https://github.com/caddyserver/caddy/pull/2507/files#diff-70219fd88fe3f36834f474ce6537ed26R762 + // 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 nil + return err } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { @@ -430,12 +472,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht // 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, repl, reqHeader, reqHost, next) + done, proxyErr = h.proxyLoopIteration(clonedReq, r, w, proxyErr, start, retries, repl, reqHeader, reqHost, next) if done { break } + retries++ } if proxyErr != nil { @@ -449,7 +493,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht // 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, +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 @@ -477,9 +521,9 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h upstream := h.LoadBalancing.SelectionPolicy.Select(upstreams, r, w) if upstream == nil { if proxyErr == nil { - proxyErr = fmt.Errorf("no upstreams available") + proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, fmt.Errorf("no upstreams available")) } - if !h.LoadBalancing.tryAgain(h.ctx, start, proxyErr, r) { + if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) { return true, proxyErr } return false, proxyErr @@ -542,7 +586,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h h.countFailure(upstream) // if we've tried long enough, break - if !h.LoadBalancing.tryAgain(h.ctx, start, proxyErr, r) { + if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) { return true, proxyErr } @@ -656,18 +700,18 @@ func (h Handler) addForwardedHeaders(req *http.Request) error { // Client IP may contain a zone if IPv6, so we need // to pull that out before parsing the IP - if idx := strings.IndexByte(clientIP, '%'); idx >= 0 { - clientIP = clientIP[:idx] + if before, _, found := strings.Cut(clientIP, "%"); found { + clientIP = before } - ip := net.ParseIP(clientIP) - if ip == nil { - return fmt.Errorf("invalid client IP address: %s", clientIP) + 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(ip) { + if ipRange.Contains(ipAddr) { trusted = true break } @@ -730,6 +774,34 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe 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, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses + for k := range h { + 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() @@ -944,16 +1016,26 @@ func (h Handler) finalizeResponse( return nil } -// tryAgain takes the time that the handler was initially invoked -// 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, proxyErr error, req *http.Request) bool { +// 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 time.Since(start) >= time.Duration(lb.TryDuration) { + 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 } @@ -976,6 +1058,11 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, proxyErr er } } + // 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 { @@ -1190,16 +1277,25 @@ type LoadBalancing struct { // 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. By default, this retry is - // disabled. Clients will wait for up to this long while the load - // balancer tries to find an available upstream host. + // 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. 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. + // 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 @@ -1291,7 +1387,7 @@ func (brc bodyReadCloser) Close() error { // bufPool is used for buffering requests and responses. var bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { return new(bytes.Buffer) }, } @@ -1309,7 +1405,7 @@ func Response(req *http.Request) (*http.Response, error) { } // handleResponseContext carries some contextual information about the -// the current proxy handling. +// current proxy handling. type handleResponseContext struct { // handler is the active proxy handler instance, so that // routes like copy_response may inherit some config @@ -1335,6 +1431,19 @@ type handleResponseContext struct { 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. diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index 125a07f9..5fc7136d 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -132,7 +132,7 @@ func (r RandomChoiceSelection) Select(pool UpstreamPool, _ *http.Request, _ http if !upstream.Available() { continue } - j := weakrand.Intn(i + 1) + j := weakrand.Intn(i + 1) //nolint:gosec if j < k { choices[j] = upstream } @@ -181,7 +181,7 @@ func (LeastConnSelection) Select(pool UpstreamPool, _ *http.Request, _ http.Resp // sample: https://en.wikipedia.org/wiki/Reservoir_sampling if numReqs == leastReqs { count++ - if (weakrand.Int() % count) == 0 { + if (weakrand.Int() % count) == 0 { //nolint:gosec bestHost = host } } @@ -475,7 +475,7 @@ func selectRandomHost(pool []*Upstream) *Upstream { // upstream will always be chosen if there is at // least one available count++ - if (weakrand.Int() % count) == 0 { + if (weakrand.Int() % count) == 0 { //nolint:gosec randomHost = upstream } } @@ -511,7 +511,7 @@ func leastRequests(upstreams []*Upstream) *Upstream { if len(best) == 0 { return nil } - return best[weakrand.Intn(len(best))] + return best[weakrand.Intn(len(best))] //nolint:gosec } // hostByHashing returns an available host from pool based on a hashable string s. diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index 6bd1af23..01d865db 100644 --- a/modules/caddyhttp/reverseproxy/streaming.go +++ b/modules/caddyhttp/reverseproxy/streaming.go @@ -20,6 +20,7 @@ package reverseproxy import ( "context" + "encoding/binary" "io" "mime" "net/http" @@ -27,6 +28,7 @@ import ( "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) { @@ -97,8 +99,26 @@ func (h Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrite return } - errc := make(chan error, 1) + // 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 @@ -209,6 +229,60 @@ func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, er } } +// 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 @@ -265,7 +339,7 @@ func (m *maxLatencyWriter) stop() { // switchProtocolCopier exists so goroutines proxying data back and // forth have nice names in stacks. type switchProtocolCopier struct { - user, backend io.ReadWriter + user, backend io.ReadWriteCloser } func (c switchProtocolCopier) copyFromBackend(errc chan<- error) { @@ -279,7 +353,7 @@ func (c switchProtocolCopier) copyToBackend(errc chan<- error) { } var streamingBufPool = sync.Pool{ - New: func() interface{} { + 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 diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go index f788dadb..b9f85a2c 100644 --- a/modules/caddyhttp/reverseproxy/upstreams.go +++ b/modules/caddyhttp/reverseproxy/upstreams.go @@ -2,6 +2,7 @@ package reverseproxy import ( "context" + "encoding/json" "fmt" weakrand "math/rand" "net" @@ -18,6 +19,7 @@ import ( func init() { caddy.RegisterModule(SRVUpstreams{}) caddy.RegisterModule(AUpstreams{}) + caddy.RegisterModule(MultiUpstreams{}) } // SRVUpstreams provides upstreams from SRV lookups. @@ -135,7 +137,7 @@ func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { _, 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 the remaining results, if any." Thus, we + // 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 @@ -211,11 +213,6 @@ func (sl srvLookup) isFresh() bool { return time.Since(sl.freshness) < time.Duration(sl.srvUpstreams.Refresh) } -var ( - srvs = make(map[string]srvLookup) - srvsMu sync.RWMutex -) - // AUpstreams provides upstreams from A/AAAA lookups. // Results are cached and refreshed at the configured // refresh interval. @@ -355,6 +352,77 @@ 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(mu) + + 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 { @@ -391,6 +459,9 @@ func (u *UpstreamResolver) ParseAddresses() error { } var ( + srvs = make(map[string]srvLookup) + srvsMu sync.RWMutex + aAaaa = make(map[string]aLookup) aAaaaMu sync.RWMutex ) diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index da922693..4316b5af 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -31,13 +31,25 @@ func init() { caddy.RegisterModule(Rewrite{}) } -// Rewrite is a middleware which can rewrite HTTP requests. +// Rewrite is a middleware which can rewrite/mutate HTTP requests. // -// The Method and URI properties are "setters": the request URI -// will be set to the given values. Other properties are "modifiers": -// they modify existing files but do not explicitly specify what the -// result will be. It is atypical to combine the use of setters and +// 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"` @@ -59,9 +71,15 @@ type Rewrite struct { 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. @@ -194,10 +212,10 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool { // before continuing, we need to check if a query string // snuck into the path component during replacements - if quPos := strings.Index(newPath, "?"); quPos > -1 { + if before, after, found := strings.Cut(newPath, "?"); found { // recompute; new path contains a query string var injectedQuery string - newPath, injectedQuery = newPath[:quPos], newPath[quPos+1:] + newPath, injectedQuery = before, after // don't overwrite explicitly-configured query string if query == "" { query = injectedQuery @@ -227,17 +245,18 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool { // strip path prefix or suffix if rewr.StripPathPrefix != "" { prefix := repl.ReplaceAll(rewr.StripPathPrefix, "") - r.URL.RawPath = strings.TrimPrefix(r.URL.RawPath, prefix) - if p, err := url.PathUnescape(r.URL.RawPath); err == nil && p != "" { - r.URL.Path = p - } else { - r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix) - } + 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, "") - changePath(r, func(pathOrRawPath string) string { - return strings.TrimSuffix(pathOrRawPath, suffix) + mergeSlashes := !strings.Contains(suffix, "//") + changePath(r, func(escapedPath string) string { + escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes) + return reverse(trimPathPrefix(reverse(escapedPath), reverse(suffix))) }) } @@ -283,7 +302,7 @@ func buildQueryString(qs string, repl *caddy.Replacer) string { // consume the component and write the result comp := qs[:end] - comp, _ = repl.ReplaceFunc(comp, func(name string, val interface{}) (interface{}, error) { + comp, _ = repl.ReplaceFunc(comp, func(name string, val any) (any, error) { if name == "http.request.uri.query" && wroteVal { return val, nil // already escaped } @@ -324,6 +343,58 @@ func buildQueryString(qs string, repl *caddy.Replacer) string { 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++ + } + + // found matching prefix, trim it + return escapedPath[iPath:] +} + +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. @@ -351,8 +422,10 @@ func (rep substrReplacer) do(r *http.Request, repl *caddy.Replacer) { find := repl.ReplaceAll(rep.Find, "") replace := repl.ReplaceAll(rep.Replace, "") + mergeSlashes := !strings.Contains(rep.Find, "//") + changePath(r, func(pathOrRawPath string) string { - return strings.Replace(pathOrRawPath, find, replace, lim) + return strings.Replace(caddyhttp.CleanPath(pathOrRawPath, mergeSlashes), find, replace, lim) }) r.URL.RawQuery = strings.Replace(r.URL.RawQuery, find, replace, lim) @@ -380,16 +453,17 @@ func (rep regexReplacer) do(r *http.Request, repl *caddy.Replacer) { }) } -// changePath updates the path on the request URL. It first executes newVal on -// req.URL.RawPath, and if the result is a valid escaping, it will be copied -// into req.URL.Path; otherwise newVal is evaluated only on req.URL.Path. func changePath(req *http.Request, newVal func(pathOrRawPath string) string) { - req.URL.RawPath = newVal(req.URL.RawPath) + 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 diff --git a/modules/caddyhttp/rewrite/rewrite_test.go b/modules/caddyhttp/rewrite/rewrite_test.go index 84dce95e..bc20c853 100644 --- a/modules/caddyhttp/rewrite/rewrite_test.go +++ b/modules/caddyhttp/rewrite/rewrite_test.go @@ -235,6 +235,42 @@ func TestRewrite(t *testing.T) { 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"}, @@ -251,6 +287,11 @@ func TestRewrite(t *testing.T) { 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"), diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index 7b2871ff..ce9bece2 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -109,6 +109,17 @@ func (r Route) Empty() bool { 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 @@ -151,7 +162,7 @@ func (routes RouteList) ProvisionHandlers(ctx caddy.Context) error { if err != nil { return fmt.Errorf("route %d: loading handler modules: %v", i, err) } - for _, handler := range handlersIface.([]interface{}) { + for _, handler := range handlersIface.([]any) { routes[i].Handlers = append(routes[i].Handlers, handler.(MiddlewareHandler)) } @@ -204,6 +215,10 @@ func wrapRoute(route Route) Middleware { // 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 } @@ -311,9 +326,9 @@ func (ms MatcherSets) AnyMatch(req *http.Request) bool { return len(ms) == 0 } -// FromInterface fills ms from an interface{} value obtained from LoadModule. -func (ms *MatcherSets) FromInterface(matcherSets interface{}) error { - for _, matcherSetIfaces := range matcherSets.([]map[string]interface{}) { +// 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) @@ -327,4 +342,15 @@ func (ms *MatcherSets) FromInterface(matcherSets interface{}) error { 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") diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index a4a976f7..f1909c41 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -16,6 +16,7 @@ package caddyhttp import ( "context" + "crypto/tls" "encoding/json" "errors" "fmt" @@ -24,11 +25,15 @@ import ( "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" @@ -36,6 +41,8 @@ import ( // 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 @@ -65,6 +72,11 @@ type Server struct { // 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"` @@ -111,41 +123,68 @@ type Server struct { // to a non-null, empty struct. Logs *ServerLogConfig `json:"logs,omitempty"` - // Enable experimental HTTP/3 support. Note that HTTP/3 is not a - // finished standard and has extremely limited client support. - // This field is not subject to compatibility promises. - ExperimentalHTTP3 bool `json:"experimental_http3,omitempty"` - - // Enables H2C ("Cleartext HTTP/2" or "H2 over TCP") support, - // which will serve HTTP/2 over plaintext TCP connections if - // the client supports it. Because this is not implemented by the - // Go standard library, using H2C is incompatible with most - // of the other options for this server. Do not enable this + // 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. - // This setting applies only to unencrypted HTTP listeners. - // ⚠️ Experimental feature; subject to change or removal. - AllowH2C bool `json:"allow_h2c,omitempty"` + // 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"` 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 - h3server *http3.Server + server *http.Server + h3server *http3.Server + addresses []caddy.NetworkAddress + + shutdownAt time.Time + shutdownAtMu *sync.RWMutex } // 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) + err := s.h3server.SetQuicHeaders(w.Header()) if err != nil { s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err)) @@ -438,6 +477,34 @@ func (s *Server) findLastRouteWithHostMatcher() int { 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(hostport string, tlsCfg *tls.Config) error { + h3ln, err := caddy.ListenQUIC(hostport, 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}, + }, + } + } + + //nolint:errcheck + go s.h3server.ServeListener(h3ln) + + return nil +} + // HTTPErrorConfig determines how to handle errors // from the HTTP handlers. type HTTPErrorConfig struct { @@ -472,8 +539,13 @@ func (*HTTPErrorConfig) WithError(r *http.Request, err error) *http.Request { 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.trace", handlerErr.Trace) 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 @@ -502,6 +574,24 @@ func (s *Server) shouldLogRequest(r *http.Request) bool { return true } +// 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 } + // 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 @@ -578,13 +668,27 @@ func (slc ServerLogConfig) getLoggerName(host string) string { 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 +} + // 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]interface{})) + 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)) diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index c587f5ee..ccc70e2d 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -15,28 +15,97 @@ 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" ) 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. + // 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. + // 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 @@ -59,10 +128,10 @@ func (StaticResponse) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // -// respond [] | [] { -// body -// close -// } +// 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. @@ -107,7 +176,7 @@ func (s *StaticResponse) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } -func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error { +func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { // close the connection immediately if s.Abort { panic(http.ErrAbortHandler) @@ -131,7 +200,23 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Hand w.Header()[field] = newVals } - // do not allow Go to sniff the content-type + // 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 } @@ -158,13 +243,196 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Hand w.WriteHeader(statusCode) // write response body - if s.Body != "" { - fmt.Fprint(w, repl.ReplaceKnown(s.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: "DEBUG"}, + }, + } + } + + // 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) diff --git a/modules/caddyhttp/templates/frontmatter.go b/modules/caddyhttp/templates/frontmatter.go index 9031e138..3f7bd0cc 100644 --- a/modules/caddyhttp/templates/frontmatter.go +++ b/modules/caddyhttp/templates/frontmatter.go @@ -10,7 +10,7 @@ import ( "gopkg.in/yaml.v3" ) -func extractFrontMatter(input string) (map[string]interface{}, string, error) { +func extractFrontMatter(input string) (map[string]any, string, error) { // get the bounds of the first non-empty line var firstLineStart, firstLineEnd int lineEmpty := true @@ -35,7 +35,7 @@ func extractFrontMatter(input string) (map[string]interface{}, string, error) { // see what kind of front matter there is, if any var closingFence []string - var fmParser func([]byte) (map[string]interface{}, error) + var fmParser func([]byte) (map[string]any, error) for _, fmType := range supportedFrontMatterTypes { if firstLine == fmType.FenceOpen { closingFence = fmType.FenceClose @@ -77,35 +77,35 @@ func extractFrontMatter(input string) (map[string]interface{}, string, error) { return fm, body, nil } -func yamlFrontMatter(input []byte) (map[string]interface{}, error) { - m := make(map[string]interface{}) +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]interface{}, error) { - m := make(map[string]interface{}) +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]interface{}, error) { +func jsonFrontMatter(input []byte) (map[string]any, error) { input = append([]byte{'{'}, input...) input = append(input, '}') - m := make(map[string]interface{}) + m := make(map[string]any) err := json.Unmarshal(input, &m) return m, err } type parsedMarkdownDoc struct { - Meta map[string]interface{} `json:"meta,omitempty"` - Body string `json:"body,omitempty"` + 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]interface{}, error) + ParseFunc func(input []byte) (map[string]any, error) } var supportedFrontMatterTypes = []frontMatterType{ diff --git a/modules/caddyhttp/templates/frontmatter_fuzz.go b/modules/caddyhttp/templates/frontmatter_fuzz.go index 361b4b62..7af30131 100644 --- a/modules/caddyhttp/templates/frontmatter_fuzz.go +++ b/modules/caddyhttp/templates/frontmatter_fuzz.go @@ -13,7 +13,6 @@ // limitations under the License. //go:build gofuzz -// +build gofuzz package templates diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 85612ba0..29d16507 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -189,6 +189,14 @@ func init() { // {{.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: @@ -296,7 +304,7 @@ func (Templates) CaddyModule() caddy.ModuleInfo { // Provision provisions t. func (t *Templates) Provision(ctx caddy.Context) error { fnModInfos := caddy.GetModules("http.handlers.templates.functions") - customFuncs := make([]template.FuncMap, len(fnModInfos), 0) + customFuncs := make([]template.FuncMap, 0, len(fnModInfos)) for _, modInfo := range fnModInfos { mod := modInfo.New() fnMod, ok := mod.(CustomFunctions) diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index 89ebab44..ca7d0055 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -45,7 +45,7 @@ import ( type TemplateContext struct { Root http.FileSystem Req *http.Request - Args []interface{} // defined by arguments to funcInclude + Args []any // defined by arguments to funcInclude RespHeader WrappedHeader CustomFuncs []template.FuncMap // functions added by plugins @@ -100,7 +100,7 @@ func (c TemplateContext) OriginalReq() http.Request { // 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 ...interface{}) (string, error) { +func (c TemplateContext) funcInclude(filename string, args ...any) (string, error) { bodyBuf := bufPool.Get().(*bytes.Buffer) bodyBuf.Reset() @@ -305,8 +305,8 @@ func (TemplateContext) funcStripHTML(s string) 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 interface{}) (string, error) { - inputStr := toString(input) +func (TemplateContext) funcMarkdown(input any) (string, error) { + inputStr := caddy.ToString(input) md := goldmark.New( goldmark.WithExtensions( @@ -341,8 +341,8 @@ func (TemplateContext) funcMarkdown(input interface{}) (string, error) { // 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 interface{}) (parsedMarkdownDoc, error) { - meta, body, err := extractFrontMatter(toString(input)) +func (TemplateContext) funcSplitFrontMatter(input any) (parsedMarkdownDoc, error) { + meta, body, err := extractFrontMatter(caddy.ToString(input)) if err != nil { return parsedMarkdownDoc{}, err } @@ -491,21 +491,8 @@ func (h WrappedHeader) Del(field string) string { return "" } -func toString(input interface{}) string { - switch v := input.(type) { - case string: - return v - case fmt.Stringer: - return v.String() - case error: - return v.Error() - default: - return fmt.Sprintf("%v", input) - } -} - var bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { return new(bytes.Buffer) }, } diff --git a/modules/caddyhttp/tracing/tracer.go b/modules/caddyhttp/tracing/tracer.go index ce23944c..ddb01e82 100644 --- a/modules/caddyhttp/tracing/tracer.go +++ b/modules/caddyhttp/tracing/tracer.go @@ -7,7 +7,6 @@ import ( "github.com/caddyserver/caddy/v2" - caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" @@ -52,7 +51,8 @@ func newOpenTelemetryWrapper( spanName: spanName, } - res, err := ot.newResource(webEngineName, caddycmd.CaddyVersion()) + version, _ := caddy.Version() + res, err := ot.newResource(webEngineName, version) if err != nil { return ot, fmt.Errorf("creating resource error: %w", err) } diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index 28d0ddfc..b4e1d897 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "net/http" + "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -37,7 +38,7 @@ func init() { // // 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]interface{} +type VarsMiddleware map[string]any // CaddyModule returns the Caddy module information. func (VarsMiddleware) CaddyModule() caddy.ModuleInfo { @@ -48,7 +49,7 @@ func (VarsMiddleware) CaddyModule() caddy.ModuleInfo { } func (m VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { - vars := r.Context().Value(VarsCtxKey).(map[string]interface{}) + 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, "") @@ -62,11 +63,10 @@ func (m VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next H // UnmarshalCaddyfile implements caddyfile.Unmarshaler. Syntax: // -// vars [ ] { -// -// ... -// } -// +// vars [ ] { +// +// ... +// } func (m *VarsMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(VarsMiddleware) @@ -109,14 +109,17 @@ func (m *VarsMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } // VarsMatcher is an HTTP request matcher which can match -// requests based on variables in the context. The key is -// the name of the variable, and the values are possible -// values the variable can be in order to match (OR'ed). +// 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). // -// As a special case, this matcher can also match on -// placeholders generally. If the key is not an HTTP chain -// variable, it will be checked to see if it is a -// placeholder name, and if so, will compare its value. +// 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. @@ -156,17 +159,17 @@ func (m VarsMatcher) Match(r *http.Request) bool { return true } - vars := r.Context().Value(VarsCtxKey).(map[string]interface{}) + vars := r.Context().Value(VarsCtxKey).(map[string]any) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) for key, vals := range m { - // look up the comparison value we will check against with this key - matcherVarNameExpanded := repl.ReplaceAll(key, "") - varValue, ok := vars[matcherVarNameExpanded] - if !ok { - // as a special case, if it's not an HTTP variable, - // see if it's a placeholder name - varValue, _ = repl.Get(matcherVarNameExpanded) + 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 @@ -250,7 +253,7 @@ func (m MatchVarsRE) Provision(ctx caddy.Context) error { // Match returns true if r matches m. func (m MatchVarsRE) Match(r *http.Request) bool { - vars := r.Context().Value(VarsCtxKey).(map[string]interface{}) + vars := r.Context().Value(VarsCtxKey).(map[string]any) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) for k, rm := range m { var varStr string @@ -290,8 +293,8 @@ func (m MatchVarsRE) Validate() error { // 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) interface{} { - varMap, ok := ctx.Value(VarsCtxKey).(map[string]interface{}) +func GetVar(ctx context.Context, key string) any { + varMap, ok := ctx.Value(VarsCtxKey).(map[string]any) if !ok { return nil } @@ -301,11 +304,21 @@ func GetVar(ctx context.Context, key string) interface{} { // SetVar sets a value in the context's variable table with // the given key. It overwrites any previous value with the // same key. -func SetVar(ctx context.Context, key string, value interface{}) { - varMap, ok := ctx.Value(VarsCtxKey).(map[string]interface{}) +// +// 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 } diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go index ca9ba469..41e78a6b 100644 --- a/modules/caddypki/ca.go +++ b/modules/caddypki/ca.go @@ -73,7 +73,7 @@ type CA struct { storage certmagic.Storage root, inter *x509.Certificate - interKey interface{} // TODO: should we just store these as crypto.Signer? + interKey any // TODO: should we just store these as crypto.Signer? mu *sync.RWMutex rootCertPath string // mainly used for logging purposes if trusting @@ -121,7 +121,7 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { // load the certs and key that will be used for signing var rootCert, interCert *x509.Certificate - var rootKey, interKey interface{} + var rootKey, interKey any var err error if ca.Root != nil { if ca.Root.Format == "" || ca.Root.Format == "pem_file" { @@ -161,7 +161,7 @@ func (ca CA) RootCertificate() *x509.Certificate { // 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() (interface{}, error) { +func (ca CA) RootKey() (any, error) { _, rootKey, err := ca.loadOrGenRoot() return rootKey, err } @@ -175,7 +175,7 @@ func (ca CA) IntermediateCertificate() *x509.Certificate { } // IntermediateKey returns the CA's intermediate private key. -func (ca CA) IntermediateKey() interface{} { +func (ca CA) IntermediateKey() any { ca.mu.RLock() defer ca.mu.RUnlock() return ca.interKey @@ -196,7 +196,7 @@ func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authorit // cert/key directly, since it's unlikely to expire // while Caddy is running (long lifetime) var issuerCert *x509.Certificate - var issuerKey interface{} + var issuerKey any issuerCert = rootCert var err error issuerKey, err = ca.RootKey() @@ -239,7 +239,7 @@ func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authorit return auth, nil } -func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) { +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) { @@ -273,7 +273,7 @@ func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey interface{}, e return rootCert, rootKey, nil } -func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) { +func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey any, err error) { repl := ca.newReplacer() rootCert, rootKey, err = generateRoot(repl.ReplaceAll(ca.RootCommonName, "")) diff --git a/modules/caddypki/certificates.go b/modules/caddypki/certificates.go index bd260da6..442a0ad6 100644 --- a/modules/caddypki/certificates.go +++ b/modules/caddypki/certificates.go @@ -22,7 +22,7 @@ import ( "github.com/smallstep/cli/crypto/x509util" ) -func generateRoot(commonName string) (rootCrt *x509.Certificate, privateKey interface{}, err error) { +func generateRoot(commonName string) (rootCrt *x509.Certificate, privateKey any, err error) { rootProfile, err := x509util.NewRootProfile(commonName) if err != nil { return diff --git a/modules/caddypki/command.go b/modules/caddypki/command.go index c26c19ad..cb86c937 100644 --- a/modules/caddypki/command.go +++ b/modules/caddypki/command.go @@ -113,7 +113,7 @@ func cmdTrust(fl caddycmd.Flags) (int, error) { } // Determine where we're sending the request to get the CA info - adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) + adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } @@ -182,7 +182,7 @@ func cmdUntrust(fl caddycmd.Flags) (int, error) { } // Determine where we're sending the request to get the CA info - adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) + adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 9552d6f6..2fe4004f 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -17,6 +17,7 @@ package caddytls import ( "context" "crypto/x509" + "errors" "fmt" "net/url" "os" @@ -250,28 +251,27 @@ 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 -// } -// } -// +// ... 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() { @@ -494,8 +494,7 @@ func onDemandAskRequest(ask string, name string) error { resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { - return fmt.Errorf("certificate for hostname '%s' not allowed; non-2xx status code %d returned from %v", - name, resp.StatusCode, ask) + return fmt.Errorf("%s: %w %s - non-2xx status code %d", name, errAskDenied, ask, resp.StatusCode) } return nil @@ -568,6 +567,11 @@ type ChainPreference struct { 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) diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 197c4095..e80d3558 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -16,6 +16,7 @@ package caddytls import ( "encoding/json" + "errors" "fmt" "net/http" "time" @@ -23,6 +24,7 @@ import ( "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. @@ -174,6 +176,13 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { 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 } } @@ -198,7 +207,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { if err != nil { return fmt.Errorf("loading external certificate manager modules: %v", err) } - for _, getCertVal := range vals.([]interface{}) { + for _, getCertVal := range vals.([]any) { ap.Managers = append(ap.Managers, getCertVal.(certmagic.Manager)) } } @@ -209,7 +218,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { if err != nil { return fmt.Errorf("loading TLS automation management module: %s", err) } - for _, issVal := range val.([]interface{}) { + for _, issVal := range val.([]any) { ap.Issuers = append(ap.Issuers, issVal.(certmagic.Issuer)) } } @@ -247,6 +256,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { MustStaple: ap.MustStaple, RenewalWindowRatio: ap.RenewalWindowRatio, KeySource: keySource, + OnEvent: tlsApp.onEvent, OnDemand: ond, OCSP: certmagic.OCSPConfig{ DisableStapling: ap.DisableOCSPStapling, diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index d6304a99..f7b9c462 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -46,7 +46,7 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading handshake matchers: %v", err) } - for _, modIface := range mods.(map[string]interface{}) { + for _, modIface := range mods.(map[string]any) { cp[i].matchers = append(cp[i].matchers, modIface.(ConnectionMatcher)) } @@ -66,7 +66,7 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading client cert verifiers: %v", err) } - for _, validator := range clientCertValidations.([]interface{}) { + for _, validator := range clientCertValidations.([]any) { cp[i].ClientAuthentication.verifiers = append(cp[i].ClientAuthentication.verifiers, validator.(ClientCertificateVerifier)) } } @@ -112,7 +112,7 @@ func (cp ConnectionPolicies) TLSConfig(_ caddy.Context) *tls.Config { continue policyLoop } } - return pol.stdTLSConfig, nil + return pol.TLSConfig, nil } return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) @@ -156,8 +156,15 @@ type ConnectionPolicy struct { // is no policy configured for the empty SNI value. DefaultSNI string `json:"default_sni,omitempty"` - matchers []ConnectionMatcher - stdTLSConfig *tls.Config + // 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 { @@ -172,8 +179,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { // 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, - PreferServerCipherSuites: true, + 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 @@ -276,7 +282,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { setDefaultTLSParams(cfg) - p.stdTLSConfig = cfg + p.TLSConfig = cfg return nil } @@ -475,8 +481,6 @@ func setDefaultTLSParams(cfg *tls.Config) { if cfg.MaxVersion == 0 { cfg.MaxVersion = tls.VersionTLS13 } - - cfg.PreferServerCipherSuites = true } // LeafCertClientAuth verifies the client's leaf certificate. diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index d41492ff..4a22bc09 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -18,6 +18,7 @@ import ( "crypto/tls" "fmt" "net" + "net/netip" "strings" "github.com/caddyserver/caddy/v2" @@ -65,8 +66,8 @@ type MatchRemoteIP struct { // The IPs or CIDR ranges to *NOT* match. NotRanges []string `json:"not_ranges,omitempty"` - cidrs []*net.IPNet - notCidrs []*net.IPNet + cidrs []netip.Prefix + notCidrs []netip.Prefix logger *zap.Logger } @@ -105,38 +106,35 @@ func (m MatchRemoteIP) Match(hello *tls.ClientHelloInfo) bool { if err != nil { ipStr = remoteAddr // weird; maybe no port? } - ip := net.ParseIP(ipStr) - if ip == nil { + 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(ip, m.cidrs)) && - (len(m.notCidrs) == 0 || !m.matches(ip, m.notCidrs)) + return (len(m.cidrs) == 0 || m.matches(ipAddr, m.cidrs)) && + (len(m.notCidrs) == 0 || !m.matches(ipAddr, m.notCidrs)) } -func (MatchRemoteIP) parseIPRange(str string) ([]*net.IPNet, error) { - var cidrs []*net.IPNet +func (MatchRemoteIP) parseIPRange(str string) ([]netip.Prefix, error) { + var cidrs []netip.Prefix if strings.Contains(str, "/") { - _, ipNet, err := net.ParseCIDR(str) + ipNet, err := netip.ParsePrefix(str) if err != nil { return nil, fmt.Errorf("parsing CIDR expression: %v", err) } cidrs = append(cidrs, ipNet) } else { - ip := net.ParseIP(str) - if ip == nil { - return nil, fmt.Errorf("invalid IP address: %s", str) + ipAddr, err := netip.ParseAddr(str) + if err != nil { + return nil, fmt.Errorf("invalid IP address: '%s': %v", str, err) } - mask := len(ip) * 8 - cidrs = append(cidrs, &net.IPNet{ - IP: ip, - Mask: net.CIDRMask(mask, mask), - }) + ip := netip.PrefixFrom(ipAddr, ipAddr.BitLen()) + cidrs = append(cidrs, ip) } return cidrs, nil } -func (MatchRemoteIP) matches(ip net.IP, ranges []*net.IPNet) bool { +func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool { for _, ipRange := range ranges { if ipRange.Contains(ip) { return true diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 429b24c9..2e532ed9 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -15,6 +15,7 @@ package caddytls import ( + "context" "crypto/tls" "encoding/json" "fmt" @@ -25,6 +26,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) @@ -73,6 +75,7 @@ type TLS struct { storageCleanTicker *time.Ticker storageCleanStop chan struct{} logger *zap.Logger + events *caddyevents.App } // CaddyModule returns the Caddy module information. @@ -85,6 +88,11 @@ func (TLS) CaddyModule() caddy.ModuleInfo { // 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(t) repl := caddy.NewReplacer() @@ -113,7 +121,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading certificate loader modules: %s", err) } - for modName, modIface := range val.(map[string]interface{}) { + 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 @@ -177,9 +185,12 @@ func (t *TLS) Provision(ctx caddy.Context) error { onDemandRateLimiter.SetWindow(0) } - // run replacer on ask URL (for environment variables) + // 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 = repl.ReplaceAll(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 @@ -189,6 +200,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { magic := certmagic.New(t.certCache, certmagic.Config{ Storage: ctx.Storage(), Logger: t.logger, + OnEvent: t.onEvent, OCSP: certmagic.OCSPConfig{ DisableStapling: t.DisableOCSPStapling, }, @@ -514,6 +526,12 @@ func (t *TLS) storageCleanInterval() time.Duration { 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 { diff --git a/modules/caddytls/zerosslissuer.go b/modules/caddytls/zerosslissuer.go index a051ed47..c7ff70fb 100644 --- a/modules/caddytls/zerosslissuer.go +++ b/modules/caddytls/zerosslissuer.go @@ -36,12 +36,20 @@ func init() { caddy.RegisterModule(new(ZeroSSLIssuer)) } -// ZeroSSLIssuer makes an ACME manager -// for managing certificates using ACME. +// 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 @@ -193,9 +201,9 @@ func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.Certificate // UnmarshalCaddyfile deserializes Caddyfile tokens into iss. // -// ... zerossl [] { -// ... -// } +// ... zerossl [] { +// ... +// } // // Any of the subdirectives for the ACME issuer can be used in the block. func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { diff --git a/modules/logging/filterencoder.go b/modules/logging/filterencoder.go index cdb552d8..6a768dd6 100644 --- a/modules/logging/filterencoder.go +++ b/modules/logging/filterencoder.go @@ -89,7 +89,7 @@ func (fe *FilterEncoder) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading log filter modules: %v", err) } - for fieldName, modIface := range vals.(map[string]interface{}) { + for fieldName, modIface := range vals.(map[string]any) { fe.Fields[fieldName] = modIface.(LogFieldFilter) } @@ -326,7 +326,7 @@ func (fe FilterEncoder) AddUintptr(key string, value uintptr) { } // AddReflected is part of the zapcore.ObjectEncoder interface. -func (fe FilterEncoder) AddReflected(key string, value interface{}) error { +func (fe FilterEncoder) AddReflected(key string, value any) error { if !fe.filtered(key, value) { return fe.wrapped.AddReflected(key, value) } @@ -367,7 +367,7 @@ func (fe FilterEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) ( // 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 interface{}) bool { +func (fe FilterEncoder) filtered(key string, value any) bool { filter, ok := fe.Fields[fe.keyPrefix+key] if !ok { return false diff --git a/modules/logging/filters.go b/modules/logging/filters.go index aa96e5ef..c2c039af 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -26,6 +26,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap/zapcore" ) @@ -456,7 +457,13 @@ func (m *CookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Filter filters the input field. func (m CookieFilter) Filter(in zapcore.Field) zapcore.Field { - originRequest := http.Request{Header: http.Header{"Cookie": []string{in.String}}} + 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)} @@ -486,7 +493,7 @@ OUTER: transformedRequest.AddCookie(c) } - in.String = transformedRequest.Header.Get("Cookie") + in.Interface = caddyhttp.LoggableStringArray(transformedRequest.Header["Cookie"]) return in } diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go index ecf1d877..2b087f28 100644 --- a/modules/logging/filters_test.go +++ b/modules/logging/filters_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap/zapcore" ) @@ -49,8 +50,14 @@ func TestCookieFilter(t *testing.T) { {hashAction, "hash", ""}, }} - out := f.Filter(zapcore.Field{String: "foo=a; foo=b; bar=c; bar=d; baz=e; hash=hashed"}) - if out.String != "foo=REDACTED; foo=REDACTED; baz=e; hash=1a06df82" { + 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) } } diff --git a/modules/logging/nopencoder.go b/modules/logging/nopencoder.go index fc3d70d9..62c1f787 100644 --- a/modules/logging/nopencoder.go +++ b/modules/logging/nopencoder.go @@ -95,7 +95,7 @@ func (nopEncoder) AddUint8(key string, value uint8) {} func (nopEncoder) AddUintptr(key string, value uintptr) {} // AddReflected is part of the zapcore.ObjectEncoder interface. -func (nopEncoder) AddReflected(key string, value interface{}) error { return nil } +func (nopEncoder) AddReflected(key string, value any) error { return nil } // OpenNamespace is part of the zapcore.ObjectEncoder interface. func (nopEncoder) OpenNamespace(key string) {} diff --git a/modules/metrics/metrics.go b/modules/metrics/metrics.go index e6d4345a..1ad392c1 100644 --- a/modules/metrics/metrics.go +++ b/modules/metrics/metrics.go @@ -56,7 +56,7 @@ type zapLogger struct { zl *zap.Logger } -func (l *zapLogger) Println(v ...interface{}) { +func (l *zapLogger) Println(v ...any) { l.zl.Sugar().Error(v...) } diff --git a/modules/metrics/metrics_test.go b/modules/metrics/metrics_test.go index d5bdf501..9eb02fbb 100644 --- a/modules/metrics/metrics_test.go +++ b/modules/metrics/metrics_test.go @@ -21,7 +21,7 @@ func TestMetricsUnmarshalCaddyfile(t *testing.T) { t.Errorf("unexpected error: %v", err) } - if m.DisableOpenMetrics != false { + if m.DisableOpenMetrics { t.Errorf("DisableOpenMetrics should've been false: %v", m.DisableOpenMetrics) } @@ -32,7 +32,7 @@ func TestMetricsUnmarshalCaddyfile(t *testing.T) { t.Errorf("unexpected error: %v", err) } - if m.DisableOpenMetrics != true { + if !m.DisableOpenMetrics { t.Errorf("DisableOpenMetrics should've been true: %v", m.DisableOpenMetrics) } diff --git a/modules/standard/imports.go b/modules/standard/imports.go index bc2d955d..a9d0b396 100644 --- a/modules/standard/imports.go +++ b/modules/standard/imports.go @@ -3,6 +3,8 @@ package 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" diff --git a/notify/notify_linux.go b/notify/notify_linux.go index 8ba49d2c..3457a5a6 100644 --- a/notify/notify_linux.go +++ b/notify/notify_linux.go @@ -12,10 +12,12 @@ // 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 ( - "io" + "fmt" "net" "os" "strings" @@ -24,9 +26,13 @@ import ( // The documentation about this IPC protocol is available here: // https://www.freedesktop.org/software/systemd/man/sd_notify.html -func sdNotify(path, payload string) error { +func sdNotify(payload string) error { + if socketPath == "" { + return nil + } + socketAddr := &net.UnixAddr{ - Name: path, + Name: socketPath, Net: "unixgram", } @@ -36,45 +42,40 @@ func sdNotify(path, payload string) error { } defer conn.Close() - if _, err := io.Copy(conn, strings.NewReader(payload)); err != nil { - return err - } - return nil + _, err = conn.Write([]byte(payload)) + return err } -// notifyReadiness notifies systemd that caddy has finished its +// Ready notifies systemd that caddy has finished its // initialization routines. -func notifyReadiness() error { - val, ok := os.LookupEnv("NOTIFY_SOCKET") - if !ok || val == "" { - return nil - } - if err := sdNotify(val, "READY=1"); err != nil { - return err - } - return nil +func Ready() error { + return sdNotify("READY=1") } -// notifyReloading notifies systemd that caddy is reloading its config. -func notifyReloading() error { - val, ok := os.LookupEnv("NOTIFY_SOCKET") - if !ok || val == "" { - return nil - } - if err := sdNotify(val, "RELOADING=1"); err != nil { - return err - } - return nil +// Reloading notifies systemd that caddy is reloading its config. +func Reloading() error { + return sdNotify("RELOADING=1") } -// notifyStopping notifies systemd that caddy is stopping. -func notifyStopping() error { - val, ok := os.LookupEnv("NOTIFY_SOCKET") - if !ok || val == "" { - return nil - } - if err := sdNotify(val, "STOPPING=1"); err != nil { - return err - } - return nil +// 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") diff --git a/notify/notify_other.go b/notify/notify_other.go index 6ffec89b..dbe9bdb9 100644 --- a/notify/notify_other.go +++ b/notify/notify_other.go @@ -12,19 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !linux -// +build !linux +//go:build !linux && !windows package notify -func notifyReadiness() error { - return nil -} - -func notifyReloading() error { - return nil -} - -func notifyStopping() error { - return nil -} +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 } diff --git a/notify/notify_windows.go b/notify/notify_windows.go new file mode 100644 index 00000000..5666a4c2 --- /dev/null +++ b/notify/notify_windows.go @@ -0,0 +1,55 @@ +// 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 } diff --git a/replacer.go b/replacer.go index bffc4244..d30a4061 100644 --- a/replacer.go +++ b/replacer.go @@ -27,7 +27,7 @@ import ( // NewReplacer returns a new Replacer. func NewReplacer() *Replacer { rep := &Replacer{ - static: make(map[string]interface{}), + static: make(map[string]any), } rep.providers = []ReplacerFunc{ globalDefaultReplacements, @@ -40,7 +40,7 @@ func NewReplacer() *Replacer { // without the global default replacements. func NewEmptyReplacer() *Replacer { rep := &Replacer{ - static: make(map[string]interface{}), + static: make(map[string]any), } rep.providers = []ReplacerFunc{ rep.fromStatic, @@ -53,7 +53,7 @@ func NewEmptyReplacer() *Replacer { // use NewReplacer to make one. type Replacer struct { providers []ReplacerFunc - static map[string]interface{} + static map[string]any } // Map adds mapFunc to the list of value providers. @@ -63,13 +63,13 @@ func (r *Replacer) Map(mapFunc ReplacerFunc) { } // Set sets a custom variable to a static value. -func (r *Replacer) Set(variable string, value interface{}) { +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) (interface{}, bool) { +func (r *Replacer) Get(variable string) (any, bool) { for _, mapFunc := range r.providers { if val, ok := mapFunc(variable); ok { return val, true @@ -78,11 +78,11 @@ func (r *Replacer) Get(variable string) (interface{}, bool) { return nil, false } -// GetString is the same as Get, but coerces the value to a -// string representation. +// 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 + return ToString(s), found } // Delete removes a variable with a static value @@ -92,7 +92,7 @@ func (r *Replacer) Delete(variable string) { } // fromStatic provides values from r.static. -func (r *Replacer) fromStatic(key string) (interface{}, bool) { +func (r *Replacer) fromStatic(key string) (any, bool) { val, ok := r.static[key] return val, ok } @@ -204,7 +204,7 @@ scan: } // convert val to a string as efficiently as possible - valStr := toString(val) + valStr := ToString(val) // write the value; if it's empty, either return // an error or write a default value @@ -230,7 +230,9 @@ scan: return sb.String(), nil } -func toString(val interface{}) string { +// 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 "" @@ -238,6 +240,8 @@ func toString(val interface{}) string { return v case fmt.Stringer: return v.String() + case error: + return v.Error() case byte: return string(v) case []byte: @@ -275,9 +279,9 @@ func toString(val interface{}) string { // 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) (interface{}, bool) +type ReplacerFunc func(key string) (any, bool) -func globalDefaultReplacements(key string) (interface{}, bool) { +func globalDefaultReplacements(key string) (any, bool) { // check environment variable const envPrefix = "env." if strings.HasPrefix(key, envPrefix) { @@ -316,7 +320,7 @@ func globalDefaultReplacements(key string) (interface{}, bool) { // 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 interface{}) (interface{}, error) +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. diff --git a/replacer_fuzz.go b/replacer_fuzz.go index a837e33f..50fb0b61 100644 --- a/replacer_fuzz.go +++ b/replacer_fuzz.go @@ -13,7 +13,6 @@ // limitations under the License. //go:build gofuzz -// +build gofuzz package caddy diff --git a/replacer_test.go b/replacer_test.go index d6ac0331..09b41c29 100644 --- a/replacer_test.go +++ b/replacer_test.go @@ -178,7 +178,7 @@ func TestReplacerSet(t *testing.T) { for _, tc := range []struct { variable string - value interface{} + value any }{ { variable: "test1", @@ -236,7 +236,7 @@ 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 interface{}, ok bool) { + func(key string) (val any, ok bool) { switch key { case "test1": return "val1", true @@ -250,7 +250,7 @@ func TestReplacerReplaceKnown(t *testing.T) { return "NOOO", false } }, - func(key string) (val interface{}, ok bool) { + func(key string) (val any, ok bool) { switch key { case "1": return "test-123", true @@ -306,7 +306,7 @@ func TestReplacerReplaceKnown(t *testing.T) { func TestReplacerDelete(t *testing.T) { rep := Replacer{ - static: map[string]interface{}{ + static: map[string]any{ "key1": "val1", "key2": "val2", "key3": "val3", @@ -341,10 +341,10 @@ func TestReplacerMap(t *testing.T) { rep := testReplacer() for i, tc := range []ReplacerFunc{ - func(key string) (val interface{}, ok bool) { + func(key string) (val any, ok bool) { return "", false }, - func(key string) (val interface{}, ok bool) { + func(key string) (val any, ok bool) { return "", false }, } { @@ -453,6 +453,6 @@ func BenchmarkReplacer(b *testing.B) { func testReplacer() Replacer { return Replacer{ providers: make([]ReplacerFunc, 0), - static: make(map[string]interface{}), + static: make(map[string]any), } } diff --git a/service_windows.go b/service_windows.go new file mode 100644 index 00000000..7590fc3c --- /dev/null +++ b/service_windows.go @@ -0,0 +1,49 @@ +// 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 ( + "github.com/caddyserver/caddy/v2/notify" + "golang.org/x/sys/windows/svc" +) + +func init() { + isService, err := svc.IsWindowsService() + if err != nil || isService { + return + } + 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 + } + } +} diff --git a/sigtrap_nonposix.go b/sigtrap_nonposix.go index 7855e13a..f80f593b 100644 --- a/sigtrap_nonposix.go +++ b/sigtrap_nonposix.go @@ -13,7 +13,6 @@ // limitations under the License. //go:build windows || plan9 || nacl || js -// +build windows plan9 nacl js package caddy diff --git a/sigtrap_posix.go b/sigtrap_posix.go index 706d0c25..7033f163 100644 --- a/sigtrap_posix.go +++ b/sigtrap_posix.go @@ -13,7 +13,6 @@ // limitations under the License. //go:build !windows && !plan9 && !nacl && !js -// +build !windows,!plan9,!nacl,!js package caddy diff --git a/usagepool.go b/usagepool.go index 96ed0f0e..7007849f 100644 --- a/usagepool.go +++ b/usagepool.go @@ -25,15 +25,15 @@ import ( // 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(). +// 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 @@ -57,13 +57,13 @@ import ( // NewUsagePool() to make a new one. type UsagePool struct { sync.RWMutex - pool map[interface{}]*usagePoolVal + pool map[any]*usagePoolVal } // NewUsagePool returns a new usage pool that is ready to use. func NewUsagePool() *UsagePool { return &UsagePool{ - pool: make(map[interface{}]*usagePoolVal), + pool: make(map[any]*usagePoolVal), } } @@ -74,7 +74,7 @@ func NewUsagePool() *UsagePool { // 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 interface{}, construct Constructor) (value interface{}, loaded bool, err error) { +func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loaded bool, err error) { var upv *usagePoolVal up.Lock() upv, loaded = up.pool[key] @@ -113,7 +113,7 @@ func (up *UsagePool) LoadOrNew(key interface{}, construct Constructor) (value in // 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 interface{}) (value interface{}, loaded bool) { +func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) { var upv *usagePoolVal up.Lock() upv, loaded = up.pool[key] @@ -144,7 +144,7 @@ func (up *UsagePool) LoadOrStore(key, val interface{}) (value interface{}, loade // 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 interface{}) bool) { +func (up *UsagePool) Range(f func(key, value any) bool) { up.RLock() defer up.RUnlock() for key, upv := range up.pool { @@ -166,7 +166,7 @@ func (up *UsagePool) Range(f func(key, value interface{}) bool) { // 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 interface{}) (deleted bool, err error) { +func (up *UsagePool) Delete(key any) (deleted bool, err error) { up.Lock() upv, ok := up.pool[key] if !ok { @@ -194,6 +194,21 @@ func (up *UsagePool) Delete(key interface{}) (deleted bool, err error) { 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) @@ -206,7 +221,7 @@ type Destructor interface { type usagePoolVal struct { refs int32 // accessed atomically; must be 64-bit aligned for 32-bit systems - value interface{} + value any err error sync.RWMutex }