diff --git a/.ecrc b/.ecrc deleted file mode 100644 index d9ee788..0000000 --- a/.ecrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Exclude": [ - ".git", - "go.mod", "go.sum", - "vendor", - "LICENSE", - "_test.go" - ] -} diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 354a828..0000000 --- a/.editorconfig +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 5a9cce6..0000000 --- a/.gitea/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -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 3035107..dfe69ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,7 @@ .idea/ -.cache/ *.iml key-database.pogreb/ acme-account.json build/ vendor/ pages -certs.sqlite -.bash_history diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index 488ca09..0000000 --- a/.golangci.yml +++ /dev/null @@ -1,34 +0,0 @@ -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 de8341c..2674e9b 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,10 +1,9 @@ -when: - branch: main +branches: main -steps: +pipeline: # use vendor to cache dependencies vendor: - image: golang:1.21 + image: golang:1.18 commands: - go mod vendor @@ -18,13 +17,9 @@ steps: - "[ $(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: codeberg.org/6543/docker-images/golang_just + image: a6543/golang_just commands: - go version - just build @@ -44,7 +39,7 @@ steps: build-tag: group: compliant - image: codeberg.org/6543/docker-images/golang_just + image: a6543/golang_just commands: - go version - just build-tag ${CI_COMMIT_TAG##v} @@ -53,13 +48,13 @@ steps: test: group: test - image: codeberg.org/6543/docker-images/golang_just + image: a6543/golang_just commands: - just test integration-tests: group: test - image: codeberg.org/6543/docker-images/golang_just + image: a6543/golang_just commands: - just integration environment: @@ -97,7 +92,6 @@ steps: from_secret: bot_token when: event: [ "push" ] - branch: ${CI_REPO_DEFAULT_BRANCH} docker-tag: image: plugins/kaniko @@ -105,7 +99,7 @@ steps: registry: codeberg.org dockerfile: Dockerfile repo: codeberg.org/codeberg/pages-server - tags: [ latest, "${CI_COMMIT_TAG}" ] + tag: [ latest, "${CI_COMMIT_TAG}" ] username: from_secret: bot_user password: diff --git a/Dockerfile b/Dockerfile index eec97de..71dd236 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,15 @@ -FROM techknowlogick/xgo as build +FROM golang:alpine as build WORKDIR /workspace +RUN apk add ca-certificates COPY . . -RUN CGO_ENABLED=1 go build -tags 'sqlite sqlite_unlock_notify netgo' -ldflags '-s -w -extldflags "-static" -linkmode external' . +RUN CGO_ENABLED=0 go build . 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"] + +ENTRYPOINT ["/pages"] \ No newline at end of file diff --git a/FEATURES.md b/FEATURES.md deleted file mode 100644 index 3d2f394..0000000 --- a/FEATURES.md +++ /dev/null @@ -1,51 +0,0 @@ -# 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 0b8f814..03a7436 100644 --- a/Justfile +++ b/Justfile @@ -1,6 +1,3 @@ -CGO_FLAGS := '-extldflags "-static" -linkmode external' -TAGS := 'sqlite sqlite_unlock_notify netgo' - dev: #!/usr/bin/env bash set -euxo pipefail @@ -9,27 +6,25 @@ 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 -tags '{{TAGS}}' . + go run . build: - CGO_ENABLED=1 go build -tags '{{TAGS}}' -ldflags '-s -w {{CGO_FLAGS}}' -v -o build/codeberg-pages-server ./ + CGO_ENABLED=0 go build -ldflags '-s -w' -v -o build/codeberg-pages-server ./ build-tag VERSION: - 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 ./ + CGO_ENABLED=0 go build -ldflags '-s -w -X "codeberg.org/codeberg/pages/server/version.Version={{VERSION}}"' -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/ integration/certs.sqlite integration/acme-account.json + rm -rf build/ tool-golangci: @hash golangci-lint> /dev/null 2>&1; if [ $? -ne 0 ]; then \ @@ -42,16 +37,13 @@ tool-gofumpt: fi test: - go test -race -cover -tags '{{TAGS}}' codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/ + go test -race codeberg.org/codeberg/pages/server/... test-run TEST: - go test -race -tags '{{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/ + go test -race -run "^{{TEST}}$" codeberg.org/codeberg/pages/server/... integration: - go test -race -tags 'integration {{TAGS}}' codeberg.org/codeberg/pages/integration/... + go test -race -tags integration codeberg.org/codeberg/pages/integration/... integration-run TEST: - 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 + go test -race -tags integration -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/... diff --git a/README.md b/README.md index fb2a4b9..700f279 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,5 @@ # 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. @@ -14,7 +8,6 @@ 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 @@ -27,19 +20,16 @@ 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 @@ -60,27 +50,28 @@ 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 `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). +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). -### Environment Variables +### Environment - `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 (must be subdomain of `PAGES_DOMAIN`). +- `RAW_DOMAIN` (default: `raw.codeberg.page`): domain for raw resources. - `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: ): 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). +- `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). 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 the email sent to the ACME API server to receive, for example, renewal reminders. +- `ACME_EMAIL` (default: `noreply@example.email`): Set this to "true" to accept the Terms of Service of your ACME provider. - `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 for available values & additional environment variables. -- `LOG_LEVEL` (default: warn): Set this to specify the level of logging. + 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. + ## Contributing to the development @@ -89,36 +80,30 @@ 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 chat room](#chat-for-admins--devs). +feel free to ping us in an issue or in a general Matrix chatgroup. -You can also contact the maintainer(s) of this project: - -- [crapStone](https://codeberg.org/crapStone) [(Matrix)](https://matrix.to/#/@crapstone:obermui.de) - -Previous maintainers: +You can also contact the maintainers of this project: - [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. -The [Architecture is explained](https://codeberg.org/Codeberg/pages-server/wiki/Architecture) in the wiki. - +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 `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 -Make sure you have [golang](https://go.dev) v1.21 or newer and [just](https://just.systems/man/en/) installed. - -run `just dev` +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 6012b6e..d93fe13 100644 --- a/cmd/certs.go +++ b/cmd/certs.go @@ -2,9 +2,11 @@ package cmd import ( "fmt" - "time" + "github.com/akrylysov/pogreb" "github.com/urfave/cli/v2" + + "codeberg.org/codeberg/pages/server/database" ) var Certs = &cli.Command{ @@ -22,26 +24,24 @@ var Certs = &cli.Command{ Action: removeCert, }, }, - Flags: CertStorageFlags, } func listCerts(ctx *cli.Context) error { - certDB, closeFn, err := openCertDB(ctx) + // TODO: make "key-database.pogreb" set via flag + keyDatabase, err := database.New("key-database.pogreb") if err != nil { - return err - } - defer closeFn() - - items, err := certDB.Items(0, 0) - if err != nil { - return err + return fmt.Errorf("could not create database: %v", err) } - 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)) + 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) } return nil } @@ -53,17 +53,20 @@ func removeCert(ctx *cli.Context) error { domains := ctx.Args().Slice() - certDB, closeFn, err := openCertDB(ctx) + // TODO: make "key-database.pogreb" set via flag + keyDatabase, err := database.New("key-database.pogreb") if err != nil { - return err + return fmt.Errorf("could not create database: %v", err) } - defer closeFn() for _, domain := range domains { fmt.Printf("Removing domain %s from the database...\n", domain) - if err := certDB.Delete(domain); err != nil { + if err := keyDatabase.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 7ac94e6..50bf6b3 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -4,159 +4,127 @@ import ( "github.com/urfave/cli/v2" ) -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"}, - }, - } +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/", + }, - 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, - }, + // 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"}, + }, - // ########################### - // ### 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"}, - }, - }...) -) + // 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"}, + }, +} diff --git a/cmd/main.go b/cmd/main.go index 683e859..1830cef 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,11 +1,12 @@ package cmd import ( + "bytes" "context" "crypto/tls" + "errors" "fmt" "net" - "net/http" "os" "strings" "time" @@ -14,135 +15,148 @@ 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 = []string{ - "fonts.codeberg.org", - "design.codeberg.org", +var AllowedCorsDomains = [][]byte{ + []byte("fonts.codeberg.org"), + []byte("design.codeberg.org"), } // BlacklistedPaths specifies forbidden path prefixes for all Codeberg Pages. // TODO: Make it a flag too -var BlacklistedPaths = []string{ - "/.well-known/acme-challenge/", +var BlacklistedPaths = [][]byte{ + []byte("/.well-known/acme-challenge/"), } // Serve sets up and starts the web server. func Serve(ctx *cli.Context) error { - // Initialize the logger. + // Initalize 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 := ctx.String("gitea-root") + giteaRoot := strings.TrimSuffix(ctx.String("gitea-root"), "/") giteaAPIToken := ctx.String("gitea-api-token") rawDomain := ctx.String("raw-domain") - 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")) + 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")) enableHTTPServer := ctx.Bool("enable-http-server") - allowedCorsDomains := AllowedCorsDomains - if rawDomain != "" { - allowedCorsDomains = append(allowedCorsDomains, rawDomain) + 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") } - // Make sure MainDomain has a trailing dot - if !strings.HasPrefix(mainDomainSuffix, ".") { - mainDomainSuffix = "." + mainDomainSuffix + allowedCorsDomains := AllowedCorsDomains + if len(rawDomain) != 0 { + allowedCorsDomains = append(allowedCorsDomains, []byte(rawDomain)) + } + + // Make sure MainDomain has a trailing dot, and GiteaRoot has no trailing slash + if !bytes.HasPrefix(mainDomainSuffix, []byte{'.'}) { + mainDomainSuffix = append([]byte{'.'}, mainDomainSuffix...) } if len(defaultBranches) == 0 { - return fmt.Errorf("no default branches set (PAGES_BRANCHES)") + defaultBranches = []string{"pages"} } - // 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() - // redirectsCache stores redirects in _redirects files - redirectsCache := cache.NewKeyValueCache() - // clientResponseCache stores responses from the Gitea server - clientResponseCache := 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() - giteaClient, err := gitea.NewClient(giteaRoot, giteaAPIToken, clientResponseCache, ctx.Bool("enable-symlink-support"), ctx.Bool("enable-lfs-support")) + giteaClient, err := gitea.NewClient(giteaRoot, giteaAPIToken, ctx.Bool("enable-symlink-support"), ctx.Bool("enable-lfs-support")) if err != nil { return fmt.Errorf("could not create new gitea client: %v", err) } - acmeClient, err := createAcmeClient(ctx, enableHTTPServer, challengeCache) - if err != nil { - return err - } + // Create handler based on settings + handler := server.Handler(mainDomainSuffix, []byte(rawDomain), + giteaClient, + giteaRoot, rawInfoPage, + BlacklistedPaths, allowedCorsDomains, + dnsLookupCache, canonicalDomainCache, branchTimestampCache, fileResponseCache, + defaultBranches, + ) - if err := certificates.SetupMainDomainCertificates(mainDomainSuffix, acmeClient, certDB); err != nil { - return err - } + fastServer := server.SetupServer(handler) + httpServer := server.SetupHTTPACMEChallengeServer(challengeCache) - // Create listener for SSL connections - log.Info().Msgf("Create TCP listener for SSL on %s", listeningSSLAddress) - listener, err := net.Listen("tcp", listeningSSLAddress) + // Setup listener and TLS + log.Info().Msgf("Listening on https://%s", listeningAddress) + listener, err := net.Listen("tcp", listeningAddress) if err != nil { return fmt.Errorf("couldn't create listener: %v", err) } - // Setup listener for SSL connections + // 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 + 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, acmeClient, mainDomainSuffix, certDB) + go certificates.MaintainCertDB(certMaintainCtx, interval, mainDomainSuffix, dnsProvider, acmeUseRateLimits, 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().Msgf("Start HTTP server listening on %s", listeningHTTPAddress) - err := http.ListenAndServe(listeningHTTPAddress, httpHandler) + log.Info().Msg("Start HTTP server listening on :80") + err := httpServer.ListenAndServe("[::]:80") if err != nil { log.Panic().Err(err).Msg("Couldn't start HTTP fastServer") } }() } - // 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 { + // Start the web fastServer + log.Info().Msgf("Start listening on %s", listener.Addr()) + err = fastServer.Serve(listener) + if err != nil { log.Panic().Err(err).Msg("Couldn't start fastServer") } diff --git a/cmd/setup.go b/cmd/setup.go deleted file mode 100644 index cde4bc9..0000000 --- a/cmd/setup.go +++ /dev/null @@ -1,64 +0,0 @@ -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/go.mod b/go.mod index eba292e..479c328 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,18 @@ module codeberg.org/codeberg/pages -go 1.21 - -toolchain go1.21.4 +go 1.18 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 - golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb - xorm.io/xorm v1.3.2 + github.com/valyala/fasthttp v1.31.0 + github.com/valyala/fastjson v1.6.3 ) require ( @@ -37,15 +31,14 @@ 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 @@ -53,28 +46,24 @@ 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/goccy/go-json v0.8.1 // indirect - github.com/gofrs/uuid v4.0.0+incompatible // indirect + github.com/gofrs/uuid v3.2.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.3.0 // indirect + github.com/google/uuid v1.1.1 // 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.12 // indirect + github.com/json-iterator/go v1.1.7 // 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 @@ -89,7 +78,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.2 // indirect + github.com/modern-go/reflect2 v1.0.1 // 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 @@ -114,17 +103,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.14.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect + golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect + golang.org/x/text v0.3.6 // 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 @@ -136,5 +125,4 @@ 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 7ea8b78..23a58bc 100644 --- a/go.sum +++ b/go.sum @@ -22,12 +22,7 @@ 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= @@ -58,38 +53,26 @@ 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/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/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= +github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 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= @@ -98,48 +81,33 @@ 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= @@ -148,13 +116,6 @@ 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= @@ -164,8 +125,6 @@ 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= @@ -177,43 +136,28 @@ 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= @@ -238,10 +182,7 @@ 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.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/golang/snappy v0.0.3/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= @@ -250,10 +191,8 @@ 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.3/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.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= @@ -266,10 +205,8 @@ 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.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 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= @@ -280,25 +217,16 @@ 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= @@ -315,10 +243,6 @@ 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= @@ -328,61 +252,12 @@ 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= @@ -396,10 +271,8 @@ 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= @@ -407,10 +280,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= @@ -421,7 +294,6 @@ 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= @@ -430,15 +302,6 @@ 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= @@ -448,25 +311,19 @@ 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= @@ -476,14 +333,9 @@ 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= @@ -504,19 +356,11 @@ 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= @@ -535,49 +379,29 @@ 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= @@ -586,41 +410,27 @@ 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= @@ -628,18 +438,12 @@ 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= @@ -658,28 +462,21 @@ 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= @@ -689,22 +486,23 @@ 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= @@ -715,13 +513,7 @@ 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= @@ -729,42 +521,26 @@ 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= @@ -775,8 +551,6 @@ 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= @@ -797,8 +571,6 @@ 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= @@ -807,7 +579,6 @@ 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= @@ -818,7 +589,6 @@ 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= @@ -834,9 +604,8 @@ 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-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/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/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= @@ -860,11 +629,9 @@ 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= @@ -874,7 +641,6 @@ 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= @@ -882,7 +648,6 @@ 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= @@ -898,22 +663,17 @@ 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-20210902050250-f475640dd07b/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-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= @@ -921,10 +681,8 @@ 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= @@ -933,7 +691,6 @@ 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= @@ -942,18 +699,14 @@ 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= @@ -962,7 +715,6 @@ 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= @@ -972,17 +724,12 @@ 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= @@ -995,7 +742,6 @@ 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= @@ -1006,7 +752,6 @@ 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= @@ -1021,15 +766,10 @@ 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= @@ -1048,13 +788,10 @@ 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= @@ -1068,7 +805,6 @@ 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= @@ -1081,129 +817,12 @@ 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/examples/haproxy-sni/.gitignore b/haproxy-sni/.gitignore similarity index 100% rename from examples/haproxy-sni/.gitignore rename to haproxy-sni/.gitignore diff --git a/examples/haproxy-sni/README.md b/haproxy-sni/README.md similarity index 100% rename from examples/haproxy-sni/README.md rename to haproxy-sni/README.md diff --git a/examples/haproxy-sni/dhparam.pem b/haproxy-sni/dhparam.pem similarity index 93% rename from examples/haproxy-sni/dhparam.pem rename to haproxy-sni/dhparam.pem index 9b182b7..088f967 100644 --- a/examples/haproxy-sni/dhparam.pem +++ b/haproxy-sni/dhparam.pem @@ -5,4 +5,4 @@ MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi 7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== ------END DH PARAMETERS----- +-----END DH PARAMETERS----- \ No newline at end of file diff --git a/examples/haproxy-sni/docker-compose.yml b/haproxy-sni/docker-compose.yml similarity index 99% rename from examples/haproxy-sni/docker-compose.yml rename to haproxy-sni/docker-compose.yml index 66ff52b..4dd8677 100644 --- a/examples/haproxy-sni/docker-compose.yml +++ b/haproxy-sni/docker-compose.yml @@ -19,4 +19,4 @@ services: volumes: - ./pages-www:/srv:ro - ./pages.Caddyfile:/etc/caddy/Caddyfile:ro - + diff --git a/examples/haproxy-sni/gitea-www/index.html b/haproxy-sni/gitea-www/index.html similarity index 100% rename from examples/haproxy-sni/gitea-www/index.html rename to haproxy-sni/gitea-www/index.html diff --git a/examples/haproxy-sni/gitea.Caddyfile b/haproxy-sni/gitea.Caddyfile similarity index 100% rename from examples/haproxy-sni/gitea.Caddyfile rename to haproxy-sni/gitea.Caddyfile diff --git a/examples/haproxy-sni/haproxy-certificates/codeberg.org.pem b/haproxy-sni/haproxy-certificates/codeberg.org.pem similarity index 100% rename from examples/haproxy-sni/haproxy-certificates/codeberg.org.pem rename to haproxy-sni/haproxy-certificates/codeberg.org.pem diff --git a/examples/haproxy-sni/haproxy-certificates/codeberg.org.pem.key b/haproxy-sni/haproxy-certificates/codeberg.org.pem.key similarity index 100% rename from examples/haproxy-sni/haproxy-certificates/codeberg.org.pem.key rename to haproxy-sni/haproxy-certificates/codeberg.org.pem.key diff --git a/examples/haproxy-sni/haproxy.cfg b/haproxy-sni/haproxy.cfg similarity index 100% rename from examples/haproxy-sni/haproxy.cfg rename to haproxy-sni/haproxy.cfg diff --git a/examples/haproxy-sni/pages-www/index.html b/haproxy-sni/pages-www/index.html similarity index 100% rename from examples/haproxy-sni/pages-www/index.html rename to haproxy-sni/pages-www/index.html diff --git a/examples/haproxy-sni/pages.Caddyfile b/haproxy-sni/pages.Caddyfile similarity index 100% rename from examples/haproxy-sni/pages.Caddyfile rename to haproxy-sni/pages.Caddyfile diff --git a/examples/haproxy-sni/test.sh b/haproxy-sni/test.sh similarity index 100% rename from examples/haproxy-sni/test.sh rename to haproxy-sni/test.sh diff --git a/html/404.html b/html/404.html new file mode 100644 index 0000000..b7ec96e --- /dev/null +++ b/html/404.html @@ -0,0 +1,37 @@ + + + + + + %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 new file mode 100644 index 0000000..325dada --- /dev/null +++ b/html/error.go @@ -0,0 +1,24 @@ +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 ff8b6b4..d223e15 100644 --- a/html/html.go +++ b/html/html.go @@ -1,53 +1,6 @@ package html -import ( - _ "embed" - "net/http" - "text/template" // do not use html/template here, we sanitize the message before passing it to the template +import _ "embed" - "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 -} +//go:embed 404.html +var NotFoundPage []byte diff --git a/html/html_test.go b/html/html_test.go deleted file mode 100644 index b395bb2..0000000 --- a/html/html_test.go +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 6094a26..0000000 --- a/html/templates/error.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - {{.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 cfb7188..6054e17 100644 --- a/integration/get_test.go +++ b/integration/get_test.go @@ -20,20 +20,18 @@ func TestGetRedirect(t *testing.T) { log.Println("=== TestGetRedirect ===") // test custom domain redirect resp, err := getTestHTTPSClient().Get("https://calciumdibromid.localhost.mock.directory:4430") - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) if !assert.EqualValues(t, http.StatusTemporaryRedirect, resp.StatusCode) { t.FailNow() } assert.EqualValues(t, "https://www.cabr2.de/", resp.Header.Get("Location")) - assert.EqualValues(t, `Temporary Redirect.`, strings.TrimSpace(string(getBytes(resp.Body)))) + assert.EqualValues(t, 0, getSize(resp.Body)) } func TestGetContent(t *testing.T) { log.Println("=== TestGetContent ===") // test get image - resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/images/827679288a.jpg") + resp, err := getTestHTTPSClient().Get("https://magiclike.localhost.mock.directory:4430/images/827679288a.jpg") assert.NoError(t, err) if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { t.FailNow() @@ -44,218 +42,81 @@ func TestGetContent(t *testing.T) { assert.Len(t, resp.Header.Get("ETag"), 42) // specify branch - resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pag/@master/") + resp, err = getTestHTTPSClient().Get("https://momar.localhost.mock.directory:4430/pag/@master/") assert.NoError(t, err) - if !assert.NotNil(t, resp) { + if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { 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"), 44) + assert.Len(t, resp.Header.Get("ETag"), 42) // access branch name contains '/' - resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/blumia/@docs~main/") + resp, err = getTestHTTPSClient().Get("https://blumia.localhost.mock.directory:4430/pages-server-integration-tests/@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"), 44) + assert.Len(t, resp.Header.Get("ETag"), 42) - // TODO: test get of non cacheable content (content size > fileCacheSizeLimit) + // TODO: test get of non cachable 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.NotNil(t, resp) { + if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { 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://cb_pages_tests.localhost.mock.directory:4430/pages-404-demo/blah") + resp, err := getTestHTTPSClient().Get("https://crystal.localhost.mock.directory:4430/pages-404-demo/blah") assert.NoError(t, err) - if !assert.NotNil(t, resp) { + if !assert.EqualValues(t, http.StatusNotFound, resp.StatusCode) { 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") - // file symlink - resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/link") + resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/link") assert.NoError(t, err) - if !assert.NotNil(t, resp) { + if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { 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://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt") + resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt") assert.NoError(t, err) - if !assert.NotNil(t, resp) { + if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { 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 6566f78..06d553f 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 STOPPED ===") + log.Println("=== TestMain: Server STOPED ===") }() time.Sleep(10 * time.Second) - m.Run() + os.Exit(m.Run()) } func startServer(ctx context.Context) error { @@ -39,16 +39,12 @@ 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.ServerFlags + app.Flags = cmd.ServeFlags go func() { if err := app.RunContext(ctx, args); err != nil { diff --git a/main.go b/main.go index 6c1d0cc..2836b86 100644 --- a/main.go +++ b/main.go @@ -8,16 +8,18 @@ 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.Version + app.Version = version app.Usage = "pages server" app.Action = cmd.Serve - app.Flags = cmd.ServerFlags + app.Flags = cmd.ServeFlags app.Commands = []*cli.Command{ cmd.Certs, } diff --git a/server/certificates/acme_client.go b/server/certificates/acme_client.go deleted file mode 100644 index ba83e50..0000000 --- a/server/certificates/acme_client.go +++ /dev/null @@ -1,95 +0,0 @@ -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 deleted file mode 100644 index 12ad7c6..0000000 --- a/server/certificates/acme_config.go +++ /dev/null @@ -1,102 +0,0 @@ -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 deleted file mode 100644 index bc9ea67..0000000 --- a/server/certificates/cached_challengers.go +++ /dev/null @@ -1,83 +0,0 @@ -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 3ae891a..b1c1329 100644 --- a/server/certificates/certificates.go +++ b/server/certificates/certificates.go @@ -1,19 +1,30 @@ 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" @@ -24,104 +35,89 @@ 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 string, +func TLSConfig(mainDomainSuffix []byte, giteaClient *gitea.Client, - acmeClient *AcmeClient, - firstDefaultBranch string, + firstDefaultBranch, + dnsProvider string, + acmeUseRateLimits bool, 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) { - 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)") + sni := strings.ToLower(strings.TrimSpace(info.ServerName)) + sniBytes := []byte(sni) + if len(sni) < 1 { + return nil, errors.New("missing sni") } - // https request init is actually a acme challenge if info.SupportedProtos != nil { for _, proto := range info.SupportedProtos { - if proto != tlsalpn01.ACMETLS1Protocol { - continue + 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 } - 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 := "" - mayObtainCert := true - if strings.HasSuffix(domain, mainDomainSuffix) || strings.EqualFold(domain, mainDomainSuffix[1:]) { + if bytes.HasSuffix(sniBytes, mainDomainSuffix) || bytes.Equal(sniBytes, mainDomainSuffix[1:]) { // deliver default certificate for the main domain (*.codeberg.page) - domain = mainDomainSuffix + sniBytes = mainDomainSuffix + sni = string(sniBytes) } else { var targetRepo, targetBranch string - targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch, dnsLookupCache) + targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(sni, string(mainDomainSuffix), firstDefaultBranch, dnsLookupCache) if targetOwner == "" { // DNS not set up, return main certificate to redirect to the docs - domain = mainDomainSuffix + sniBytes = mainDomainSuffix + sni = string(sniBytes) } else { - targetOpt := &upstream.Options{ - TargetOwner: targetOwner, - TargetRepo: targetRepo, - TargetBranch: targetBranch, - } - _, valid := targetOpt.CheckCanonicalDomain(giteaClient, domain, mainDomainSuffix, canonicalDomainCache) + _, _ = targetRepo, targetBranch + _, valid := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, sni, string(mainDomainSuffix), canonicalDomainCache) if !valid { - // We shouldn't obtain a certificate when we cannot check if the - // repository has specified this domain in the `.domains` file. - mayObtainCert = false + sniBytes = mainDomainSuffix + sni = string(sniBytes) } } } - if tlsCertificate, ok := keyCache.Get(domain); ok { + if tlsCertificate, ok := keyCache.Get(sni); ok { // we can use an existing certificate object return tlsCertificate.(*tls.Certificate), nil } - var tlsCertificate *tls.Certificate + var tlsCertificate tls.Certificate var err error - 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) { + var ok bool + if tlsCertificate, ok = retrieveCertFromDB(sniBytes, mainDomainSuffix, dnsProvider, acmeUseRateLimits, certDB); !ok { + // request a new certificate + if bytes.Equal(sniBytes, 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 = acmeClient.obtainCert(acmeClient.legoClient, []string{domain}, nil, targetOwner, false, mainDomainSuffix, certDB) + tlsCertificate, err = obtainCert(acmeClient, []string{sni}, nil, targetOwner, dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB) if err != nil { return nil, err } } - if err := keyCache.Set(domain, tlsCertificate, 15*time.Minute); err != nil { + if err := keyCache.Set(sni, &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, }, @@ -140,115 +136,159 @@ func TLSConfig(mainDomainSuffix string, } } -func (c *AcmeClient) checkUserLimit(user string) error { - userLimit, ok := c.acmeClientCertificateLimitPerUser[user] +func checkUserLimit(user string) error { + userLimit, ok := acmeClientCertificateLimitPerUser[user] if !ok { - // Each user can only add 10 new domains per day. + // Each Codeberg user can only add 10 new domains per day. userLimit = equalizer.NewTokenBucket(10, time.Hour*24) - c.acmeClientCertificateLimitPerUser[user] = userLimit + acmeClientCertificateLimitPerUser[user] = userLimit } if !userLimit.Ask() { - return fmt.Errorf("user '%s' error: %w", user, ErrUserRateLimitExceeded) + return errors.New("rate limit exceeded: 10 certificates per user per 24 hours") } return nil } -func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProvider bool, certDB database.CertDB) (*tls.Certificate, error) { +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) { // parse certificate from database - res, err := certDB.Get(sni) + res, err := certDB.Get(string(sni)) if err != nil { - return nil, err - } else if res == nil { - return nil, database.ErrNotFound + panic(err) // TODO: no panic + } + if res == nil { + return tls.Certificate{}, false } tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey) if err != nil { - return nil, err + panic(err) } // TODO: document & put into own function - if !strings.EqualFold(sni, mainDomainSuffix) { + if !bytes.Equal(sni, mainDomainSuffix) { tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0]) if err != nil { - return nil, fmt.Errorf("error parsing leaf tlsCert: %w", err) + panic(err) } // renew certificates 7 days before they expire - if tlsCertificate.Leaf.NotAfter.Before(time.Now().Add(7 * 24 * time.Hour)) { - // TODO: use ValidTill of custom cert struct + if !tlsCertificate.Leaf.NotAfter.After(time.Now().Add(7 * 24 * time.Hour)) { + // TODO: add ValidUntil to custom res 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, nil + return tlsCertificate, true } } - // TODO: make a queue ? go (func() { res.CSR = nil // acme client doesn't like CSR to be set - 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) + 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) } })() } } - return &tlsCertificate, nil + return tlsCertificate, true } -func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user string, useDnsProvider bool, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) { +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) { name := strings.TrimPrefix(domains[0], "*") - if useDnsProvider && len(domains[0]) > 0 && domains[0][0] == '*' { + if dnsProvider == "" && len(domains[0]) > 0 && domains[0][0] == '*' { domains = domains[1:] } // lock to avoid simultaneous requests - _, working := c.obtainLocks.LoadOrStore(name, struct{}{}) + _, working := obtainLocks.LoadOrStore(name, struct{}{}) if working { for working { time.Sleep(100 * time.Millisecond) - _, working = c.obtainLocks.Load(name) + _, working = obtainLocks.Load(name) } - cert, err := c.retrieveCertFromDB(name, mainDomainSuffix, useDnsProvider, keyDatabase) - if err != nil { - return nil, fmt.Errorf("certificate failed in synchronous request: %w", err) + cert, ok := retrieveCertFromDB([]byte(name), mainDomainSuffix, dnsProvider, acmeUseRateLimits, keyDatabase) + if !ok { + return tls.Certificate{}, errors.New("certificate failed in synchronous request") } return cert, nil } - defer c.obtainLocks.Delete(name) + defer obtainLocks.Delete(name) if acmeClient == nil { - return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase) + return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", string(mainDomainSuffix), keyDatabase), nil } // request actual cert var res *certificate.Resource var err error if renew != nil && renew.CertURL != "" { - if c.acmeUseRateLimits { - c.acmeClientRequestLimit.Take() + if acmeUseRateLimits { + 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 := c.checkUserLimit(user); err != nil { - return nil, err + if err := checkUserLimit(user); err != nil { + return tls.Certificate{}, err } } - if c.acmeUseRateLimits { - c.acmeClientOrderLimit.Take() - c.acmeClientRequestLimit.Take() + if acmeUseRateLimits { + acmeClientOrderLimit.Take() + acmeClientRequestLimit.Take() } log.Debug().Msgf("Re-requesting new certificate for %v", domains) res, err = acmeClient.Certificate.Obtain(certificate.ObtainRequest{ @@ -256,58 +296,163 @@ func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew 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 { - 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()) { + if err == nil && tlsCertificate.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 { - mockC, err2 := mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase) - if err2 != nil { - return nil, errors.Join(err, err2) - } - return mockC, err + return mockCert(domains[0], err.Error(), string(mainDomainSuffix), keyDatabase), err } - return &tlsCertificate, nil + return tlsCertificate, nil } } - return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase) + return mockCert(domains[0], err.Error(), string(mainDomainSuffix), keyDatabase), err } log.Debug().Msgf("Obtained certificate for %v", domains) if err := keyDatabase.Put(name, res); err != nil { - return nil, err + return tls.Certificate{}, err } tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey) if err != nil { - return nil, err + return tls.Certificate{}, err } - return &tlsCertificate, nil + return tlsCertificate, nil } -func SetupMainDomainCertificates(mainDomainSuffix string, acmeClient *AcmeClient, certDB database.CertDB) error { +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 +} + +func SetupCertificates(mainDomainSuffix []byte, dnsProvider string, acmeConfig *lego.Config, acmeUseRateLimits, enableHTTPServer bool, challengeCache cache.SetGetKey, certDB database.CertDB) error { // getting main cert before ACME account so that we can fail here without hitting rate limits - mainCertBytes, err := certDB.Get(mainDomainSuffix) - if err != nil && !errors.Is(err, database.ErrNotFound) { - return fmt.Errorf("cert database is not working: %w", err) + 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") + } + } } if mainCertBytes == nil { - _, err = acmeClient.obtainCert(acmeClient.dnsChallengerLegoClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, nil, "", true, mainDomainSuffix, certDB) + _, err = obtainCert(mainDomainAcmeClient, []string{"*" + string(mainDomainSuffix), string(mainDomainSuffix[1:])}, nil, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB) if err != nil { log.Error().Err(err).Msg("Couldn't renew main domain certificate, continuing with mock certs only") } @@ -316,45 +461,58 @@ func SetupMainDomainCertificates(mainDomainSuffix string, acmeClient *AcmeClient return nil } -func MaintainCertDB(ctx context.Context, interval time.Duration, acmeClient *AcmeClient, mainDomainSuffix string, certDB database.CertDB) { +func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffix []byte, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) { for { - // delete expired certs that will be invalid until next clean up - threshold := time.Now().Add(interval) + // clean up expired certs + now := time.Now() 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) + } - 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++ - } + 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++ } } } - log.Debug().Msgf("Removed %d expired certificates from the database", 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) } // update main cert - res, err := certDB.Get(mainDomainSuffix) + res, err := certDB.Get(string(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", 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", string(mainDomainSuffix)) } else { tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate) - 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 + + // renew main certificate 30 days before it expires + if !tlsCertificates[0].NotAfter.After(time.Now().Add(30 * 24 * time.Hour)) { go (func() { - _, err = acmeClient.obtainCert(acmeClient.dnsChallengerLegoClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, res, "", true, mainDomainSuffix, certDB) + _, err = obtainCert(mainDomainAcmeClient, []string{"*" + string(mainDomainSuffix), string(mainDomainSuffix[1:])}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB) if err != nil { log.Error().Err(err).Msg("Couldn't renew certificate for main domain") } @@ -369,12 +527,3 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, acmeClient *Acm } } } - -// 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 a28d0f4..0e87e6e 100644 --- a/server/certificates/mock.go +++ b/server/certificates/mock.go @@ -13,15 +13,14 @@ 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, error) { +func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) tls.Certificate { key, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048) if err != nil { - return nil, err + panic(err) } template := x509.Certificate{ @@ -53,7 +52,7 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) key, ) if err != nil { - return nil, err + panic(err) } out := &bytes.Buffer{} @@ -62,7 +61,7 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) Type: "CERTIFICATE", }) if err != nil { - return nil, err + panic(err) } outBytes := out.Bytes() res := &certificate.Resource{ @@ -76,12 +75,12 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) databaseName = mainDomainSuffix } if err := keyDatabase.Put(databaseName, res); err != nil { - log.Error().Err(err) + panic(err) } tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey) if err != nil { - return nil, err + panic(err) } - return &tlsCertificate, nil + return tlsCertificate } diff --git a/server/certificates/mock_test.go b/server/certificates/mock_test.go index 644e8a9..1cbd1f6 100644 --- a/server/certificates/mock_test.go +++ b/server/certificates/mock_test.go @@ -3,18 +3,14 @@ package certificates import ( "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "codeberg.org/codeberg/pages/server/database" + "github.com/stretchr/testify/assert" ) func TestMockCert(t *testing.T) { - 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) + db, err := database.NewTmpDB() 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 deleted file mode 100644 index 6650164..0000000 --- a/server/context/context.go +++ /dev/null @@ -1,62 +0,0 @@ -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 7fdbae7..3ba3efc 100644 --- a/server/database/interface.go +++ b/server/database/interface.go @@ -1,78 +1,15 @@ package database import ( - "fmt" - - "github.com/go-acme/lego/v4/certcrypto" + "github.com/akrylysov/pogreb" "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 - 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 + Compact() (string, error) + Items() *pogreb.ItemIterator } diff --git a/server/database/mock.go b/server/database/mock.go index e7e2c38..e6c1b5a 100644 --- a/server/database/mock.go +++ b/server/database/mock.go @@ -1,122 +1,55 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. - package database import ( - certificate "github.com/go-acme/lego/v4/certificate" - mock "github.com/stretchr/testify/mock" + "fmt" + "time" + + "github.com/OrlovEvgeny/go-mcache" + "github.com/akrylysov/pogreb" + "github.com/go-acme/lego/v4/certificate" ) -// MockCertDB is an autogenerated mock type for the CertDB type -type MockCertDB struct { - mock.Mock +var _ CertDB = tmpDB{} + +type tmpDB struct { + intern *mcache.CacheDriver + ttl time.Duration } -// Close provides a mock function with given fields: -func (_m *MockCertDB) Close() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 +func (p tmpDB) Close() error { + _ = p.intern.Close() + 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) Put(name string, cert *certificate.Resource) error { + return p.intern.Set(name, cert, p.ttl) } -// 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) +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) } - 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 + return cert.(*certificate.Resource), nil } -// 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 (p tmpDB) Delete(key string) error { + p.intern.Remove(key) + return 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 +func (p tmpDB) Compact() (string, error) { + p.intern.Truncate() + return "Truncate done", nil } -type mockConstructorTestingTNewMockCertDB interface { - mock.TestingT - Cleanup(func()) +func (p tmpDB) Items() *pogreb.ItemIterator { + panic("ItemIterator not implemented for tmpDB") } -// 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 +func NewTmpDB() (CertDB, error) { + return &tmpDB{ + intern: mcache.New(), + ttl: time.Minute, + }, nil } diff --git a/server/database/setup.go b/server/database/setup.go new file mode 100644 index 0000000..1c5a0af --- /dev/null +++ b/server/database/setup.go @@ -0,0 +1,109 @@ +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 deleted file mode 100644 index 217b6d1..0000000 --- a/server/database/xorm.go +++ /dev/null @@ -1,151 +0,0 @@ -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 deleted file mode 100644 index 50d8a7f..0000000 --- a/server/database/xorm_test.go +++ /dev/null @@ -1,92 +0,0 @@ -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 new file mode 100644 index 0000000..bb2413b --- /dev/null +++ b/server/dns/const.go @@ -0,0 +1,6 @@ +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 c11b278..d30f1e0 100644 --- a/server/dns/dns.go +++ b/server/dns/dns.go @@ -3,16 +3,10 @@ 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) { @@ -30,7 +24,7 @@ func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLo names, err := net.LookupTXT(domain) if err == nil { for _, name := range names { - name = strings.TrimSuffix(strings.TrimSpace(name), ".") + name = strings.TrimSuffix(name, ".") if strings.HasSuffix(name, mainDomainSuffix) { cname = name break @@ -52,9 +46,9 @@ func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLo targetBranch = cnameParts[len(cnameParts)-3] } if targetRepo == "" { - targetRepo = defaultPagesRepo + targetRepo = firstDefaultBranch } - if targetBranch == "" && targetRepo != defaultPagesRepo { + if targetBranch == "" && targetRepo != firstDefaultBranch { 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 af61edf..932ff3c 100644 --- a/server/gitea/cache.go +++ b/server/gitea/cache.go @@ -1,115 +1,12 @@ 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 - IsSymlink bool - ETag string - MimeType string - Body []byte + Exists bool + ETag []byte + 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 f3bda54..16cba84 100644 --- a/server/gitea/client.go +++ b/server/gitea/client.go @@ -1,295 +1,142 @@ 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" +) - "codeberg.org/codeberg/pages/server/cache" - "codeberg.org/codeberg/pages/server/version" +const ( + giteaAPIRepos = "/api/v1/repos/" + giteaObjectTypeHeader = "X-Gitea-Object-Type" ) 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 { - sdkClient *gitea.Client - responseCache cache.SetGetKey - - giteaRoot string + giteaRoot string + giteaAPIToken string + fastClient *fasthttp.Client + infoTimeout time.Duration + contentTimeout time.Duration followSymlinks bool supportLFS bool - - forbiddenMimeTypes map[string]bool - defaultMimeType string } -func NewClient(giteaRoot, giteaAPIToken string, respCache cache.SetGetKey, followSymlinks, supportLFS bool) (*Client, error) { - rootURL, err := url.Parse(giteaRoot) - if err != nil { - return nil, err +// 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) { + rootURL, err := url.Parse(giteaRoot) 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{ - sdkClient: sdk, - responseCache: respCache, - - giteaRoot: giteaRoot, + giteaRoot: giteaRoot, + giteaAPIToken: giteaAPIToken, + infoTimeout: 5 * time.Second, + contentTimeout: 10 * time.Second, + fastClient: getFastHTTPClient(), 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) { - reader, _, _, err := client.ServeRawContent(targetOwner, targetRepo, ref, resource) + resp, err := client.ServeRawContent(targetOwner, targetRepo, ref, resource) if err != nil { return nil, err } - defer reader.Close() - return io.ReadAll(reader) + return resp.Body(), nil } -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() - - // 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 - } +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)) } - - // 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) (*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) + resp, err := client.do(client.contentTimeout, apiURL) if err != nil { - 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 nil, err + } + + 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) } - return &BranchTimestamp{}, err - } - if resp.StatusCode != http.StatusOK { - return &BranchTimestamp{}, fmt.Errorf("unexpected status code '%d'", resp.StatusCode) - } - stamp := &BranchTimestamp{ - Branch: branch.Name, - Timestamp: branch.Commit.Timestamp, - } + return resp, nil - 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") + case fasthttp.StatusNotFound: + return nil, ErrorNotFound + + default: + return nil, fmt.Errorf("unexpected status code '%d'", resp.StatusCode()) } - return stamp, nil +} + +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) + if err != nil { + return time.Time{}, err + } + if res.StatusCode() != fasthttp.StatusOK { + return time.Time{}, fmt.Errorf("unexpected status code '%d'", res.StatusCode()) + } + return time.Parse(time.RFC3339, fastjson.GetString(res.Body(), "commit", "timestamp")) } func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (string, error) { - 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) + url := joinURL(client.giteaRoot, giteaAPIRepos, repoOwner, repoName) + res, err := client.do(client.infoTimeout, url) if err != nil { return "", err } - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("unexpected status code '%d'", resp.StatusCode) + if res.StatusCode() != fasthttp.StatusOK { + return "", fmt.Errorf("unexpected status code '%d'", res.StatusCode()) } - - 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 + return fastjson.GetString(res.Body(), "default_branch"), nil } -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 +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 } diff --git a/server/gitea/client_test.go b/server/gitea/client_test.go new file mode 100644 index 0000000..7dbad68 --- /dev/null +++ b/server/gitea/client_test.go @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..4ff0f4a --- /dev/null +++ b/server/gitea/fasthttp.go @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000..eaa2feb --- /dev/null +++ b/server/handler.go @@ -0,0 +1,314 @@ +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 deleted file mode 100644 index 7da5d39..0000000 --- a/server/handler/handler.go +++ /dev/null @@ -1,113 +0,0 @@ -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 deleted file mode 100644 index 8742be4..0000000 --- a/server/handler/handler_custom_domain.go +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index caa8209..0000000 --- a/server/handler/handler_raw_domain.go +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index 6c14393..0000000 --- a/server/handler/handler_sub_domain.go +++ /dev/null @@ -1,156 +0,0 @@ -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 deleted file mode 100644 index d04ebda..0000000 --- a/server/handler/handler_test.go +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index 1ab73ae..0000000 --- a/server/handler/hsts.go +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 838ae27..0000000 --- a/server/handler/try.go +++ /dev/null @@ -1,77 +0,0 @@ -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 new file mode 100644 index 0000000..a731ebf --- /dev/null +++ b/server/handler_test.go @@ -0,0 +1,52 @@ +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 new file mode 100644 index 0000000..6d55ddf --- /dev/null +++ b/server/helpers.go @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000..176bb42 --- /dev/null +++ b/server/setup.go @@ -0,0 +1,53 @@ +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 new file mode 100644 index 0000000..24831c4 --- /dev/null +++ b/server/try.go @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000..247e1d1 --- /dev/null +++ b/server/upstream/const.go @@ -0,0 +1,24 @@ +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 eb30394..553c148 100644 --- a/server/upstream/domains.go +++ b/server/upstream/domains.go @@ -1,70 +1,50 @@ 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 (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) +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) for _, domain := range domains { if domain == actualDomain { valid = true break } } - 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) + } 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 + } + } } - if domain == actualDomain { + domains = append(domains, targetOwner+mainDomainSuffix) + if domains[len(domains)-1] == 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 deleted file mode 100644 index 9575a3f..0000000 --- a/server/upstream/header.go +++ /dev/null @@ -1,28 +0,0 @@ -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 ac0ab3f..0714dcd 100644 --- a/server/upstream/helper.go +++ b/server/upstream/helper.go @@ -1,47 +1,76 @@ package upstream import ( - "errors" - "fmt" - - "github.com/rs/zerolog/log" + "mime" + "path" + "strconv" + "strings" + "time" + "codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/gitea" ) -// 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() +type branchTimestamp struct { + Branch string + Timestamp time.Time +} - if o.TargetBranch == "" { +// 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 { // Get default branch - defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(o.TargetOwner, o.TargetRepo) + defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(owner, repo) if err != nil { - log.Err(err).Msg("Couldn't fetch default branch from repository") - return false, err + _ = branchTimestampCache.Set(owner+"/"+repo+"/", nil, defaultBranchCacheTimeout) + return nil } - log.Debug().Msgf("Successfully fetched default branch %q from Gitea", defaultBranch) - o.TargetBranch = defaultBranch + result.Branch = defaultBranch } - timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(o.TargetOwner, o.TargetRepo, o.TargetBranch) + timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(owner, repo, result.Branch) if err != nil { - if !errors.Is(err, gitea.ErrorNotFound) { - log.Error().Err(err).Msg("Could not get latest commit timestamp from branch") + 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" } - return false, err } - - 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 + return mimeType } -func (o *Options) ContentWebLink(giteaClient *gitea.Client) string { - return giteaClient.ContentWebLink(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath) + "; rel=\"canonical\"" +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) } diff --git a/server/upstream/redirects.go b/server/upstream/redirects.go deleted file mode 100644 index ab6c971..0000000 --- a/server/upstream/redirects.go +++ /dev/null @@ -1,117 +0,0 @@ -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 1a444e4..0e27727 100644 --- a/server/upstream/upstream.go +++ b/server/upstream/upstream.go @@ -1,28 +1,20 @@ 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", @@ -35,81 +27,68 @@ var upstreamNotFoundPages = []string{ // Options provides various options for the upstream request. type Options struct { - TargetOwner string - TargetRepo string - TargetBranch string - TargetPath string + TargetOwner, + TargetRepo, + TargetBranch, + TargetPath, // Used for debugging purposes. Host string - TryIndexPages bool - BranchTimestamp time.Time + DefaultMimeType string + ForbiddenMimeTypes map[string]bool + 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 *context.Context, giteaClient *gitea.Client, redirectsCache cache.SetGetKey) bool { - log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger() - - if o.TargetOwner == "" || o.TargetRepo == "" { - html.ReturnErrorPage(ctx, "gitea client: either repo owner or name info is missing", http.StatusBadRequest) - return true - } +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() // 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 - } + branch := GetBranchTimestamp(giteaClient, o.TargetOwner, o.TargetRepo, o.TargetBranch, branchTimestampCache) - // handle unexpected errors - if err != nil { - html.ReturnErrorPage(ctx, - fmt.Sprintf("could not get timestamp of branch %q: '%v'", o.TargetBranch, err), - http.StatusFailedDependency) + 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) + 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 - } + 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) + return true } - log.Trace().Msg("check response against last modified: outdated") } log.Debug().Msg("Preparing") - reader, header, statusCode, err := giteaClient.ServeRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath) - if reader != nil { - defer reader.Close() + // 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()) } - log.Debug().Msg("Aquisting") - // 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 - } - + // Handle errors + if (err != nil && errors.Is(err, gitea.ErrorNotFound)) || (res == nil && !cachedResponse.Exists) { if o.TryIndexPages { // copy the o struct & try if an index page exists optionsForIndexPages := *o @@ -117,20 +96,25 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client, redi optionsForIndexPages.appendTrailingSlash = true for _, indexPage := range upstreamIndexPages { optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage - if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) { + if optionsForIndexPages.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { + _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{ + Exists: false, + }, fileCacheTimeout) return true } } // compatibility fix for GitHub Pages (/example → /example.html) optionsForIndexPages.appendTrailingSlash = false - optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Path(), "/") + ".html" + optionsForIndexPages.redirectIfExists = strings.TrimSuffix(string(ctx.Request.URI().Path()), "/") + ".html" optionsForIndexPages.TargetPath = o.TargetPath + ".html" - if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) { + if optionsForIndexPages.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { + _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{ + Exists: false, + }, fileCacheTimeout) return true } } - - ctx.StatusCode = http.StatusNotFound + ctx.Response.SetStatusCode(fasthttp.StatusNotFound) if o.TryIndexPages { // copy the o struct & try if a not found page exists optionsForNotFoundPages := *o @@ -138,71 +122,92 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client, redi optionsForNotFoundPages.appendTrailingSlash = false for _, notFoundPage := range upstreamNotFoundPages { optionsForNotFoundPages.TargetPath = "/" + notFoundPage - if optionsForNotFoundPages.Upstream(ctx, giteaClient, redirectsCache) { + if optionsForNotFoundPages.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { + _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{ + Exists: false, + }, fileCacheTimeout) 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 } - - // 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) + 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) 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 && !strings.HasSuffix(ctx.Path(), "/") { - ctx.Redirect(ctx.Path()+"/", http.StatusTemporaryRedirect) + if o.appendTrailingSlash && !bytes.HasSuffix(ctx.Request.URI().Path(), []byte{'/'}) { + ctx.Redirect(string(ctx.Request.URI().Path())+"/", fasthttp.StatusTemporaryRedirect) return true } - if strings.HasSuffix(ctx.Path(), "/index.html") && !o.ServeRaw { - ctx.Redirect(strings.TrimSuffix(ctx.Path(), "index.html"), http.StatusTemporaryRedirect) + if bytes.HasSuffix(ctx.Request.URI().Path(), []byte("/index.html")) { + ctx.Redirect(strings.TrimSuffix(string(ctx.Request.URI().Path()), "index.html"), fasthttp.StatusTemporaryRedirect) return true } if o.redirectIfExists != "" { - ctx.Redirect(o.redirectIfExists, http.StatusTemporaryRedirect) + ctx.Redirect(o.redirectIfExists, fasthttp.StatusTemporaryRedirect) return true } + log.Debug().Msg("Handling error") - // Set ETag & MIME - o.setHeader(ctx, header) + // 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) log.Debug().Msg("Prepare response") - ctx.RespWriter.WriteHeader(ctx.StatusCode) - // Write the response body to the original request - 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 - } - } + 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)) + } + } 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) + } + return true } diff --git a/server/utils/utils.go b/server/utils/utils.go index 91ed359..7be330f 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -1,27 +1,11 @@ package utils -import ( - "net/url" - "path" - "strings" -) +import "bytes" -func TrimHostPort(host string) string { - i := strings.IndexByte(host, ':') +func TrimHostPort(host []byte) []byte { + i := bytes.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 b8fcea9..3dc0632 100644 --- a/server/utils/utils_test.go +++ b/server/utils/utils_test.go @@ -7,63 +7,7 @@ import ( ) func TestTrimHostPort(t *testing.T) { - 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) - } + assert.EqualValues(t, "aa", TrimHostPort([]byte("aa"))) + assert.EqualValues(t, "", TrimHostPort([]byte(":"))) + assert.EqualValues(t, "example.com", TrimHostPort([]byte("example.com:80"))) }