diff --git a/.ecrc b/.ecrc new file mode 100644 index 0000000..d9ee788 --- /dev/null +++ b/.ecrc @@ -0,0 +1,9 @@ +{ + "Exclude": [ + ".git", + "go.mod", "go.sum", + "vendor", + "LICENSE", + "_test.go" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..354a828 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +tab_width = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.go] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false +indent_size = 1 diff --git a/.gitea/ISSUE_TEMPLATE/config.yml b/.gitea/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..5a9cce6 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Codeberg Pages Usage Support + url: https://codeberg.org/Codeberg/Community/issues/ + about: If you need help with configuring Codeberg Pages on codeberg.org, please go here. diff --git a/.gitignore b/.gitignore index dfe69ac..3035107 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ .idea/ +.cache/ *.iml key-database.pogreb/ acme-account.json build/ vendor/ pages +certs.sqlite +.bash_history diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..488ca09 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,34 @@ +linters-settings: + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - importShadow + - ifElseChain + - hugeParam + +linters: + disable-all: true + enable: + - unconvert + - gocritic + - gofumpt + - bidichk + - errcheck + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - misspell + - staticcheck + - typecheck + - unused + - whitespace + +run: + timeout: 5m diff --git a/.woodpecker.yml b/.woodpecker.yml index 2674e9b..de8341c 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,9 +1,10 @@ -branches: main +when: + branch: main -pipeline: +steps: # use vendor to cache dependencies vendor: - image: golang:1.18 + image: golang:1.21 commands: - go mod vendor @@ -17,9 +18,13 @@ pipeline: - "[ $(gofumpt -extra -l . | wc -l) != 0 ] && { echo 'code not formated'; exit 1; }" - golangci-lint run --timeout 5m --build-tags integration + editor-config: + group: compliant + image: mstruebing/editorconfig-checker + build: group: compliant - image: a6543/golang_just + image: codeberg.org/6543/docker-images/golang_just commands: - go version - just build @@ -39,7 +44,7 @@ pipeline: build-tag: group: compliant - image: a6543/golang_just + image: codeberg.org/6543/docker-images/golang_just commands: - go version - just build-tag ${CI_COMMIT_TAG##v} @@ -48,13 +53,13 @@ pipeline: test: group: test - image: a6543/golang_just + image: codeberg.org/6543/docker-images/golang_just commands: - just test integration-tests: group: test - image: a6543/golang_just + image: codeberg.org/6543/docker-images/golang_just commands: - just integration environment: @@ -92,6 +97,7 @@ pipeline: from_secret: bot_token when: event: [ "push" ] + branch: ${CI_REPO_DEFAULT_BRANCH} docker-tag: image: plugins/kaniko @@ -99,7 +105,7 @@ pipeline: registry: codeberg.org dockerfile: Dockerfile repo: codeberg.org/codeberg/pages-server - tag: [ latest, "${CI_COMMIT_TAG}" ] + tags: [ latest, "${CI_COMMIT_TAG}" ] username: from_secret: bot_user password: diff --git a/Dockerfile b/Dockerfile index 71dd236..eec97de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,14 @@ -FROM golang:alpine as build +FROM techknowlogick/xgo as build WORKDIR /workspace -RUN apk add ca-certificates COPY . . -RUN CGO_ENABLED=0 go build . +RUN CGO_ENABLED=1 go build -tags 'sqlite sqlite_unlock_notify netgo' -ldflags '-s -w -extldflags "-static" -linkmode external' . FROM scratch COPY --from=build /workspace/pages /pages COPY --from=build \ /etc/ssl/certs/ca-certificates.crt \ /etc/ssl/certs/ca-certificates.crt - -ENTRYPOINT ["/pages"] \ No newline at end of file + +ENTRYPOINT ["/pages"] diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 0000000..3d2f394 --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,51 @@ +# Features + +## Custom domains + +Custom domains can be used by creating a `.domains` file with the domain name, e.g.: + +```text +codeberg.page +``` + +You also have to set some DNS records, see the [Codeberg Documentation](https://docs.codeberg.org/codeberg-pages/using-custom-domain/). + +## Redirects + +Redirects can be created with a `_redirects` file with the following format: + +```text +# Comment +from to [status] +``` + +* Lines starting with `#` are ignored +* `from` - the path to redirect from (Note: repository and branch names are removed from request URLs) +* `to` - the path or URL to redirect to +* `status` - status code to use when redirecting (default 301) + +### Status codes + +* `200` - returns content from specified path (no external URLs) without changing the URL (rewrite) +* `301` - Moved Permanently (Permanent redirect) +* `302` - Found (Temporary redirect) + +### Examples + +#### SPA (single-page application) rewrite + +Redirects all paths to `/index.html` for single-page apps. + +```text +/* /index.html 200 +``` + +#### Splats + +Redirects every path under `/articles` to `/posts` while keeping the path. + +```text +/articles/* /posts/:splat 302 +``` + +Example: `/articles/2022/10/12/post-1/` -> `/posts/2022/10/12/post-1/` diff --git a/Justfile b/Justfile index 03a7436..0b8f814 100644 --- a/Justfile +++ b/Justfile @@ -1,3 +1,6 @@ +CGO_FLAGS := '-extldflags "-static" -linkmode external' +TAGS := 'sqlite sqlite_unlock_notify netgo' + dev: #!/usr/bin/env bash set -euxo pipefail @@ -6,25 +9,27 @@ dev: export PAGES_DOMAIN=localhost.mock.directory export RAW_DOMAIN=raw.localhost.mock.directory export PORT=4430 + export HTTP_PORT=8880 + export ENABLE_HTTP_SERVER=true export LOG_LEVEL=trace - go run . + go run -tags '{{TAGS}}' . build: - CGO_ENABLED=0 go build -ldflags '-s -w' -v -o build/codeberg-pages-server ./ + CGO_ENABLED=1 go build -tags '{{TAGS}}' -ldflags '-s -w {{CGO_FLAGS}}' -v -o build/codeberg-pages-server ./ build-tag VERSION: - CGO_ENABLED=0 go build -ldflags '-s -w -X "codeberg.org/codeberg/pages/server/version.Version={{VERSION}}"' -v -o build/codeberg-pages-server ./ + CGO_ENABLED=1 go build -tags '{{TAGS}}' -ldflags '-s -w -X "codeberg.org/codeberg/pages/server/version.Version={{VERSION}}" {{CGO_FLAGS}}' -v -o build/codeberg-pages-server ./ lint: tool-golangci tool-gofumpt - [ $(gofumpt -extra -l . | wc -l) != 0 ] && { echo 'code not formated'; exit 1; }; \ golangci-lint run --timeout 5m --build-tags integration + # TODO: run editorconfig-checker fmt: tool-gofumpt gofumpt -w --extra . clean: go clean ./... - rm -rf build/ + rm -rf build/ integration/certs.sqlite integration/acme-account.json tool-golangci: @hash golangci-lint> /dev/null 2>&1; if [ $? -ne 0 ]; then \ @@ -37,13 +42,16 @@ tool-gofumpt: fi test: - go test -race codeberg.org/codeberg/pages/server/... + go test -race -cover -tags '{{TAGS}}' codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/ test-run TEST: - go test -race -run "^{{TEST}}$" codeberg.org/codeberg/pages/server/... + go test -race -tags '{{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/ integration: - go test -race -tags integration codeberg.org/codeberg/pages/integration/... + go test -race -tags 'integration {{TAGS}}' codeberg.org/codeberg/pages/integration/... integration-run TEST: - go test -race -tags integration -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/... + go test -race -tags 'integration {{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/... + +docker: + docker run --rm -it --user $(id -u) -v $(pwd):/work --workdir /work -e HOME=/work codeberg.org/6543/docker-images/golang_just diff --git a/README.md b/README.md index 700f279..fb2a4b9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Codeberg Pages +[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue)](https://opensource.org/license/eupl-1-2/) +[![status-badge](https://ci.codeberg.org/api/badges/Codeberg/pages-server/status.svg)](https://ci.codeberg.org/Codeberg/pages-server) + + + + Gitea lacks the ability to host static pages from Git. The Codeberg Pages Server addresses this lack by implementing a standalone service that connects to Gitea via API. @@ -8,6 +14,7 @@ It is suitable to be deployed by other Gitea instances, too, to offer static pag **End user documentation** can mainly be found at the [Wiki](https://codeberg.org/Codeberg/pages-server/wiki/Overview) and the [Codeberg Documentation](https://docs.codeberg.org/codeberg-pages/). + Get It On Codeberg ## Quickstart @@ -20,16 +27,19 @@ first line will be the canonical domain/URL; all other occurrences will be redir 2) add a CNAME entry to your domain, pointing to `[[{branch}.]{repo}.]{owner}.codeberg.page` (repo defaults to "pages", "branch" defaults to the default branch if "repo" is "pages", or to "pages" if "repo" is something else. If the branch name contains slash characters, you need to replace "/" in the branch name to "~"): - `www.example.org. IN CNAME main.pages.example.codeberg.page.` + `www.example.org. IN CNAME main.pages.example.codeberg.page.` 3) if a CNAME is set for "www.example.org", you can redirect there from the naked domain by adding an ALIAS record for "example.org" (if your provider allows ALIAS or similar records, otherwise use A/AAAA), together with a TXT record that points to your repo (just like the CNAME record): - `example.org IN ALIAS codeberg.page.` - `example.org IN TXT main.pages.example.codeberg.page.` + `example.org IN ALIAS codeberg.page.` + `example.org IN TXT main.pages.example.codeberg.page.` Certificates are generated, updated and cleaned up automatically via Let's Encrypt through a TLS challenge. +## Chat for admins & devs + +[matrix: #gitea-pages-server:matrix.org](https://matrix.to/#/#gitea-pages-server:matrix.org) ## Deployment @@ -50,28 +60,27 @@ but if you want to run it on a shared IP address (and not a standalone), you'll need to configure your reverse proxy not to terminate the TLS connections, but forward the requests on the IP level to the Pages Server. -You can check out a proof of concept in the `haproxy-sni` folder, -and especially have a look at [this section of the haproxy.cfg](https://codeberg.org/Codeberg/pages-server/src/branch/main/haproxy-sni/haproxy.cfg#L38). +You can check out a proof of concept in the `examples/haproxy-sni` folder, +and especially have a look at [this section of the haproxy.cfg](https://codeberg.org/Codeberg/pages-server/src/branch/main/examples/haproxy-sni/haproxy.cfg#L38). -### Environment +### Environment Variables - `HOST` & `PORT` (default: `[::]` & `443`): listen address. - `PAGES_DOMAIN` (default: `codeberg.page`): main domain for pages. -- `RAW_DOMAIN` (default: `raw.codeberg.page`): domain for raw resources. +- `RAW_DOMAIN` (default: `raw.codeberg.page`): domain for raw resources (must be subdomain of `PAGES_DOMAIN`). - `GITEA_ROOT` (default: `https://codeberg.org`): root of the upstream Gitea instance. - `GITEA_API_TOKEN` (default: empty): API token for the Gitea instance to access non-public (e.g. limited) repos. -- `RAW_INFO_PAGE` (default: https://docs.codeberg.org/pages/raw-content/): info page for raw resources, shown if no resource is provided. -- `ACME_API` (default: https://acme-v02.api.letsencrypt.org/directory): set this to https://acme.mock.director to use invalid certificates without any verification (great for debugging). +- `RAW_INFO_PAGE` (default: ): info page for raw resources, shown if no resource is provided. +- `ACME_API` (default: ): set this to to use invalid certificates without any verification (great for debugging). ZeroSSL might be better in the future as it doesn't have rate limits and doesn't clash with the official Codeberg certificates (which are using Let's Encrypt), but I couldn't get it to work yet. -- `ACME_EMAIL` (default: `noreply@example.email`): Set this to "true" to accept the Terms of Service of your ACME provider. +- `ACME_EMAIL` (default: `noreply@example.email`): Set the email sent to the ACME API server to receive, for example, renewal reminders. - `ACME_EAB_KID` & `ACME_EAB_HMAC` (default: don't use EAB): EAB credentials, for example for ZeroSSL. - `ACME_ACCEPT_TERMS` (default: use self-signed certificate): Set this to "true" to accept the Terms of Service of your ACME provider. - `ACME_USE_RATE_LIMITS` (default: true): Set this to false to disable rate limits, e.g. with ZeroSSL. - `ENABLE_HTTP_SERVER` (default: false): Set this to true to enable the HTTP-01 challenge and redirect all other HTTP requests to HTTPS. Currently only works with port 80. - `DNS_PROVIDER` (default: use self-signed certificate): Code of the ACME DNS provider for the main domain wildcard. - See https://go-acme.github.io/lego/dns/ for available values & additional environment variables. -- `DEBUG` (default: false): Set this to true to enable debug logging. - + See for available values & additional environment variables. +- `LOG_LEVEL` (default: warn): Set this to specify the level of logging. ## Contributing to the development @@ -80,30 +89,36 @@ Since we are working nicely in a team, it might be hard at times to get started (still check out the issues, we always aim to have some things to get you started). If you have any questions, want to work on a feature or could imagine collaborating with us for some time, -feel free to ping us in an issue or in a general Matrix chatgroup. +feel free to ping us in an issue or in a general [Matrix chat room](#chat-for-admins--devs). -You can also contact the maintainers of this project: +You can also contact the maintainer(s) of this project: + +- [crapStone](https://codeberg.org/crapStone) [(Matrix)](https://matrix.to/#/@crapstone:obermui.de) + +Previous maintainers: - [momar](https://codeberg.org/momar) [(Matrix)](https://matrix.to/#/@moritz:wuks.space) - [6543](https://codeberg.org/6543) [(Matrix)](https://matrix.to/#/@marddl:obermui.de) ### First steps -The code of this repository is split in several modules. -While heavy refactoring work is currently undergo, you can easily understand the basic structure: +The code of this repository is split in several modules. +The [Architecture is explained](https://codeberg.org/Codeberg/pages-server/wiki/Architecture) in the wiki. + The `cmd` folder holds the data necessary for interacting with the service via the cli. -If you are considering to deploy the service yourself, make sure to check it out. The heart of the software lives in the `server` folder and is split in several modules. -After scanning the code, you should quickly be able to understand their function and start hacking on them. Again: Feel free to get in touch with us for any questions that might arise. Thank you very much. - ### Test Server -run `just dev` +Make sure you have [golang](https://go.dev) v1.21 or newer and [just](https://just.systems/man/en/) installed. + +run `just dev` now this pages should work: - - https://magiclike.localhost.mock.directory:4430/ - - https://momar.localhost.mock.directory:4430/ci-testing/ - - https://momar.localhost.mock.directory:4430/pag/@master/ \ No newline at end of file + +- +- +- +- diff --git a/cmd/certs.go b/cmd/certs.go index d93fe13..6012b6e 100644 --- a/cmd/certs.go +++ b/cmd/certs.go @@ -2,11 +2,9 @@ package cmd import ( "fmt" + "time" - "github.com/akrylysov/pogreb" "github.com/urfave/cli/v2" - - "codeberg.org/codeberg/pages/server/database" ) var Certs = &cli.Command{ @@ -24,24 +22,26 @@ var Certs = &cli.Command{ Action: removeCert, }, }, + Flags: CertStorageFlags, } func listCerts(ctx *cli.Context) error { - // TODO: make "key-database.pogreb" set via flag - keyDatabase, err := database.New("key-database.pogreb") + certDB, closeFn, err := openCertDB(ctx) if err != nil { - return fmt.Errorf("could not create database: %v", err) + return err + } + defer closeFn() + + items, err := certDB.Items(0, 0) + if err != nil { + return err } - items := keyDatabase.Items() - for domain, _, err := items.Next(); err != pogreb.ErrIterationDone; domain, _, err = items.Next() { - if err != nil { - return err - } - if domain[0] == '.' { - fmt.Printf("*") - } - fmt.Printf("%s\n", domain) + fmt.Printf("Domain\tValidTill\n\n") + for _, cert := range items { + fmt.Printf("%s\t%s\n", + cert.Domain, + time.Unix(cert.ValidTill, 0).Format(time.RFC3339)) } return nil } @@ -53,20 +53,17 @@ func removeCert(ctx *cli.Context) error { domains := ctx.Args().Slice() - // TODO: make "key-database.pogreb" set via flag - keyDatabase, err := database.New("key-database.pogreb") + certDB, closeFn, err := openCertDB(ctx) if err != nil { - return fmt.Errorf("could not create database: %v", err) + return err } + defer closeFn() for _, domain := range domains { fmt.Printf("Removing domain %s from the database...\n", domain) - if err := keyDatabase.Delete(domain); err != nil { + if err := certDB.Delete(domain); err != nil { return err } } - if err := keyDatabase.Close(); err != nil { - return err - } return nil } diff --git a/cmd/flags.go b/cmd/flags.go index 50bf6b3..7ac94e6 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -4,127 +4,159 @@ import ( "github.com/urfave/cli/v2" ) -var ServeFlags = []cli.Flag{ - // MainDomainSuffix specifies the main domain (starting with a dot) for which subdomains shall be served as static - // pages, or used for comparison in CNAME lookups. Static pages can be accessed through - // https://{owner}.{MainDomain}[/{repo}], with repo defaulting to "pages". - &cli.StringFlag{ - Name: "pages-domain", - Usage: "specifies the main domain (starting with a dot) for which subdomains shall be served as static pages", - EnvVars: []string{"PAGES_DOMAIN"}, - Value: "codeberg.page", - }, - // Default branches to fetch assets from - &cli.StringFlag{ - Name: "pages-branches", - Usage: "defines branches to fetch assets from", - EnvVars: []string{"PAGES_BRANCHES"}, - Value: "pages", - }, - // GiteaRoot specifies the root URL of the Gitea instance, without a trailing slash. - &cli.StringFlag{ - Name: "gitea-root", - Usage: "specifies the root URL of the Gitea instance, without a trailing slash.", - EnvVars: []string{"GITEA_ROOT"}, - Value: "https://codeberg.org", - }, - // GiteaApiToken specifies an api token for the Gitea instance - &cli.StringFlag{ - Name: "gitea-api-token", - Usage: "specifies an api token for the Gitea instance", - EnvVars: []string{"GITEA_API_TOKEN"}, - Value: "", - }, - // RawDomain specifies the domain from which raw repository content shall be served in the following format: - // https://{RawDomain}/{owner}/{repo}[/{branch|tag|commit}/{version}]/{filepath...} - // (set to []byte(nil) to disable raw content hosting) - &cli.StringFlag{ - Name: "raw-domain", - Usage: "specifies the domain from which raw repository content shall be served, not set disable raw content hosting", - EnvVars: []string{"RAW_DOMAIN"}, - Value: "raw.codeberg.page", - }, - // RawInfoPage will be shown (with a redirect) when trying to access RawDomain directly (or without owner/repo/path). - &cli.StringFlag{ - Name: "raw-info-page", - Usage: "will be shown (with a redirect) when trying to access $RAW_DOMAIN directly (or without owner/repo/path)", - EnvVars: []string{"RAW_INFO_PAGE"}, - Value: "https://docs.codeberg.org/codeberg-pages/raw-content/", - }, +var ( + CertStorageFlags = []cli.Flag{ + &cli.StringFlag{ + Name: "db-type", + Usage: "Specify the database driver. Valid options are \"sqlite3\", \"mysql\" and \"postgres\". Read more at https://xorm.io", + Value: "sqlite3", + EnvVars: []string{"DB_TYPE"}, + }, + &cli.StringFlag{ + Name: "db-conn", + Usage: "Specify the database connection. For \"sqlite3\" it's the filepath. Read more at https://go.dev/doc/tutorial/database-access", + Value: "certs.sqlite", + EnvVars: []string{"DB_CONN"}, + }, + } - // Server - &cli.StringFlag{ - Name: "host", - Usage: "specifies host of listening address", - EnvVars: []string{"HOST"}, - Value: "[::]", - }, - &cli.StringFlag{ - Name: "port", - Usage: "specifies port of listening address", - EnvVars: []string{"PORT"}, - Value: "443", - }, - &cli.BoolFlag{ - Name: "enable-http-server", - // TODO: desc - EnvVars: []string{"ENABLE_HTTP_SERVER"}, - }, - // Server Options - &cli.BoolFlag{ - Name: "enable-lfs-support", - Usage: "enable lfs support, require gitea v1.17.0 as backend", - EnvVars: []string{"ENABLE_LFS_SUPPORT"}, - Value: true, - }, - &cli.BoolFlag{ - Name: "enable-symlink-support", - Usage: "follow symlinks if enabled, require gitea v1.18.0 as backend", - EnvVars: []string{"ENABLE_SYMLINK_SUPPORT"}, - Value: true, - }, - &cli.StringFlag{ - Name: "log-level", - Value: "warn", - Usage: "specify at which log level should be logged. Possible options: info, warn, error, fatal", - EnvVars: []string{"LOG_LEVEL"}, - }, + ServerFlags = append(CertStorageFlags, []cli.Flag{ + // ############# + // ### Gitea ### + // ############# + // GiteaRoot specifies the root URL of the Gitea instance, without a trailing slash. + &cli.StringFlag{ + Name: "gitea-root", + Usage: "specifies the root URL of the Gitea instance, without a trailing slash.", + EnvVars: []string{"GITEA_ROOT"}, + Value: "https://codeberg.org", + }, + // GiteaApiToken specifies an api token for the Gitea instance + &cli.StringFlag{ + Name: "gitea-api-token", + Usage: "specifies an api token for the Gitea instance", + EnvVars: []string{"GITEA_API_TOKEN"}, + Value: "", + }, + &cli.BoolFlag{ + Name: "enable-lfs-support", + Usage: "enable lfs support, require gitea >= v1.17.0 as backend", + EnvVars: []string{"ENABLE_LFS_SUPPORT"}, + Value: true, + }, + &cli.BoolFlag{ + Name: "enable-symlink-support", + Usage: "follow symlinks if enabled, require gitea >= v1.18.0 as backend", + EnvVars: []string{"ENABLE_SYMLINK_SUPPORT"}, + Value: true, + }, - // ACME - &cli.StringFlag{ - Name: "acme-api-endpoint", - EnvVars: []string{"ACME_API"}, - Value: "https://acme-v02.api.letsencrypt.org/directory", - }, - &cli.StringFlag{ - Name: "acme-email", - EnvVars: []string{"ACME_EMAIL"}, - Value: "noreply@example.email", - }, - &cli.BoolFlag{ - Name: "acme-use-rate-limits", - // TODO: Usage - EnvVars: []string{"ACME_USE_RATE_LIMITS"}, - Value: true, - }, - &cli.BoolFlag{ - Name: "acme-accept-terms", - // TODO: Usage - EnvVars: []string{"ACME_ACCEPT_TERMS"}, - }, - &cli.StringFlag{ - Name: "acme-eab-kid", - // TODO: Usage - EnvVars: []string{"ACME_EAB_KID"}, - }, - &cli.StringFlag{ - Name: "acme-eab-hmac", - // TODO: Usage - EnvVars: []string{"ACME_EAB_HMAC"}, - }, - &cli.StringFlag{ - Name: "dns-provider", - // TODO: Usage - EnvVars: []string{"DNS_PROVIDER"}, - }, -} + // ########################### + // ### Page Server Domains ### + // ########################### + // MainDomainSuffix specifies the main domain (starting with a dot) for which subdomains shall be served as static + // pages, or used for comparison in CNAME lookups. Static pages can be accessed through + // https://{owner}.{MainDomain}[/{repo}], with repo defaulting to "pages". + &cli.StringFlag{ + Name: "pages-domain", + Usage: "specifies the main domain (starting with a dot) for which subdomains shall be served as static pages", + EnvVars: []string{"PAGES_DOMAIN"}, + Value: "codeberg.page", + }, + // RawDomain specifies the domain from which raw repository content shall be served in the following format: + // https://{RawDomain}/{owner}/{repo}[/{branch|tag|commit}/{version}]/{filepath...} + // (set to []byte(nil) to disable raw content hosting) + &cli.StringFlag{ + Name: "raw-domain", + Usage: "specifies the domain from which raw repository content shall be served, not set disable raw content hosting", + EnvVars: []string{"RAW_DOMAIN"}, + Value: "raw.codeberg.page", + }, + + // ######################### + // ### Page Server Setup ### + // ######################### + &cli.StringFlag{ + Name: "host", + Usage: "specifies host of listening address", + EnvVars: []string{"HOST"}, + Value: "[::]", + }, + &cli.UintFlag{ + Name: "port", + Usage: "specifies the https port to listen to ssl requests", + EnvVars: []string{"PORT", "HTTPS_PORT"}, + Value: 443, + }, + &cli.UintFlag{ + Name: "http-port", + Usage: "specifies the http port, you also have to enable http server via ENABLE_HTTP_SERVER=true", + EnvVars: []string{"HTTP_PORT"}, + Value: 80, + }, + &cli.BoolFlag{ + Name: "enable-http-server", + Usage: "start a http server to redirect to https and respond to http acme challenges", + EnvVars: []string{"ENABLE_HTTP_SERVER"}, + }, + &cli.StringFlag{ + Name: "log-level", + Value: "warn", + Usage: "specify at which log level should be logged. Possible options: info, warn, error, fatal", + EnvVars: []string{"LOG_LEVEL"}, + }, + // Default branches to fetch assets from + &cli.StringSliceFlag{ + Name: "pages-branch", + Usage: "define a branch to fetch assets from", + EnvVars: []string{"PAGES_BRANCHES"}, + Value: cli.NewStringSlice("pages"), + }, + + // ############################ + // ### ACME Client Settings ### + // ############################ + &cli.StringFlag{ + Name: "acme-api-endpoint", + EnvVars: []string{"ACME_API"}, + Value: "https://acme-v02.api.letsencrypt.org/directory", + }, + &cli.StringFlag{ + Name: "acme-email", + EnvVars: []string{"ACME_EMAIL"}, + Value: "noreply@example.email", + }, + &cli.BoolFlag{ + Name: "acme-use-rate-limits", + // TODO: Usage + EnvVars: []string{"ACME_USE_RATE_LIMITS"}, + Value: true, + }, + &cli.BoolFlag{ + Name: "acme-accept-terms", + Usage: "To accept the ACME ToS", + EnvVars: []string{"ACME_ACCEPT_TERMS"}, + }, + &cli.StringFlag{ + Name: "acme-eab-kid", + Usage: "Register the current account to the ACME server with external binding.", + EnvVars: []string{"ACME_EAB_KID"}, + }, + &cli.StringFlag{ + Name: "acme-eab-hmac", + Usage: "Register the current account to the ACME server with external binding.", + EnvVars: []string{"ACME_EAB_HMAC"}, + }, + &cli.StringFlag{ + Name: "dns-provider", + Usage: "Use DNS-Challenge for main domain. Read more at: https://go-acme.github.io/lego/dns/", + EnvVars: []string{"DNS_PROVIDER"}, + }, + &cli.StringFlag{ + Name: "acme-account-config", + Usage: "json file of acme account", + Value: "acme-account.json", + EnvVars: []string{"ACME_ACCOUNT_CONFIG"}, + }, + }...) +) diff --git a/cmd/main.go b/cmd/main.go index 1830cef..683e859 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,12 +1,11 @@ package cmd import ( - "bytes" "context" "crypto/tls" - "errors" "fmt" "net" + "net/http" "os" "strings" "time" @@ -15,148 +14,135 @@ import ( "github.com/rs/zerolog/log" "github.com/urfave/cli/v2" - "codeberg.org/codeberg/pages/server" "codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/certificates" - "codeberg.org/codeberg/pages/server/database" "codeberg.org/codeberg/pages/server/gitea" + "codeberg.org/codeberg/pages/server/handler" ) // AllowedCorsDomains lists the domains for which Cross-Origin Resource Sharing is allowed. // TODO: make it a flag -var AllowedCorsDomains = [][]byte{ - []byte("fonts.codeberg.org"), - []byte("design.codeberg.org"), +var AllowedCorsDomains = []string{ + "fonts.codeberg.org", + "design.codeberg.org", } // BlacklistedPaths specifies forbidden path prefixes for all Codeberg Pages. // TODO: Make it a flag too -var BlacklistedPaths = [][]byte{ - []byte("/.well-known/acme-challenge/"), +var BlacklistedPaths = []string{ + "/.well-known/acme-challenge/", } // Serve sets up and starts the web server. func Serve(ctx *cli.Context) error { - // Initalize the logger. + // Initialize the logger. logLevel, err := zerolog.ParseLevel(ctx.String("log-level")) if err != nil { return err } log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger().Level(logLevel) - giteaRoot := strings.TrimSuffix(ctx.String("gitea-root"), "/") + giteaRoot := ctx.String("gitea-root") giteaAPIToken := ctx.String("gitea-api-token") rawDomain := ctx.String("raw-domain") - mainDomainSuffix := []byte(ctx.String("pages-domain")) - defaultBranches := strings.Split(strings.ReplaceAll(ctx.String("pages-branches"), " ", ""), ",") - rawInfoPage := ctx.String("raw-info-page") - listeningAddress := fmt.Sprintf("%s:%s", ctx.String("host"), ctx.String("port")) + defaultBranches := ctx.StringSlice("pages-branch") + mainDomainSuffix := ctx.String("pages-domain") + listeningHost := ctx.String("host") + listeningSSLPort := ctx.Uint("port") + listeningSSLAddress := fmt.Sprintf("%s:%d", listeningHost, listeningSSLPort) + listeningHTTPAddress := fmt.Sprintf("%s:%d", listeningHost, ctx.Uint("http-port")) enableHTTPServer := ctx.Bool("enable-http-server") - acmeAPI := ctx.String("acme-api-endpoint") - acmeMail := ctx.String("acme-email") - acmeUseRateLimits := ctx.Bool("acme-use-rate-limits") - acmeAcceptTerms := ctx.Bool("acme-accept-terms") - acmeEabKID := ctx.String("acme-eab-kid") - acmeEabHmac := ctx.String("acme-eab-hmac") - dnsProvider := ctx.String("dns-provider") - if (!acmeAcceptTerms || dnsProvider == "") && acmeAPI != "https://acme.mock.directory" { - return errors.New("you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER, unless $ACME_API is set to https://acme.mock.directory") - } - allowedCorsDomains := AllowedCorsDomains - if len(rawDomain) != 0 { - allowedCorsDomains = append(allowedCorsDomains, []byte(rawDomain)) + if rawDomain != "" { + allowedCorsDomains = append(allowedCorsDomains, rawDomain) } - // Make sure MainDomain has a trailing dot, and GiteaRoot has no trailing slash - if !bytes.HasPrefix(mainDomainSuffix, []byte{'.'}) { - mainDomainSuffix = append([]byte{'.'}, mainDomainSuffix...) + // Make sure MainDomain has a trailing dot + if !strings.HasPrefix(mainDomainSuffix, ".") { + mainDomainSuffix = "." + mainDomainSuffix } if len(defaultBranches) == 0 { - defaultBranches = []string{"pages"} + return fmt.Errorf("no default branches set (PAGES_BRANCHES)") } + // Init ssl cert database + certDB, closeFn, err := openCertDB(ctx) + if err != nil { + return err + } + defer closeFn() + keyCache := cache.NewKeyValueCache() challengeCache := cache.NewKeyValueCache() // canonicalDomainCache stores canonical domains canonicalDomainCache := cache.NewKeyValueCache() // dnsLookupCache stores DNS lookups for custom domains dnsLookupCache := cache.NewKeyValueCache() - // branchTimestampCache stores branch timestamps for faster cache checking - branchTimestampCache := cache.NewKeyValueCache() - // fileResponseCache stores responses from the Gitea server - // TODO: make this an MRU cache with a size limit - fileResponseCache := cache.NewKeyValueCache() + // redirectsCache stores redirects in _redirects files + redirectsCache := cache.NewKeyValueCache() + // clientResponseCache stores responses from the Gitea server + clientResponseCache := cache.NewKeyValueCache() - giteaClient, err := gitea.NewClient(giteaRoot, giteaAPIToken, ctx.Bool("enable-symlink-support"), ctx.Bool("enable-lfs-support")) + giteaClient, err := gitea.NewClient(giteaRoot, giteaAPIToken, clientResponseCache, ctx.Bool("enable-symlink-support"), ctx.Bool("enable-lfs-support")) if err != nil { return fmt.Errorf("could not create new gitea client: %v", err) } - // Create handler based on settings - handler := server.Handler(mainDomainSuffix, []byte(rawDomain), - giteaClient, - giteaRoot, rawInfoPage, - BlacklistedPaths, allowedCorsDomains, - dnsLookupCache, canonicalDomainCache, branchTimestampCache, fileResponseCache, - defaultBranches, - ) + acmeClient, err := createAcmeClient(ctx, enableHTTPServer, challengeCache) + if err != nil { + return err + } - fastServer := server.SetupServer(handler) - httpServer := server.SetupHTTPACMEChallengeServer(challengeCache) + if err := certificates.SetupMainDomainCertificates(mainDomainSuffix, acmeClient, certDB); err != nil { + return err + } - // Setup listener and TLS - log.Info().Msgf("Listening on https://%s", listeningAddress) - listener, err := net.Listen("tcp", listeningAddress) + // Create listener for SSL connections + log.Info().Msgf("Create TCP listener for SSL on %s", listeningSSLAddress) + listener, err := net.Listen("tcp", listeningSSLAddress) if err != nil { return fmt.Errorf("couldn't create listener: %v", err) } - // TODO: make "key-database.pogreb" set via flag - certDB, err := database.New("key-database.pogreb") - if err != nil { - return fmt.Errorf("could not create database: %v", err) - } - defer certDB.Close() //nolint:errcheck // database has no close ... sync behave like it - + // Setup listener for SSL connections listener = tls.NewListener(listener, certificates.TLSConfig(mainDomainSuffix, giteaClient, + acmeClient, defaultBranches[0], - dnsProvider, - acmeUseRateLimits, keyCache, challengeCache, dnsLookupCache, canonicalDomainCache, certDB)) - acmeConfig, err := certificates.SetupAcmeConfig(acmeAPI, acmeMail, acmeEabHmac, acmeEabKID, acmeAcceptTerms) - if err != nil { - return err - } - - if err := certificates.SetupCertificates(mainDomainSuffix, dnsProvider, acmeConfig, acmeUseRateLimits, enableHTTPServer, challengeCache, certDB); err != nil { - return err - } - interval := 12 * time.Hour certMaintainCtx, cancelCertMaintain := context.WithCancel(context.Background()) defer cancelCertMaintain() - go certificates.MaintainCertDB(certMaintainCtx, interval, mainDomainSuffix, dnsProvider, acmeUseRateLimits, certDB) + go certificates.MaintainCertDB(certMaintainCtx, interval, acmeClient, mainDomainSuffix, certDB) if enableHTTPServer { + // Create handler for http->https redirect and http acme challenges + httpHandler := certificates.SetupHTTPACMEChallengeServer(challengeCache, listeningSSLPort) + + // Create listener for http and start listening go func() { - log.Info().Msg("Start HTTP server listening on :80") - err := httpServer.ListenAndServe("[::]:80") + log.Info().Msgf("Start HTTP server listening on %s", listeningHTTPAddress) + err := http.ListenAndServe(listeningHTTPAddress, httpHandler) if err != nil { log.Panic().Err(err).Msg("Couldn't start HTTP fastServer") } }() } - // Start the web fastServer - log.Info().Msgf("Start listening on %s", listener.Addr()) - err = fastServer.Serve(listener) - if err != nil { + // Create ssl handler based on settings + sslHandler := handler.Handler(mainDomainSuffix, rawDomain, + giteaClient, + BlacklistedPaths, allowedCorsDomains, + defaultBranches, + dnsLookupCache, canonicalDomainCache, redirectsCache) + + // Start the ssl listener + log.Info().Msgf("Start SSL server using TCP listener on %s", listener.Addr()) + if err := http.Serve(listener, sslHandler); err != nil { log.Panic().Err(err).Msg("Couldn't start fastServer") } diff --git a/cmd/setup.go b/cmd/setup.go new file mode 100644 index 0000000..cde4bc9 --- /dev/null +++ b/cmd/setup.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "errors" + "fmt" + + "github.com/rs/zerolog/log" + "github.com/urfave/cli/v2" + + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/certificates" + "codeberg.org/codeberg/pages/server/database" +) + +var ErrAcmeMissConfig = errors.New("ACME client has wrong config") + +func openCertDB(ctx *cli.Context) (certDB database.CertDB, closeFn func(), err error) { + certDB, err = database.NewXormDB(ctx.String("db-type"), ctx.String("db-conn")) + if err != nil { + return nil, nil, fmt.Errorf("could not connect to database: %w", err) + } + + closeFn = func() { + if err := certDB.Close(); err != nil { + log.Error().Err(err) + } + } + + return certDB, closeFn, nil +} + +func createAcmeClient(ctx *cli.Context, enableHTTPServer bool, challengeCache cache.SetGetKey) (*certificates.AcmeClient, error) { + acmeAPI := ctx.String("acme-api-endpoint") + acmeMail := ctx.String("acme-email") + acmeEabHmac := ctx.String("acme-eab-hmac") + acmeEabKID := ctx.String("acme-eab-kid") + acmeAcceptTerms := ctx.Bool("acme-accept-terms") + dnsProvider := ctx.String("dns-provider") + acmeUseRateLimits := ctx.Bool("acme-use-rate-limits") + acmeAccountConf := ctx.String("acme-account-config") + + // check config + if (!acmeAcceptTerms || dnsProvider == "") && acmeAPI != "https://acme.mock.directory" { + return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig) + } + if acmeEabHmac != "" && acmeEabKID == "" { + return nil, fmt.Errorf("%w: ACME_EAB_HMAC also needs ACME_EAB_KID to be set", ErrAcmeMissConfig) + } else if acmeEabHmac == "" && acmeEabKID != "" { + return nil, fmt.Errorf("%w: ACME_EAB_KID also needs ACME_EAB_HMAC to be set", ErrAcmeMissConfig) + } + + return certificates.NewAcmeClient( + acmeAccountConf, + acmeAPI, + acmeMail, + acmeEabHmac, + acmeEabKID, + dnsProvider, + acmeAcceptTerms, + enableHTTPServer, + acmeUseRateLimits, + challengeCache, + ) +} diff --git a/haproxy-sni/.gitignore b/examples/haproxy-sni/.gitignore similarity index 100% rename from haproxy-sni/.gitignore rename to examples/haproxy-sni/.gitignore diff --git a/haproxy-sni/README.md b/examples/haproxy-sni/README.md similarity index 100% rename from haproxy-sni/README.md rename to examples/haproxy-sni/README.md diff --git a/haproxy-sni/dhparam.pem b/examples/haproxy-sni/dhparam.pem similarity index 93% rename from haproxy-sni/dhparam.pem rename to examples/haproxy-sni/dhparam.pem index 088f967..9b182b7 100644 --- a/haproxy-sni/dhparam.pem +++ b/examples/haproxy-sni/dhparam.pem @@ -5,4 +5,4 @@ MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi 7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== ------END DH PARAMETERS----- \ No newline at end of file +-----END DH PARAMETERS----- diff --git a/haproxy-sni/docker-compose.yml b/examples/haproxy-sni/docker-compose.yml similarity index 99% rename from haproxy-sni/docker-compose.yml rename to examples/haproxy-sni/docker-compose.yml index 4dd8677..66ff52b 100644 --- a/haproxy-sni/docker-compose.yml +++ b/examples/haproxy-sni/docker-compose.yml @@ -19,4 +19,4 @@ services: volumes: - ./pages-www:/srv:ro - ./pages.Caddyfile:/etc/caddy/Caddyfile:ro - + diff --git a/haproxy-sni/gitea-www/index.html b/examples/haproxy-sni/gitea-www/index.html similarity index 100% rename from haproxy-sni/gitea-www/index.html rename to examples/haproxy-sni/gitea-www/index.html diff --git a/haproxy-sni/gitea.Caddyfile b/examples/haproxy-sni/gitea.Caddyfile similarity index 100% rename from haproxy-sni/gitea.Caddyfile rename to examples/haproxy-sni/gitea.Caddyfile diff --git a/haproxy-sni/haproxy-certificates/codeberg.org.pem b/examples/haproxy-sni/haproxy-certificates/codeberg.org.pem similarity index 100% rename from haproxy-sni/haproxy-certificates/codeberg.org.pem rename to examples/haproxy-sni/haproxy-certificates/codeberg.org.pem diff --git a/haproxy-sni/haproxy-certificates/codeberg.org.pem.key b/examples/haproxy-sni/haproxy-certificates/codeberg.org.pem.key similarity index 100% rename from haproxy-sni/haproxy-certificates/codeberg.org.pem.key rename to examples/haproxy-sni/haproxy-certificates/codeberg.org.pem.key diff --git a/haproxy-sni/haproxy.cfg b/examples/haproxy-sni/haproxy.cfg similarity index 100% rename from haproxy-sni/haproxy.cfg rename to examples/haproxy-sni/haproxy.cfg diff --git a/haproxy-sni/pages-www/index.html b/examples/haproxy-sni/pages-www/index.html similarity index 100% rename from haproxy-sni/pages-www/index.html rename to examples/haproxy-sni/pages-www/index.html diff --git a/haproxy-sni/pages.Caddyfile b/examples/haproxy-sni/pages.Caddyfile similarity index 100% rename from haproxy-sni/pages.Caddyfile rename to examples/haproxy-sni/pages.Caddyfile diff --git a/haproxy-sni/test.sh b/examples/haproxy-sni/test.sh similarity index 100% rename from haproxy-sni/test.sh rename to examples/haproxy-sni/test.sh diff --git a/go.mod b/go.mod index 479c328..eba292e 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,24 @@ module codeberg.org/codeberg/pages -go 1.18 +go 1.21 + +toolchain go1.21.4 require ( + code.gitea.io/sdk/gitea v0.16.1-0.20231115014337-e23e8aa3004f github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a - github.com/akrylysov/pogreb v0.10.1 github.com/go-acme/lego/v4 v4.5.3 + github.com/go-sql-driver/mysql v1.6.0 github.com/joho/godotenv v1.4.0 + github.com/lib/pq v1.10.7 + github.com/mattn/go-sqlite3 v1.14.16 + github.com/microcosm-cc/bluemonday v1.0.26 github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad github.com/rs/zerolog v1.27.0 github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 - github.com/valyala/fasthttp v1.31.0 - github.com/valyala/fastjson v1.6.3 + golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb + xorm.io/xorm v1.3.2 ) require ( @@ -31,14 +37,15 @@ require ( github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1 // indirect github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183 // indirect - github.com/andybalholm/brotli v1.0.2 // indirect github.com/aws/aws-sdk-go v1.39.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cenkalti/backoff/v4 v4.1.1 // indirect github.com/cloudflare/cloudflare-go v0.20.0 // indirect github.com/cpu/goacmedns v0.1.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect github.com/deepmap/oapi-codegen v1.6.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dnsimple/dnsimple-go v0.70.1 // indirect @@ -46,24 +53,28 @@ require ( github.com/fatih/structs v1.1.0 // indirect github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect github.com/go-errors/errors v1.0.1 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect - github.com/gofrs/uuid v3.2.0+incompatible // indirect + github.com/goccy/go-json v0.8.1 // indirect + github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/uuid v1.1.1 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/gophercloud/gophercloud v0.16.0 // indirect github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect + github.com/gorilla/css v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect github.com/jarcoal/httpmock v1.0.6 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/json-iterator/go v1.1.7 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect - github.com/klauspost/compress v1.13.4 // indirect github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect @@ -78,7 +89,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect github.com/nrdcg/auroradns v1.0.1 // indirect github.com/nrdcg/desec v0.6.0 // indirect @@ -103,17 +114,17 @@ require ( github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/spf13/cast v1.3.1 // indirect github.com/stretchr/objx v0.3.0 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect github.com/transip/gotransip/v6 v6.6.1 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14 // indirect github.com/vultr/govultr/v2 v2.7.1 // indirect go.opencensus.io v0.22.3 // indirect go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277 // indirect - golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect - golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect - golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect - golang.org/x/text v0.3.6 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect google.golang.org/api v0.20.0 // indirect google.golang.org/appengine v1.6.5 // indirect @@ -125,4 +136,5 @@ require ( gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + xorm.io/builder v0.3.12 // indirect ) diff --git a/go.sum b/go.sum index 23a58bc..7ea8b78 100644 --- a/go.sum +++ b/go.sum @@ -22,7 +22,12 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA 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= +code.gitea.io/sdk/gitea v0.16.1-0.20231115014337-e23e8aa3004f h1:nMmwDgUIAWj9XQjzHz5unC3ZMfhhwHRk6rnuwLzdu1o= +code.gitea.io/sdk/gitea v0.16.1-0.20231115014337-e23e8aa3004f/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= +gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= github.com/Azure/azure-sdk-for-go v32.4.0+incompatible h1:1JP8SKfroEakYiQU2ZyPDosh8w2Tg9UopKt88VyQPt4= github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= @@ -53,26 +58,38 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM 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/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a h1:Cf4CrDeyrIcuIiJZEZJAH5dapqQ6J3OmP/vHPbDjaFA= github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a/go.mod h1:ig6eVXkYn/9dz0Vm8UdLf+E0u1bE6kBSn3n2hqk6jas= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1 h1:bLzehmpyCwQiqCE1Qe9Ny6fbFqs7hPlmo9vKv2orUxs= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1/go.mod h1:kX6YddBkXqqywAe8c9LyvgTCyFuZCTMF4cRPQhc3Fy8= -github.com/akrylysov/pogreb v0.10.1 h1:FqlR8VR7uCbJdfUob916tPM+idpKgeESDXOA1K0DK4w= -github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= 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/aliyun/alibaba-cloud-sdk-go v1.61.1183 h1:dkj8/dxOQ4L1XpwCzRLqukvUBbxuNdz3FeyvHFnRjmo= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA= -github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= -github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.39.0 h1:74BBwkEmiqBbi2CGflEh34l0YNtIibTjZsibGarkNjo= github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -81,33 +98,48 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.20.0 h1:y2a6KwYHTFxhw+8PLhz0q5hpTGj6Un3W1pbpQLhzFaE= github.com/cloudflare/cloudflare-go v0.20.0/go.mod h1:sPWL/lIC6biLEdyGZwBQ1rGQKF1FhM7N60fuNiFdYTI= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +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-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.3.3-0.20220203105225-a9a7ef127534/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/cpu/goacmedns v0.1.1 h1:DM3H2NiN2oam7QljgGY5ygy4yDXhK5Z4JUnqaugs2C4= github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ= 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/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= 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/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= github.com/deepmap/oapi-codegen v1.6.1 h1:2BvsmRb6pogGNtr8Ann+esAbSKFXx2CZN18VpAMecnw= github.com/deepmap/oapi-codegen v1.6.1/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= @@ -116,6 +148,13 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/ github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnsimple/dnsimple-go v0.70.1 h1:cSZndVjttLpgplDuesY4LFIvfKf/zRA1J7mCATBbzSM= github.com/dnsimple/dnsimple-go v0.70.1/go.mod h1:F9WHww9cC76hrnwGFfAfrqdW99j3MOYasQcIwTS/aUk= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/exoscale/egoscale v0.67.0 h1:qgWh7T5IZGrNWtg6ib4dr+76WThvB+odTtGG+DGbXF8= @@ -125,6 +164,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -136,28 +177,43 @@ github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJ github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= +github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI= +github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +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/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -182,7 +238,10 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= 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= @@ -191,8 +250,10 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.3/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.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -205,8 +266,10 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -217,16 +280,25 @@ github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae h1:Hi3IgB9RQDE15 github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae/go.mod h1:wx8HMD8oQD0Ryhz6+6ykq75PJ79iPyEqYHfwZ4l7OsA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +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/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -243,6 +315,10 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -252,12 +328,61 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/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/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= 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/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU= github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI= +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/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= +github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE= +github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc= +github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jarcoal/httpmock v1.0.6 h1:e81vOSexXU3mJuJ4l//geOmKIt+Vkxerk1feQBC8D0g= github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= @@ -271,8 +396,10 @@ github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -280,10 +407,10 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b h1:DzHy0GlWeF0KAglaTMY7Q+khIFoG8toHP+wLFBVBQJc= github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -294,6 +421,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/labbsr0x/bindman-dns-webhook v1.0.2 h1:I7ITbmQPAVwrDdhd6dHKi+MYJTJqPCK0jE6YNBAevnk= @@ -302,6 +430,15 @@ github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk= github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w= github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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/linode/linodego v0.31.1 h1:dBtjKo7J9UhNFhTOclEXb12RRyQDaRBxISdONVuU+DA= github.com/linode/linodego v0.31.1/go.mod h1:BR0gVkCJffEdIGJSl6bHR80Ty+Uvg/2jkjmrWaFectM= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= @@ -311,19 +448,25 @@ github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAW github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= github.com/liquidweb/liquidweb-go v1.6.3 h1:NVHvcnX3eb3BltiIoA+gLYn15nOpkYkdizOEYGSKrk4= github.com/liquidweb/liquidweb-go v1.6.3/go.mod h1:SuXXp+thr28LnjEw18AYtWwIbWMHSUiajPQs8T9c/Rc= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= 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.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= @@ -333,9 +476,14 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= @@ -356,11 +504,19 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/nrdcg/auroradns v1.0.1 h1:m/kBq83Xvy3cU261MOknd8BdnOk12q4lAWM+kOdsC2Y= github.com/nrdcg/auroradns v1.0.1/go.mod h1:y4pc0i9QXYlFCWrhWrUSIETnZgrf4KuwjDIWmmXo3JI= @@ -379,29 +535,49 @@ github.com/nrdcg/porkbun v0.1.1/go.mod h1:JWl/WKnguWos4mjfp4YizvvToigk9qpQwrodOk github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/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.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.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.14.0 h1:ep6kpPVwmr/nTbklSx2nrLNSIO62DoYAhnPNIMhK8gI= github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr97jXRSSN5MxTgG/zU= github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk= github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA= +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/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -410,27 +586,41 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs= github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= 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.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/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= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 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.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad h1:WtSUHi5zthjudjIi3L6QmL/V9vpJPbc/j/F2u55d3fs= github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad/go.mod h1:h0+DiDRe2Y+6iHTjIq/9HzUq7NII/Nffp0HkFrsAKq4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +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/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= @@ -438,12 +628,18 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sacloud/libsacloud v1.36.2 h1:aosI7clbQ9IU0Hj+3rpk3SKJop5nLPpLThnWCivPqjI= github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f h1:WSnaD0/cvbKJgSTYbjAPf4RJXVvNNDAwVm+W8wEmnGE= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= @@ -462,21 +658,28 @@ github.com/softlayer/softlayer-go v1.0.3/go.mod h1:6HepcfAXROz0Rf63krk5hPZyHT6qy github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +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/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -486,23 +689,22 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +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/transip/gotransip/v6 v6.6.1 h1:nsCU1ErZS5G0FeOpgGXc4FsWvBff9GPswSMggsC4564= github.com/transip/gotransip/v6 v6.6.1/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g= github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo= github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +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.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE= -github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= -github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= -github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14 h1:TFXGGMHmml4rs29PdPisC/aaCzOxUu1Vsh9on/IpUfE= github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg= github.com/vultr/govultr/v2 v2.7.1 h1:uF9ERet++Gb+7Cqs3p1P6b6yebeaZqVd7t5P2uZCaJU= @@ -513,7 +715,13 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= 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/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -521,26 +729,42 @@ go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277 h1:d9qaMM+ODpCq+9We41//fu/sHsTnXcrqd1en3x+GKy4= go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 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= @@ -551,6 +775,8 @@ 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-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= +golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -571,6 +797,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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= @@ -579,6 +807,7 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -589,6 +818,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -604,8 +834,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -629,9 +860,11 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -641,6 +874,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -648,6 +882,7 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -663,17 +898,22 @@ golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= +golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/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.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -681,8 +921,10 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -691,6 +933,7 @@ golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs= golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -699,14 +942,18 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-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= @@ -715,6 +962,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -724,12 +972,17 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -742,6 +995,7 @@ google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= @@ -752,6 +1006,7 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -766,10 +1021,15 @@ google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171 h1:xes2Q2k+d/+YNXVw0FpZkIDJiaux4OVrRKXRAzH6A0U= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= @@ -788,10 +1048,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0= gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -805,6 +1068,7 @@ gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -817,12 +1081,129 @@ 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 h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U= +modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= +modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= +modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= +modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= +modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= +modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= +modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= +modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= +modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= +modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= +modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= +modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= +modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= +modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= +modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= +modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= +modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= +modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= +modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= +modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= +modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= +modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= +modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= +modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= +modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= +modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= +modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= +modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= +modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= +modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4= +modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= +modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= +modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= +modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= +modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8= +modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= +modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= +modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= +modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= +modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= +modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= +modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= +modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= +modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= +modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= +modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= +modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= +modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= +modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= +modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= +modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= +modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= +modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= +modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= +modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= +modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= +modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= +modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= +modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= +modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= +modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= +modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= +modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= +modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= +modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= +modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= +modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= +modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE= +modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= +modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14= +modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= +modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE= +modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8= +modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= +xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM= +xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= +xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM= +xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= diff --git a/html/404.html b/html/404.html deleted file mode 100644 index b7ec96e..0000000 --- a/html/404.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - %status - - - - - - - - - -

- Page not found! -

-
- Sorry, but this page couldn't be found or is inaccessible (%status).
- We hope this isn't a problem on our end ;) - Make sure to check the troubleshooting section in the Docs! -
- - - Static pages made easy - Codeberg Pages - - - diff --git a/html/error.go b/html/error.go deleted file mode 100644 index 325dada..0000000 --- a/html/error.go +++ /dev/null @@ -1,24 +0,0 @@ -package html - -import ( - "bytes" - "strconv" - - "github.com/valyala/fasthttp" -) - -// ReturnErrorPage sets the response status code and writes NotFoundPage to the response body, with "%status" replaced -// with the provided status code. -func ReturnErrorPage(ctx *fasthttp.RequestCtx, code int) { - ctx.Response.SetStatusCode(code) - ctx.Response.Header.SetContentType("text/html; charset=utf-8") - message := fasthttp.StatusMessage(code) - if code == fasthttp.StatusMisdirectedRequest { - message += " - domain not specified in .domains file" - } - if code == fasthttp.StatusFailedDependency { - message += " - target repo/branch doesn't exist or is private" - } - // TODO: use template engine? - ctx.Response.SetBody(bytes.ReplaceAll(NotFoundPage, []byte("%status"), []byte(strconv.Itoa(code)+" "+message))) -} diff --git a/html/html.go b/html/html.go index d223e15..ff8b6b4 100644 --- a/html/html.go +++ b/html/html.go @@ -1,6 +1,53 @@ package html -import _ "embed" +import ( + _ "embed" + "net/http" + "text/template" // do not use html/template here, we sanitize the message before passing it to the template -//go:embed 404.html -var NotFoundPage []byte + "codeberg.org/codeberg/pages/server/context" + "github.com/microcosm-cc/bluemonday" + "github.com/rs/zerolog/log" +) + +//go:embed templates/error.html +var errorPage string + +var ( + errorTemplate = template.Must(template.New("error").Parse(errorPage)) + sanitizer = createBlueMondayPolicy() +) + +type TemplateContext struct { + StatusCode int + StatusText string + Message string +} + +// ReturnErrorPage sets the response status code and writes the error page to the response body. +// The error page contains a sanitized version of the message and the statusCode both in text and numeric form. +// +// Currently, only the following html tags are supported: +func ReturnErrorPage(ctx *context.Context, msg string, statusCode int) { + ctx.RespWriter.Header().Set("Content-Type", "text/html; charset=utf-8") + ctx.RespWriter.WriteHeader(statusCode) + + templateContext := TemplateContext{ + StatusCode: statusCode, + StatusText: http.StatusText(statusCode), + Message: sanitizer.Sanitize(msg), + } + + err := errorTemplate.Execute(ctx.RespWriter, templateContext) + if err != nil { + log.Err(err).Str("message", msg).Int("status", statusCode).Msg("could not write response") + } +} + +func createBlueMondayPolicy() *bluemonday.Policy { + p := bluemonday.NewPolicy() + + p.AllowElements("code") + + return p +} diff --git a/html/html_test.go b/html/html_test.go new file mode 100644 index 0000000..b395bb2 --- /dev/null +++ b/html/html_test.go @@ -0,0 +1,54 @@ +package html + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSanitizerSimpleString(t *testing.T) { + str := "simple text message without any html elements" + + assert.Equal(t, str, sanitizer.Sanitize(str)) +} + +func TestSanitizerStringWithCodeTag(t *testing.T) { + str := "simple text message with html tag" + + assert.Equal(t, str, sanitizer.Sanitize(str)) +} + +func TestSanitizerStringWithCodeTagWithAttribute(t *testing.T) { + str := "simple text message with html tag" + expected := "simple text message with html tag" + + assert.Equal(t, expected, sanitizer.Sanitize(str)) +} + +func TestSanitizerStringWithATag(t *testing.T) { + str := "simple text message with a link to another page" + expected := "simple text message with a link to another page" + + assert.Equal(t, expected, sanitizer.Sanitize(str)) +} + +func TestSanitizerStringWithATagAndHref(t *testing.T) { + str := "simple text message with a link to another page" + expected := "simple text message with a link to another page" + + assert.Equal(t, expected, sanitizer.Sanitize(str)) +} + +func TestSanitizerStringWithImgTag(t *testing.T) { + str := "simple text message with a \"not" + expected := "simple text message with a " + + assert.Equal(t, expected, sanitizer.Sanitize(str)) +} + +func TestSanitizerStringWithImgTagAndOnerrorAttribute(t *testing.T) { + str := "simple text message with a \"not" + expected := "simple text message with a " + + assert.Equal(t, expected, sanitizer.Sanitize(str)) +} diff --git a/html/templates/error.html b/html/templates/error.html new file mode 100644 index 0000000..6094a26 --- /dev/null +++ b/html/templates/error.html @@ -0,0 +1,69 @@ + + + + + + {{.StatusText}} + + + + + + + + + + +

{{.StatusText}} ({{.StatusCode}})!

+
+

Sorry, but this page couldn't be served.

+

"{{.Message}}"

+

+ We hope this isn't a problem on our end ;) - Make sure to check the + troubleshooting section in the Docs! +

+
+ + + Static pages made easy - + Codeberg Pages + + + diff --git a/integration/get_test.go b/integration/get_test.go index 6054e17..cfb7188 100644 --- a/integration/get_test.go +++ b/integration/get_test.go @@ -20,18 +20,20 @@ func TestGetRedirect(t *testing.T) { log.Println("=== TestGetRedirect ===") // test custom domain redirect resp, err := getTestHTTPSClient().Get("https://calciumdibromid.localhost.mock.directory:4430") - assert.NoError(t, err) + if !assert.NoError(t, err) { + t.FailNow() + } if !assert.EqualValues(t, http.StatusTemporaryRedirect, resp.StatusCode) { t.FailNow() } assert.EqualValues(t, "https://www.cabr2.de/", resp.Header.Get("Location")) - assert.EqualValues(t, 0, getSize(resp.Body)) + assert.EqualValues(t, `Temporary Redirect.`, strings.TrimSpace(string(getBytes(resp.Body)))) } func TestGetContent(t *testing.T) { log.Println("=== TestGetContent ===") // test get image - resp, err := getTestHTTPSClient().Get("https://magiclike.localhost.mock.directory:4430/images/827679288a.jpg") + resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/images/827679288a.jpg") assert.NoError(t, err) if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { t.FailNow() @@ -42,81 +44,218 @@ func TestGetContent(t *testing.T) { assert.Len(t, resp.Header.Get("ETag"), 42) // specify branch - resp, err = getTestHTTPSClient().Get("https://momar.localhost.mock.directory:4430/pag/@master/") + resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pag/@master/") assert.NoError(t, err) - if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { + if !assert.NotNil(t, resp) { t.FailNow() } + assert.EqualValues(t, http.StatusOK, resp.StatusCode) assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) assert.True(t, getSize(resp.Body) > 1000) - assert.Len(t, resp.Header.Get("ETag"), 42) + assert.Len(t, resp.Header.Get("ETag"), 44) // access branch name contains '/' - resp, err = getTestHTTPSClient().Get("https://blumia.localhost.mock.directory:4430/pages-server-integration-tests/@docs~main/") + resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/blumia/@docs~main/") assert.NoError(t, err) if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { t.FailNow() } assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) assert.True(t, getSize(resp.Body) > 100) - assert.Len(t, resp.Header.Get("ETag"), 42) + assert.Len(t, resp.Header.Get("ETag"), 44) - // TODO: test get of non cachable content (content size > fileCacheSizeLimit) + // TODO: test get of non cacheable content (content size > fileCacheSizeLimit) } func TestCustomDomain(t *testing.T) { log.Println("=== TestCustomDomain ===") resp, err := getTestHTTPSClient().Get("https://mock-pages.codeberg-test.org:4430/README.md") assert.NoError(t, err) - if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { + if !assert.NotNil(t, resp) { t.FailNow() } + assert.EqualValues(t, http.StatusOK, resp.StatusCode) assert.EqualValues(t, "text/markdown; charset=utf-8", resp.Header.Get("Content-Type")) assert.EqualValues(t, "106", resp.Header.Get("Content-Length")) assert.EqualValues(t, 106, getSize(resp.Body)) } +func TestCustomDomainRedirects(t *testing.T) { + log.Println("=== TestCustomDomainRedirects ===") + // test redirect from default pages domain to custom domain + resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/test_pages-server_custom-mock-domain/@main/README.md") + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusTemporaryRedirect, resp.StatusCode) + assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) + // TODO: custom port is not evaluated (witch does hurt tests & dev env only) + // assert.EqualValues(t, "https://mock-pages.codeberg-test.org:4430/@main/README.md", resp.Header.Get("Location")) + assert.EqualValues(t, "https://mock-pages.codeberg-test.org/@main/README.md", resp.Header.Get("Location")) + assert.EqualValues(t, `https:/codeberg.org/6543/test_pages-server_custom-mock-domain/src/branch/main/README.md; rel="canonical"; rel="canonical"`, resp.Header.Get("Link")) + + // test redirect from an custom domain to the primary custom domain (www.example.com -> example.com) + // regression test to https://codeberg.org/Codeberg/pages-server/issues/153 + resp, err = getTestHTTPSClient().Get("https://mock-pages-redirect.codeberg-test.org:4430/README.md") + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusTemporaryRedirect, resp.StatusCode) + assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) + // TODO: custom port is not evaluated (witch does hurt tests & dev env only) + // assert.EqualValues(t, "https://mock-pages.codeberg-test.org:4430/README.md", resp.Header.Get("Location")) + assert.EqualValues(t, "https://mock-pages.codeberg-test.org/README.md", resp.Header.Get("Location")) +} + +func TestRawCustomDomain(t *testing.T) { + log.Println("=== TestRawCustomDomain ===") + // test raw domain response for custom domain branch + resp, err := getTestHTTPSClient().Get("https://raw.localhost.mock.directory:4430/cb_pages_tests/raw-test/example") // need cb_pages_tests fork + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + assert.EqualValues(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type")) + assert.EqualValues(t, "76", resp.Header.Get("Content-Length")) + assert.EqualValues(t, 76, getSize(resp.Body)) +} + +func TestRawIndex(t *testing.T) { + log.Println("=== TestRawIndex ===") + // test raw domain response for index.html + resp, err := getTestHTTPSClient().Get("https://raw.localhost.mock.directory:4430/cb_pages_tests/raw-test/@branch-test/index.html") // need cb_pages_tests fork + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + assert.EqualValues(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type")) + assert.EqualValues(t, "597", resp.Header.Get("Content-Length")) + assert.EqualValues(t, 597, getSize(resp.Body)) +} + func TestGetNotFound(t *testing.T) { log.Println("=== TestGetNotFound ===") // test custom not found pages - resp, err := getTestHTTPSClient().Get("https://crystal.localhost.mock.directory:4430/pages-404-demo/blah") + resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pages-404-demo/blah") assert.NoError(t, err) - if !assert.EqualValues(t, http.StatusNotFound, resp.StatusCode) { + if !assert.NotNil(t, resp) { t.FailNow() } + assert.EqualValues(t, http.StatusNotFound, resp.StatusCode) assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) assert.EqualValues(t, "37", resp.Header.Get("Content-Length")) assert.EqualValues(t, 37, getSize(resp.Body)) } +func TestRedirect(t *testing.T) { + log.Println("=== TestRedirect ===") + // test redirects + resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/some_redirects/redirect") + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusMovedPermanently, resp.StatusCode) + assert.EqualValues(t, "https://example.com/", resp.Header.Get("Location")) +} + +func TestSPARedirect(t *testing.T) { + log.Println("=== TestSPARedirect ===") + // test SPA redirects + url := "https://cb_pages_tests.localhost.mock.directory:4430/some_redirects/app/aqdjw" + resp, err := getTestHTTPSClient().Get(url) + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + assert.EqualValues(t, url, resp.Request.URL.String()) + assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) + assert.EqualValues(t, "258", resp.Header.Get("Content-Length")) + assert.EqualValues(t, 258, getSize(resp.Body)) +} + +func TestSplatRedirect(t *testing.T) { + log.Println("=== TestSplatRedirect ===") + // test splat redirects + resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/some_redirects/articles/qfopefe") + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusMovedPermanently, resp.StatusCode) + assert.EqualValues(t, "/posts/qfopefe", resp.Header.Get("Location")) +} + func TestFollowSymlink(t *testing.T) { log.Printf("=== TestFollowSymlink ===\n") - resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/link") + // file symlink + resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/link") assert.NoError(t, err) - if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { + if !assert.NotNil(t, resp) { t.FailNow() } + assert.EqualValues(t, http.StatusOK, resp.StatusCode) assert.EqualValues(t, "application/octet-stream", resp.Header.Get("Content-Type")) assert.EqualValues(t, "4", resp.Header.Get("Content-Length")) body := getBytes(resp.Body) assert.EqualValues(t, 4, len(body)) assert.EqualValues(t, "abc\n", string(body)) + + // relative file links (../index.html file in this case) + resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/dir_aim/some/") + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) + assert.EqualValues(t, "an index\n", string(getBytes(resp.Body))) } func TestLFSSupport(t *testing.T) { log.Printf("=== TestLFSSupport ===\n") - resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt") + resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt") assert.NoError(t, err) - if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { + if !assert.NotNil(t, resp) { t.FailNow() } + assert.EqualValues(t, http.StatusOK, resp.StatusCode) body := strings.TrimSpace(string(getBytes(resp.Body))) assert.EqualValues(t, 12, len(body)) assert.EqualValues(t, "actual value", body) } +func TestGetOptions(t *testing.T) { + log.Println("=== TestGetOptions ===") + req, _ := http.NewRequest(http.MethodOptions, "https://mock-pages.codeberg-test.org:4430/README.md", http.NoBody) + resp, err := getTestHTTPSClient().Do(req) + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusNoContent, resp.StatusCode) + assert.EqualValues(t, "GET, HEAD, OPTIONS", resp.Header.Get("Allow")) +} + +func TestHttpRedirect(t *testing.T) { + log.Println("=== TestHttpRedirect ===") + resp, err := getTestHTTPSClient().Get("http://mock-pages.codeberg-test.org:8880/README.md") + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusMovedPermanently, resp.StatusCode) + assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) + assert.EqualValues(t, "https://mock-pages.codeberg-test.org:4430/README.md", resp.Header.Get("Location")) +} + func getTestHTTPSClient() *http.Client { cookieJar, _ := cookiejar.New(nil) return &http.Client{ diff --git a/integration/main_test.go b/integration/main_test.go index 06d553f..6566f78 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -23,12 +23,12 @@ func TestMain(m *testing.M) { } defer func() { serverCancel() - log.Println("=== TestMain: Server STOPED ===") + log.Println("=== TestMain: Server STOPPED ===") }() time.Sleep(10 * time.Second) - os.Exit(m.Run()) + m.Run() } func startServer(ctx context.Context) error { @@ -39,12 +39,16 @@ func startServer(ctx context.Context) error { setEnvIfNotSet("ACME_API", "https://acme.mock.directory") setEnvIfNotSet("PAGES_DOMAIN", "localhost.mock.directory") setEnvIfNotSet("RAW_DOMAIN", "raw.localhost.mock.directory") + setEnvIfNotSet("PAGES_BRANCHES", "pages,main,master") setEnvIfNotSet("PORT", "4430") + setEnvIfNotSet("HTTP_PORT", "8880") + setEnvIfNotSet("ENABLE_HTTP_SERVER", "true") + setEnvIfNotSet("DB_TYPE", "sqlite3") app := cli.NewApp() app.Name = "pages-server" app.Action = cmd.Serve - app.Flags = cmd.ServeFlags + app.Flags = cmd.ServerFlags go func() { if err := app.RunContext(ctx, args); err != nil { diff --git a/main.go b/main.go index 2836b86..6c1d0cc 100644 --- a/main.go +++ b/main.go @@ -8,18 +8,16 @@ import ( "github.com/urfave/cli/v2" "codeberg.org/codeberg/pages/cmd" + "codeberg.org/codeberg/pages/server/version" ) -// can be changed with -X on compile -var version = "dev" - func main() { app := cli.NewApp() app.Name = "pages-server" - app.Version = version + app.Version = version.Version app.Usage = "pages server" app.Action = cmd.Serve - app.Flags = cmd.ServeFlags + app.Flags = cmd.ServerFlags app.Commands = []*cli.Command{ cmd.Certs, } diff --git a/server/certificates/acme_client.go b/server/certificates/acme_client.go new file mode 100644 index 0000000..ba83e50 --- /dev/null +++ b/server/certificates/acme_client.go @@ -0,0 +1,95 @@ +package certificates + +import ( + "fmt" + "sync" + "time" + + "github.com/go-acme/lego/v4/lego" + "github.com/go-acme/lego/v4/providers/dns" + "github.com/reugn/equalizer" + "github.com/rs/zerolog/log" + + "codeberg.org/codeberg/pages/server/cache" +) + +type AcmeClient struct { + legoClient *lego.Client + dnsChallengerLegoClient *lego.Client + + obtainLocks sync.Map + + acmeUseRateLimits bool + + // limiter + acmeClientOrderLimit *equalizer.TokenBucket + acmeClientRequestLimit *equalizer.TokenBucket + acmeClientFailLimit *equalizer.TokenBucket + acmeClientCertificateLimitPerUser map[string]*equalizer.TokenBucket +} + +func NewAcmeClient(acmeAccountConf, acmeAPI, acmeMail, acmeEabHmac, acmeEabKID, dnsProvider string, acmeAcceptTerms, enableHTTPServer, acmeUseRateLimits bool, challengeCache cache.SetGetKey) (*AcmeClient, error) { + acmeConfig, err := setupAcmeConfig(acmeAccountConf, acmeAPI, acmeMail, acmeEabHmac, acmeEabKID, acmeAcceptTerms) + if err != nil { + return nil, err + } + + acmeClient, err := lego.NewClient(acmeConfig) + if err != nil { + log.Fatal().Err(err).Msg("Can't create ACME client, continuing with mock certs only") + } else { + err = acmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache}) + if err != nil { + log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider") + } + if enableHTTPServer { + err = acmeClient.Challenge.SetHTTP01Provider(AcmeHTTPChallengeProvider{challengeCache}) + if err != nil { + log.Error().Err(err).Msg("Can't create HTTP-01 provider") + } + } + } + + mainDomainAcmeClient, err := lego.NewClient(acmeConfig) + if err != nil { + log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only") + } else { + if dnsProvider == "" { + // using mock server, don't use wildcard certs + err := mainDomainAcmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache}) + if err != nil { + log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider") + } + } else { + // use DNS-Challenge https://go-acme.github.io/lego/dns/ + provider, err := dns.NewDNSChallengeProviderByName(dnsProvider) + if err != nil { + return nil, fmt.Errorf("can not create DNS Challenge provider: %w", err) + } + if err := mainDomainAcmeClient.Challenge.SetDNS01Provider(provider); err != nil { + return nil, fmt.Errorf("can not create DNS-01 provider: %w", err) + } + } + } + + return &AcmeClient{ + legoClient: acmeClient, + dnsChallengerLegoClient: mainDomainAcmeClient, + + acmeUseRateLimits: acmeUseRateLimits, + + obtainLocks: sync.Map{}, + + // limiter + + // rate limit is 300 / 3 hours, we want 200 / 2 hours but to refill more often, so that's 25 new domains every 15 minutes + // TODO: when this is used a lot, we probably have to think of a somewhat better solution? + acmeClientOrderLimit: equalizer.NewTokenBucket(25, 15*time.Minute), + // rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests) + acmeClientRequestLimit: equalizer.NewTokenBucket(5, 1*time.Second), + // rate limit is 5 / hour https://letsencrypt.org/docs/failed-validation-limit/ + acmeClientFailLimit: equalizer.NewTokenBucket(5, 1*time.Hour), + // checkUserLimit() use this to rate also per user + acmeClientCertificateLimitPerUser: map[string]*equalizer.TokenBucket{}, + }, nil +} diff --git a/server/certificates/acme_config.go b/server/certificates/acme_config.go new file mode 100644 index 0000000..12ad7c6 --- /dev/null +++ b/server/certificates/acme_config.go @@ -0,0 +1,102 @@ +package certificates + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/json" + "fmt" + "os" + + "github.com/go-acme/lego/v4/certcrypto" + "github.com/go-acme/lego/v4/lego" + "github.com/go-acme/lego/v4/registration" + "github.com/rs/zerolog/log" +) + +const challengePath = "/.well-known/acme-challenge/" + +func setupAcmeConfig(configFile, acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcceptTerms bool) (*lego.Config, error) { + var myAcmeAccount AcmeAccount + var myAcmeConfig *lego.Config + + if account, err := os.ReadFile(configFile); err == nil { + log.Info().Msgf("found existing acme account config file '%s'", configFile) + if err := json.Unmarshal(account, &myAcmeAccount); err != nil { + return nil, err + } + myAcmeAccount.Key, err = certcrypto.ParsePEMPrivateKey([]byte(myAcmeAccount.KeyPEM)) + if err != nil { + return nil, err + } + myAcmeConfig = lego.NewConfig(&myAcmeAccount) + myAcmeConfig.CADirURL = acmeAPI + myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048 + + // Validate Config + _, err := lego.NewClient(myAcmeConfig) + if err != nil { + log.Info().Err(err).Msg("config validation failed, you might just delete the config file and let it recreate") + return nil, fmt.Errorf("acme config validation failed: %w", err) + } + return myAcmeConfig, nil + } else if !os.IsNotExist(err) { + return nil, err + } + + log.Info().Msgf("no existing acme account config found, try to create a new one") + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + myAcmeAccount = AcmeAccount{ + Email: acmeMail, + Key: privateKey, + KeyPEM: string(certcrypto.PEMEncode(privateKey)), + } + myAcmeConfig = lego.NewConfig(&myAcmeAccount) + myAcmeConfig.CADirURL = acmeAPI + myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048 + tempClient, err := lego.NewClient(myAcmeConfig) + if err != nil { + log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only") + } else { + // accept terms & log in to EAB + if acmeEabKID == "" || acmeEabHmac == "" { + reg, err := tempClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: acmeAcceptTerms}) + if err != nil { + log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only") + } else { + myAcmeAccount.Registration = reg + } + } else { + reg, err := tempClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: acmeAcceptTerms, + Kid: acmeEabKID, + HmacEncoded: acmeEabHmac, + }) + if err != nil { + log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only") + } else { + myAcmeAccount.Registration = reg + } + } + + if myAcmeAccount.Registration != nil { + acmeAccountJSON, err := json.Marshal(myAcmeAccount) + if err != nil { + log.Error().Err(err).Msg("json.Marshalfailed, waiting for manual restart to avoid rate limits") + select {} + } + log.Info().Msgf("new acme account created. write to config file '%s'", configFile) + err = os.WriteFile(configFile, acmeAccountJSON, 0o600) + if err != nil { + log.Error().Err(err).Msg("os.WriteFile failed, waiting for manual restart to avoid rate limits") + select {} + } + } + } + + return myAcmeConfig, nil +} diff --git a/server/certificates/cached_challengers.go b/server/certificates/cached_challengers.go new file mode 100644 index 0000000..bc9ea67 --- /dev/null +++ b/server/certificates/cached_challengers.go @@ -0,0 +1,83 @@ +package certificates + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/rs/zerolog/log" + + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/context" +) + +type AcmeTLSChallengeProvider struct { + challengeCache cache.SetGetKey +} + +// make sure AcmeTLSChallengeProvider match Provider interface +var _ challenge.Provider = AcmeTLSChallengeProvider{} + +func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error { + return a.challengeCache.Set(domain, keyAuth, 1*time.Hour) +} + +func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error { + a.challengeCache.Remove(domain) + return nil +} + +type AcmeHTTPChallengeProvider struct { + challengeCache cache.SetGetKey +} + +// make sure AcmeHTTPChallengeProvider match Provider interface +var _ challenge.Provider = AcmeHTTPChallengeProvider{} + +func (a AcmeHTTPChallengeProvider) Present(domain, token, keyAuth string) error { + return a.challengeCache.Set(domain+"/"+token, keyAuth, 1*time.Hour) +} + +func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error { + a.challengeCache.Remove(domain + "/" + token) + return nil +} + +func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey, sslPort uint) http.HandlerFunc { + // handle custom-ssl-ports to be added on https redirects + portPart := "" + if sslPort != 443 { + portPart = fmt.Sprintf(":%d", sslPort) + } + + return func(w http.ResponseWriter, req *http.Request) { + ctx := context.New(w, req) + domain := ctx.TrimHostPort() + + // it's an acme request + if strings.HasPrefix(ctx.Path(), challengePath) { + challenge, ok := challengeCache.Get(domain + "/" + strings.TrimPrefix(ctx.Path(), challengePath)) + if !ok || challenge == nil { + log.Info().Msgf("HTTP-ACME challenge for '%s' failed: token not found", domain) + ctx.String("no challenge for this token", http.StatusNotFound) + } + log.Info().Msgf("HTTP-ACME challenge for '%s' succeeded", domain) + ctx.String(challenge.(string)) + return + } + + // it's a normal http request that needs to be redirected + u, err := url.Parse(fmt.Sprintf("https://%s%s%s", domain, portPart, ctx.Path())) + if err != nil { + log.Error().Err(err).Msg("could not craft http to https redirect") + ctx.String("", http.StatusInternalServerError) + } + + newURL := u.String() + log.Debug().Msgf("redirect http to https: %s", newURL) + ctx.Redirect(newURL, http.StatusMovedPermanently) + } +} diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go index b1c1329..3ae891a 100644 --- a/server/certificates/certificates.go +++ b/server/certificates/certificates.go @@ -1,30 +1,19 @@ package certificates import ( - "bytes" "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" "crypto/tls" "crypto/x509" - "encoding/gob" - "encoding/json" "errors" "fmt" - "os" "strconv" "strings" - "sync" "time" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/go-acme/lego/v4/lego" - "github.com/go-acme/lego/v4/providers/dns" - "github.com/go-acme/lego/v4/registration" "github.com/reugn/equalizer" "github.com/rs/zerolog/log" @@ -35,89 +24,104 @@ import ( "codeberg.org/codeberg/pages/server/upstream" ) +var ErrUserRateLimitExceeded = errors.New("rate limit exceeded: 10 certificates per user per 24 hours") + // TLSConfig returns the configuration for generating, serving and cleaning up Let's Encrypt certificates. -func TLSConfig(mainDomainSuffix []byte, +func TLSConfig(mainDomainSuffix string, giteaClient *gitea.Client, - firstDefaultBranch, - dnsProvider string, - acmeUseRateLimits bool, + acmeClient *AcmeClient, + firstDefaultBranch string, keyCache, challengeCache, dnsLookupCache, canonicalDomainCache cache.SetGetKey, certDB database.CertDB, ) *tls.Config { return &tls.Config{ // check DNS name & get certificate from Let's Encrypt GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { - sni := strings.ToLower(strings.TrimSpace(info.ServerName)) - sniBytes := []byte(sni) - if len(sni) < 1 { - return nil, errors.New("missing sni") + domain := strings.ToLower(strings.TrimSpace(info.ServerName)) + if len(domain) < 1 { + return nil, errors.New("missing domain info via SNI (RFC 4366, Section 3.1)") } + // https request init is actually a acme challenge if info.SupportedProtos != nil { for _, proto := range info.SupportedProtos { - if proto == tlsalpn01.ACMETLS1Protocol { - challenge, ok := challengeCache.Get(sni) - if !ok { - return nil, errors.New("no challenge for this domain") - } - cert, err := tlsalpn01.ChallengeCert(sni, challenge.(string)) - if err != nil { - return nil, err - } - return cert, nil + if proto != tlsalpn01.ACMETLS1Protocol { + continue } + log.Info().Msgf("Detect ACME-TLS1 challenge for '%s'", domain) + + challenge, ok := challengeCache.Get(domain) + if !ok { + return nil, errors.New("no challenge for this domain") + } + cert, err := tlsalpn01.ChallengeCert(domain, challenge.(string)) + if err != nil { + return nil, err + } + return cert, nil } } targetOwner := "" - if bytes.HasSuffix(sniBytes, mainDomainSuffix) || bytes.Equal(sniBytes, mainDomainSuffix[1:]) { + mayObtainCert := true + if strings.HasSuffix(domain, mainDomainSuffix) || strings.EqualFold(domain, mainDomainSuffix[1:]) { // deliver default certificate for the main domain (*.codeberg.page) - sniBytes = mainDomainSuffix - sni = string(sniBytes) + domain = mainDomainSuffix } else { var targetRepo, targetBranch string - targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(sni, string(mainDomainSuffix), firstDefaultBranch, dnsLookupCache) + targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch, dnsLookupCache) if targetOwner == "" { // DNS not set up, return main certificate to redirect to the docs - sniBytes = mainDomainSuffix - sni = string(sniBytes) + domain = mainDomainSuffix } else { - _, _ = targetRepo, targetBranch - _, valid := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, sni, string(mainDomainSuffix), canonicalDomainCache) + targetOpt := &upstream.Options{ + TargetOwner: targetOwner, + TargetRepo: targetRepo, + TargetBranch: targetBranch, + } + _, valid := targetOpt.CheckCanonicalDomain(giteaClient, domain, mainDomainSuffix, canonicalDomainCache) if !valid { - sniBytes = mainDomainSuffix - sni = string(sniBytes) + // We shouldn't obtain a certificate when we cannot check if the + // repository has specified this domain in the `.domains` file. + mayObtainCert = false } } } - if tlsCertificate, ok := keyCache.Get(sni); ok { + if tlsCertificate, ok := keyCache.Get(domain); ok { // we can use an existing certificate object return tlsCertificate.(*tls.Certificate), nil } - var tlsCertificate tls.Certificate + var tlsCertificate *tls.Certificate var err error - var ok bool - if tlsCertificate, ok = retrieveCertFromDB(sniBytes, mainDomainSuffix, dnsProvider, acmeUseRateLimits, certDB); !ok { - // request a new certificate - if bytes.Equal(sniBytes, mainDomainSuffix) { + if tlsCertificate, err = acmeClient.retrieveCertFromDB(domain, mainDomainSuffix, false, certDB); err != nil { + if !errors.Is(err, database.ErrNotFound) { + return nil, err + } + // we could not find a cert in db, request a new certificate + + // first check if we are allowed to obtain a cert for this domain + if strings.EqualFold(domain, mainDomainSuffix) { return nil, errors.New("won't request certificate for main domain, something really bad has happened") } + if !mayObtainCert { + return nil, fmt.Errorf("won't request certificate for %q", domain) + } - tlsCertificate, err = obtainCert(acmeClient, []string{sni}, nil, targetOwner, dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB) + tlsCertificate, err = acmeClient.obtainCert(acmeClient.legoClient, []string{domain}, nil, targetOwner, false, mainDomainSuffix, certDB) if err != nil { return nil, err } } - if err := keyCache.Set(sni, &tlsCertificate, 15*time.Minute); err != nil { + if err := keyCache.Set(domain, tlsCertificate, 15*time.Minute); err != nil { return nil, err } - return &tlsCertificate, nil + return tlsCertificate, nil }, - PreferServerCipherSuites: true, NextProtos: []string{ + "h2", "http/1.1", tlsalpn01.ACMETLS1Protocol, }, @@ -136,159 +140,115 @@ func TLSConfig(mainDomainSuffix []byte, } } -func checkUserLimit(user string) error { - userLimit, ok := acmeClientCertificateLimitPerUser[user] +func (c *AcmeClient) checkUserLimit(user string) error { + userLimit, ok := c.acmeClientCertificateLimitPerUser[user] if !ok { - // Each Codeberg user can only add 10 new domains per day. + // Each user can only add 10 new domains per day. userLimit = equalizer.NewTokenBucket(10, time.Hour*24) - acmeClientCertificateLimitPerUser[user] = userLimit + c.acmeClientCertificateLimitPerUser[user] = userLimit } if !userLimit.Ask() { - return errors.New("rate limit exceeded: 10 certificates per user per 24 hours") + return fmt.Errorf("user '%s' error: %w", user, ErrUserRateLimitExceeded) } return nil } -var ( - acmeClient, mainDomainAcmeClient *lego.Client - acmeClientCertificateLimitPerUser = map[string]*equalizer.TokenBucket{} -) - -// rate limit is 300 / 3 hours, we want 200 / 2 hours but to refill more often, so that's 25 new domains every 15 minutes -// TODO: when this is used a lot, we probably have to think of a somewhat better solution? -var acmeClientOrderLimit = equalizer.NewTokenBucket(25, 15*time.Minute) - -// rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests) -var acmeClientRequestLimit = equalizer.NewTokenBucket(5, 1*time.Second) - -type AcmeTLSChallengeProvider struct { - challengeCache cache.SetGetKey -} - -// make sure AcmeTLSChallengeProvider match Provider interface -var _ challenge.Provider = AcmeTLSChallengeProvider{} - -func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error { - return a.challengeCache.Set(domain, keyAuth, 1*time.Hour) -} - -func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error { - a.challengeCache.Remove(domain) - return nil -} - -type AcmeHTTPChallengeProvider struct { - challengeCache cache.SetGetKey -} - -// make sure AcmeHTTPChallengeProvider match Provider interface -var _ challenge.Provider = AcmeHTTPChallengeProvider{} - -func (a AcmeHTTPChallengeProvider) Present(domain, token, keyAuth string) error { - return a.challengeCache.Set(domain+"/"+token, keyAuth, 1*time.Hour) -} - -func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error { - a.challengeCache.Remove(domain + "/" + token) - return nil -} - -func retrieveCertFromDB(sni, mainDomainSuffix []byte, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) (tls.Certificate, bool) { +func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProvider bool, certDB database.CertDB) (*tls.Certificate, error) { // parse certificate from database - res, err := certDB.Get(string(sni)) + res, err := certDB.Get(sni) if err != nil { - panic(err) // TODO: no panic - } - if res == nil { - return tls.Certificate{}, false + return nil, err + } else if res == nil { + return nil, database.ErrNotFound } tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey) if err != nil { - panic(err) + return nil, err } // TODO: document & put into own function - if !bytes.Equal(sni, mainDomainSuffix) { + if !strings.EqualFold(sni, mainDomainSuffix) { tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0]) if err != nil { - panic(err) + return nil, fmt.Errorf("error parsing leaf tlsCert: %w", err) } // renew certificates 7 days before they expire - if !tlsCertificate.Leaf.NotAfter.After(time.Now().Add(7 * 24 * time.Hour)) { - // TODO: add ValidUntil to custom res struct + if tlsCertificate.Leaf.NotAfter.Before(time.Now().Add(7 * 24 * time.Hour)) { + // TODO: use ValidTill of custom cert struct if res.CSR != nil && len(res.CSR) > 0 { // CSR stores the time when the renewal shall be tried again nextTryUnix, err := strconv.ParseInt(string(res.CSR), 10, 64) if err == nil && time.Now().Before(time.Unix(nextTryUnix, 0)) { - return tlsCertificate, true + return &tlsCertificate, nil } } + // TODO: make a queue ? go (func() { res.CSR = nil // acme client doesn't like CSR to be set - tlsCertificate, err = obtainCert(acmeClient, []string{string(sni)}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB) - if err != nil { - log.Error().Msgf("Couldn't renew certificate for %s: %v", string(sni), err) + if _, err := c.obtainCert(c.legoClient, []string{sni}, res, "", useDnsProvider, mainDomainSuffix, certDB); err != nil { + log.Error().Msgf("Couldn't renew certificate for %s: %v", sni, err) } })() } } - return tlsCertificate, true + return &tlsCertificate, nil } -var obtainLocks = sync.Map{} - -func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user, dnsProvider string, mainDomainSuffix []byte, acmeUseRateLimits bool, keyDatabase database.CertDB) (tls.Certificate, error) { +func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user string, useDnsProvider bool, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) { name := strings.TrimPrefix(domains[0], "*") - if dnsProvider == "" && len(domains[0]) > 0 && domains[0][0] == '*' { + if useDnsProvider && len(domains[0]) > 0 && domains[0][0] == '*' { domains = domains[1:] } // lock to avoid simultaneous requests - _, working := obtainLocks.LoadOrStore(name, struct{}{}) + _, working := c.obtainLocks.LoadOrStore(name, struct{}{}) if working { for working { time.Sleep(100 * time.Millisecond) - _, working = obtainLocks.Load(name) + _, working = c.obtainLocks.Load(name) } - cert, ok := retrieveCertFromDB([]byte(name), mainDomainSuffix, dnsProvider, acmeUseRateLimits, keyDatabase) - if !ok { - return tls.Certificate{}, errors.New("certificate failed in synchronous request") + cert, err := c.retrieveCertFromDB(name, mainDomainSuffix, useDnsProvider, keyDatabase) + if err != nil { + return nil, fmt.Errorf("certificate failed in synchronous request: %w", err) } return cert, nil } - defer obtainLocks.Delete(name) + defer c.obtainLocks.Delete(name) if acmeClient == nil { - return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", string(mainDomainSuffix), keyDatabase), nil + return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase) } // request actual cert var res *certificate.Resource var err error if renew != nil && renew.CertURL != "" { - if acmeUseRateLimits { - acmeClientRequestLimit.Take() + if c.acmeUseRateLimits { + c.acmeClientRequestLimit.Take() } log.Debug().Msgf("Renewing certificate for: %v", domains) res, err = acmeClient.Certificate.Renew(*renew, true, false, "") if err != nil { log.Error().Err(err).Msgf("Couldn't renew certificate for %v, trying to request a new one", domains) + if c.acmeUseRateLimits { + c.acmeClientFailLimit.Take() + } res = nil } } if res == nil { if user != "" { - if err := checkUserLimit(user); err != nil { - return tls.Certificate{}, err + if err := c.checkUserLimit(user); err != nil { + return nil, err } } - if acmeUseRateLimits { - acmeClientOrderLimit.Take() - acmeClientRequestLimit.Take() + if c.acmeUseRateLimits { + c.acmeClientOrderLimit.Take() + c.acmeClientRequestLimit.Take() } log.Debug().Msgf("Re-requesting new certificate for %v", domains) res, err = acmeClient.Certificate.Obtain(certificate.ObtainRequest{ @@ -296,163 +256,58 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re Bundle: true, MustStaple: false, }) + if c.acmeUseRateLimits && err != nil { + c.acmeClientFailLimit.Take() + } } if err != nil { log.Error().Err(err).Msgf("Couldn't obtain again a certificate or %v", domains) if renew != nil && renew.CertURL != "" { tlsCertificate, err := tls.X509KeyPair(renew.Certificate, renew.PrivateKey) - if err == nil && tlsCertificate.Leaf.NotAfter.After(time.Now()) { + if err != nil { + mockC, err2 := mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase) + if err2 != nil { + return nil, errors.Join(err, err2) + } + return mockC, err + } + leaf, err := leaf(&tlsCertificate) + if err == nil && leaf.NotAfter.After(time.Now()) { // avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at renew.CSR = []byte(strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10)) if err := keyDatabase.Put(name, renew); err != nil { - return mockCert(domains[0], err.Error(), string(mainDomainSuffix), keyDatabase), err + mockC, err2 := mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase) + if err2 != nil { + return nil, errors.Join(err, err2) + } + return mockC, err } - return tlsCertificate, nil + return &tlsCertificate, nil } } - return mockCert(domains[0], err.Error(), string(mainDomainSuffix), keyDatabase), err + return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase) } log.Debug().Msgf("Obtained certificate for %v", domains) if err := keyDatabase.Put(name, res); err != nil { - return tls.Certificate{}, err + return nil, err } tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey) if err != nil { - return tls.Certificate{}, err - } - return tlsCertificate, nil -} - -func SetupAcmeConfig(acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcceptTerms bool) (*lego.Config, error) { - const configFile = "acme-account.json" - var myAcmeAccount AcmeAccount - var myAcmeConfig *lego.Config - - if account, err := os.ReadFile(configFile); err == nil { - if err := json.Unmarshal(account, &myAcmeAccount); err != nil { - return nil, err - } - myAcmeAccount.Key, err = certcrypto.ParsePEMPrivateKey([]byte(myAcmeAccount.KeyPEM)) - if err != nil { - return nil, err - } - myAcmeConfig = lego.NewConfig(&myAcmeAccount) - myAcmeConfig.CADirURL = acmeAPI - myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048 - - // Validate Config - _, err := lego.NewClient(myAcmeConfig) - if err != nil { - // TODO: should we fail hard instead? - log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only") - } - return myAcmeConfig, nil - } else if !os.IsNotExist(err) { return nil, err } - - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, err - } - myAcmeAccount = AcmeAccount{ - Email: acmeMail, - Key: privateKey, - KeyPEM: string(certcrypto.PEMEncode(privateKey)), - } - myAcmeConfig = lego.NewConfig(&myAcmeAccount) - myAcmeConfig.CADirURL = acmeAPI - myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048 - tempClient, err := lego.NewClient(myAcmeConfig) - if err != nil { - log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only") - } else { - // accept terms & log in to EAB - if acmeEabKID == "" || acmeEabHmac == "" { - reg, err := tempClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: acmeAcceptTerms}) - if err != nil { - log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only") - } else { - myAcmeAccount.Registration = reg - } - } else { - reg, err := tempClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ - TermsOfServiceAgreed: acmeAcceptTerms, - Kid: acmeEabKID, - HmacEncoded: acmeEabHmac, - }) - if err != nil { - log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only") - } else { - myAcmeAccount.Registration = reg - } - } - - if myAcmeAccount.Registration != nil { - acmeAccountJSON, err := json.Marshal(myAcmeAccount) - if err != nil { - log.Error().Err(err).Msg("json.Marshalfailed, waiting for manual restart to avoid rate limits") - select {} - } - err = os.WriteFile(configFile, acmeAccountJSON, 0o600) - if err != nil { - log.Error().Err(err).Msg("os.WriteFile failed, waiting for manual restart to avoid rate limits") - select {} - } - } - } - - return myAcmeConfig, nil + return &tlsCertificate, nil } -func SetupCertificates(mainDomainSuffix []byte, dnsProvider string, acmeConfig *lego.Config, acmeUseRateLimits, enableHTTPServer bool, challengeCache cache.SetGetKey, certDB database.CertDB) error { +func SetupMainDomainCertificates(mainDomainSuffix string, acmeClient *AcmeClient, certDB database.CertDB) error { // getting main cert before ACME account so that we can fail here without hitting rate limits - mainCertBytes, err := certDB.Get(string(mainDomainSuffix)) - if err != nil { - return fmt.Errorf("cert database is not working") - } - - acmeClient, err = lego.NewClient(acmeConfig) - if err != nil { - log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only") - } else { - err = acmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache}) - if err != nil { - log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider") - } - if enableHTTPServer { - err = acmeClient.Challenge.SetHTTP01Provider(AcmeHTTPChallengeProvider{challengeCache}) - if err != nil { - log.Error().Err(err).Msg("Can't create HTTP-01 provider") - } - } - } - - mainDomainAcmeClient, err = lego.NewClient(acmeConfig) - if err != nil { - log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only") - } else { - if dnsProvider == "" { - // using mock server, don't use wildcard certs - err := mainDomainAcmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache}) - if err != nil { - log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider") - } - } else { - provider, err := dns.NewDNSChallengeProviderByName(dnsProvider) - if err != nil { - log.Error().Err(err).Msg("Can't create DNS Challenge provider") - } - err = mainDomainAcmeClient.Challenge.SetDNS01Provider(provider) - if err != nil { - log.Error().Err(err).Msg("Can't create DNS-01 provider") - } - } + mainCertBytes, err := certDB.Get(mainDomainSuffix) + if err != nil && !errors.Is(err, database.ErrNotFound) { + return fmt.Errorf("cert database is not working: %w", err) } if mainCertBytes == nil { - _, err = obtainCert(mainDomainAcmeClient, []string{"*" + string(mainDomainSuffix), string(mainDomainSuffix[1:])}, nil, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB) + _, err = acmeClient.obtainCert(acmeClient.dnsChallengerLegoClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, nil, "", true, mainDomainSuffix, certDB) if err != nil { log.Error().Err(err).Msg("Couldn't renew main domain certificate, continuing with mock certs only") } @@ -461,58 +316,45 @@ func SetupCertificates(mainDomainSuffix []byte, dnsProvider string, acmeConfig * return nil } -func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffix []byte, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) { +func MaintainCertDB(ctx context.Context, interval time.Duration, acmeClient *AcmeClient, mainDomainSuffix string, certDB database.CertDB) { for { - // clean up expired certs - now := time.Now() + // delete expired certs that will be invalid until next clean up + threshold := time.Now().Add(interval) expiredCertCount := 0 - keyDatabaseIterator := certDB.Items() - key, resBytes, err := keyDatabaseIterator.Next() - for err == nil { - if !bytes.Equal(key, mainDomainSuffix) { - resGob := bytes.NewBuffer(resBytes) - resDec := gob.NewDecoder(resGob) - res := &certificate.Resource{} - err = resDec.Decode(res) - if err != nil { - panic(err) - } - tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate) - if err != nil || !tlsCertificates[0].NotAfter.After(now) { - err := certDB.Delete(string(key)) - if err != nil { - log.Error().Err(err).Msgf("Deleting expired certificate for %q failed", string(key)) - } else { - expiredCertCount++ + certs, err := certDB.Items(0, 0) + if err != nil { + log.Error().Err(err).Msg("could not get certs from list") + } else { + for _, cert := range certs { + if !strings.EqualFold(cert.Domain, strings.TrimPrefix(mainDomainSuffix, ".")) { + if time.Unix(cert.ValidTill, 0).Before(threshold) { + err := certDB.Delete(cert.Domain) + if err != nil { + log.Error().Err(err).Msgf("Deleting expired certificate for %q failed", cert.Domain) + } else { + expiredCertCount++ + } } } } - key, resBytes, err = keyDatabaseIterator.Next() - } - log.Debug().Msgf("Removed %d expired certificates from the database", expiredCertCount) - - // compact the database - msg, err := certDB.Compact() - if err != nil { - log.Error().Err(err).Msg("Compacting key database failed") - } else { - log.Debug().Msgf("Compacted key database: %s", msg) + log.Debug().Msgf("Removed %d expired certificates from the database", expiredCertCount) } // update main cert - res, err := certDB.Get(string(mainDomainSuffix)) + res, err := certDB.Get(mainDomainSuffix) if err != nil { log.Error().Msgf("Couldn't get cert for domain %q", mainDomainSuffix) } else if res == nil { - log.Error().Msgf("Couldn't renew certificate for main domain %q expected main domain cert to exist, but it's missing - seems like the database is corrupted", string(mainDomainSuffix)) + log.Error().Msgf("Couldn't renew certificate for main domain %q expected main domain cert to exist, but it's missing - seems like the database is corrupted", mainDomainSuffix) } else { tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate) - - // renew main certificate 30 days before it expires - if !tlsCertificates[0].NotAfter.After(time.Now().Add(30 * 24 * time.Hour)) { + if err != nil { + log.Error().Err(fmt.Errorf("could not parse cert for mainDomainSuffix: %w", err)) + } else if tlsCertificates[0].NotAfter.Before(time.Now().Add(30 * 24 * time.Hour)) { + // renew main certificate 30 days before it expires go (func() { - _, err = obtainCert(mainDomainAcmeClient, []string{"*" + string(mainDomainSuffix), string(mainDomainSuffix[1:])}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB) + _, err = acmeClient.obtainCert(acmeClient.dnsChallengerLegoClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, res, "", true, mainDomainSuffix, certDB) if err != nil { log.Error().Err(err).Msg("Couldn't renew certificate for main domain") } @@ -527,3 +369,12 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi } } } + +// leaf returns the parsed leaf certificate, either from c.leaf or by parsing +// the corresponding c.Certificate[0]. +func leaf(c *tls.Certificate) (*x509.Certificate, error) { + if c.Leaf != nil { + return c.Leaf, nil + } + return x509.ParseCertificate(c.Certificate[0]) +} diff --git a/server/certificates/mock.go b/server/certificates/mock.go index 0e87e6e..a28d0f4 100644 --- a/server/certificates/mock.go +++ b/server/certificates/mock.go @@ -13,14 +13,15 @@ import ( "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" + "github.com/rs/zerolog/log" "codeberg.org/codeberg/pages/server/database" ) -func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) tls.Certificate { +func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) { key, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048) if err != nil { - panic(err) + return nil, err } template := x509.Certificate{ @@ -52,7 +53,7 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) key, ) if err != nil { - panic(err) + return nil, err } out := &bytes.Buffer{} @@ -61,7 +62,7 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) Type: "CERTIFICATE", }) if err != nil { - panic(err) + return nil, err } outBytes := out.Bytes() res := &certificate.Resource{ @@ -75,12 +76,12 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) databaseName = mainDomainSuffix } if err := keyDatabase.Put(databaseName, res); err != nil { - panic(err) + log.Error().Err(err) } tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey) if err != nil { - panic(err) + return nil, err } - return tlsCertificate + return &tlsCertificate, nil } diff --git a/server/certificates/mock_test.go b/server/certificates/mock_test.go index 1cbd1f6..644e8a9 100644 --- a/server/certificates/mock_test.go +++ b/server/certificates/mock_test.go @@ -3,14 +3,18 @@ package certificates import ( "testing" - "codeberg.org/codeberg/pages/server/database" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "codeberg.org/codeberg/pages/server/database" ) func TestMockCert(t *testing.T) { - db, err := database.NewTmpDB() + db := database.NewMockCertDB(t) + db.Mock.On("Put", mock.Anything, mock.Anything).Return(nil) + + cert, err := mockCert("example.com", "some error msg", "codeberg.page", db) assert.NoError(t, err) - cert := mockCert("example.com", "some error msg", "codeberg.page", db) if assert.NotEmpty(t, cert) { assert.NotEmpty(t, cert.Certificate) } diff --git a/server/context/context.go b/server/context/context.go new file mode 100644 index 0000000..6650164 --- /dev/null +++ b/server/context/context.go @@ -0,0 +1,62 @@ +package context + +import ( + stdContext "context" + "net/http" + + "codeberg.org/codeberg/pages/server/utils" +) + +type Context struct { + RespWriter http.ResponseWriter + Req *http.Request + StatusCode int +} + +func New(w http.ResponseWriter, r *http.Request) *Context { + return &Context{ + RespWriter: w, + Req: r, + StatusCode: http.StatusOK, + } +} + +func (c *Context) Context() stdContext.Context { + if c.Req != nil { + return c.Req.Context() + } + return stdContext.Background() +} + +func (c *Context) Response() *http.Response { + if c.Req != nil && c.Req.Response != nil { + return c.Req.Response + } + return nil +} + +func (c *Context) String(raw string, status ...int) { + code := http.StatusOK + if len(status) != 0 { + code = status[0] + } + c.RespWriter.WriteHeader(code) + _, _ = c.RespWriter.Write([]byte(raw)) +} + +func (c *Context) Redirect(uri string, statusCode int) { + http.Redirect(c.RespWriter, c.Req, uri, statusCode) +} + +// Path returns the cleaned requested path. +func (c *Context) Path() string { + return utils.CleanPath(c.Req.URL.Path) +} + +func (c *Context) Host() string { + return c.Req.URL.Host +} + +func (c *Context) TrimHostPort() string { + return utils.TrimHostPort(c.Req.Host) +} diff --git a/server/database/interface.go b/server/database/interface.go index 3ba3efc..7fdbae7 100644 --- a/server/database/interface.go +++ b/server/database/interface.go @@ -1,15 +1,78 @@ package database import ( - "github.com/akrylysov/pogreb" + "fmt" + + "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" + "github.com/rs/zerolog/log" ) +//go:generate go install github.com/vektra/mockery/v2@latest +//go:generate mockery --name CertDB --output . --filename mock.go --inpackage --case underscore + type CertDB interface { Close() error Put(name string, cert *certificate.Resource) error Get(name string) (*certificate.Resource, error) Delete(key string) error - Compact() (string, error) - Items() *pogreb.ItemIterator + Items(page, pageSize int) ([]*Cert, error) +} + +type Cert struct { + Domain string `xorm:"pk NOT NULL UNIQUE 'domain'"` + Created int64 `xorm:"created NOT NULL DEFAULT 0 'created'"` + Updated int64 `xorm:"updated NOT NULL DEFAULT 0 'updated'"` + ValidTill int64 `xorm:" NOT NULL DEFAULT 0 'valid_till'"` + // certificate.Resource + CertURL string `xorm:"'cert_url'"` + CertStableURL string `xorm:"'cert_stable_url'"` + PrivateKey []byte `xorm:"'private_key'"` + Certificate []byte `xorm:"'certificate'"` + IssuerCertificate []byte `xorm:"'issuer_certificate'"` +} + +func (c Cert) Raw() *certificate.Resource { + return &certificate.Resource{ + Domain: c.Domain, + CertURL: c.CertURL, + CertStableURL: c.CertStableURL, + PrivateKey: c.PrivateKey, + Certificate: c.Certificate, + IssuerCertificate: c.IssuerCertificate, + } +} + +func toCert(name string, c *certificate.Resource) (*Cert, error) { + tlsCertificates, err := certcrypto.ParsePEMBundle(c.Certificate) + if err != nil { + return nil, err + } + if len(tlsCertificates) == 0 || tlsCertificates[0] == nil { + err := fmt.Errorf("parsed cert resource has no cert") + log.Error().Err(err).Str("domain", c.Domain).Msgf("cert: %v", c) + return nil, err + } + validTill := tlsCertificates[0].NotAfter.Unix() + + // handle wildcard certs + if name[:1] == "." { + name = "*" + name + } + if name != c.Domain { + err := fmt.Errorf("domain key '%s' and cert domain '%s' not equal", name, c.Domain) + log.Error().Err(err).Msg("toCert conversion did discover mismatch") + // TODO: fail hard: return nil, err + } + + return &Cert{ + Domain: c.Domain, + ValidTill: validTill, + + CertURL: c.CertURL, + CertStableURL: c.CertStableURL, + PrivateKey: c.PrivateKey, + Certificate: c.Certificate, + IssuerCertificate: c.IssuerCertificate, + }, nil } diff --git a/server/database/mock.go b/server/database/mock.go index e6c1b5a..e7e2c38 100644 --- a/server/database/mock.go +++ b/server/database/mock.go @@ -1,55 +1,122 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + package database import ( - "fmt" - "time" - - "github.com/OrlovEvgeny/go-mcache" - "github.com/akrylysov/pogreb" - "github.com/go-acme/lego/v4/certificate" + certificate "github.com/go-acme/lego/v4/certificate" + mock "github.com/stretchr/testify/mock" ) -var _ CertDB = tmpDB{} - -type tmpDB struct { - intern *mcache.CacheDriver - ttl time.Duration +// MockCertDB is an autogenerated mock type for the CertDB type +type MockCertDB struct { + mock.Mock } -func (p tmpDB) Close() error { - _ = p.intern.Close() - return nil -} +// Close provides a mock function with given fields: +func (_m *MockCertDB) Close() error { + ret := _m.Called() -func (p tmpDB) Put(name string, cert *certificate.Resource) error { - return p.intern.Set(name, cert, p.ttl) -} - -func (p tmpDB) Get(name string) (*certificate.Resource, error) { - cert, has := p.intern.Get(name) - if !has { - return nil, fmt.Errorf("cert for '%s' not found", name) + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) } - return cert.(*certificate.Resource), nil + + return r0 } -func (p tmpDB) Delete(key string) error { - p.intern.Remove(key) - return nil +// Delete provides a mock function with given fields: key +func (_m *MockCertDB) Delete(key string) error { + ret := _m.Called(key) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(key) + } else { + r0 = ret.Error(0) + } + + return r0 } -func (p tmpDB) Compact() (string, error) { - p.intern.Truncate() - return "Truncate done", nil +// Get provides a mock function with given fields: name +func (_m *MockCertDB) Get(name string) (*certificate.Resource, error) { + ret := _m.Called(name) + + var r0 *certificate.Resource + var r1 error + if rf, ok := ret.Get(0).(func(string) (*certificate.Resource, error)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) *certificate.Resource); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*certificate.Resource) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (p tmpDB) Items() *pogreb.ItemIterator { - panic("ItemIterator not implemented for tmpDB") +// Items provides a mock function with given fields: page, pageSize +func (_m *MockCertDB) Items(page int, pageSize int) ([]*Cert, error) { + ret := _m.Called(page, pageSize) + + var r0 []*Cert + var r1 error + if rf, ok := ret.Get(0).(func(int, int) ([]*Cert, error)); ok { + return rf(page, pageSize) + } + if rf, ok := ret.Get(0).(func(int, int) []*Cert); ok { + r0 = rf(page, pageSize) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*Cert) + } + } + + if rf, ok := ret.Get(1).(func(int, int) error); ok { + r1 = rf(page, pageSize) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func NewTmpDB() (CertDB, error) { - return &tmpDB{ - intern: mcache.New(), - ttl: time.Minute, - }, nil +// Put provides a mock function with given fields: name, cert +func (_m *MockCertDB) Put(name string, cert *certificate.Resource) error { + ret := _m.Called(name, cert) + + var r0 error + if rf, ok := ret.Get(0).(func(string, *certificate.Resource) error); ok { + r0 = rf(name, cert) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewMockCertDB interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockCertDB creates a new instance of MockCertDB. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockCertDB(t mockConstructorTestingTNewMockCertDB) *MockCertDB { + mock := &MockCertDB{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } diff --git a/server/database/setup.go b/server/database/setup.go deleted file mode 100644 index 1c5a0af..0000000 --- a/server/database/setup.go +++ /dev/null @@ -1,109 +0,0 @@ -package database - -import ( - "bytes" - "context" - "encoding/gob" - "fmt" - "time" - - "github.com/akrylysov/pogreb" - "github.com/akrylysov/pogreb/fs" - "github.com/go-acme/lego/v4/certificate" - "github.com/rs/zerolog/log" -) - -var _ CertDB = aDB{} - -type aDB struct { - ctx context.Context - cancel context.CancelFunc - intern *pogreb.DB - syncInterval time.Duration -} - -func (p aDB) Close() error { - p.cancel() - return p.intern.Sync() -} - -func (p aDB) Put(name string, cert *certificate.Resource) error { - var resGob bytes.Buffer - if err := gob.NewEncoder(&resGob).Encode(cert); err != nil { - return err - } - return p.intern.Put([]byte(name), resGob.Bytes()) -} - -func (p aDB) Get(name string) (*certificate.Resource, error) { - cert := &certificate.Resource{} - resBytes, err := p.intern.Get([]byte(name)) - if err != nil { - return nil, err - } - if resBytes == nil { - return nil, nil - } - if err = gob.NewDecoder(bytes.NewBuffer(resBytes)).Decode(cert); err != nil { - return nil, err - } - return cert, nil -} - -func (p aDB) Delete(key string) error { - return p.intern.Delete([]byte(key)) -} - -func (p aDB) Compact() (string, error) { - result, err := p.intern.Compact() - if err != nil { - return "", err - } - return fmt.Sprintf("%+v", result), nil -} - -func (p aDB) Items() *pogreb.ItemIterator { - return p.intern.Items() -} - -var _ CertDB = &aDB{} - -func (p aDB) sync() { - for { - err := p.intern.Sync() - if err != nil { - log.Error().Err(err).Msg("Syncing cert database failed") - } - select { - case <-p.ctx.Done(): - return - case <-time.After(p.syncInterval): - } - } -} - -func New(path string) (CertDB, error) { - if path == "" { - return nil, fmt.Errorf("path not set") - } - db, err := pogreb.Open(path, &pogreb.Options{ - BackgroundSyncInterval: 30 * time.Second, - BackgroundCompactionInterval: 6 * time.Hour, - FileSystem: fs.OSMMap, - }) - if err != nil { - return nil, err - } - - ctx, cancel := context.WithCancel(context.Background()) - result := &aDB{ - ctx: ctx, - cancel: cancel, - intern: db, - syncInterval: 5 * time.Minute, - } - - go result.sync() - - return result, nil -} diff --git a/server/database/xorm.go b/server/database/xorm.go new file mode 100644 index 0000000..217b6d1 --- /dev/null +++ b/server/database/xorm.go @@ -0,0 +1,151 @@ +package database + +import ( + "errors" + "fmt" + + "github.com/rs/zerolog/log" + + "github.com/go-acme/lego/v4/certificate" + "xorm.io/xorm" + + // register sql driver + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" +) + +var _ CertDB = xDB{} + +var ErrNotFound = errors.New("entry not found") + +type xDB struct { + engine *xorm.Engine +} + +func NewXormDB(dbType, dbConn string) (CertDB, error) { + if !supportedDriver(dbType) { + return nil, fmt.Errorf("not supported db type '%s'", dbType) + } + if dbConn == "" { + return nil, fmt.Errorf("no db connection provided") + } + + e, err := xorm.NewEngine(dbType, dbConn) + if err != nil { + return nil, err + } + + if err := e.Sync2(new(Cert)); err != nil { + return nil, fmt.Errorf("could not sync db model :%w", err) + } + + return &xDB{ + engine: e, + }, nil +} + +func (x xDB) Close() error { + return x.engine.Close() +} + +func (x xDB) Put(domain string, cert *certificate.Resource) error { + log.Trace().Str("domain", cert.Domain).Msg("inserting cert to db") + + domain = integrationTestReplacements(domain) + c, err := toCert(domain, cert) + if err != nil { + return err + } + + sess := x.engine.NewSession() + if err := sess.Begin(); err != nil { + return err + } + defer sess.Close() + + if exist, _ := sess.ID(c.Domain).Exist(new(Cert)); exist { + if _, err := sess.ID(c.Domain).Update(c); err != nil { + return err + } + } else { + if _, err = sess.Insert(c); err != nil { + return err + } + } + + return sess.Commit() +} + +func (x xDB) Get(domain string) (*certificate.Resource, error) { + // handle wildcard certs + if domain[:1] == "." { + domain = "*" + domain + } + domain = integrationTestReplacements(domain) + + cert := new(Cert) + log.Trace().Str("domain", domain).Msg("get cert from db") + if found, err := x.engine.ID(domain).Get(cert); err != nil { + return nil, err + } else if !found { + return nil, fmt.Errorf("%w: name='%s'", ErrNotFound, domain) + } + return cert.Raw(), nil +} + +func (x xDB) Delete(domain string) error { + // handle wildcard certs + if domain[:1] == "." { + domain = "*" + domain + } + domain = integrationTestReplacements(domain) + + log.Trace().Str("domain", domain).Msg("delete cert from db") + _, err := x.engine.ID(domain).Delete(new(Cert)) + return err +} + +// Items return al certs from db, if pageSize is 0 it does not use limit +func (x xDB) Items(page, pageSize int) ([]*Cert, error) { + // paginated return + if pageSize > 0 { + certs := make([]*Cert, 0, pageSize) + if page >= 0 { + page = 1 + } + err := x.engine.Limit(pageSize, (page-1)*pageSize).Find(&certs) + return certs, err + } + + // return all + certs := make([]*Cert, 0, 64) + err := x.engine.Find(&certs) + return certs, err +} + +// Supported database drivers +const ( + DriverSqlite = "sqlite3" + DriverMysql = "mysql" + DriverPostgres = "postgres" +) + +func supportedDriver(driver string) bool { + switch driver { + case DriverMysql, DriverPostgres, DriverSqlite: + return true + default: + return false + } +} + +// integrationTestReplacements is needed because integration tests use a single domain cert, +// while production use a wildcard cert +// TODO: find a better way to handle this +func integrationTestReplacements(domainKey string) string { + if domainKey == "*.localhost.mock.directory" { + return "localhost.mock.directory" + } + return domainKey +} diff --git a/server/database/xorm_test.go b/server/database/xorm_test.go new file mode 100644 index 0000000..50d8a7f --- /dev/null +++ b/server/database/xorm_test.go @@ -0,0 +1,92 @@ +package database + +import ( + "errors" + "testing" + + "github.com/go-acme/lego/v4/certificate" + "github.com/stretchr/testify/assert" + "xorm.io/xorm" +) + +func newTestDB(t *testing.T) *xDB { + e, err := xorm.NewEngine("sqlite3", ":memory:") + assert.NoError(t, err) + assert.NoError(t, e.Sync2(new(Cert))) + return &xDB{engine: e} +} + +func TestSanitizeWildcardCerts(t *testing.T) { + certDB := newTestDB(t) + + _, err := certDB.Get(".not.found") + assert.True(t, errors.Is(err, ErrNotFound)) + + // TODO: cert key and domain mismatch are don not fail hard jet + // https://codeberg.org/Codeberg/pages-server/src/commit/d8595cee882e53d7f44f1ddc4ef8a1f7b8f31d8d/server/database/interface.go#L64 + // + // assert.Error(t, certDB.Put(".wildcard.de", &certificate.Resource{ + // Domain: "*.localhost.mock.directory", + // Certificate: localhost_mock_directory_certificate, + // })) + + // insert new wildcard cert + assert.NoError(t, certDB.Put(".wildcard.de", &certificate.Resource{ + Domain: "*.wildcard.de", + Certificate: localhost_mock_directory_certificate, + })) + + // update existing cert + assert.NoError(t, certDB.Put(".wildcard.de", &certificate.Resource{ + Domain: "*.wildcard.de", + Certificate: localhost_mock_directory_certificate, + })) + + c1, err := certDB.Get(".wildcard.de") + assert.NoError(t, err) + c2, err := certDB.Get("*.wildcard.de") + assert.NoError(t, err) + assert.EqualValues(t, c1, c2) +} + +var localhost_mock_directory_certificate = []byte(`-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgIIJyBaXHmLk6gwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE +AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA0OWE0ZmIwHhcNMjMwMjEwMDEwOTA2 +WhcNMjgwMjEwMDEwOTA2WjAjMSEwHwYDVQQDExhsb2NhbGhvc3QubW9jay5kaXJl +Y3RvcnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIU/CjzS7t62Gj +neEMqvP7sn99ULT7AEUzEfWL05fWG2z714qcUg1hXkZLgdVDgmsCpplyddip7+2t +ZH/9rLPLMqJphzvOL4CF6jDLbeifETtKyjnt9vUZFnnNWcP3tu8lo8iYSl08qsUI +Pp/hiEriAQzCDjTbR5m9xUPNPYqxzcS4ALzmmCX9Qfc4CuuhMkdv2G4TT7rylWrA +SCSRPnGjeA7pCByfNrO/uXbxmzl3sMO3k5sqgMkx1QIHEN412V8+vtx88mt2sM6k +xjzGZWWKXlRq+oufIKX9KPplhsCjMH6E3VNAzgOPYDqXagtUcGmLWghURltO8Mt2 +zwM6OgjjAgMBAAGjgaUwgaIwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG +AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSMQvlJ1755 +sarf8i1KNqj7s5o/aDAfBgNVHSMEGDAWgBTcZcxJMhWdP7MecHCCpNkFURC/YzAj +BgNVHREEHDAaghhsb2NhbGhvc3QubW9jay5kaXJlY3RvcnkwDQYJKoZIhvcNAQEL +BQADggEBACcd7TT28OWwzQN2PcH0aG38JX5Wp2iOS/unDCfWjNAztXHW7nBDMxza +VtyebkJfccexpuVuOsjOX+bww0vtEYIvKX3/GbkhogksBrNkE0sJZtMnZWMR33wa +YxAy/kJBTmLi02r8fX9ZhwjldStHKBav4USuP7DXZjrgX7LFQhR4LIDrPaYqQRZ8 +ltC3mM9LDQ9rQyIFP5cSBMO3RUAm4I8JyLoOdb/9G2uxjHr7r6eG1g8DmLYSKBsQ +mWGQDOYgR3cGltDe2yMxM++yHY+b1uhxGOWMrDA1+1k7yI19LL8Ifi2FMovDfu/X +JxYk1NNNtdctwaYJFenmGQvDaIq1KgE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDUDCCAjigAwIBAgIIKBJ7IIA6W1swDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVUGViYmxlIFJvb3QgQ0EgNTdmZjE2MCAXDTIzMDIwOTA1MzMxMloYDzIwNTMw +MjA5MDUzMzEyWjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDQ5 +YTRmYjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOvlqRx8SXQFWo2 +gFCiXxls53eENcyr8+meFyjgnS853eEvplaPxoa2MREKd+ZYxM8EMMfj2XGvR3UI +aqR5QyLQ9ihuRqvQo4fG91usBHgH+vDbGPdMX8gDmm9HgnmtOVhSKJU+M2jfE1SW +UuWB9xOa3LMreTXbTNfZEMoXf+GcWZMbx5WPgEga3DvfmV+RsfNvB55eD7YAyZgF +ZnQ3Dskmnxxlkz0EGgd7rqhFHHNB9jARlL22gITADwoWZidlr3ciM9DISymRKQ0c +mRN15fQjNWdtuREgJlpXecbYQMGhdTOmFrqdHkveD1o63rGSC4z+s/APV6xIbcRp +aNpO7L8CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNxlzEky +FZ0/sx5wcIKk2QVREL9jMB8GA1UdIwQYMBaAFOqfkm9rebIz4z0SDIKW5edLg5JM +MA0GCSqGSIb3DQEBCwUAA4IBAQBRG9AHEnyj2fKzVDDbQaKHjAF5jh0gwyHoIeRK +FkP9mQNSWxhvPWI0tK/E49LopzmVuzSbDd5kZsaii73rAs6f6Rf9W5veo3AFSEad +stM+Zv0f2vWB38nuvkoCRLXMX+QUeuL65rKxdEpyArBju4L3/PqAZRgMLcrH+ak8 +nvw5RdAq+Km/ZWyJgGikK6cfMmh91YALCDFnoWUWrCjkBaBFKrG59ONV9f0IQX07 +aNfFXFCF5l466xw9dHjw5iaFib10cpY3iq4kyPYIMs6uaewkCtxWKKjiozM4g4w3 +HqwyUyZ52WUJOJ/6G9DJLDtN3fgGR+IAp8BhYd5CqOscnt3h +-----END CERTIFICATE-----`) diff --git a/server/dns/const.go b/server/dns/const.go deleted file mode 100644 index bb2413b..0000000 --- a/server/dns/const.go +++ /dev/null @@ -1,6 +0,0 @@ -package dns - -import "time" - -// lookupCacheTimeout specifies the timeout for the DNS lookup cache. -var lookupCacheTimeout = 15 * time.Minute diff --git a/server/dns/dns.go b/server/dns/dns.go index d30f1e0..c11b278 100644 --- a/server/dns/dns.go +++ b/server/dns/dns.go @@ -3,10 +3,16 @@ package dns import ( "net" "strings" + "time" "codeberg.org/codeberg/pages/server/cache" ) +// lookupCacheTimeout specifies the timeout for the DNS lookup cache. +var lookupCacheTimeout = 15 * time.Minute + +var defaultPagesRepo = "pages" + // GetTargetFromDNS searches for CNAME or TXT entries on the request domain ending with MainDomainSuffix. // If everything is fine, it returns the target data. func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLookupCache cache.SetGetKey) (targetOwner, targetRepo, targetBranch string) { @@ -24,7 +30,7 @@ func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLo names, err := net.LookupTXT(domain) if err == nil { for _, name := range names { - name = strings.TrimSuffix(name, ".") + name = strings.TrimSuffix(strings.TrimSpace(name), ".") if strings.HasSuffix(name, mainDomainSuffix) { cname = name break @@ -46,9 +52,9 @@ func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLo targetBranch = cnameParts[len(cnameParts)-3] } if targetRepo == "" { - targetRepo = firstDefaultBranch + targetRepo = defaultPagesRepo } - if targetBranch == "" && targetRepo != firstDefaultBranch { + if targetBranch == "" && targetRepo != defaultPagesRepo { targetBranch = firstDefaultBranch } // if targetBranch is still empty, the caller must find the default branch diff --git a/server/gitea/cache.go b/server/gitea/cache.go index 932ff3c..af61edf 100644 --- a/server/gitea/cache.go +++ b/server/gitea/cache.go @@ -1,12 +1,115 @@ package gitea +import ( + "bytes" + "fmt" + "io" + "net/http" + "time" + + "github.com/rs/zerolog/log" + + "codeberg.org/codeberg/pages/server/cache" +) + +const ( + // defaultBranchCacheTimeout specifies the timeout for the default branch cache. It can be quite long. + defaultBranchCacheTimeout = 15 * time.Minute + + // branchExistenceCacheTimeout specifies the timeout for the branch timestamp & existence cache. It should be shorter + // than fileCacheTimeout, as that gets invalidated if the branch timestamp has changed. That way, repo changes will be + // picked up faster, while still allowing the content to be cached longer if nothing changes. + branchExistenceCacheTimeout = 5 * time.Minute + + // fileCacheTimeout specifies the timeout for the file content cache - you might want to make this quite long, depending + // on your available memory. + // TODO: move as option into cache interface + fileCacheTimeout = 5 * time.Minute + + // fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default. + fileCacheSizeLimit = int64(1000 * 1000) +) + type FileResponse struct { - Exists bool - ETag []byte - MimeType string - Body []byte + Exists bool + IsSymlink bool + ETag string + MimeType string + Body []byte } func (f FileResponse) IsEmpty() bool { return len(f.Body) != 0 } + +func (f FileResponse) createHttpResponse(cacheKey string) (header http.Header, statusCode int) { + header = make(http.Header) + + if f.Exists { + statusCode = http.StatusOK + } else { + statusCode = http.StatusNotFound + } + + if f.IsSymlink { + header.Set(giteaObjectTypeHeader, objTypeSymlink) + } + header.Set(ETagHeader, f.ETag) + header.Set(ContentTypeHeader, f.MimeType) + header.Set(ContentLengthHeader, fmt.Sprintf("%d", len(f.Body))) + header.Set(PagesCacheIndicatorHeader, "true") + + log.Trace().Msgf("fileCache for %q used", cacheKey) + return header, statusCode +} + +type BranchTimestamp struct { + Branch string + Timestamp time.Time + notFound bool +} + +type writeCacheReader struct { + originalReader io.ReadCloser + buffer *bytes.Buffer + rileResponse *FileResponse + cacheKey string + cache cache.SetGetKey + hasError bool +} + +func (t *writeCacheReader) Read(p []byte) (n int, err error) { + n, err = t.originalReader.Read(p) + if err != nil && err != io.EOF { + log.Trace().Err(err).Msgf("[cache] original reader for %q has returned an error", t.cacheKey) + t.hasError = true + } else if n > 0 { + _, _ = t.buffer.Write(p[:n]) + } + return +} + +func (t *writeCacheReader) Close() error { + if !t.hasError { + fc := *t.rileResponse + fc.Body = t.buffer.Bytes() + _ = t.cache.Set(t.cacheKey, fc, fileCacheTimeout) + } + log.Trace().Msgf("cacheReader for %q saved=%t closed", t.cacheKey, !t.hasError) + return t.originalReader.Close() +} + +func (f FileResponse) CreateCacheReader(r io.ReadCloser, cache cache.SetGetKey, cacheKey string) io.ReadCloser { + if r == nil || cache == nil || cacheKey == "" { + log.Error().Msg("could not create CacheReader") + return nil + } + + return &writeCacheReader{ + originalReader: r, + buffer: bytes.NewBuffer(make([]byte, 0)), + rileResponse: &f, + cache: cache, + cacheKey: cacheKey, + } +} diff --git a/server/gitea/client.go b/server/gitea/client.go index 16cba84..f3bda54 100644 --- a/server/gitea/client.go +++ b/server/gitea/client.go @@ -1,142 +1,295 @@ package gitea import ( + "bytes" "errors" "fmt" + "io" + "mime" + "net/http" "net/url" + "path" + "strconv" "strings" "time" + "code.gitea.io/sdk/gitea" "github.com/rs/zerolog/log" - "github.com/valyala/fasthttp" - "github.com/valyala/fastjson" -) -const ( - giteaAPIRepos = "/api/v1/repos/" - giteaObjectTypeHeader = "X-Gitea-Object-Type" + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/version" ) var ErrorNotFound = errors.New("not found") +const ( + // cache key prefixes + branchTimestampCacheKeyPrefix = "branchTime" + defaultBranchCacheKeyPrefix = "defaultBranch" + rawContentCacheKeyPrefix = "rawContent" + + // pages server + PagesCacheIndicatorHeader = "X-Pages-Cache" + symlinkReadLimit = 10000 + + // gitea + giteaObjectTypeHeader = "X-Gitea-Object-Type" + objTypeSymlink = "symlink" + + // std + ETagHeader = "ETag" + ContentTypeHeader = "Content-Type" + ContentLengthHeader = "Content-Length" +) + type Client struct { - giteaRoot string - giteaAPIToken string - fastClient *fasthttp.Client - infoTimeout time.Duration - contentTimeout time.Duration + sdkClient *gitea.Client + responseCache cache.SetGetKey + + giteaRoot string followSymlinks bool supportLFS bool + + forbiddenMimeTypes map[string]bool + defaultMimeType string } -// TODO: once golang v1.19 is min requirement, we can switch to 'JoinPath()' of 'net/url' package -func joinURL(baseURL string, paths ...string) string { - p := make([]string, 0, len(paths)) - for i := range paths { - path := strings.TrimSpace(paths[i]) - path = strings.Trim(path, "/") - if len(path) != 0 { - p = append(p, path) - } - } - - return baseURL + "/" + strings.Join(p, "/") -} - -func NewClient(giteaRoot, giteaAPIToken string, followSymlinks, supportLFS bool) (*Client, error) { +func NewClient(giteaRoot, giteaAPIToken string, respCache cache.SetGetKey, followSymlinks, supportLFS bool) (*Client, error) { rootURL, err := url.Parse(giteaRoot) + if err != nil { + return nil, err + } giteaRoot = strings.Trim(rootURL.String(), "/") + stdClient := http.Client{Timeout: 10 * time.Second} + + // TODO: pass down + var ( + forbiddenMimeTypes map[string]bool + defaultMimeType string + ) + + if forbiddenMimeTypes == nil { + forbiddenMimeTypes = make(map[string]bool) + } + if defaultMimeType == "" { + defaultMimeType = "application/octet-stream" + } + + sdk, err := gitea.NewClient( + giteaRoot, + gitea.SetHTTPClient(&stdClient), + gitea.SetToken(giteaAPIToken), + gitea.SetUserAgent("pages-server/"+version.Version), + ) + return &Client{ - giteaRoot: giteaRoot, - giteaAPIToken: giteaAPIToken, - infoTimeout: 5 * time.Second, - contentTimeout: 10 * time.Second, - fastClient: getFastHTTPClient(), + sdkClient: sdk, + responseCache: respCache, + + giteaRoot: giteaRoot, followSymlinks: followSymlinks, supportLFS: supportLFS, + + forbiddenMimeTypes: forbiddenMimeTypes, + defaultMimeType: defaultMimeType, }, err } +func (client *Client) ContentWebLink(targetOwner, targetRepo, branch, resource string) string { + return path.Join(client.giteaRoot, targetOwner, targetRepo, "src/branch", branch, resource) +} + func (client *Client) GiteaRawContent(targetOwner, targetRepo, ref, resource string) ([]byte, error) { - resp, err := client.ServeRawContent(targetOwner, targetRepo, ref, resource) + reader, _, _, err := client.ServeRawContent(targetOwner, targetRepo, ref, resource) if err != nil { return nil, err } - return resp.Body(), nil + defer reader.Close() + return io.ReadAll(reader) } -func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource string) (*fasthttp.Response, error) { - var apiURL string - if client.supportLFS { - apiURL = joinURL(client.giteaRoot, giteaAPIRepos, targetOwner, targetRepo, "media", resource+"?ref="+url.QueryEscape(ref)) - } else { - apiURL = joinURL(client.giteaRoot, giteaAPIRepos, targetOwner, targetRepo, "raw", resource+"?ref="+url.QueryEscape(ref)) - } - resp, err := client.do(client.contentTimeout, apiURL) - if err != nil { - return nil, err - } +func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource string) (io.ReadCloser, http.Header, int, error) { + cacheKey := fmt.Sprintf("%s/%s/%s|%s|%s", rawContentCacheKeyPrefix, targetOwner, targetRepo, ref, resource) + log := log.With().Str("cache_key", cacheKey).Logger() - if err != nil { - return nil, err - } - - switch resp.StatusCode() { - case fasthttp.StatusOK: - objType := string(resp.Header.Peek(giteaObjectTypeHeader)) - log.Trace().Msgf("server raw content object: %s", objType) - if client.followSymlinks && objType == "symlink" { - // TODO: limit to 1000 chars if we switched to std - linkDest := strings.TrimSpace(string(resp.Body())) - log.Debug().Msgf("follow symlink from '%s' to '%s'", resource, linkDest) - return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest) + // handle if cache entry exist + if cache, ok := client.responseCache.Get(cacheKey); ok { + cache := cache.(FileResponse) + cachedHeader, cachedStatusCode := cache.createHttpResponse(cacheKey) + // TODO: check against some timestamp mismatch?!? + if cache.Exists { + if cache.IsSymlink { + linkDest := string(cache.Body) + log.Debug().Msgf("[cache] follow symlink from %q to %q", resource, linkDest) + return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest) + } else { + log.Debug().Msg("[cache] return bytes") + return io.NopCloser(bytes.NewReader(cache.Body)), cachedHeader, cachedStatusCode, nil + } + } else { + return nil, cachedHeader, cachedStatusCode, ErrorNotFound } - - return resp, nil - - case fasthttp.StatusNotFound: - return nil, ErrorNotFound - - default: - return nil, fmt.Errorf("unexpected status code '%d'", resp.StatusCode()) } + + // not in cache, open reader via gitea api + reader, resp, err := client.sdkClient.GetFileReader(targetOwner, targetRepo, ref, resource, client.supportLFS) + if resp != nil { + switch resp.StatusCode { + case http.StatusOK: + // first handle symlinks + { + objType := resp.Header.Get(giteaObjectTypeHeader) + log.Trace().Msgf("server raw content object %q", objType) + if client.followSymlinks && objType == objTypeSymlink { + defer reader.Close() + // read limited chars for symlink + linkDestBytes, err := io.ReadAll(io.LimitReader(reader, symlinkReadLimit)) + if err != nil { + return nil, nil, http.StatusInternalServerError, err + } + linkDest := strings.TrimSpace(string(linkDestBytes)) + + // handle relative links + // we first remove the link from the path, and make a relative join (resolve parent paths like "/../" too) + linkDest = path.Join(path.Dir(resource), linkDest) + + // we store symlink not content to reduce duplicates in cache + if err := client.responseCache.Set(cacheKey, FileResponse{ + Exists: true, + IsSymlink: true, + Body: []byte(linkDest), + ETag: resp.Header.Get(ETagHeader), + }, fileCacheTimeout); err != nil { + log.Error().Err(err).Msg("[cache] error on cache write") + } + + log.Debug().Msgf("follow symlink from %q to %q", resource, linkDest) + return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest) + } + } + + // now we are sure it's content so set the MIME type + mimeType := client.getMimeTypeByExtension(resource) + resp.Response.Header.Set(ContentTypeHeader, mimeType) + + if !shouldRespBeSavedToCache(resp.Response) { + return reader, resp.Response.Header, resp.StatusCode, err + } + + // now we write to cache and respond at the same time + fileResp := FileResponse{ + Exists: true, + ETag: resp.Header.Get(ETagHeader), + MimeType: mimeType, + } + return fileResp.CreateCacheReader(reader, client.responseCache, cacheKey), resp.Response.Header, resp.StatusCode, nil + + case http.StatusNotFound: + if err := client.responseCache.Set(cacheKey, FileResponse{ + Exists: false, + ETag: resp.Header.Get(ETagHeader), + }, fileCacheTimeout); err != nil { + log.Error().Err(err).Msg("[cache] error on cache write") + } + + return nil, resp.Response.Header, http.StatusNotFound, ErrorNotFound + default: + return nil, resp.Response.Header, resp.StatusCode, fmt.Errorf("unexpected status code '%d'", resp.StatusCode) + } + } + return nil, nil, http.StatusInternalServerError, err } -func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchName string) (time.Time, error) { - url := joinURL(client.giteaRoot, giteaAPIRepos, repoOwner, repoName, "branches", branchName) - res, err := client.do(client.infoTimeout, url) +func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchName string) (*BranchTimestamp, error) { + cacheKey := fmt.Sprintf("%s/%s/%s/%s", branchTimestampCacheKeyPrefix, repoOwner, repoName, branchName) + + if stamp, ok := client.responseCache.Get(cacheKey); ok && stamp != nil { + branchTimeStamp := stamp.(*BranchTimestamp) + if branchTimeStamp.notFound { + log.Trace().Msgf("[cache] use branch %q not found", branchName) + return &BranchTimestamp{}, ErrorNotFound + } + log.Trace().Msgf("[cache] use branch %q exist", branchName) + return branchTimeStamp, nil + } + + branch, resp, err := client.sdkClient.GetRepoBranch(repoOwner, repoName, branchName) if err != nil { - return time.Time{}, err + if resp != nil && resp.StatusCode == http.StatusNotFound { + log.Trace().Msgf("[cache] set cache branch %q not found", branchName) + if err := client.responseCache.Set(cacheKey, &BranchTimestamp{Branch: branchName, notFound: true}, branchExistenceCacheTimeout); err != nil { + log.Error().Err(err).Msg("[cache] error on cache write") + } + return &BranchTimestamp{}, ErrorNotFound + } + return &BranchTimestamp{}, err } - if res.StatusCode() != fasthttp.StatusOK { - return time.Time{}, fmt.Errorf("unexpected status code '%d'", res.StatusCode()) + if resp.StatusCode != http.StatusOK { + return &BranchTimestamp{}, fmt.Errorf("unexpected status code '%d'", resp.StatusCode) } - return time.Parse(time.RFC3339, fastjson.GetString(res.Body(), "commit", "timestamp")) + + stamp := &BranchTimestamp{ + Branch: branch.Name, + Timestamp: branch.Commit.Timestamp, + } + + log.Trace().Msgf("set cache branch [%s] exist", branchName) + if err := client.responseCache.Set(cacheKey, stamp, branchExistenceCacheTimeout); err != nil { + log.Error().Err(err).Msg("[cache] error on cache write") + } + return stamp, nil } func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (string, error) { - url := joinURL(client.giteaRoot, giteaAPIRepos, repoOwner, repoName) - res, err := client.do(client.infoTimeout, url) + cacheKey := fmt.Sprintf("%s/%s/%s", defaultBranchCacheKeyPrefix, repoOwner, repoName) + + if branch, ok := client.responseCache.Get(cacheKey); ok && branch != nil { + return branch.(string), nil + } + + repo, resp, err := client.sdkClient.GetRepo(repoOwner, repoName) if err != nil { return "", err } - if res.StatusCode() != fasthttp.StatusOK { - return "", fmt.Errorf("unexpected status code '%d'", res.StatusCode()) + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code '%d'", resp.StatusCode) } - return fastjson.GetString(res.Body(), "default_branch"), nil + + branch := repo.DefaultBranch + if err := client.responseCache.Set(cacheKey, branch, defaultBranchCacheTimeout); err != nil { + log.Error().Err(err).Msg("[cache] error on cache write") + } + return branch, nil } -func (client *Client) do(timeout time.Duration, url string) (*fasthttp.Response, error) { - req := fasthttp.AcquireRequest() - - req.SetRequestURI(url) - req.Header.Set(fasthttp.HeaderAuthorization, "token "+client.giteaAPIToken) - res := fasthttp.AcquireResponse() - - err := client.fastClient.DoTimeout(req, res, timeout) - - return res, err +func (client *Client) getMimeTypeByExtension(resource string) string { + mimeType := mime.TypeByExtension(path.Ext(resource)) + mimeTypeSplit := strings.SplitN(mimeType, ";", 2) + if client.forbiddenMimeTypes[mimeTypeSplit[0]] || mimeType == "" { + mimeType = client.defaultMimeType + } + log.Trace().Msgf("probe mime of %q is %q", resource, mimeType) + return mimeType +} + +func shouldRespBeSavedToCache(resp *http.Response) bool { + if resp == nil { + return false + } + + contentLengthRaw := resp.Header.Get(ContentLengthHeader) + if contentLengthRaw == "" { + return false + } + + contentLength, err := strconv.ParseInt(contentLengthRaw, 10, 64) + if err != nil { + log.Error().Err(err).Msg("could not parse content length") + } + + // if content to big or could not be determined we not cache it + return contentLength > 0 && contentLength < fileCacheSizeLimit } diff --git a/server/gitea/client_test.go b/server/gitea/client_test.go deleted file mode 100644 index 7dbad68..0000000 --- a/server/gitea/client_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package gitea - -import ( - "net/url" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestJoinURL(t *testing.T) { - baseURL := "" - assert.EqualValues(t, "/", joinURL(baseURL)) - assert.EqualValues(t, "/", joinURL(baseURL, "", "")) - - baseURL = "http://wwow.url.com" - assert.EqualValues(t, "http://wwow.url.com/a/b/c/d", joinURL(baseURL, "a", "b/c/", "d")) - - baseURL = "http://wow.url.com/subpath/2" - assert.EqualValues(t, "http://wow.url.com/subpath/2/content.pdf", joinURL(baseURL, "/content.pdf")) - assert.EqualValues(t, "http://wow.url.com/subpath/2/wonderful.jpg", joinURL(baseURL, "wonderful.jpg")) - assert.EqualValues(t, "http://wow.url.com/subpath/2/raw/wonderful.jpg?ref=main", joinURL(baseURL, "raw", "wonderful.jpg"+"?ref="+url.QueryEscape("main"))) - assert.EqualValues(t, "http://wow.url.com/subpath/2/raw/wonderful.jpg%3Fref=main", joinURL(baseURL, "raw", "wonderful.jpg%3Fref=main")) -} diff --git a/server/gitea/fasthttp.go b/server/gitea/fasthttp.go deleted file mode 100644 index 4ff0f4a..0000000 --- a/server/gitea/fasthttp.go +++ /dev/null @@ -1,15 +0,0 @@ -package gitea - -import ( - "time" - - "github.com/valyala/fasthttp" -) - -func getFastHTTPClient() *fasthttp.Client { - return &fasthttp.Client{ - MaxConnDuration: 60 * time.Second, - MaxConnWaitTimeout: 1000 * time.Millisecond, - MaxConnsPerHost: 128 * 16, // TODO: adjust bottlenecks for best performance with Gitea! - } -} diff --git a/server/handler.go b/server/handler.go deleted file mode 100644 index eaa2feb..0000000 --- a/server/handler.go +++ /dev/null @@ -1,314 +0,0 @@ -package server - -import ( - "bytes" - "strings" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/valyala/fasthttp" - - "codeberg.org/codeberg/pages/html" - "codeberg.org/codeberg/pages/server/cache" - "codeberg.org/codeberg/pages/server/dns" - "codeberg.org/codeberg/pages/server/gitea" - "codeberg.org/codeberg/pages/server/upstream" - "codeberg.org/codeberg/pages/server/utils" - "codeberg.org/codeberg/pages/server/version" -) - -// Handler handles a single HTTP request to the web server. -func Handler(mainDomainSuffix, rawDomain []byte, - giteaClient *gitea.Client, - giteaRoot, rawInfoPage string, - blacklistedPaths, allowedCorsDomains [][]byte, - dnsLookupCache, canonicalDomainCache, branchTimestampCache, fileResponseCache cache.SetGetKey, - defaultBranches []string, -) func(ctx *fasthttp.RequestCtx) { - return func(ctx *fasthttp.RequestCtx) { - log := log.With().Strs("Handler", []string{string(ctx.Request.Host()), string(ctx.Request.Header.RequestURI())}).Logger() - - ctx.Response.Header.Set("Server", "CodebergPages/"+version.Version) - - // Force new default from specification (since November 2020) - see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#strict-origin-when-cross-origin - ctx.Response.Header.Set("Referrer-Policy", "strict-origin-when-cross-origin") - - // Enable browser caching for up to 10 minutes - ctx.Response.Header.Set("Cache-Control", "public, max-age=600") - - trimmedHost := utils.TrimHostPort(ctx.Request.Host()) - - // Add HSTS for RawDomain and MainDomainSuffix - if hsts := GetHSTSHeader(trimmedHost, mainDomainSuffix, rawDomain); hsts != "" { - ctx.Response.Header.Set("Strict-Transport-Security", hsts) - } - - // Block all methods not required for static pages - if !ctx.IsGet() && !ctx.IsHead() && !ctx.IsOptions() { - ctx.Response.Header.Set("Allow", "GET, HEAD, OPTIONS") - ctx.Error("Method not allowed", fasthttp.StatusMethodNotAllowed) - return - } - - // Block blacklisted paths (like ACME challenges) - for _, blacklistedPath := range blacklistedPaths { - if bytes.HasPrefix(ctx.Path(), blacklistedPath) { - html.ReturnErrorPage(ctx, fasthttp.StatusForbidden) - return - } - } - - // Allow CORS for specified domains - allowCors := false - for _, allowedCorsDomain := range allowedCorsDomains { - if bytes.Equal(trimmedHost, allowedCorsDomain) { - allowCors = true - break - } - } - if allowCors { - ctx.Response.Header.Set("Access-Control-Allow-Origin", "*") - ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, HEAD") - } - ctx.Response.Header.Set("Allow", "GET, HEAD, OPTIONS") - if ctx.IsOptions() { - ctx.Response.Header.SetStatusCode(fasthttp.StatusNoContent) - return - } - - // Prepare request information to Gitea - var targetOwner, targetRepo, targetBranch, targetPath string - targetOptions := &upstream.Options{ - TryIndexPages: true, - } - - // tryBranch checks if a branch exists and populates the target variables. If canonicalLink is non-empty, it will - // also disallow search indexing and add a Link header to the canonical URL. - tryBranch := func(log zerolog.Logger, repo, branch string, path []string, canonicalLink string) bool { - if repo == "" { - log.Debug().Msg("tryBranch: repo is empty") - return false - } - - // Replace "~" to "/" so we can access branch that contains slash character - // Branch name cannot contain "~" so doing this is okay - branch = strings.ReplaceAll(branch, "~", "/") - - // Check if the branch exists, otherwise treat it as a file path - branchTimestampResult := upstream.GetBranchTimestamp(giteaClient, targetOwner, repo, branch, branchTimestampCache) - if branchTimestampResult == nil { - log.Debug().Msg("tryBranch: branch doesn't exist") - return false - } - - // Branch exists, use it - targetRepo = repo - targetPath = strings.Trim(strings.Join(path, "/"), "/") - targetBranch = branchTimestampResult.Branch - - targetOptions.BranchTimestamp = branchTimestampResult.Timestamp - - if canonicalLink != "" { - // Hide from search machines & add canonical link - ctx.Response.Header.Set("X-Robots-Tag", "noarchive, noindex") - ctx.Response.Header.Set("Link", - strings.NewReplacer("%b", targetBranch, "%p", targetPath).Replace(canonicalLink)+ - "; rel=\"canonical\"", - ) - } - - log.Debug().Msg("tryBranch: true") - return true - } - - log.Debug().Msg("Preparing") - if rawDomain != nil && bytes.Equal(trimmedHost, rawDomain) { - // Serve raw content from RawDomain - log.Debug().Msg("Serving raw domain") - - targetOptions.TryIndexPages = false - if targetOptions.ForbiddenMimeTypes == nil { - targetOptions.ForbiddenMimeTypes = make(map[string]bool) - } - targetOptions.ForbiddenMimeTypes["text/html"] = true - targetOptions.DefaultMimeType = "text/plain; charset=utf-8" - - pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/") - if len(pathElements) < 2 { - // https://{RawDomain}/{owner}/{repo}[/@{branch}]/{path} is required - ctx.Redirect(rawInfoPage, fasthttp.StatusTemporaryRedirect) - return - } - targetOwner = pathElements[0] - targetRepo = pathElements[1] - - // raw.codeberg.org/example/myrepo/@main/index.html - if len(pathElements) > 2 && strings.HasPrefix(pathElements[2], "@") { - log.Debug().Msg("Preparing raw domain, now trying with specified branch") - if tryBranch(log, - targetRepo, pathElements[2][1:], pathElements[3:], - giteaRoot+"/"+targetOwner+"/"+targetRepo+"/src/branch/%b/%p", - ) { - log.Info().Msg("tryBranch, now trying upstream 1") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, - targetOptions, targetOwner, targetRepo, targetBranch, targetPath, - canonicalDomainCache, branchTimestampCache, fileResponseCache) - return - } - log.Warn().Msg("Path missed a branch") - html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) - return - } - - log.Debug().Msg("Preparing raw domain, now trying with default branch") - tryBranch(log, - targetRepo, "", pathElements[2:], - giteaRoot+"/"+targetOwner+"/"+targetRepo+"/src/branch/%b/%p", - ) - log.Info().Msg("tryBranch, now trying upstream 2") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, - targetOptions, targetOwner, targetRepo, targetBranch, targetPath, - canonicalDomainCache, branchTimestampCache, fileResponseCache) - return - - } else if bytes.HasSuffix(trimmedHost, mainDomainSuffix) { - // Serve pages from subdomains of MainDomainSuffix - log.Info().Msg("Serve pages from main domain suffix") - - pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/") - targetOwner = string(bytes.TrimSuffix(trimmedHost, mainDomainSuffix)) - targetRepo = pathElements[0] - targetPath = strings.Trim(strings.Join(pathElements[1:], "/"), "/") - - if targetOwner == "www" { - // www.codeberg.page redirects to codeberg.page // TODO: rm hardcoded - use cname? - ctx.Redirect("https://"+string(mainDomainSuffix[1:])+string(ctx.Path()), fasthttp.StatusPermanentRedirect) - return - } - - // Check if the first directory is a repo with the second directory as a branch - // example.codeberg.page/myrepo/@main/index.html - if len(pathElements) > 1 && strings.HasPrefix(pathElements[1], "@") { - if targetRepo == "pages" { - // example.codeberg.org/pages/@... redirects to example.codeberg.org/@... - ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), fasthttp.StatusTemporaryRedirect) - return - } - - log.Debug().Msg("Preparing main domain, now trying with specified repo & branch") - if tryBranch(log, - pathElements[0], pathElements[1][1:], pathElements[2:], - "/"+pathElements[0]+"/%p", - ) { - log.Info().Msg("tryBranch, now trying upstream 3") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, - targetOptions, targetOwner, targetRepo, targetBranch, targetPath, - canonicalDomainCache, branchTimestampCache, fileResponseCache) - } else { - log.Warn().Msg("tryBranch: upstream 3 failed") - html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) - } - return - } - - // Check if the first directory is a branch for the "pages" repo - // example.codeberg.page/@main/index.html - if strings.HasPrefix(pathElements[0], "@") { - log.Debug().Msg("Preparing main domain, now trying with specified branch") - if tryBranch(log, - "pages", pathElements[0][1:], pathElements[1:], "/%p") { - log.Info().Msg("tryBranch, now trying upstream 4") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, - targetOptions, targetOwner, targetRepo, targetBranch, targetPath, - canonicalDomainCache, branchTimestampCache, fileResponseCache) - } else { - log.Warn().Msg("tryBranch: upstream 4 failed") - html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) - } - return - } - - for _, branch := range defaultBranches { - // Check if the first directory is a repo with a default branch - // example.codeberg.page/myrepo/index.html - // example.codeberg.page/{PAGES_BRANCHE}/... is not allowed here. - log.Debug().Msg("main domain preparations, now trying with specified repo") - if pathElements[0] != branch && tryBranch(log, - pathElements[0], branch, pathElements[1:], "") { - log.Info().Msg("tryBranch, now trying upstream 5") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, - targetOptions, targetOwner, targetRepo, targetBranch, targetPath, - canonicalDomainCache, branchTimestampCache, fileResponseCache) - return - } - - // Try to use the "pages" repo on its default branch - // example.codeberg.page/index.html - log.Debug().Msg("main domain preparations, now trying with default repo/branch") - if tryBranch(log, - branch, "", pathElements, "") { - log.Info().Msg("tryBranch, now trying upstream 6") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, - targetOptions, targetOwner, targetRepo, targetBranch, targetPath, - canonicalDomainCache, branchTimestampCache, fileResponseCache) - return - } - } - - // Couldn't find a valid repo/branch - - html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) - return - } else { - trimmedHostStr := string(trimmedHost) - - // Serve pages from external domains - targetOwner, targetRepo, targetBranch = dns.GetTargetFromDNS(trimmedHostStr, string(mainDomainSuffix), defaultBranches[0], dnsLookupCache) - if targetOwner == "" { - html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) - return - } - - pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/") - canonicalLink := "" - if strings.HasPrefix(pathElements[0], "@") { - targetBranch = pathElements[0][1:] - pathElements = pathElements[1:] - canonicalLink = "/%p" - } - - // Try to use the given repo on the given branch or the default branch - log.Debug().Msg("Preparing custom domain, now trying with details from DNS") - if tryBranch(log, - targetRepo, targetBranch, pathElements, canonicalLink) { - canonicalDomain, valid := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, trimmedHostStr, string(mainDomainSuffix), canonicalDomainCache) - if !valid { - log.Warn().Msg("Custom domains, domain from DNS isn't valid/canonical") - html.ReturnErrorPage(ctx, fasthttp.StatusMisdirectedRequest) - return - } else if canonicalDomain != trimmedHostStr { - // only redirect if the target is also a codeberg page! - targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], string(mainDomainSuffix), defaultBranches[0], dnsLookupCache) - if targetOwner != "" { - ctx.Redirect("https://"+canonicalDomain+string(ctx.RequestURI()), fasthttp.StatusTemporaryRedirect) - return - } - - log.Warn().Msg("Custom domains, targetOwner from DNS is empty") - html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) - return - } - - log.Info().Msg("tryBranch, now trying upstream 7 %s") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, - targetOptions, targetOwner, targetRepo, targetBranch, targetPath, - canonicalDomainCache, branchTimestampCache, fileResponseCache) - return - } - - log.Warn().Msg("Couldn't handle request, none of the options succeed") - html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) - return - } - } -} diff --git a/server/handler/handler.go b/server/handler/handler.go new file mode 100644 index 0000000..7da5d39 --- /dev/null +++ b/server/handler/handler.go @@ -0,0 +1,113 @@ +package handler + +import ( + "net/http" + "strings" + + "github.com/rs/zerolog/log" + + "codeberg.org/codeberg/pages/html" + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/context" + "codeberg.org/codeberg/pages/server/gitea" +) + +const ( + headerAccessControlAllowOrigin = "Access-Control-Allow-Origin" + headerAccessControlAllowMethods = "Access-Control-Allow-Methods" + defaultPagesRepo = "pages" +) + +// Handler handles a single HTTP request to the web server. +func Handler(mainDomainSuffix, rawDomain string, + giteaClient *gitea.Client, + blacklistedPaths, allowedCorsDomains []string, + defaultPagesBranches []string, + dnsLookupCache, canonicalDomainCache, redirectsCache cache.SetGetKey, +) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + log := log.With().Strs("Handler", []string{req.Host, req.RequestURI}).Logger() + ctx := context.New(w, req) + + ctx.RespWriter.Header().Set("Server", "pages-server") + + // Force new default from specification (since November 2020) - see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#strict-origin-when-cross-origin + ctx.RespWriter.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + + // Enable browser caching for up to 10 minutes + ctx.RespWriter.Header().Set("Cache-Control", "public, max-age=600") + + trimmedHost := ctx.TrimHostPort() + + // Add HSTS for RawDomain and MainDomainSuffix + if hsts := getHSTSHeader(trimmedHost, mainDomainSuffix, rawDomain); hsts != "" { + ctx.RespWriter.Header().Set("Strict-Transport-Security", hsts) + } + + // Handle all http methods + ctx.RespWriter.Header().Set("Allow", http.MethodGet+", "+http.MethodHead+", "+http.MethodOptions) + switch ctx.Req.Method { + case http.MethodOptions: + // return Allow header + ctx.RespWriter.WriteHeader(http.StatusNoContent) + return + case http.MethodGet, + http.MethodHead: + // end switch case and handle allowed requests + break + default: + // Block all methods not required for static pages + ctx.String("Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Block blacklisted paths (like ACME challenges) + for _, blacklistedPath := range blacklistedPaths { + if strings.HasPrefix(ctx.Path(), blacklistedPath) { + html.ReturnErrorPage(ctx, "requested path is blacklisted", http.StatusForbidden) + return + } + } + + // Allow CORS for specified domains + allowCors := false + for _, allowedCorsDomain := range allowedCorsDomains { + if strings.EqualFold(trimmedHost, allowedCorsDomain) { + allowCors = true + break + } + } + if allowCors { + ctx.RespWriter.Header().Set(headerAccessControlAllowOrigin, "*") + ctx.RespWriter.Header().Set(headerAccessControlAllowMethods, http.MethodGet+", "+http.MethodHead) + } + + // Prepare request information to Gitea + pathElements := strings.Split(strings.Trim(ctx.Path(), "/"), "/") + + if rawDomain != "" && strings.EqualFold(trimmedHost, rawDomain) { + log.Debug().Msg("raw domain request detected") + handleRaw(log, ctx, giteaClient, + mainDomainSuffix, + trimmedHost, + pathElements, + canonicalDomainCache, redirectsCache) + } else if strings.HasSuffix(trimmedHost, mainDomainSuffix) { + log.Debug().Msg("subdomain request detected") + handleSubDomain(log, ctx, giteaClient, + mainDomainSuffix, + defaultPagesBranches, + trimmedHost, + pathElements, + canonicalDomainCache, redirectsCache) + } else { + log.Debug().Msg("custom domain request detected") + handleCustomDomain(log, ctx, giteaClient, + mainDomainSuffix, + trimmedHost, + pathElements, + defaultPagesBranches[0], + dnsLookupCache, canonicalDomainCache, redirectsCache) + } + } +} diff --git a/server/handler/handler_custom_domain.go b/server/handler/handler_custom_domain.go new file mode 100644 index 0000000..8742be4 --- /dev/null +++ b/server/handler/handler_custom_domain.go @@ -0,0 +1,72 @@ +package handler + +import ( + "net/http" + "path" + "strings" + + "codeberg.org/codeberg/pages/html" + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/context" + "codeberg.org/codeberg/pages/server/dns" + "codeberg.org/codeberg/pages/server/gitea" + "codeberg.org/codeberg/pages/server/upstream" + "github.com/rs/zerolog" +) + +func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client, + mainDomainSuffix string, + trimmedHost string, + pathElements []string, + firstDefaultBranch string, + dnsLookupCache, canonicalDomainCache, redirectsCache cache.SetGetKey, +) { + // Serve pages from custom domains + targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, firstDefaultBranch, dnsLookupCache) + if targetOwner == "" { + html.ReturnErrorPage(ctx, + "could not obtain repo owner from custom domain", + http.StatusFailedDependency) + return + } + + pathParts := pathElements + canonicalLink := false + if strings.HasPrefix(pathElements[0], "@") { + targetBranch = pathElements[0][1:] + pathParts = pathElements[1:] + canonicalLink = true + } + + // Try to use the given repo on the given branch or the default branch + log.Debug().Msg("custom domain preparations, now trying with details from DNS") + if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ + TryIndexPages: true, + TargetOwner: targetOwner, + TargetRepo: targetRepo, + TargetBranch: targetBranch, + TargetPath: path.Join(pathParts...), + }, canonicalLink); works { + canonicalDomain, valid := targetOpt.CheckCanonicalDomain(giteaClient, trimmedHost, mainDomainSuffix, canonicalDomainCache) + if !valid { + html.ReturnErrorPage(ctx, "domain not specified in .domains file", http.StatusMisdirectedRequest) + return + } else if canonicalDomain != trimmedHost { + // only redirect if the target is also a codeberg page! + targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, firstDefaultBranch, dnsLookupCache) + if targetOwner != "" { + ctx.Redirect("https://"+canonicalDomain+"/"+targetOpt.TargetPath, http.StatusTemporaryRedirect) + return + } + + html.ReturnErrorPage(ctx, "target is no codeberg page", http.StatusFailedDependency) + return + } + + log.Debug().Msg("tryBranch, now trying upstream 7") + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache) + return + } + + html.ReturnErrorPage(ctx, "could not find target for custom domain", http.StatusFailedDependency) +} diff --git a/server/handler/handler_raw_domain.go b/server/handler/handler_raw_domain.go new file mode 100644 index 0000000..caa8209 --- /dev/null +++ b/server/handler/handler_raw_domain.go @@ -0,0 +1,71 @@ +package handler + +import ( + "fmt" + "net/http" + "path" + "strings" + + "github.com/rs/zerolog" + + "codeberg.org/codeberg/pages/html" + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/context" + "codeberg.org/codeberg/pages/server/gitea" + "codeberg.org/codeberg/pages/server/upstream" +) + +func handleRaw(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client, + mainDomainSuffix string, + trimmedHost string, + pathElements []string, + canonicalDomainCache, redirectsCache cache.SetGetKey, +) { + // Serve raw content from RawDomain + log.Debug().Msg("raw domain") + + if len(pathElements) < 2 { + html.ReturnErrorPage( + ctx, + "a url in the form of https://{domain}/{owner}/{repo}[/@{branch}]/{path} is required", + http.StatusBadRequest, + ) + + return + } + + // raw.codeberg.org/example/myrepo/@main/index.html + if len(pathElements) > 2 && strings.HasPrefix(pathElements[2], "@") { + log.Debug().Msg("raw domain preparations, now trying with specified branch") + if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ + ServeRaw: true, + TargetOwner: pathElements[0], + TargetRepo: pathElements[1], + TargetBranch: pathElements[2][1:], + TargetPath: path.Join(pathElements[3:]...), + }, true); works { + log.Trace().Msg("tryUpstream: serve raw domain with specified branch") + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache) + return + } + log.Debug().Msg("missing branch info") + html.ReturnErrorPage(ctx, "missing branch info", http.StatusFailedDependency) + return + } + + log.Debug().Msg("raw domain preparations, now trying with default branch") + if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ + TryIndexPages: false, + ServeRaw: true, + TargetOwner: pathElements[0], + TargetRepo: pathElements[1], + TargetPath: path.Join(pathElements[2:]...), + }, true); works { + log.Trace().Msg("tryUpstream: serve raw domain with default branch") + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache) + } else { + html.ReturnErrorPage(ctx, + fmt.Sprintf("raw domain could not find repo %s/%s or repo is empty", targetOpt.TargetOwner, targetOpt.TargetRepo), + http.StatusNotFound) + } +} diff --git a/server/handler/handler_sub_domain.go b/server/handler/handler_sub_domain.go new file mode 100644 index 0000000..6c14393 --- /dev/null +++ b/server/handler/handler_sub_domain.go @@ -0,0 +1,156 @@ +package handler + +import ( + "fmt" + "net/http" + "path" + "strings" + + "github.com/rs/zerolog" + "golang.org/x/exp/slices" + + "codeberg.org/codeberg/pages/html" + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/context" + "codeberg.org/codeberg/pages/server/gitea" + "codeberg.org/codeberg/pages/server/upstream" +) + +func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client, + mainDomainSuffix string, + defaultPagesBranches []string, + trimmedHost string, + pathElements []string, + canonicalDomainCache, redirectsCache cache.SetGetKey, +) { + // Serve pages from subdomains of MainDomainSuffix + log.Debug().Msg("main domain suffix") + + targetOwner := strings.TrimSuffix(trimmedHost, mainDomainSuffix) + targetRepo := pathElements[0] + + if targetOwner == "www" { + // www.codeberg.page redirects to codeberg.page // TODO: rm hardcoded - use cname? + ctx.Redirect("https://"+mainDomainSuffix[1:]+ctx.Path(), http.StatusPermanentRedirect) + return + } + + // Check if the first directory is a repo with the second directory as a branch + // example.codeberg.page/myrepo/@main/index.html + if len(pathElements) > 1 && strings.HasPrefix(pathElements[1], "@") { + if targetRepo == defaultPagesRepo { + // example.codeberg.org/pages/@... redirects to example.codeberg.org/@... + ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), http.StatusTemporaryRedirect) + return + } + + log.Debug().Msg("main domain preparations, now trying with specified repo & branch") + if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ + TryIndexPages: true, + TargetOwner: targetOwner, + TargetRepo: pathElements[0], + TargetBranch: pathElements[1][1:], + TargetPath: path.Join(pathElements[2:]...), + }, true); works { + log.Trace().Msg("tryUpstream: serve with specified repo and branch") + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache) + } else { + html.ReturnErrorPage( + ctx, + formatSetBranchNotFoundMessage(pathElements[1][1:], targetOwner, pathElements[0]), + http.StatusFailedDependency, + ) + } + return + } + + // Check if the first directory is a branch for the defaultPagesRepo + // example.codeberg.page/@main/index.html + if strings.HasPrefix(pathElements[0], "@") { + targetBranch := pathElements[0][1:] + + // if the default pages branch can be determined exactly, it does not need to be set + if len(defaultPagesBranches) == 1 && slices.Contains(defaultPagesBranches, targetBranch) { + // example.codeberg.org/@pages/... redirects to example.codeberg.org/... + ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), http.StatusTemporaryRedirect) + return + } + + log.Debug().Msg("main domain preparations, now trying with specified branch") + if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ + TryIndexPages: true, + TargetOwner: targetOwner, + TargetRepo: defaultPagesRepo, + TargetBranch: targetBranch, + TargetPath: path.Join(pathElements[1:]...), + }, true); works { + log.Trace().Msg("tryUpstream: serve default pages repo with specified branch") + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache) + } else { + html.ReturnErrorPage( + ctx, + formatSetBranchNotFoundMessage(targetBranch, targetOwner, defaultPagesRepo), + http.StatusFailedDependency, + ) + } + return + } + + for _, defaultPagesBranch := range defaultPagesBranches { + // Check if the first directory is a repo with a default pages branch + // example.codeberg.page/myrepo/index.html + // example.codeberg.page/{PAGES_BRANCHE}/... is not allowed here. + log.Debug().Msg("main domain preparations, now trying with specified repo") + if pathElements[0] != defaultPagesBranch { + if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ + TryIndexPages: true, + TargetOwner: targetOwner, + TargetRepo: pathElements[0], + TargetBranch: defaultPagesBranch, + TargetPath: path.Join(pathElements[1:]...), + }, false); works { + log.Debug().Msg("tryBranch, now trying upstream 5") + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache) + return + } + } + + // Try to use the defaultPagesRepo on an default pages branch + // example.codeberg.page/index.html + log.Debug().Msg("main domain preparations, now trying with default repo") + if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ + TryIndexPages: true, + TargetOwner: targetOwner, + TargetRepo: defaultPagesRepo, + TargetBranch: defaultPagesBranch, + TargetPath: path.Join(pathElements...), + }, false); works { + log.Debug().Msg("tryBranch, now trying upstream 6") + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache) + return + } + } + + // Try to use the defaultPagesRepo on its default branch + // example.codeberg.page/index.html + log.Debug().Msg("main domain preparations, now trying with default repo/branch") + if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ + TryIndexPages: true, + TargetOwner: targetOwner, + TargetRepo: defaultPagesRepo, + TargetPath: path.Join(pathElements...), + }, false); works { + log.Debug().Msg("tryBranch, now trying upstream 6") + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache) + return + } + + // Couldn't find a valid repo/branch + html.ReturnErrorPage(ctx, + fmt.Sprintf("could not find a valid repository or branch for repository: %s", targetRepo), + http.StatusNotFound) +} + +func formatSetBranchNotFoundMessage(branch, owner, repo string) string { + return fmt.Sprintf("explicitly set branch %q does not exist at %s/%s", branch, owner, repo) +} diff --git a/server/handler/handler_test.go b/server/handler/handler_test.go new file mode 100644 index 0000000..d04ebda --- /dev/null +++ b/server/handler/handler_test.go @@ -0,0 +1,51 @@ +package handler + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/gitea" + "github.com/rs/zerolog/log" +) + +func TestHandlerPerformance(t *testing.T) { + giteaClient, _ := gitea.NewClient("https://codeberg.org", "", cache.NewKeyValueCache(), false, false) + testHandler := Handler( + "codeberg.page", "raw.codeberg.org", + giteaClient, + []string{"/.well-known/acme-challenge/"}, + []string{"raw.codeberg.org", "fonts.codeberg.org", "design.codeberg.org"}, + []string{"pages"}, + cache.NewKeyValueCache(), + cache.NewKeyValueCache(), + cache.NewKeyValueCache(), + ) + + testCase := func(uri string, status int) { + t.Run(uri, func(t *testing.T) { + req := httptest.NewRequest("GET", uri, http.NoBody) + w := httptest.NewRecorder() + + log.Printf("Start: %v\n", time.Now()) + start := time.Now() + testHandler(w, req) + end := time.Now() + log.Printf("Done: %v\n", time.Now()) + + resp := w.Result() + + if resp.StatusCode != status { + t.Errorf("request failed with status code %d", resp.StatusCode) + } else { + t.Logf("request took %d milliseconds", end.Sub(start).Milliseconds()) + } + }) + } + + testCase("https://mondstern.codeberg.page/", 404) // TODO: expect 200 + testCase("https://codeberg.page/", 404) // TODO: expect 200 + testCase("https://example.momar.xyz/", 424) +} diff --git a/server/handler/hsts.go b/server/handler/hsts.go new file mode 100644 index 0000000..1ab73ae --- /dev/null +++ b/server/handler/hsts.go @@ -0,0 +1,15 @@ +package handler + +import ( + "strings" +) + +// getHSTSHeader returns a HSTS header with includeSubdomains & preload for MainDomainSuffix and RawDomain, or an empty +// string for custom domains. +func getHSTSHeader(host, mainDomainSuffix, rawDomain string) string { + if strings.HasSuffix(host, mainDomainSuffix) || strings.EqualFold(host, rawDomain) { + return "max-age=63072000; includeSubdomains; preload" + } else { + return "" + } +} diff --git a/server/handler/try.go b/server/handler/try.go new file mode 100644 index 0000000..838ae27 --- /dev/null +++ b/server/handler/try.go @@ -0,0 +1,77 @@ +package handler + +import ( + "net/http" + "strings" + + "github.com/rs/zerolog" + + "codeberg.org/codeberg/pages/html" + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/context" + "codeberg.org/codeberg/pages/server/gitea" + "codeberg.org/codeberg/pages/server/upstream" +) + +// tryUpstream forwards the target request to the Gitea API, and shows an error page on failure. +func tryUpstream(ctx *context.Context, giteaClient *gitea.Client, + mainDomainSuffix, trimmedHost string, + options *upstream.Options, + canonicalDomainCache cache.SetGetKey, + redirectsCache cache.SetGetKey, +) { + // check if a canonical domain exists on a request on MainDomain + if strings.HasSuffix(trimmedHost, mainDomainSuffix) && !options.ServeRaw { + canonicalDomain, _ := options.CheckCanonicalDomain(giteaClient, "", mainDomainSuffix, canonicalDomainCache) + if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix) { + canonicalPath := ctx.Req.RequestURI + if options.TargetRepo != defaultPagesRepo { + path := strings.SplitN(canonicalPath, "/", 3) + if len(path) >= 3 { + canonicalPath = "/" + path[2] + } + } + ctx.Redirect("https://"+canonicalDomain+canonicalPath, http.StatusTemporaryRedirect) + return + } + } + + // Add host for debugging. + options.Host = trimmedHost + + // Try to request the file from the Gitea API + if !options.Upstream(ctx, giteaClient, redirectsCache) { + html.ReturnErrorPage(ctx, "gitea client failed", ctx.StatusCode) + } +} + +// tryBranch checks if a branch exists and populates the target variables. If canonicalLink is non-empty, +// it will also disallow search indexing and add a Link header to the canonical URL. +func tryBranch(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client, + targetOptions *upstream.Options, canonicalLink bool, +) (*upstream.Options, bool) { + if targetOptions.TargetOwner == "" || targetOptions.TargetRepo == "" { + log.Debug().Msg("tryBranch: owner or repo is empty") + return nil, false + } + + // Replace "~" to "/" so we can access branch that contains slash character + // Branch name cannot contain "~" so doing this is okay + targetOptions.TargetBranch = strings.ReplaceAll(targetOptions.TargetBranch, "~", "/") + + // Check if the branch exists, otherwise treat it as a file path + branchExist, _ := targetOptions.GetBranchTimestamp(giteaClient) + if !branchExist { + log.Debug().Msg("tryBranch: branch doesn't exist") + return nil, false + } + + if canonicalLink { + // Hide from search machines & add canonical link + ctx.RespWriter.Header().Set("X-Robots-Tag", "noarchive, noindex") + ctx.RespWriter.Header().Set("Link", targetOptions.ContentWebLink(giteaClient)+"; rel=\"canonical\"") + } + + log.Debug().Msg("tryBranch: true") + return targetOptions, true +} diff --git a/server/handler_test.go b/server/handler_test.go deleted file mode 100644 index a731ebf..0000000 --- a/server/handler_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package server - -import ( - "fmt" - "testing" - "time" - - "github.com/valyala/fasthttp" - - "codeberg.org/codeberg/pages/server/cache" - "codeberg.org/codeberg/pages/server/gitea" -) - -func TestHandlerPerformance(t *testing.T) { - giteaRoot := "https://codeberg.org" - giteaClient, _ := gitea.NewClient(giteaRoot, "", false, false) - testHandler := Handler( - []byte("codeberg.page"), []byte("raw.codeberg.org"), - giteaClient, - giteaRoot, "https://docs.codeberg.org/pages/raw-content/", - [][]byte{[]byte("/.well-known/acme-challenge/")}, - [][]byte{[]byte("raw.codeberg.org"), []byte("fonts.codeberg.org"), []byte("design.codeberg.org")}, - cache.NewKeyValueCache(), - cache.NewKeyValueCache(), - cache.NewKeyValueCache(), - cache.NewKeyValueCache(), - []string{"pages"}, - ) - - testCase := func(uri string, status int) { - ctx := &fasthttp.RequestCtx{ - Request: *fasthttp.AcquireRequest(), - Response: *fasthttp.AcquireResponse(), - } - ctx.Request.SetRequestURI(uri) - fmt.Printf("Start: %v\n", time.Now()) - start := time.Now() - testHandler(ctx) - end := time.Now() - fmt.Printf("Done: %v\n", time.Now()) - if ctx.Response.StatusCode() != status { - t.Errorf("request failed with status code %d", ctx.Response.StatusCode()) - } else { - t.Logf("request took %d milliseconds", end.Sub(start).Milliseconds()) - } - } - - testCase("https://mondstern.codeberg.page/", 424) // TODO: expect 200 - testCase("https://mondstern.codeberg.page/", 424) // TODO: expect 200 - testCase("https://example.momar.xyz/", 424) // TODO: expect 200 - testCase("https://codeberg.page/", 424) // TODO: expect 200 -} diff --git a/server/helpers.go b/server/helpers.go deleted file mode 100644 index 6d55ddf..0000000 --- a/server/helpers.go +++ /dev/null @@ -1,15 +0,0 @@ -package server - -import ( - "bytes" -) - -// GetHSTSHeader returns a HSTS header with includeSubdomains & preload for MainDomainSuffix and RawDomain, or an empty -// string for custom domains. -func GetHSTSHeader(host, mainDomainSuffix, rawDomain []byte) string { - if bytes.HasSuffix(host, mainDomainSuffix) || bytes.Equal(host, rawDomain) { - return "max-age=63072000; includeSubdomains; preload" - } else { - return "" - } -} diff --git a/server/setup.go b/server/setup.go deleted file mode 100644 index 176bb42..0000000 --- a/server/setup.go +++ /dev/null @@ -1,53 +0,0 @@ -package server - -import ( - "bytes" - "fmt" - "net/http" - "time" - - "github.com/rs/zerolog/log" - "github.com/valyala/fasthttp" - - "codeberg.org/codeberg/pages/server/cache" - "codeberg.org/codeberg/pages/server/utils" -) - -type fasthttpLogger struct{} - -func (fasthttpLogger) Printf(format string, args ...interface{}) { - log.Printf("FastHTTP: %s", fmt.Sprintf(format, args...)) -} - -func SetupServer(handler fasthttp.RequestHandler) *fasthttp.Server { - // Enable compression by wrapping the handler with the compression function provided by FastHTTP - compressedHandler := fasthttp.CompressHandlerBrotliLevel(handler, fasthttp.CompressBrotliBestSpeed, fasthttp.CompressBestSpeed) - - return &fasthttp.Server{ - Handler: compressedHandler, - DisablePreParseMultipartForm: true, - NoDefaultServerHeader: true, - NoDefaultDate: true, - ReadTimeout: 30 * time.Second, // needs to be this high for ACME certificates with ZeroSSL & HTTP-01 challenge - Logger: fasthttpLogger{}, - } -} - -func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey) *fasthttp.Server { - challengePath := []byte("/.well-known/acme-challenge/") - - return &fasthttp.Server{ - Handler: func(ctx *fasthttp.RequestCtx) { - if bytes.HasPrefix(ctx.Path(), challengePath) { - challenge, ok := challengeCache.Get(string(utils.TrimHostPort(ctx.Host())) + "/" + string(bytes.TrimPrefix(ctx.Path(), challengePath))) - if !ok || challenge == nil { - ctx.SetStatusCode(http.StatusNotFound) - ctx.SetBodyString("no challenge for this token") - } - ctx.SetBodyString(challenge.(string)) - } else { - ctx.Redirect("https://"+string(ctx.Host())+string(ctx.RequestURI()), http.StatusMovedPermanently) - } - }, - } -} diff --git a/server/try.go b/server/try.go deleted file mode 100644 index 24831c4..0000000 --- a/server/try.go +++ /dev/null @@ -1,50 +0,0 @@ -package server - -import ( - "bytes" - "strings" - - "github.com/valyala/fasthttp" - - "codeberg.org/codeberg/pages/html" - "codeberg.org/codeberg/pages/server/cache" - "codeberg.org/codeberg/pages/server/gitea" - "codeberg.org/codeberg/pages/server/upstream" -) - -// tryUpstream forwards the target request to the Gitea API, and shows an error page on failure. -func tryUpstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client, - mainDomainSuffix, trimmedHost []byte, - - targetOptions *upstream.Options, - targetOwner, targetRepo, targetBranch, targetPath string, - - canonicalDomainCache, branchTimestampCache, fileResponseCache cache.SetGetKey, -) { - // check if a canonical domain exists on a request on MainDomain - if bytes.HasSuffix(trimmedHost, mainDomainSuffix) { - canonicalDomain, _ := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, "", string(mainDomainSuffix), canonicalDomainCache) - if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], string(mainDomainSuffix)) { - canonicalPath := string(ctx.RequestURI()) - if targetRepo != "pages" { - path := strings.SplitN(canonicalPath, "/", 3) - if len(path) >= 3 { - canonicalPath = "/" + path[2] - } - } - ctx.Redirect("https://"+canonicalDomain+canonicalPath, fasthttp.StatusTemporaryRedirect) - return - } - } - - targetOptions.TargetOwner = targetOwner - targetOptions.TargetRepo = targetRepo - targetOptions.TargetBranch = targetBranch - targetOptions.TargetPath = targetPath - targetOptions.Host = string(trimmedHost) - - // Try to request the file from the Gitea API - if !targetOptions.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { - html.ReturnErrorPage(ctx, ctx.Response.StatusCode()) - } -} diff --git a/server/upstream/const.go b/server/upstream/const.go deleted file mode 100644 index 247e1d1..0000000 --- a/server/upstream/const.go +++ /dev/null @@ -1,24 +0,0 @@ -package upstream - -import "time" - -// defaultBranchCacheTimeout specifies the timeout for the default branch cache. It can be quite long. -var defaultBranchCacheTimeout = 15 * time.Minute - -// branchExistenceCacheTimeout specifies the timeout for the branch timestamp & existence cache. It should be shorter -// than fileCacheTimeout, as that gets invalidated if the branch timestamp has changed. That way, repo changes will be -// picked up faster, while still allowing the content to be cached longer if nothing changes. -var branchExistenceCacheTimeout = 5 * time.Minute - -// fileCacheTimeout specifies the timeout for the file content cache - you might want to make this quite long, depending -// on your available memory. -// TODO: move as option into cache interface -var fileCacheTimeout = 5 * time.Minute - -// fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default. -var fileCacheSizeLimit = 1024 * 1024 - -// canonicalDomainCacheTimeout specifies the timeout for the canonical domain cache. -var canonicalDomainCacheTimeout = 15 * time.Minute - -const canonicalDomainConfig = ".domains" diff --git a/server/upstream/domains.go b/server/upstream/domains.go index 553c148..eb30394 100644 --- a/server/upstream/domains.go +++ b/server/upstream/domains.go @@ -1,50 +1,70 @@ package upstream import ( + "errors" "strings" + "time" + + "github.com/rs/zerolog/log" "codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/gitea" ) +// canonicalDomainCacheTimeout specifies the timeout for the canonical domain cache. +var canonicalDomainCacheTimeout = 15 * time.Minute + +const canonicalDomainConfig = ".domains" + // CheckCanonicalDomain returns the canonical domain specified in the repo (using the `.domains` file). -func CheckCanonicalDomain(giteaClient *gitea.Client, targetOwner, targetRepo, targetBranch, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.SetGetKey) (string, bool) { - var ( - domains []string - valid bool - ) - if cachedValue, ok := canonicalDomainCache.Get(targetOwner + "/" + targetRepo + "/" + targetBranch); ok { - domains = cachedValue.([]string) +func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.SetGetKey) (domain string, valid bool) { + // Check if this request is cached. + if cachedValue, ok := canonicalDomainCache.Get(o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch); ok { + domains := cachedValue.([]string) for _, domain := range domains { if domain == actualDomain { valid = true break } } - } else { - body, err := giteaClient.GiteaRawContent(targetOwner, targetRepo, targetBranch, canonicalDomainConfig) - if err == nil { - for _, domain := range strings.Split(string(body), "\n") { - domain = strings.ToLower(domain) - domain = strings.TrimSpace(domain) - domain = strings.TrimPrefix(domain, "http://") - domain = strings.TrimPrefix(domain, "https://") - if len(domain) > 0 && !strings.HasPrefix(domain, "#") && !strings.ContainsAny(domain, "\t /") && strings.ContainsRune(domain, '.') { - domains = append(domains, domain) - } - if domain == actualDomain { - valid = true - } - } + return domains[0], valid + } + + body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig) + if err != nil && !errors.Is(err, gitea.ErrorNotFound) { + log.Error().Err(err).Msgf("could not read %s of %s/%s", canonicalDomainConfig, o.TargetOwner, o.TargetRepo) + } + + var domains []string + for _, domain := range strings.Split(string(body), "\n") { + domain = strings.ToLower(domain) + domain = strings.TrimSpace(domain) + domain = strings.TrimPrefix(domain, "http://") + domain = strings.TrimPrefix(domain, "https://") + if len(domain) > 0 && !strings.HasPrefix(domain, "#") && !strings.ContainsAny(domain, "\t /") && strings.ContainsRune(domain, '.') { + domains = append(domains, domain) } - domains = append(domains, targetOwner+mainDomainSuffix) - if domains[len(domains)-1] == actualDomain { + if domain == actualDomain { valid = true } - if targetRepo != "" && targetRepo != "pages" { - domains[len(domains)-1] += "/" + targetRepo - } - _ = canonicalDomainCache.Set(targetOwner+"/"+targetRepo+"/"+targetBranch, domains, canonicalDomainCacheTimeout) } + + // Add [owner].[pages-domain] as valid domain. + domains = append(domains, o.TargetOwner+mainDomainSuffix) + if domains[len(domains)-1] == actualDomain { + valid = true + } + + // If the target repository isn't called pages, add `/[repository]` to the + // previous valid domain. + if o.TargetRepo != "" && o.TargetRepo != "pages" { + domains[len(domains)-1] += "/" + o.TargetRepo + } + + // Add result to cache. + _ = canonicalDomainCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, domains, canonicalDomainCacheTimeout) + + // Return the first domain from the list and return if any of the domains + // matched the requested domain. return domains[0], valid } diff --git a/server/upstream/header.go b/server/upstream/header.go new file mode 100644 index 0000000..9575a3f --- /dev/null +++ b/server/upstream/header.go @@ -0,0 +1,28 @@ +package upstream + +import ( + "net/http" + "time" + + "codeberg.org/codeberg/pages/server/context" + "codeberg.org/codeberg/pages/server/gitea" +) + +// setHeader set values to response header +func (o *Options) setHeader(ctx *context.Context, header http.Header) { + if eTag := header.Get(gitea.ETagHeader); eTag != "" { + ctx.RespWriter.Header().Set(gitea.ETagHeader, eTag) + } + if cacheIndicator := header.Get(gitea.PagesCacheIndicatorHeader); cacheIndicator != "" { + ctx.RespWriter.Header().Set(gitea.PagesCacheIndicatorHeader, cacheIndicator) + } + if length := header.Get(gitea.ContentLengthHeader); length != "" { + ctx.RespWriter.Header().Set(gitea.ContentLengthHeader, length) + } + if mime := header.Get(gitea.ContentTypeHeader); mime == "" || o.ServeRaw { + ctx.RespWriter.Header().Set(gitea.ContentTypeHeader, rawMime) + } else { + ctx.RespWriter.Header().Set(gitea.ContentTypeHeader, mime) + } + ctx.RespWriter.Header().Set(headerLastModified, o.BranchTimestamp.In(time.UTC).Format(time.RFC1123)) +} diff --git a/server/upstream/helper.go b/server/upstream/helper.go index 0714dcd..ac0ab3f 100644 --- a/server/upstream/helper.go +++ b/server/upstream/helper.go @@ -1,76 +1,47 @@ package upstream import ( - "mime" - "path" - "strconv" - "strings" - "time" + "errors" + "fmt" + + "github.com/rs/zerolog/log" - "codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/gitea" ) -type branchTimestamp struct { - Branch string - Timestamp time.Time -} +// GetBranchTimestamp finds the default branch (if branch is "") and save branch and it's last modification time to Options +func (o *Options) GetBranchTimestamp(giteaClient *gitea.Client) (bool, error) { + log := log.With().Strs("BranchInfo", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch}).Logger() -// GetBranchTimestamp finds the default branch (if branch is "") and returns the last modification time of the branch -// (or nil if the branch doesn't exist) -func GetBranchTimestamp(giteaClient *gitea.Client, owner, repo, branch string, branchTimestampCache cache.SetGetKey) *branchTimestamp { - if result, ok := branchTimestampCache.Get(owner + "/" + repo + "/" + branch); ok { - if result == nil { - return nil - } - return result.(*branchTimestamp) - } - result := &branchTimestamp{ - Branch: branch, - } - if len(branch) == 0 { + if o.TargetBranch == "" { // Get default branch - defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(owner, repo) + defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(o.TargetOwner, o.TargetRepo) if err != nil { - _ = branchTimestampCache.Set(owner+"/"+repo+"/", nil, defaultBranchCacheTimeout) - return nil + log.Err(err).Msg("Couldn't fetch default branch from repository") + return false, err } - result.Branch = defaultBranch + log.Debug().Msgf("Successfully fetched default branch %q from Gitea", defaultBranch) + o.TargetBranch = defaultBranch } - timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(owner, repo, result.Branch) + timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(o.TargetOwner, o.TargetRepo, o.TargetBranch) if err != nil { - return nil - } - result.Timestamp = timestamp - _ = branchTimestampCache.Set(owner+"/"+repo+"/"+branch, result, branchExistenceCacheTimeout) - return result -} - -func (o *Options) getMimeTypeByExtension() string { - if o.ForbiddenMimeTypes == nil { - o.ForbiddenMimeTypes = make(map[string]bool) - } - mimeType := mime.TypeByExtension(path.Ext(o.TargetPath)) - mimeTypeSplit := strings.SplitN(mimeType, ";", 2) - if o.ForbiddenMimeTypes[mimeTypeSplit[0]] || mimeType == "" { - if o.DefaultMimeType != "" { - mimeType = o.DefaultMimeType - } else { - mimeType = "application/octet-stream" + if !errors.Is(err, gitea.ErrorNotFound) { + log.Error().Err(err).Msg("Could not get latest commit timestamp from branch") } + return false, err } - return mimeType + + if timestamp == nil || timestamp.Branch == "" { + return false, fmt.Errorf("empty response") + } + + log.Debug().Msgf("Successfully fetched latest commit timestamp from branch: %#v", timestamp) + o.BranchTimestamp = timestamp.Timestamp + o.TargetBranch = timestamp.Branch + return true, nil } -func (o *Options) generateUri() string { - return path.Join(o.TargetOwner, o.TargetRepo, "raw", o.TargetBranch, o.TargetPath) -} - -func (o *Options) generateUriClientArgs() (targetOwner, targetRepo, ref, resource string) { - return o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath -} - -func (o *Options) timestamp() string { - return strconv.FormatInt(o.BranchTimestamp.Unix(), 10) +func (o *Options) ContentWebLink(giteaClient *gitea.Client) string { + return giteaClient.ContentWebLink(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath) + "; rel=\"canonical\"" } diff --git a/server/upstream/redirects.go b/server/upstream/redirects.go new file mode 100644 index 0000000..ab6c971 --- /dev/null +++ b/server/upstream/redirects.go @@ -0,0 +1,117 @@ +package upstream + +import ( + "strconv" + "strings" + "time" + + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/context" + "codeberg.org/codeberg/pages/server/gitea" + "github.com/rs/zerolog/log" +) + +type Redirect struct { + From string + To string + StatusCode int +} + +// redirectsCacheTimeout specifies the timeout for the redirects cache. +var redirectsCacheTimeout = 10 * time.Minute + +const redirectsConfig = "_redirects" + +// getRedirects returns redirects specified in the _redirects file. +func (o *Options) getRedirects(giteaClient *gitea.Client, redirectsCache cache.SetGetKey) []Redirect { + var redirects []Redirect + cacheKey := o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch + + // Check for cached redirects + if cachedValue, ok := redirectsCache.Get(cacheKey); ok { + redirects = cachedValue.([]Redirect) + } else { + // Get _redirects file and parse + body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, redirectsConfig) + if err == nil { + for _, line := range strings.Split(string(body), "\n") { + redirectArr := strings.Fields(line) + + // Ignore comments and invalid lines + if strings.HasPrefix(line, "#") || len(redirectArr) < 2 { + continue + } + + // Get redirect status code + statusCode := 301 + if len(redirectArr) == 3 { + statusCode, err = strconv.Atoi(redirectArr[2]) + if err != nil { + log.Info().Err(err).Msgf("could not read %s of %s/%s", redirectsConfig, o.TargetOwner, o.TargetRepo) + } + } + + redirects = append(redirects, Redirect{ + From: redirectArr[0], + To: redirectArr[1], + StatusCode: statusCode, + }) + } + } + _ = redirectsCache.Set(cacheKey, redirects, redirectsCacheTimeout) + } + return redirects +} + +func (o *Options) matchRedirects(ctx *context.Context, giteaClient *gitea.Client, redirects []Redirect, redirectsCache cache.SetGetKey) (final bool) { + if len(redirects) > 0 { + for _, redirect := range redirects { + reqUrl := ctx.Req.RequestURI + // remove repo and branch from request url + reqUrl = strings.TrimPrefix(reqUrl, "/"+o.TargetRepo) + reqUrl = strings.TrimPrefix(reqUrl, "/@"+o.TargetBranch) + + // check if from url matches request url + if strings.TrimSuffix(redirect.From, "/") == strings.TrimSuffix(reqUrl, "/") { + // do rewrite if status code is 200 + if redirect.StatusCode == 200 { + o.TargetPath = redirect.To + o.Upstream(ctx, giteaClient, redirectsCache) + return true + } else { + ctx.Redirect(redirect.To, redirect.StatusCode) + return true + } + } + + // handle wildcard redirects + trimmedFromUrl := strings.TrimSuffix(redirect.From, "/*") + if strings.HasSuffix(redirect.From, "/*") && strings.HasPrefix(reqUrl, trimmedFromUrl) { + if strings.Contains(redirect.To, ":splat") { + splatUrl := strings.ReplaceAll(redirect.To, ":splat", strings.TrimPrefix(reqUrl, trimmedFromUrl)) + // do rewrite if status code is 200 + if redirect.StatusCode == 200 { + o.TargetPath = splatUrl + o.Upstream(ctx, giteaClient, redirectsCache) + return true + } else { + ctx.Redirect(splatUrl, redirect.StatusCode) + return true + } + } else { + // do rewrite if status code is 200 + if redirect.StatusCode == 200 { + o.TargetPath = redirect.To + o.Upstream(ctx, giteaClient, redirectsCache) + return true + } else { + ctx.Redirect(redirect.To, redirect.StatusCode) + return true + } + } + } + } + } + + return false +} diff --git a/server/upstream/upstream.go b/server/upstream/upstream.go index 0e27727..1a444e4 100644 --- a/server/upstream/upstream.go +++ b/server/upstream/upstream.go @@ -1,20 +1,28 @@ package upstream import ( - "bytes" "errors" + "fmt" "io" + "net/http" "strings" "time" "github.com/rs/zerolog/log" - "github.com/valyala/fasthttp" "codeberg.org/codeberg/pages/html" "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/context" "codeberg.org/codeberg/pages/server/gitea" ) +const ( + headerLastModified = "Last-Modified" + headerIfModifiedSince = "If-Modified-Since" + + rawMime = "text/plain; charset=utf-8" +) + // upstreamIndexPages lists pages that may be considered as index pages for directories. var upstreamIndexPages = []string{ "index.html", @@ -27,68 +35,81 @@ var upstreamNotFoundPages = []string{ // Options provides various options for the upstream request. type Options struct { - TargetOwner, - TargetRepo, - TargetBranch, - TargetPath, + TargetOwner string + TargetRepo string + TargetBranch string + TargetPath string // Used for debugging purposes. Host string - DefaultMimeType string - ForbiddenMimeTypes map[string]bool - TryIndexPages bool - BranchTimestamp time.Time + TryIndexPages bool + BranchTimestamp time.Time // internal appendTrailingSlash bool redirectIfExists string + + ServeRaw bool } // Upstream requests a file from the Gitea API at GiteaRoot and writes it to the request context. -func (o *Options) Upstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client, branchTimestampCache, fileResponseCache cache.SetGetKey) (final bool) { - log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath, o.Host}).Logger() +func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client, redirectsCache cache.SetGetKey) bool { + log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger() - // Check if the branch exists and when it was modified - if o.BranchTimestamp.IsZero() { - branch := GetBranchTimestamp(giteaClient, o.TargetOwner, o.TargetRepo, o.TargetBranch, branchTimestampCache) - - if branch == nil { - html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) - return true - } - o.TargetBranch = branch.Branch - o.BranchTimestamp = branch.Timestamp - } - - if o.TargetOwner == "" || o.TargetRepo == "" || o.TargetBranch == "" { - html.ReturnErrorPage(ctx, fasthttp.StatusBadRequest) + if o.TargetOwner == "" || o.TargetRepo == "" { + html.ReturnErrorPage(ctx, "gitea client: either repo owner or name info is missing", http.StatusBadRequest) return true } - // Check if the browser has a cached version - if ifModifiedSince, err := time.Parse(time.RFC1123, string(ctx.Request.Header.Peek("If-Modified-Since"))); err == nil { - if !ifModifiedSince.Before(o.BranchTimestamp) { - ctx.Response.SetStatusCode(fasthttp.StatusNotModified) + // Check if the branch exists and when it was modified + if o.BranchTimestamp.IsZero() { + branchExist, err := o.GetBranchTimestamp(giteaClient) + // handle 404 + if err != nil && errors.Is(err, gitea.ErrorNotFound) || !branchExist { + html.ReturnErrorPage(ctx, + fmt.Sprintf("branch %q for %s/%s not found", o.TargetBranch, o.TargetOwner, o.TargetRepo), + http.StatusNotFound) return true } + + // handle unexpected errors + if err != nil { + html.ReturnErrorPage(ctx, + fmt.Sprintf("could not get timestamp of branch %q: '%v'", o.TargetBranch, err), + http.StatusFailedDependency) + return true + } + } + + // Check if the browser has a cached version + if ctx.Response() != nil { + if ifModifiedSince, err := time.Parse(time.RFC1123, ctx.Response().Header.Get(headerIfModifiedSince)); err == nil { + if ifModifiedSince.After(o.BranchTimestamp) { + ctx.RespWriter.WriteHeader(http.StatusNotModified) + log.Trace().Msg("check response against last modified: valid") + return true + } + } + log.Trace().Msg("check response against last modified: outdated") } log.Debug().Msg("Preparing") - // Make a GET request to the upstream URL - uri := o.generateUri() - var res *fasthttp.Response - var cachedResponse gitea.FileResponse - var err error - if cachedValue, ok := fileResponseCache.Get(uri + "?timestamp=" + o.timestamp()); ok && !cachedValue.(gitea.FileResponse).IsEmpty() { - cachedResponse = cachedValue.(gitea.FileResponse) - } else { - res, err = giteaClient.ServeRawContent(o.generateUriClientArgs()) + reader, header, statusCode, err := giteaClient.ServeRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath) + if reader != nil { + defer reader.Close() } + log.Debug().Msg("Aquisting") - // Handle errors - if (err != nil && errors.Is(err, gitea.ErrorNotFound)) || (res == nil && !cachedResponse.Exists) { + // Handle not found error + if err != nil && errors.Is(err, gitea.ErrorNotFound) { + // Get and match redirects + redirects := o.getRedirects(giteaClient, redirectsCache) + if o.matchRedirects(ctx, giteaClient, redirects, redirectsCache) { + return true + } + if o.TryIndexPages { // copy the o struct & try if an index page exists optionsForIndexPages := *o @@ -96,25 +117,20 @@ func (o *Options) Upstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client, optionsForIndexPages.appendTrailingSlash = true for _, indexPage := range upstreamIndexPages { optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage - if optionsForIndexPages.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { - _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{ - Exists: false, - }, fileCacheTimeout) + if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) { return true } } // compatibility fix for GitHub Pages (/example → /example.html) optionsForIndexPages.appendTrailingSlash = false - optionsForIndexPages.redirectIfExists = strings.TrimSuffix(string(ctx.Request.URI().Path()), "/") + ".html" + optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Path(), "/") + ".html" optionsForIndexPages.TargetPath = o.TargetPath + ".html" - if optionsForIndexPages.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { - _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{ - Exists: false, - }, fileCacheTimeout) + if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) { return true } } - ctx.Response.SetStatusCode(fasthttp.StatusNotFound) + + ctx.StatusCode = http.StatusNotFound if o.TryIndexPages { // copy the o struct & try if a not found page exists optionsForNotFoundPages := *o @@ -122,92 +138,71 @@ func (o *Options) Upstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client, optionsForNotFoundPages.appendTrailingSlash = false for _, notFoundPage := range upstreamNotFoundPages { optionsForNotFoundPages.TargetPath = "/" + notFoundPage - if optionsForNotFoundPages.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { - _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{ - Exists: false, - }, fileCacheTimeout) + if optionsForNotFoundPages.Upstream(ctx, giteaClient, redirectsCache) { return true } } } - if res != nil { - // Update cache if the request is fresh - _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{ - Exists: false, - }, fileCacheTimeout) - } + return false } - if res != nil && (err != nil || res.StatusCode() != fasthttp.StatusOK) { - log.Warn().Msgf("Couldn't fetch contents from %q: %v (status code %d)", uri, err, res.StatusCode()) - html.ReturnErrorPage(ctx, fasthttp.StatusInternalServerError) + + // handle unexpected client errors + if err != nil || reader == nil || statusCode != http.StatusOK { + log.Debug().Msg("Handling error") + var msg string + + if err != nil { + msg = "gitea client: returned unexpected error" + log.Error().Err(err).Msg(msg) + msg = fmt.Sprintf("%s: '%v'", msg, err) + } + if reader == nil { + msg = "gitea client: returned no reader" + log.Error().Msg(msg) + } + if statusCode != http.StatusOK { + msg = fmt.Sprintf("gitea client: couldn't fetch contents: %d - %s", statusCode, http.StatusText(statusCode)) + log.Error().Msg(msg) + } + + html.ReturnErrorPage(ctx, msg, http.StatusInternalServerError) return true } // Append trailing slash if missing (for index files), and redirect to fix filenames in general // o.appendTrailingSlash is only true when looking for index pages - if o.appendTrailingSlash && !bytes.HasSuffix(ctx.Request.URI().Path(), []byte{'/'}) { - ctx.Redirect(string(ctx.Request.URI().Path())+"/", fasthttp.StatusTemporaryRedirect) + if o.appendTrailingSlash && !strings.HasSuffix(ctx.Path(), "/") { + ctx.Redirect(ctx.Path()+"/", http.StatusTemporaryRedirect) return true } - if bytes.HasSuffix(ctx.Request.URI().Path(), []byte("/index.html")) { - ctx.Redirect(strings.TrimSuffix(string(ctx.Request.URI().Path()), "index.html"), fasthttp.StatusTemporaryRedirect) + if strings.HasSuffix(ctx.Path(), "/index.html") && !o.ServeRaw { + ctx.Redirect(strings.TrimSuffix(ctx.Path(), "index.html"), http.StatusTemporaryRedirect) return true } if o.redirectIfExists != "" { - ctx.Redirect(o.redirectIfExists, fasthttp.StatusTemporaryRedirect) + ctx.Redirect(o.redirectIfExists, http.StatusTemporaryRedirect) return true } - log.Debug().Msg("Handling error") - // Set the MIME type - mimeType := o.getMimeTypeByExtension() - ctx.Response.Header.SetContentType(mimeType) - - // Set ETag - if cachedResponse.Exists { - ctx.Response.Header.SetBytesV(fasthttp.HeaderETag, cachedResponse.ETag) - } else if res != nil { - cachedResponse.ETag = res.Header.Peek(fasthttp.HeaderETag) - ctx.Response.Header.SetBytesV(fasthttp.HeaderETag, cachedResponse.ETag) - } - - if ctx.Response.StatusCode() != fasthttp.StatusNotFound { - // Everything's okay so far - ctx.Response.SetStatusCode(fasthttp.StatusOK) - } - ctx.Response.Header.SetLastModified(o.BranchTimestamp) + // Set ETag & MIME + o.setHeader(ctx, header) log.Debug().Msg("Prepare response") + ctx.RespWriter.WriteHeader(ctx.StatusCode) + // Write the response body to the original request - var cacheBodyWriter bytes.Buffer - if res != nil { - if res.Header.ContentLength() > fileCacheSizeLimit { - // fasthttp else will set "Content-Length: 0" - ctx.Response.SetBodyStream(&strings.Reader{}, -1) - - err = res.BodyWriteTo(ctx.Response.BodyWriter()) - } else { - // TODO: cache is half-empty if request is cancelled - does the ctx.Err() below do the trick? - err = res.BodyWriteTo(io.MultiWriter(ctx.Response.BodyWriter(), &cacheBodyWriter)) + if reader != nil { + _, err := io.Copy(ctx.RespWriter, reader) + if err != nil { + log.Error().Err(err).Msgf("Couldn't write body for %q", o.TargetPath) + html.ReturnErrorPage(ctx, "", http.StatusInternalServerError) + return true } - } else { - _, err = ctx.Write(cachedResponse.Body) } - if err != nil { - log.Error().Err(err).Msgf("Couldn't write body for %q", uri) - html.ReturnErrorPage(ctx, fasthttp.StatusInternalServerError) - return true - } - log.Debug().Msg("Sending response") - if res != nil && res.Header.ContentLength() <= fileCacheSizeLimit && ctx.Err() == nil { - cachedResponse.Exists = true - cachedResponse.MimeType = mimeType - cachedResponse.Body = cacheBodyWriter.Bytes() - _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), cachedResponse, fileCacheTimeout) - } + log.Debug().Msg("Sending response") return true } diff --git a/server/utils/utils.go b/server/utils/utils.go index 7be330f..91ed359 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -1,11 +1,27 @@ package utils -import "bytes" +import ( + "net/url" + "path" + "strings" +) -func TrimHostPort(host []byte) []byte { - i := bytes.IndexByte(host, ':') +func TrimHostPort(host string) string { + i := strings.IndexByte(host, ':') if i >= 0 { return host[:i] } return host } + +func CleanPath(uriPath string) string { + unescapedPath, _ := url.PathUnescape(uriPath) + cleanedPath := path.Join("/", unescapedPath) + + // If the path refers to a directory, add a trailing slash. + if !strings.HasSuffix(cleanedPath, "/") && (strings.HasSuffix(unescapedPath, "/") || strings.HasSuffix(unescapedPath, "/.") || strings.HasSuffix(unescapedPath, "/..")) { + cleanedPath += "/" + } + + return cleanedPath +} diff --git a/server/utils/utils_test.go b/server/utils/utils_test.go index 3dc0632..b8fcea9 100644 --- a/server/utils/utils_test.go +++ b/server/utils/utils_test.go @@ -7,7 +7,63 @@ import ( ) func TestTrimHostPort(t *testing.T) { - assert.EqualValues(t, "aa", TrimHostPort([]byte("aa"))) - assert.EqualValues(t, "", TrimHostPort([]byte(":"))) - assert.EqualValues(t, "example.com", TrimHostPort([]byte("example.com:80"))) + assert.EqualValues(t, "aa", TrimHostPort("aa")) + assert.EqualValues(t, "", TrimHostPort(":")) + assert.EqualValues(t, "example.com", TrimHostPort("example.com:80")) +} + +// TestCleanPath is mostly copied from fasthttp, to keep the behaviour we had before migrating away from it. +// Source (MIT licensed): https://github.com/valyala/fasthttp/blob/v1.48.0/uri_test.go#L154 +// Copyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors +func TestCleanPath(t *testing.T) { + // double slash + testURIPathNormalize(t, "/aa//bb", "/aa/bb") + + // triple slash + testURIPathNormalize(t, "/x///y/", "/x/y/") + + // multi slashes + testURIPathNormalize(t, "/abc//de///fg////", "/abc/de/fg/") + + // encoded slashes + testURIPathNormalize(t, "/xxxx%2fyyy%2f%2F%2F", "/xxxx/yyy/") + + // dotdot + testURIPathNormalize(t, "/aaa/..", "/") + + // dotdot with trailing slash + testURIPathNormalize(t, "/xxx/yyy/../", "/xxx/") + + // multi dotdots + testURIPathNormalize(t, "/aaa/bbb/ccc/../../ddd", "/aaa/ddd") + + // dotdots separated by other data + testURIPathNormalize(t, "/a/b/../c/d/../e/..", "/a/c/") + + // too many dotdots + testURIPathNormalize(t, "/aaa/../../../../xxx", "/xxx") + testURIPathNormalize(t, "/../../../../../..", "/") + testURIPathNormalize(t, "/../../../../../../", "/") + + // encoded dotdots + testURIPathNormalize(t, "/aaa%2Fbbb%2F%2E.%2Fxxx", "/aaa/xxx") + + // double slash with dotdots + testURIPathNormalize(t, "/aaa////..//b", "/b") + + // fake dotdot + testURIPathNormalize(t, "/aaa/..bbb/ccc/..", "/aaa/..bbb/") + + // single dot + testURIPathNormalize(t, "/a/./b/././c/./d.html", "/a/b/c/d.html") + testURIPathNormalize(t, "./foo/", "/foo/") + testURIPathNormalize(t, "./../.././../../aaa/bbb/../../../././../", "/") + testURIPathNormalize(t, "./a/./.././../b/./foo.html", "/b/foo.html") +} + +func testURIPathNormalize(t *testing.T, requestURI, expectedPath string) { + cleanedPath := CleanPath(requestURI) + if cleanedPath != expectedPath { + t.Fatalf("Unexpected path %q. Expected %q. requestURI=%q", cleanedPath, expectedPath, requestURI) + } }