From 884e727f5fa7412f2c0d331b55b4884edba02db3 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 24 Apr 2023 20:52:12 +0200 Subject: [PATCH] feat: initial commit --- .env.dist | 0 .gitignore | 9 + .goreleaser.yaml | 128 +++ Dockerfile | 30 + Jenkinsfile | 59 ++ Makefile | 142 +++ README.md | 15 + cmd/bouncer/main.go | 31 + commitlint.config.js | 1 + doc/README.md | 13 + go.mod | 89 ++ go.sum | 899 ++++++++++++++++++ internal/admin/authz.go | 101 ++ internal/admin/init.go | 42 + internal/admin/option.go | 31 + internal/admin/proxy.go | 234 +++++ internal/admin/server.go | 140 +++ internal/auth/jwt/authenticator.go | 72 ++ internal/auth/jwt/jwt.go | 62 ++ internal/auth/jwt/user.go | 32 + internal/auth/middleware.go | 79 ++ internal/client/client.go | 144 +++ internal/client/create_proxy.go | 29 + internal/client/delete_proxy.go | 26 + internal/client/get_proxy.go | 26 + internal/client/options.go | 24 + internal/client/query_proxy.go | 75 ++ internal/client/update_proxy.go | 29 + internal/command/admin/auth/create_token.go | 54 ++ internal/command/admin/auth/root.go | 15 + internal/command/admin/root.go | 17 + internal/command/admin/run.go | 54 ++ internal/command/client/apierr/wrap.go | 91 ++ internal/command/client/flag/flag.go | 98 ++ internal/command/client/flag/util.go | 11 + internal/command/client/proxy/create.go | 62 ++ internal/command/client/proxy/delete.go | 56 ++ internal/command/client/proxy/flag/flag.go | 33 + internal/command/client/proxy/get.go | 49 + internal/command/client/proxy/query.go | 63 ++ internal/command/client/proxy/root.go | 18 + internal/command/client/proxy/update.go | 72 ++ internal/command/client/proxy/util.go | 31 + internal/command/client/root.go | 16 + internal/command/common/flags.go | 7 + internal/command/common/load_config.go | 27 + internal/command/config/dump.go | 36 + internal/command/config/root.go | 13 + internal/command/main.go | 107 +++ internal/command/proxy/root.go | 15 + internal/command/proxy/run.go | 54 ++ internal/config/admin_server.go | 27 + internal/config/config.go | 64 ++ internal/config/config_test.go | 16 + internal/config/cors.go | 20 + internal/config/environment.go | 125 +++ internal/config/http.go | 13 + internal/config/logger.go | 15 + internal/config/proxy_server.go | 11 + internal/config/redis.go | 19 + internal/config/testdata/config.yml | 6 + internal/format/json/writer.go | 38 + internal/format/prop.go | 49 + internal/format/registry.go | 46 + internal/format/table/prop.go | 61 ++ internal/format/table/writer.go | 80 ++ internal/format/table/writer_test.go | 86 ++ internal/format/writer.go | 19 + internal/imports/format/format_import.go | 6 + internal/jwk/jwk.go | 140 +++ internal/jwk/jwk_test.go | 40 + internal/proxy/init.go | 42 + .../proxy/middleware/director/director.go | 90 ++ internal/proxy/option.go | 31 + internal/proxy/server.go | 110 +++ internal/queue/options.go | 9 + internal/queue/queue.go | 32 + internal/queue/redis/repository.go | 65 ++ internal/queue/repository.go | 16 + internal/setup/proxy_repository.go | 19 + internal/setup/queue_repository.go | 20 + internal/store/error.go | 5 + internal/store/id.go | 19 + internal/store/proxy.go | 27 + internal/store/proxy_repository.go | 73 ++ internal/store/redis/proxy_repository.go | 253 +++++ internal/store/redis/proxy_repository_test.go | 64 ++ internal/store/testsuite/proxy_repository.go | 191 ++++ misc/jenkins/Dockerfile | 24 + misc/logo/bouncer.svg | 39 + misc/packaging/common/config.yml | 33 + .../common/postinstall-bouncer-admin.sh | 73 ++ .../common/postinstall-bouncer-proxy.sh | 73 ++ misc/packaging/openrc/bouncer-admin.openrc.sh | 15 + misc/packaging/openrc/bouncer-proxy.openrc.sh | 15 + .../systemd/bouncer-admin.systemd.service | 12 + .../systemd/bouncer-proxy.systemd.service | 12 + modd.conf | 12 + 98 files changed, 5816 insertions(+) create mode 100644 .env.dist create mode 100644 .gitignore create mode 100644 .goreleaser.yaml create mode 100644 Dockerfile create mode 100644 Jenkinsfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/bouncer/main.go create mode 100644 commitlint.config.js create mode 100644 doc/README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/admin/authz.go create mode 100644 internal/admin/init.go create mode 100644 internal/admin/option.go create mode 100644 internal/admin/proxy.go create mode 100644 internal/admin/server.go create mode 100644 internal/auth/jwt/authenticator.go create mode 100644 internal/auth/jwt/jwt.go create mode 100644 internal/auth/jwt/user.go create mode 100644 internal/auth/middleware.go create mode 100644 internal/client/client.go create mode 100644 internal/client/create_proxy.go create mode 100644 internal/client/delete_proxy.go create mode 100644 internal/client/get_proxy.go create mode 100644 internal/client/options.go create mode 100644 internal/client/query_proxy.go create mode 100644 internal/client/update_proxy.go create mode 100644 internal/command/admin/auth/create_token.go create mode 100644 internal/command/admin/auth/root.go create mode 100644 internal/command/admin/root.go create mode 100644 internal/command/admin/run.go create mode 100644 internal/command/client/apierr/wrap.go create mode 100644 internal/command/client/flag/flag.go create mode 100644 internal/command/client/flag/util.go create mode 100644 internal/command/client/proxy/create.go create mode 100644 internal/command/client/proxy/delete.go create mode 100644 internal/command/client/proxy/flag/flag.go create mode 100644 internal/command/client/proxy/get.go create mode 100644 internal/command/client/proxy/query.go create mode 100644 internal/command/client/proxy/root.go create mode 100644 internal/command/client/proxy/update.go create mode 100644 internal/command/client/proxy/util.go create mode 100644 internal/command/client/root.go create mode 100644 internal/command/common/flags.go create mode 100644 internal/command/common/load_config.go create mode 100644 internal/command/config/dump.go create mode 100644 internal/command/config/root.go create mode 100644 internal/command/main.go create mode 100644 internal/command/proxy/root.go create mode 100644 internal/command/proxy/run.go create mode 100644 internal/config/admin_server.go create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/config/cors.go create mode 100644 internal/config/environment.go create mode 100644 internal/config/http.go create mode 100644 internal/config/logger.go create mode 100644 internal/config/proxy_server.go create mode 100644 internal/config/redis.go create mode 100644 internal/config/testdata/config.yml create mode 100644 internal/format/json/writer.go create mode 100644 internal/format/prop.go create mode 100644 internal/format/registry.go create mode 100644 internal/format/table/prop.go create mode 100644 internal/format/table/writer.go create mode 100644 internal/format/table/writer_test.go create mode 100644 internal/format/writer.go create mode 100644 internal/imports/format/format_import.go create mode 100644 internal/jwk/jwk.go create mode 100644 internal/jwk/jwk_test.go create mode 100644 internal/proxy/init.go create mode 100644 internal/proxy/middleware/director/director.go create mode 100644 internal/proxy/option.go create mode 100644 internal/proxy/server.go create mode 100644 internal/queue/options.go create mode 100644 internal/queue/queue.go create mode 100644 internal/queue/redis/repository.go create mode 100644 internal/queue/repository.go create mode 100644 internal/setup/proxy_repository.go create mode 100644 internal/setup/queue_repository.go create mode 100644 internal/store/error.go create mode 100644 internal/store/id.go create mode 100644 internal/store/proxy.go create mode 100644 internal/store/proxy_repository.go create mode 100644 internal/store/redis/proxy_repository.go create mode 100644 internal/store/redis/proxy_repository_test.go create mode 100644 internal/store/testsuite/proxy_repository.go create mode 100644 misc/jenkins/Dockerfile create mode 100644 misc/logo/bouncer.svg create mode 100644 misc/packaging/common/config.yml create mode 100644 misc/packaging/common/postinstall-bouncer-admin.sh create mode 100644 misc/packaging/common/postinstall-bouncer-proxy.sh create mode 100644 misc/packaging/openrc/bouncer-admin.openrc.sh create mode 100644 misc/packaging/openrc/bouncer-proxy.openrc.sh create mode 100644 misc/packaging/systemd/bouncer-admin.systemd.service create mode 100644 misc/packaging/systemd/bouncer-proxy.systemd.service create mode 100644 modd.conf diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..022f496 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.env +/bin +/dist +/tools +/.gitea-release +/config.yml +/admin-key.json +/.bouncer-token +/data \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..98936af --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,128 @@ +project_name: bouncer +before: + hooks: + - go mod tidy + - go generate ./... +builds: + - id: bouncer + env: + - CGO_ENABLED=0 + ldflags: + - -s + - -w + - -X 'main.GitRef={{ .Commit }}' + - -X 'main.ProjectVersion={{ .Version }}' + - -X 'main.BuildDate={{ .Date }}' + - -X 'main.DefaultConfigPath=/etc/bouncer/config.yml' + gcflags: + - -trimpath="${PWD}" + asmflags: + - -trimpath="${PWD}" + goos: + - linux + goarch: + - amd64 + - arm64 + - "386" + main: ./cmd/bouncer +archives: + - id: bouncer + builds: ["bouncer"] + name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' + files: + - README.md + - misc/packaging/common/config.yml +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Version }}" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' +nfpms: + - id: bouncer-bin + builds: + - "bouncer" + package_name: bouncer-bin + homepage: https://forge.cadoles.com/Cadoles/bouncer + maintainer: Cadoles + description: |- + reverse proxy server with dynamic queuing management - binaries + license: AGPL-3.0 + formats: + - apk + - deb + - rpm + contents: + - src: misc/packaging/common/config.yml + dst: /etc/bouncer/config.yml + type: config + - id: bouncer-admin + meta: true + package_name: bouncer-admin + homepage: https://forge.cadoles.com/Cadoles/bouncer + maintainer: Cadoles + dependencies: + - bouncer-bin + description: |- + reverse proxy server with dynamic queuing management - administration service + license: AGPL-3.0 + formats: + - apk + - deb + - rpm + contents: + - src: misc/packaging/systemd/bouncer-admin.systemd.service + dst: /usr/lib/systemd/system/bouncer-admin.service + packager: deb + - src: misc/packaging/systemd/bouncer-admin.systemd.service + dst: /usr/lib/systemd/system/bouncer-admin.service + packager: rpm + - src: misc/packaging/openrc/bouncer-admin.openrc.sh + dst: /etc/init.d/bouncer-admin + file_info: + mode: 0755 + packager: apk + - dst: /var/log/bouncer + type: dir + file_info: + mode: 0700 + packager: apk + scripts: + postinstall: "misc/packaging/common/postinstall-bouncer-admin.sh" + - id: bouncer-proxy + meta: true + dependencies: + - bouncer-bin + package_name: bouncer-proxy + homepage: https://forge.cadoles.com/Cadoles/bouncer + maintainer: Cadoles + description: |- + reverse proxy server with dynamic queuing management - proxy service + license: AGPL-3.0 + formats: + - apk + - deb + - rpm + contents: + - src: misc/packaging/systemd/bouncer-proxy.systemd.service + dst: /usr/lib/systemd/system/bouncer-proxy.service + packager: deb + - src: misc/packaging/systemd/bouncer-proxy.systemd.service + dst: /usr/lib/systemd/system/bouncer-proxy.service + packager: rpm + - src: misc/packaging/openrc/bouncer-proxy.openrc.sh + dst: /etc/init.d/bouncer-proxy + file_info: + mode: 0755 + packager: apk + - dst: /var/log/bouncer + type: dir + file_info: + mode: 0700 + packager: apk + scripts: + postinstall: "misc/packaging/common/postinstall-bouncer-proxy.sh" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8215bc3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM golang:1.19 AS BUILD + +RUN apt-get update \ + && apt-get install -y make + +COPY . /src + +WORKDIR /src + +RUN make GORELEASER_ARGS='build --rm-dist --single-target --snapshot' goreleaser + +FROM busybox:latest AS RUNTIME + +ARG DUMB_INIT_VERSION=1.2.5 + +RUN mkdir -p /usr/local/bin \ + && wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_x86_64 \ + && chmod +x /usr/local/bin/dumb-init + +ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] + +COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1 /app +COPY --from=BUILD /src/config.yml /etc/bouncer/config.yml + +EXPOSE 8080 +EXPOSE 8081 + +ENTRYPOINT ["/app/bouncer"] + +CMD ["bouncer", "run", "-c", "/etc/bouncer/config.yml"] \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..d340a5c --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,59 @@ +@Library('cadoles') _ + +pipeline { + agent { + dockerfile { + label 'docker' + filename 'Dockerfile' + dir 'misc/jenkins' + } + } + + stages { + stage('Cancel older jobs') { + steps { + script { + def buildNumber = env.BUILD_NUMBER as int + if (buildNumber > 1) milestone(buildNumber - 1) + milestone(buildNumber) + } + } + } + + stage('Run unit tests') { + steps { + script { + sh 'make test' + } + } + } + + stage('Release') { + when { + anyOf { + branch 'master' + branch 'develop' + } + } + steps { + script { + withCredentials([ + usernamePassword([ + credentialsId: 'forge-jenkins', + usernameVariable: 'GITEA_RELEASE_USERNAME', + passwordVariable: 'GITEA_RELEASE_PASSWORD' + ]) + ]) { + sh 'make gitea-release' + } + } + } + } + } + + post { + always { + cleanWs() + } + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..206a9b5 --- /dev/null +++ b/Makefile @@ -0,0 +1,142 @@ +LINT_ARGS ?= --timeout 5m +GORELEASER_VERSION ?= v1.13.1 +GORELEASER_ARGS ?= release --snapshot --rm-dist +GITCHLOG_ARGS ?= +SHELL := /bin/bash + +BOUNCER_VERSION ?= +GIT_VERSION := $(shell git describe --always) +DATE_VERSION := $(shell date +%Y.%-m.%-d) +FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,) + +DOCKER_IMAGE_NAME ?= cadoles/bouncer +DOCKER_IMAGE_TAG ?= $(FULL_VERSION) + +GOTEST_ARGS ?= -short + +OPENWRT_DEVICE ?= 192.168.1.1 + +watch: tools/modd/bin/modd deps ## Watching updated files - live reload + ( set -o allexport && source .env && set +o allexport && tools/modd/bin/modd ) + +.PHONY: test +test: test-go ## Executing tests + +test-go: deps + ( set -o allexport && source .env && set +o allexport && go test -v -count=1 $(GOTEST_ARGS) ./... ) + +test-install-script: tools/bin/bash_unit + tools/bin/bash_unit ./misc/script/test_install.sh + +tools/bin/bash_unit: + mkdir -p tools/bin + cd tools/bin && bash <(curl -s https://raw.githubusercontent.com/pgrange/bash_unit/master/install.sh) + +lint: ## Lint sources code + golangci-lint run --enable-all $(LINT_ARGS) + +build: build-bouncer ## Build artefacts + +build-bouncer: deps ## Build executable + CGO_ENABLED=0 go build \ + -v \ + -ldflags "\ + -X 'main.GitRef=$(shell git rev-parse --short HEAD)' \ + -X 'main.ProjectVersion=$(FULL_VERSION)' \ + -X 'main.BuildDate=$(shell date --utc --rfc-3339=seconds)' \ + " \ + -o ./bin/bouncer \ + ./cmd/bouncer + +.env: + cp -f .env.dist .env + +config.yml: + bin/bouncer config dump > config.yml + +run: .env + ( set -o allexport && source .env && set +o allexport && bin/bouncer $(BOUNCER_CMD)) + +.PHONY: deps +deps: .env + +.PHONY: goreleaser +goreleaser: deps + ( set -o allexport && source .env && set +o allexport && VERSION=$(GORELEASER_VERSION) curl -sfL https://goreleaser.com/static/run | GORELEASER_CURRENT_TAG="$(FULL_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) ) + +.PHONY: start-release +start-release: + if [ -z "$(BOUNCER_VERSION)" ]; then echo "You must define environment variable BOUNCER_VERSION"; exit 1; fi + + git flow release start $(BOUNCER_VERSION) + + # Update package.json version + jq '.version = "$(BOUNCER_VERSION)"' package.json | sponge package.json + git add package.json + git commit -m "chore: bump to version $(BOUNCER_VERSION)" --allow-empty + + echo "Commit you additional modifications then execute 'make finish-release'" + +.PHONY: finish-release +finish-release: + git flow release finish -m "v$(BOUNCER_VERSION)" + git push --all + git push --tags + +install-git-hooks: + git config core.hooksPath .githooks + +docker-build: + docker build -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) . + +docker-release: + docker push $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) + +gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser + mkdir -p .gitea-release + rm -rf .gitea-release/* + + cp dist/*.tar.gz .gitea-release/ + cp dist/*.apk .gitea-release/ + cp dist/*.deb .gitea-release/ + cp dist/*.rpm .gitea-release/ + + GITEA_RELEASE_PROJECT="bouncer" \ + GITEA_RELEASE_ORG="Cadoles" \ + GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \ + GITEA_RELEASE_VERSION="$(FULL_VERSION)" \ + GITEA_RELEASE_NAME="$(FULL_VERSION)" \ + GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \ + GITEA_RELEASE_IS_DRAFT="false" \ + GITEA_RELEASE_BODY="" \ + GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \ + tools/gitea-release/bin/gitea-release.sh + +tools/gitea-release/bin/gitea-release.sh: + mkdir -p tools/gitea-release/bin + curl --output tools/gitea-release/bin/gitea-release.sh https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/gitea/gitea-release.sh + chmod +x tools/gitea-release/bin/gitea-release.sh + +tools/modd/bin/modd: + mkdir -p tools/modd/bin + GOBIN=$(PWD)/tools/modd/bin go install github.com/cortesi/modd/cmd/modd@latest + +full-version: + @echo $(FULL_VERSION) + +.bouncer-token: + bin/bouncer admin auth create-token --role writer --subject dev-writer > .bouncer-token + +run-redis: + docker kill bouncer-redis || exit 0 + docker run --rm -t \ + --name bouncer-redis \ + -v $(PWD)/data/redis:/data \ + -p 6379:6379 \ + redis:alpine3.17 \ + redis-server --save 60 1 --loglevel warning + +redis-shell: + docker exec -it \ + bouncer-redis \ + redis-cli \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f3354e --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +

+ +

+ +# Bouncer + +Serveur mandataire inverse (_"reverse proxy"_) filtrant avec gestion de files d'attente dynamiques. + +## Documentation + +[Voir le répertoire `doc/`](./doc) + +## Licence + +AGPL-3.0 \ No newline at end of file diff --git a/cmd/bouncer/main.go b/cmd/bouncer/main.go new file mode 100644 index 0000000..60b98e1 --- /dev/null +++ b/cmd/bouncer/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/command" + "forge.cadoles.com/cadoles/bouncer/internal/command/admin" + "forge.cadoles.com/cadoles/bouncer/internal/command/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/config" + "forge.cadoles.com/cadoles/bouncer/internal/command/proxy" + + _ "forge.cadoles.com/cadoles/bouncer/internal/imports/format" +) + +// nolint: gochecknoglobals +var ( + GitRef = "unknown" + ProjectVersion = "unknown" + DefaultConfigPath = "" + BuildDate = time.Now().UTC().Format(time.RFC3339) +) + +func main() { + command.Main( + BuildDate, ProjectVersion, GitRef, DefaultConfigPath, + admin.Root(), + proxy.Root(), + client.Root(), + config.Root(), + ) +} diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..6eaf62b --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1 @@ +module.exports = {extends: ['@commitlint/config-conventional']} \ No newline at end of file diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..22cf570 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,13 @@ +# Documentation + +## Guide d'utilisation + +> TODO + +## Référence + +> TODO + +## Tutoriels + +> TODO \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8ad8553 --- /dev/null +++ b/go.mod @@ -0,0 +1,89 @@ +module forge.cadoles.com/cadoles/bouncer + +go 1.20 + +require ( + forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333 + github.com/btcsuite/btcd/btcutil v1.1.3 + github.com/go-chi/chi/v5 v5.0.8 + github.com/jedib0t/go-pretty/v6 v6.4.6 + github.com/ory/dockertest/v3 v3.10.0 + github.com/redis/go-redis/v9 v9.0.4 +) + +require ( + cloud.google.com/go v0.99.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/docker/cli v20.10.17+incompatible // indirect + github.com/docker/docker v20.10.13+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.5 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +require ( + cdr.dev/slog v1.4.2 // indirect + github.com/alecthomas/chroma v0.10.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/dlclark/regexp2 v1.9.0 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/go-chi/cors v1.2.1 + github.com/go-playground/locales v0.12.1 // indirect + github.com/go-playground/universal-translator v0.16.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/uuid v1.3.0 + github.com/leodido/go-urn v1.1.0 // indirect + github.com/lestrrat-go/blackmagic v1.0.1 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jwx/v2 v2.0.9 + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lib/pq v1.10.0 // indirect + github.com/lithammer/shortuuid/v4 v4.0.0 + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/pkg/errors v0.9.1 + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/urfave/cli/v2 v2.25.3 + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b + go.opencensus.io v0.24.0 // indirect + golang.org/x/crypto v0.8.0 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/term v0.7.0 // indirect + golang.org/x/tools v0.7.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + gopkg.in/go-playground/validator.v9 v9.29.1 // indirect + gopkg.in/yaml.v3 v3.0.1 +) + +// replace forge.cadoles.com/Cadoles/go-proxy => ../go-proxy diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..99db070 --- /dev/null +++ b/go.sum @@ -0,0 +1,899 @@ +cdr.dev/slog v1.4.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= +cdr.dev/slog v1.4.2 h1:fIfiqASYQFJBZiASwL825atyzeA96NsqSxx2aL61P8I= +cdr.dev/slog v1.4.2/go.mod h1:0EkH+GkFNxizNR+GAXUEdUHanxUH5t9zqPILmPM/Vn8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333 h1:dAajr9wX8WuFPrwjbKNXRmbF+4AaAT7bUj66G7gdZ+c= +forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333/go.mod h1:o8ZK5v/3J1dRmklFVn1l6WHAyQ3LgegyHjRIT8KLAFw= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +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/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= +github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= +github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +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/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI= +github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= +github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v20.10.13+incompatible h1:5s7uxnKZG+b8hYWlPYUi6x1Sjpq2MSt96d15eLZeHyw= +github.com/docker/docker v20.10.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +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-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= +github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= +github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= +github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= +github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.0.9 h1:TRX4Q630UXxPVLvP5vGaqVJO7S+0PE6msRZUsFSBoC8= +github.com/lestrrat-go/jwx/v2 v2.0.9/go.mod h1:K68euYaR95FnL0hIQB8VvzL70vB7pSifbJUydCTPmgM= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= +github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +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.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc= +github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= +github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b h1:nkvOl8TCj/mErADnwFFynjxBtC+hHsrESw6rw56JGmg= +gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b/go.mod h1:3sus4zjoUv1GB7eDLL60QaPkUnXJCWBpjvbe0jWifeY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-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-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/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-20191210023423-ac6580df4449/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= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/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-20210616094352-59db8d763f22/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-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/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-20190624222133-a101b041ded4/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-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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 h1:ErU+UA6wxadoU8nWrsy5MZUVBs75K17zUCsUCIfrXCE= +google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +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= diff --git a/internal/admin/authz.go b/internal/admin/authz.go new file mode 100644 index 0000000..10b5f61 --- /dev/null +++ b/internal/admin/authz.go @@ -0,0 +1,101 @@ +package admin + +import ( + "context" + "fmt" + "net/http" + + "forge.cadoles.com/cadoles/bouncer/internal/auth" + "forge.cadoles.com/cadoles/bouncer/internal/auth/jwt" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/api" + "gitlab.com/wpetit/goweb/logger" +) + +var ErrCodeForbidden api.ErrorCode = "forbidden" + +func assertReadAccess(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + reqUser, ok := assertRequestUser(w, r) + if !ok { + return + } + + switch user := reqUser.(type) { + case *jwt.User: + role := user.Role() + if role == jwt.RoleReader || role == jwt.RoleWriter { + h.ServeHTTP(w, r) + + return + } + + default: + logUnexpectedUserType(r.Context(), reqUser) + } + + forbidden(w, r) + } + + return http.HandlerFunc(fn) +} + +func assertWriteAccess(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + reqUser, ok := assertRequestUser(w, r) + if !ok { + return + } + + switch user := reqUser.(type) { + case *jwt.User: + role := user.Role() + if role == jwt.RoleWriter { + h.ServeHTTP(w, r) + + return + } + + default: + logUnexpectedUserType(r.Context(), reqUser) + } + + forbidden(w, r) + } + + return http.HandlerFunc(fn) +} + +func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool) { + ctx := r.Context() + user, err := auth.CtxUser(ctx) + if err != nil { + logger.Error(ctx, "could not retrieve user", logger.E(errors.WithStack(err))) + + forbidden(w, r) + + return nil, false + } + + if user == nil { + forbidden(w, r) + + return nil, false + } + + return user, true +} + +func forbidden(w http.ResponseWriter, r *http.Request) { + logger.Warn(r.Context(), "forbidden", logger.F("path", r.URL.Path)) + + api.ErrorResponse(w, http.StatusForbidden, ErrCodeForbidden, nil) +} + +func logUnexpectedUserType(ctx context.Context, user auth.User) { + logger.Error( + ctx, "unexpected user type", + logger.F("subject", user.Subject()), + logger.F("type", fmt.Sprintf("%T", user)), + ) +} diff --git a/internal/admin/init.go b/internal/admin/init.go new file mode 100644 index 0000000..bd01274 --- /dev/null +++ b/internal/admin/init.go @@ -0,0 +1,42 @@ +package admin + +import ( + "context" + + "forge.cadoles.com/cadoles/bouncer/internal/setup" + "github.com/pkg/errors" +) + +func (s *Server) initRepositories(ctx context.Context) error { + if err := s.initQueueRepository(ctx); err != nil { + return errors.WithStack(err) + } + + if err := s.initProxyRepository(ctx); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (s *Server) initQueueRepository(ctx context.Context) error { + queueRepository, err := setup.NewQueueRepository(ctx, s.redisConfig) + if err != nil { + return errors.WithStack(err) + } + + s.queueRepository = queueRepository + + return nil +} + +func (s *Server) initProxyRepository(ctx context.Context) error { + proxyRepository, err := setup.NewProxyRepository(ctx, s.redisConfig) + if err != nil { + return errors.WithStack(err) + } + + s.proxyRepository = proxyRepository + + return nil +} diff --git a/internal/admin/option.go b/internal/admin/option.go new file mode 100644 index 0000000..27fc832 --- /dev/null +++ b/internal/admin/option.go @@ -0,0 +1,31 @@ +package admin + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/config" +) + +type Option struct { + ServerConfig config.AdminServerConfig + RedisConfig config.RedisConfig +} + +type OptionFunc func(*Option) + +func defaultOption() *Option { + return &Option{ + ServerConfig: config.NewDefaultAdminServerConfig(), + RedisConfig: config.NewDefaultRedisConfig(), + } +} + +func WithServerConfig(conf config.AdminServerConfig) OptionFunc { + return func(opt *Option) { + opt.ServerConfig = conf + } +} + +func WithRedisConfig(conf config.RedisConfig) OptionFunc { + return func(opt *Option) { + opt.RedisConfig = conf + } +} diff --git a/internal/admin/proxy.go b/internal/admin/proxy.go new file mode 100644 index 0000000..c99d269 --- /dev/null +++ b/internal/admin/proxy.go @@ -0,0 +1,234 @@ +package admin + +import ( + "net/http" + "net/url" + "strconv" + "strings" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/go-chi/chi/v5" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/api" + "gitlab.com/wpetit/goweb/logger" +) + +type QueryProxyResponse struct { + Proxies []*store.ProxyHeader `json:"proxies"` +} + +func (s *Server) queryProxy(w http.ResponseWriter, r *http.Request) { + limit, ok := getIntQueryParam(w, r, "limit", 10) + if !ok { + return + } + + offset, ok := getIntQueryParam(w, r, "offset", 0) + if !ok { + return + } + + options := []store.QueryProxyOptionFunc{ + store.WithProxyQueryLimit(int(limit)), + store.WithProxyQueryOffset(int(offset)), + } + + ids, ok := getStringableSliceValues[store.ProxyID](w, r, "ids", nil) + if !ok { + return + } + + if ids != nil { + options = append(options, store.WithProxyQueryIDs(ids...)) + } + + ctx := r.Context() + + proxies, err := s.proxyRepository.QueryProxy( + ctx, + options..., + ) + if err != nil { + logger.Error(ctx, "could not list proxies", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + api.DataResponse(w, http.StatusOK, QueryProxyResponse{ + Proxies: proxies, + }) +} + +type GetProxyResponse struct { + Proxy *store.Proxy `json:"proxy"` +} + +func (s *Server) getProxy(w http.ResponseWriter, r *http.Request) { + proxyID, ok := getProxyID(w, r) + if !ok { + return + } + + ctx := r.Context() + + proxy, err := s.proxyRepository.GetProxy(ctx, proxyID) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil) + + return + } + + logger.Error(ctx, "could not get proxy", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + api.DataResponse(w, http.StatusOK, GetProxyResponse{ + Proxy: proxy, + }) +} + +type DeleteProxyResponse struct { + ProxyID store.ProxyID `json:"proxyId"` +} + +func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) { + proxyID, ok := getProxyID(w, r) + if !ok { + return + } + + ctx := r.Context() + + if err := s.proxyRepository.DeleteProxy(ctx, proxyID); err != nil { + if errors.Is(err, store.ErrNotFound) { + api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil) + + return + } + + logger.Error(ctx, "could not delete proxy", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + api.DataResponse(w, http.StatusOK, DeleteProxyResponse{ + ProxyID: proxyID, + }) +} + +type CreateProxyRequest struct { + To string `json:"to" validate:"required"` + From []string `json:"from" validate:"required"` +} + +type CreateProxyResponse struct { + Proxy *store.Proxy `json:"proxy"` +} + +func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + createProxyReq := &CreateProxyRequest{} + if ok := api.Bind(w, r, createProxyReq); !ok { + return + } + + to, err := url.Parse(createProxyReq.To) + if err != nil { + logger.Error(r.Context(), "could not parse to parameter", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) + + return + } + + proxy, err := s.proxyRepository.CreateProxy(ctx, to, createProxyReq.From...) + if err != nil { + logger.Error(ctx, "could not update agent", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + api.DataResponse(w, http.StatusOK, struct { + Proxy *store.Proxy `json:"proxy"` + }{ + Proxy: proxy, + }) +} + +type UpdateProxyRequest struct{} + +func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) { +} + +func getProxyID(w http.ResponseWriter, r *http.Request) (store.ProxyID, bool) { + rawProxyID := chi.URLParam(r, "proxyID") + + proxyID, err := store.ParseProxyID(rawProxyID) + if err != nil { + logger.Error(r.Context(), "could not parse proxy id", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) + + return "", false + } + + return proxyID, true +} + +func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defaultValue int64) (int64, bool) { + rawValue := r.URL.Query().Get(param) + if rawValue != "" { + value, err := strconv.ParseInt(rawValue, 10, 64) + if err != nil { + logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) + + return 0, false + } + + return value, true + } + + return defaultValue, true +} + +func getStringSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []string) ([]string, bool) { + rawValue := r.URL.Query().Get(param) + if rawValue != "" { + values := strings.Split(rawValue, ",") + + return values, true + } + + return defaultValue, true +} + +func getStringableSliceValues[T ~string](w http.ResponseWriter, r *http.Request, param string, defaultValue []T) ([]T, bool) { + rawValue := r.URL.Query().Get(param) + + if rawValue != "" { + rawValues := strings.Split(rawValue, ",") + ids := make([]T, 0, len(rawValues)) + + for _, rv := range rawValues { + id, err := store.ParseID[T](rv) + if err != nil { + logger.Error(r.Context(), "could not parse ids slice param", logger.F("param", param), logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) + + return nil, false + } + + ids = append(ids, id) + } + + return ids, true + } + + return defaultValue, true +} diff --git a/internal/admin/server.go b/internal/admin/server.go new file mode 100644 index 0000000..2afc614 --- /dev/null +++ b/internal/admin/server.go @@ -0,0 +1,140 @@ +package admin + +import ( + "context" + "fmt" + "log" + "net" + "net/http" + + "forge.cadoles.com/cadoles/bouncer/internal/auth" + "forge.cadoles.com/cadoles/bouncer/internal/auth/jwt" + "forge.cadoles.com/cadoles/bouncer/internal/config" + "forge.cadoles.com/cadoles/bouncer/internal/jwk" + "forge.cadoles.com/cadoles/bouncer/internal/queue" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type Server struct { + serverConfig config.AdminServerConfig + redisConfig config.RedisConfig + queueRepository queue.Repository + proxyRepository store.ProxyRepository +} + +func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) { + errs := make(chan error) + addrs := make(chan net.Addr) + + go s.run(ctx, addrs, errs) + + return addrs, errs +} + +func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan error) { + defer func() { + close(errs) + close(addrs) + }() + + ctx, cancel := context.WithCancel(parentCtx) + defer cancel() + + if err := s.initRepositories(ctx); err != nil { + errs <- errors.WithStack(err) + + return + } + + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.serverConfig.HTTP.Host, s.serverConfig.HTTP.Port)) + if err != nil { + errs <- errors.WithStack(err) + + return + } + + addrs <- listener.Addr() + + defer func() { + if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + errs <- errors.WithStack(err) + } + }() + + go func() { + <-ctx.Done() + + if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + log.Printf("%+v", errors.WithStack(err)) + } + }() + + key, err := jwk.LoadOrGenerate(string(s.serverConfig.Auth.PrivateKey), jwk.DefaultKeySize) + if err != nil { + errs <- errors.WithStack(err) + + return + } + + keys, err := jwk.PublicKeySet(key) + if err != nil { + errs <- errors.WithStack(err) + + return + } + + router := chi.NewRouter() + + router.Use(middleware.Logger) + + corsMiddleware := cors.New(cors.Options{ + AllowedOrigins: s.serverConfig.CORS.AllowedOrigins, + AllowedMethods: s.serverConfig.CORS.AllowedMethods, + AllowCredentials: bool(s.serverConfig.CORS.AllowCredentials), + AllowedHeaders: s.serverConfig.CORS.AllowedHeaders, + Debug: bool(s.serverConfig.CORS.Debug), + }) + + router.Use(corsMiddleware.Handler) + + router.Route("/api/v1", func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Use(auth.Middleware( + jwt.NewAuthenticator(keys, string(s.serverConfig.Auth.Issuer), jwt.DefaultAcceptableSkew), + )) + + r.Route("/proxies", func(r chi.Router) { + r.With(assertReadAccess).Get("/", s.queryProxy) + r.With(assertWriteAccess).Post("/", s.createProxy) + r.With(assertReadAccess).Get("/{proxyID}", s.getProxy) + r.With(assertWriteAccess).Put("/{proxyID}", s.updateProxy) + r.With(assertWriteAccess).Delete("/{proxyID}", s.deleteProxy) + }) + }) + }) + + logger.Info(ctx, "http server listening") + + if err := http.Serve(listener, router); err != nil && !errors.Is(err, net.ErrClosed) { + errs <- errors.WithStack(err) + } + + logger.Info(ctx, "http server exiting") +} + +func NewServer(funcs ...OptionFunc) *Server { + opt := defaultOption() + for _, fn := range funcs { + fn(opt) + } + + return &Server{ + serverConfig: opt.ServerConfig, + redisConfig: opt.RedisConfig, + } +} diff --git a/internal/auth/jwt/authenticator.go b/internal/auth/jwt/authenticator.go new file mode 100644 index 0000000..b1bec10 --- /dev/null +++ b/internal/auth/jwt/authenticator.go @@ -0,0 +1,72 @@ +package jwt + +import ( + "context" + "net/http" + "strings" + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/auth" + "forge.cadoles.com/cadoles/bouncer/internal/jwk" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +const DefaultAcceptableSkew = 5 * time.Minute + +type Authenticator struct { + keys jwk.Set + issuer string + acceptableSkew time.Duration +} + +// Authenticate implements auth.Authenticator. +func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth.User, error) { + ctx = logger.With(r.Context(), logger.F("remoteAddr", r.RemoteAddr)) + + authorization := r.Header.Get("Authorization") + if authorization == "" { + return nil, errors.WithStack(auth.ErrUnauthenticated) + } + + rawToken := strings.TrimPrefix(authorization, "Bearer ") + if rawToken == "" { + return nil, errors.WithStack(auth.ErrUnauthenticated) + } + + token, err := parseToken(ctx, a.keys, a.issuer, rawToken, a.acceptableSkew) + if err != nil { + return nil, errors.WithStack(err) + } + + rawRole, exists := token.Get(keyRole) + if !exists { + return nil, errors.New("could not find 'thumbprint' claim") + } + + role, ok := rawRole.(string) + if !ok { + return nil, errors.Errorf("unexpected '%s' claim value: '%v'", keyRole, rawRole) + } + + if !isValidRole(role) { + return nil, errors.Errorf("invalid role '%s'", role) + } + + user := &User{ + subject: token.Subject(), + role: Role(role), + } + + return user, nil +} + +func NewAuthenticator(keys jwk.Set, issuer string, acceptableSkew time.Duration) *Authenticator { + return &Authenticator{ + keys: keys, + issuer: issuer, + acceptableSkew: acceptableSkew, + } +} + +var _ auth.Authenticator = &Authenticator{} diff --git a/internal/auth/jwt/jwt.go b/internal/auth/jwt/jwt.go new file mode 100644 index 0000000..47638fc --- /dev/null +++ b/internal/auth/jwt/jwt.go @@ -0,0 +1,62 @@ +package jwt + +import ( + "context" + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/jwk" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/pkg/errors" +) + +const keyRole = "role" + +func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string, acceptableSkew time.Duration) (jwt.Token, error) { + token, err := jwt.Parse( + []byte(rawToken), + jwt.WithKeySet(keys, jws.WithRequireKid(false)), + jwt.WithIssuer(issuer), + jwt.WithValidate(true), + jwt.WithAcceptableSkew(acceptableSkew), + ) + if err != nil { + return nil, errors.WithStack(err) + } + + return token, nil +} + +func GenerateToken(ctx context.Context, key jwk.Key, issuer, subject string, role Role) (string, error) { + token := jwt.New() + + if err := token.Set(jwt.SubjectKey, subject); err != nil { + return "", errors.WithStack(err) + } + + if err := token.Set(jwt.IssuerKey, issuer); err != nil { + return "", errors.WithStack(err) + } + + if err := token.Set(keyRole, role); err != nil { + return "", errors.WithStack(err) + } + + now := time.Now().UTC() + + if err := token.Set(jwt.NotBeforeKey, now); err != nil { + return "", errors.WithStack(err) + } + + if err := token.Set(jwt.IssuedAtKey, now); err != nil { + return "", errors.WithStack(err) + } + + rawToken, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, key)) + if err != nil { + return "", errors.WithStack(err) + } + + return string(rawToken), nil +} diff --git a/internal/auth/jwt/user.go b/internal/auth/jwt/user.go new file mode 100644 index 0000000..216c6a8 --- /dev/null +++ b/internal/auth/jwt/user.go @@ -0,0 +1,32 @@ +package jwt + +import "forge.cadoles.com/cadoles/bouncer/internal/auth" + +type Role string + +const ( + RoleWriter Role = "writer" + RoleReader Role = "reader" +) + +func isValidRole(r string) bool { + rr := Role(r) + + return rr == RoleWriter || rr == RoleReader +} + +type User struct { + subject string + role Role +} + +// Subject implements auth.User +func (u *User) Subject() string { + return u.subject +} + +func (u *User) Role() Role { + return u.role +} + +var _ auth.User = &User{} diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go new file mode 100644 index 0000000..c62ebb5 --- /dev/null +++ b/internal/auth/middleware.go @@ -0,0 +1,79 @@ +package auth + +import ( + "context" + "net/http" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/api" + "gitlab.com/wpetit/goweb/logger" +) + +const ( + ErrCodeUnauthorized api.ErrorCode = "unauthorized" + ErrCodeForbidden api.ErrorCode = "forbidden" +) + +type contextKey string + +const ( + contextKeyUser contextKey = "user" +) + +func CtxUser(ctx context.Context) (User, error) { + user, ok := ctx.Value(contextKeyUser).(User) + if !ok { + return nil, errors.Errorf("unexpected user type: expected '%T', got '%T'", new(User), ctx.Value(contextKeyUser)) + } + + return user, nil +} + +var ErrUnauthenticated = errors.New("unauthenticated") + +type User interface { + Subject() string +} + +type Authenticator interface { + Authenticate(context.Context, *http.Request) (User, error) +} + +func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + ctx := logger.With(r.Context(), logger.F("remoteAddr", r.RemoteAddr)) + + var ( + user User + err error + ) + + for _, auth := range authenticators { + user, err = auth.Authenticate(ctx, r) + if err != nil { + logger.Debug(ctx, "could not authenticate request", logger.E(errors.WithStack(err))) + + continue + } + + if user != nil { + break + } + } + + if user == nil { + api.ErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, nil) + + return + } + + ctx = logger.With(ctx, logger.F("user", user.Subject())) + ctx = context.WithValue(ctx, contextKeyUser, user) + + h.ServeHTTP(w, r.WithContext(ctx)) + } + + return http.HandlerFunc(fn) + } +} diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..8c8d2fb --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,144 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/api" + "gitlab.com/wpetit/goweb/logger" +) + +type Client struct { + http *http.Client + defaultOpts Options + serverURL string +} + +func (c *Client) apiGet(ctx context.Context, path string, result any, funcs ...OptionFunc) error { + if err := c.apiDo(ctx, http.MethodGet, path, nil, result, funcs...); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (c *Client) apiPost(ctx context.Context, path string, payload any, result any, funcs ...OptionFunc) error { + if err := c.apiDo(ctx, http.MethodPost, path, payload, result, funcs...); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (c *Client) apiPut(ctx context.Context, path string, payload any, result any, funcs ...OptionFunc) error { + if err := c.apiDo(ctx, http.MethodPut, path, payload, result, funcs...); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (c *Client) apiDelete(ctx context.Context, path string, payload any, result any, funcs ...OptionFunc) error { + if err := c.apiDo(ctx, http.MethodDelete, path, payload, result, funcs...); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (c *Client) apiDo(ctx context.Context, method string, path string, payload any, response any, funcs ...OptionFunc) error { + opts := c.defaultOptions() + for _, fn := range funcs { + fn(opts) + } + + url := c.serverURL + path + + logger.Debug( + ctx, "new http request", + logger.F("method", method), + logger.F("url", url), + logger.F("payload", payload), + ) + + var buf bytes.Buffer + + encoder := json.NewEncoder(&buf) + + if err := encoder.Encode(payload); err != nil { + return errors.WithStack(err) + } + + req, err := http.NewRequest(method, url, &buf) + if err != nil { + return errors.WithStack(err) + } + + for key, values := range opts.Headers { + for _, v := range values { + req.Header.Add(key, v) + } + } + + res, err := c.http.Do(req) + if err != nil { + return errors.WithStack(err) + } + + defer res.Body.Close() + + decoder := json.NewDecoder(res.Body) + + if err := decoder.Decode(&response); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (c *Client) defaultOptions() *Options { + return &Options{ + Headers: c.defaultOpts.Headers, + } +} + +func withResponse[T any]() struct { + Data T + Error *api.Error +} { + return struct { + Data T + Error *api.Error + }{} +} + +func joinSlice[T any](items []T) string { + str := "" + + for idx, item := range items { + if idx != 0 { + str += "," + } + + str += fmt.Sprintf("%v", item) + } + + return str +} + +func New(serverURL string, funcs ...OptionFunc) *Client { + opts := Options{} + for _, fn := range funcs { + fn(&opts) + } + + return &Client{ + serverURL: serverURL, + http: &http.Client{}, + defaultOpts: opts, + } +} diff --git a/internal/client/create_proxy.go b/internal/client/create_proxy.go new file mode 100644 index 0000000..489ab9c --- /dev/null +++ b/internal/client/create_proxy.go @@ -0,0 +1,29 @@ +package client + +import ( + "context" + "net/url" + + "forge.cadoles.com/cadoles/bouncer/internal/admin" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +func (c *Client) CreateProxy(ctx context.Context, to *url.URL, from []string, funcs ...OptionFunc) (*store.Proxy, error) { + request := admin.CreateProxyRequest{ + To: to.String(), + From: from, + } + + response := withResponse[admin.CreateProxyResponse]() + + if err := c.apiPost(ctx, "/api/v1/proxies", request, &response); err != nil { + return nil, errors.WithStack(err) + } + + if response.Error != nil { + return nil, errors.WithStack(response.Error) + } + + return response.Data.Proxy, nil +} diff --git a/internal/client/delete_proxy.go b/internal/client/delete_proxy.go new file mode 100644 index 0000000..6f012e1 --- /dev/null +++ b/internal/client/delete_proxy.go @@ -0,0 +1,26 @@ +package client + +import ( + "context" + "fmt" + + "forge.cadoles.com/cadoles/bouncer/internal/admin" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +func (c *Client) DeleteProxy(ctx context.Context, proxyID store.ProxyID, funcs ...OptionFunc) (store.ProxyID, error) { + response := withResponse[admin.DeleteProxyResponse]() + + path := fmt.Sprintf("/api/v1/proxies/%s", proxyID) + + if err := c.apiDelete(ctx, path, nil, &response, funcs...); err != nil { + return "", errors.WithStack(err) + } + + if response.Error != nil { + return "", errors.WithStack(response.Error) + } + + return response.Data.ProxyID, nil +} diff --git a/internal/client/get_proxy.go b/internal/client/get_proxy.go new file mode 100644 index 0000000..3fb2b72 --- /dev/null +++ b/internal/client/get_proxy.go @@ -0,0 +1,26 @@ +package client + +import ( + "context" + "fmt" + + "forge.cadoles.com/cadoles/bouncer/internal/admin" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +func (c *Client) GetProxy(ctx context.Context, proxyID store.ProxyID, funcs ...OptionFunc) (*store.Proxy, error) { + response := withResponse[admin.GetProxyResponse]() + + path := fmt.Sprintf("/api/v1/proxies/%s", proxyID) + + if err := c.apiGet(ctx, path, &response, funcs...); err != nil { + return nil, errors.WithStack(err) + } + + if response.Error != nil { + return nil, errors.WithStack(response.Error) + } + + return response.Data.Proxy, nil +} diff --git a/internal/client/options.go b/internal/client/options.go new file mode 100644 index 0000000..85fd12b --- /dev/null +++ b/internal/client/options.go @@ -0,0 +1,24 @@ +package client + +import "net/http" + +type Options struct { + Headers http.Header +} + +type OptionFunc func(*Options) + +func WithToken(token string) OptionFunc { + return func(o *Options) { + if o.Headers == nil { + o.Headers = http.Header{} + } + o.Headers.Set("Authorization", "Bearer "+token) + } +} + +func WithHeaders(headers http.Header) OptionFunc { + return func(o *Options) { + o.Headers = headers + } +} diff --git a/internal/client/query_proxy.go b/internal/client/query_proxy.go new file mode 100644 index 0000000..10bdde3 --- /dev/null +++ b/internal/client/query_proxy.go @@ -0,0 +1,75 @@ +package client + +import ( + "context" + "fmt" + "net/url" + + "forge.cadoles.com/cadoles/bouncer/internal/admin" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +type QueryProxyOptionFunc func(*QueryProxyOptions) + +type QueryProxyOptions struct { + Options []OptionFunc + Limit *int + Offset *int + IDs []store.ProxyID +} + +func WithQueryProxyOptions(funcs ...OptionFunc) QueryProxyOptionFunc { + return func(opts *QueryProxyOptions) { + opts.Options = funcs + } +} + +func WithQueryProxyLimit(limit int) QueryProxyOptionFunc { + return func(opts *QueryProxyOptions) { + opts.Limit = &limit + } +} + +func WithQueryProxyOffset(offset int) QueryProxyOptionFunc { + return func(opts *QueryProxyOptions) { + opts.Offset = &offset + } +} + +func WithQueryProxyID(ids ...store.ProxyID) QueryProxyOptionFunc { + return func(opts *QueryProxyOptions) { + opts.IDs = ids + } +} + +func (c *Client) QueryProxy(ctx context.Context, funcs ...QueryProxyOptionFunc) ([]*store.ProxyHeader, error) { + options := &QueryProxyOptions{} + for _, fn := range funcs { + fn(options) + } + + query := url.Values{} + + if options.IDs != nil && len(options.IDs) > 0 { + query.Set("ids", joinSlice(options.IDs)) + } + + path := fmt.Sprintf("/api/v1/proxies?%s", query.Encode()) + + response := withResponse[admin.QueryProxyResponse]() + + if options.Options == nil { + options.Options = make([]OptionFunc, 0) + } + + if err := c.apiGet(ctx, path, &response, options.Options...); err != nil { + return nil, errors.WithStack(err) + } + + if response.Error != nil { + return nil, errors.WithStack(response.Error) + } + + return response.Data.Proxies, nil +} diff --git a/internal/client/update_proxy.go b/internal/client/update_proxy.go new file mode 100644 index 0000000..f03c051 --- /dev/null +++ b/internal/client/update_proxy.go @@ -0,0 +1,29 @@ +package client + +import ( + "context" + "net/url" + + "forge.cadoles.com/cadoles/bouncer/internal/admin" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +func (c *Client) UpdateProxy(ctx context.Context, to *url.URL, from []string, funcs ...OptionFunc) (*store.Proxy, error) { + request := admin.CreateProxyRequest{ + To: to.String(), + From: from, + } + + response := withResponse[admin.CreateProxyResponse]() + + if err := c.apiPost(ctx, "/api/v1/proxies", request, &response); err != nil { + return nil, errors.WithStack(err) + } + + if response.Error != nil { + return nil, errors.WithStack(response.Error) + } + + return response.Data.Proxy, nil +} diff --git a/internal/command/admin/auth/create_token.go b/internal/command/admin/auth/create_token.go new file mode 100644 index 0000000..d639ac9 --- /dev/null +++ b/internal/command/admin/auth/create_token.go @@ -0,0 +1,54 @@ +package auth + +import ( + "fmt" + + "forge.cadoles.com/cadoles/bouncer/internal/auth/jwt" + "forge.cadoles.com/cadoles/bouncer/internal/command/common" + "forge.cadoles.com/cadoles/bouncer/internal/jwk" + "github.com/lithammer/shortuuid/v4" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func CreateTokenCommand() *cli.Command { + return &cli.Command{ + Name: "create-token", + Usage: "Create a new authentication token", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "role", + Usage: fmt.Sprintf("associate `ROLE` to the token (available: %v)", []jwt.Role{jwt.RoleReader, jwt.RoleWriter}), + Value: string(jwt.RoleReader), + }, + &cli.StringFlag{ + Name: "subject", + Usage: "associate `SUBJECT` to the token", + Value: fmt.Sprintf("user-%s", shortuuid.New()), + }, + }, + Action: func(ctx *cli.Context) error { + conf, err := common.LoadConfig(ctx) + if err != nil { + return errors.Wrap(err, "Could not load configuration") + } + + subject := ctx.String("subject") + role := ctx.String("role") + + key, err := jwk.LoadOrGenerate(string(conf.Admin.Auth.PrivateKey), jwk.DefaultKeySize) + if err != nil { + return errors.WithStack(err) + } + + token, err := jwt.GenerateToken(ctx.Context, key, string(conf.Admin.Auth.Issuer), subject, jwt.Role(role)) + if err != nil { + return errors.WithStack(err) + } + + fmt.Println(token) + + return nil + }, + } +} diff --git a/internal/command/admin/auth/root.go b/internal/command/admin/auth/root.go new file mode 100644 index 0000000..ccb554a --- /dev/null +++ b/internal/command/admin/auth/root.go @@ -0,0 +1,15 @@ +package auth + +import ( + "github.com/urfave/cli/v2" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "auth", + Usage: "Authentication related commands", + Subcommands: []*cli.Command{ + CreateTokenCommand(), + }, + } +} diff --git a/internal/command/admin/root.go b/internal/command/admin/root.go new file mode 100644 index 0000000..feccb2b --- /dev/null +++ b/internal/command/admin/root.go @@ -0,0 +1,17 @@ +package admin + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/auth" + "github.com/urfave/cli/v2" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "admin", + Usage: "Admin server related commands", + Subcommands: []*cli.Command{ + RunCommand(), + auth.Root(), + }, + } +} diff --git a/internal/command/admin/run.go b/internal/command/admin/run.go new file mode 100644 index 0000000..99b568e --- /dev/null +++ b/internal/command/admin/run.go @@ -0,0 +1,54 @@ +package admin + +import ( + "fmt" + "strings" + + "forge.cadoles.com/cadoles/bouncer/internal/admin" + "forge.cadoles.com/cadoles/bouncer/internal/command/common" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" + "gitlab.com/wpetit/goweb/logger" +) + +func RunCommand() *cli.Command { + flags := common.Flags() + + return &cli.Command{ + Name: "run", + Usage: "Run the admin server", + Flags: flags, + Action: func(ctx *cli.Context) error { + conf, err := common.LoadConfig(ctx) + if err != nil { + return errors.Wrap(err, "could not load configuration") + } + + logger.SetFormat(logger.Format(conf.Logger.Format)) + logger.SetLevel(logger.Level(conf.Logger.Level)) + + srv := admin.NewServer( + admin.WithServerConfig(conf.Admin), + admin.WithRedisConfig(conf.Redis), + ) + + addrs, srvErrs := srv.Start(ctx.Context) + + select { + case addr := <-addrs: + url := fmt.Sprintf("http://%s", addr.String()) + url = strings.Replace(url, "0.0.0.0", "127.0.0.1", 1) + + logger.Info(ctx.Context, "listening", logger.F("url", url)) + case err = <-srvErrs: + return errors.WithStack(err) + } + + if err = <-srvErrs; err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/client/apierr/wrap.go b/internal/command/client/apierr/wrap.go new file mode 100644 index 0000000..952c2c3 --- /dev/null +++ b/internal/command/client/apierr/wrap.go @@ -0,0 +1,91 @@ +package apierr + +import ( + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/api" +) + +func Wrap(err error) error { + apiErr := &api.Error{} + if !errors.As(err, &apiErr) { + return err + } + + switch apiErr.Code { + case api.ErrCodeInvalidFieldValue: + return wrapInvalidFieldValueErr(apiErr) + + default: + return wrapApiErrorWithMessage(apiErr) + } +} + +func wrapApiErrorWithMessage(err *api.Error) error { + data, ok := err.Data.(map[string]any) + if !ok { + return err + } + + rawMessage, exists := data["message"] + if !exists { + return err + } + + message, ok := rawMessage.(string) + if !ok { + return err + } + + return errors.Wrapf(err, message) +} + +func wrapInvalidFieldValueErr(err *api.Error) error { + data, ok := err.Data.(map[string]any) + if !ok { + return err + } + + rawFields, exists := data["Fields"] + if !exists { + return err + } + + fields, ok := rawFields.([]any) + if !ok { + return err + } + + var ( + field string + rule string + ) + + if len(fields) == 0 { + return err + } + + firstField, ok := fields[0].(map[string]any) + if !ok { + return err + } + + param, ok := firstField["Param"].(string) + if !ok { + return err + } + + tag, ok := firstField["Tag"].(string) + if !ok { + return err + } + + fieldName, ok := firstField["Field"].(string) + if !ok { + return err + } + + field = fieldName + rule = tag + "=" + param + + return errors.Wrapf(err, "server expected field '%s' to match rule '%s'", field, rule) +} diff --git a/internal/command/client/flag/flag.go b/internal/command/client/flag/flag.go new file mode 100644 index 0000000..4a2ec3b --- /dev/null +++ b/internal/command/client/flag/flag.go @@ -0,0 +1,98 @@ +package flag + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + + "forge.cadoles.com/cadoles/bouncer/internal/format" + "forge.cadoles.com/cadoles/bouncer/internal/format/table" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func ComposeFlags(flags ...cli.Flag) []cli.Flag { + baseFlags := []cli.Flag{ + &cli.StringFlag{ + Name: "server", + Aliases: []string{"s"}, + Usage: "use `SERVER` as server url", + Value: "http://127.0.0.1:8081", + }, + &cli.StringFlag{ + Name: "format", + Aliases: []string{"f"}, + Usage: fmt.Sprintf("use `FORMAT` as output format (available: %s)", format.Available()), + Value: string(table.Format), + }, + &cli.StringFlag{ + Name: "output-mode", + Aliases: []string{"m"}, + Usage: fmt.Sprintf("use `MODE` as output mode (available: %s)", []format.OutputMode{format.OutputModeCompact, format.OutputModeWide}), + Value: string(format.OutputModeCompact), + }, + &cli.StringFlag{ + Name: "token", + Aliases: []string{"t"}, + EnvVars: []string{`BOUNCER_TOKEN`}, + Usage: "use `TOKEN` as authentication token", + }, + &cli.StringFlag{ + Name: "token-file", + EnvVars: []string{`BOUNCER_TOKEN_FILE`}, + Usage: "use `TOKEN_FILE` as file containing the authentication token", + Value: ".bouncer-token", + TakesFile: true, + }, + } + + flags = append(flags, baseFlags...) + + return flags +} + +type BaseFlags struct { + ServerURL string + Format format.Format + OutputMode format.OutputMode + Token string + TokenFile string +} + +func GetBaseFlags(ctx *cli.Context) *BaseFlags { + serverURL := ctx.String("server") + rawFormat := ctx.String("format") + rawOutputMode := ctx.String("output-mode") + tokenFile := ctx.String("token-file") + token := ctx.String("token") + + return &BaseFlags{ + ServerURL: serverURL, + Format: format.Format(rawFormat), + OutputMode: format.OutputMode(rawOutputMode), + Token: token, + TokenFile: tokenFile, + } +} + +func GetToken(flags *BaseFlags) (string, error) { + if flags.Token != "" { + return flags.Token, nil + } + + if flags.TokenFile == "" { + return "", nil + } + + rawToken, err := ioutil.ReadFile(flags.TokenFile) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return "", errors.WithStack(err) + } + + if rawToken == nil { + return "", nil + } + + return strings.TrimSpace(string(rawToken)), nil +} diff --git a/internal/command/client/flag/util.go b/internal/command/client/flag/util.go new file mode 100644 index 0000000..de2af79 --- /dev/null +++ b/internal/command/client/flag/util.go @@ -0,0 +1,11 @@ +package flag + +func AsAnySlice[T any](src []T) []any { + dst := make([]any, len(src)) + + for i, s := range src { + dst[i] = s + } + + return dst +} diff --git a/internal/command/client/proxy/create.go b/internal/command/client/proxy/create.go new file mode 100644 index 0000000..972bee4 --- /dev/null +++ b/internal/command/client/proxy/create.go @@ -0,0 +1,62 @@ +package proxy + +import ( + "net/url" + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/client/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/client/flag" + "forge.cadoles.com/cadoles/bouncer/internal/format" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func CreateCommand() *cli.Command { + return &cli.Command{ + Name: "create", + Usage: "Create proxy", + Flags: clientFlag.ComposeFlags( + &cli.StringFlag{ + Name: "to", + Usage: "SET `TO` as proxy destination url", + Value: "", + }, + &cli.StringSliceFlag{ + Name: "from", + Usage: "Set `FROM` as patterns to match incoming requests", + Value: cli.NewStringSlice("*"), + }, + ), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + to, err := url.Parse(ctx.String("to")) + if err != nil { + return errors.Wrap(err, "'to' parameter should be a valid url") + } + + from := ctx.StringSlice("from") + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + proxy, err := client.CreateProxy(ctx.Context, to, from) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := proxyHints(baseFlags.OutputMode) + + if err := format.Write(baseFlags.Format, os.Stdout, hints, proxy); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/client/proxy/delete.go b/internal/command/client/proxy/delete.go new file mode 100644 index 0000000..e3d258d --- /dev/null +++ b/internal/command/client/proxy/delete.go @@ -0,0 +1,56 @@ +package proxy + +import ( + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/client/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/client/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/client/proxy/flag" + "forge.cadoles.com/cadoles/bouncer/internal/format" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func DeleteCommand() *cli.Command { + return &cli.Command{ + Name: "delete", + Usage: "Delete proxy", + Flags: proxyFlag.WithProxyFlags(), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + proxyID, err := proxyFlag.AssertProxyID(ctx) + if err != nil { + return errors.WithStack(err) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + proxyID, err = client.DeleteProxy(ctx.Context, proxyID) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := format.Hints{ + OutputMode: baseFlags.OutputMode, + } + + if err := format.Write(baseFlags.Format, os.Stdout, hints, struct { + ID store.ProxyID `json:"id"` + }{ + ID: proxyID, + }); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/client/proxy/flag/flag.go b/internal/command/client/proxy/flag/flag.go new file mode 100644 index 0000000..84b49e8 --- /dev/null +++ b/internal/command/client/proxy/flag/flag.go @@ -0,0 +1,33 @@ +package flag + +import ( + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/client/flag" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func WithProxyFlags(flags ...cli.Flag) []cli.Flag { + baseFlags := clientFlag.ComposeFlags( + &cli.StringFlag{ + Name: "proxy-id", + Aliases: []string{"p"}, + Usage: "use `PROXY_ID` as targeted proxy", + Value: "", + }, + ) + + flags = append(flags, baseFlags...) + + return flags +} + +func AssertProxyID(ctx *cli.Context) (store.ProxyID, error) { + rawProxyID := ctx.String("proxy-id") + + if rawProxyID == "" { + return "", errors.New("'proxy-id' cannot be empty") + } + + return store.ProxyID(rawProxyID), nil +} diff --git a/internal/command/client/proxy/get.go b/internal/command/client/proxy/get.go new file mode 100644 index 0000000..d8134d9 --- /dev/null +++ b/internal/command/client/proxy/get.go @@ -0,0 +1,49 @@ +package proxy + +import ( + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/client/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/client/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/client/proxy/flag" + "forge.cadoles.com/cadoles/bouncer/internal/format" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func GetCommand() *cli.Command { + return &cli.Command{ + Name: "get", + Usage: "Get proxy", + Flags: proxyFlag.WithProxyFlags(), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + proxyID, err := proxyFlag.AssertProxyID(ctx) + if err != nil { + return errors.WithStack(err) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + proxy, err := client.GetProxy(ctx.Context, proxyID) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := proxyHints(baseFlags.OutputMode) + + if err := format.Write(baseFlags.Format, os.Stdout, hints, proxy); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/client/proxy/query.go b/internal/command/client/proxy/query.go new file mode 100644 index 0000000..65f980f --- /dev/null +++ b/internal/command/client/proxy/query.go @@ -0,0 +1,63 @@ +package proxy + +import ( + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/client/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/client/flag" + "forge.cadoles.com/cadoles/bouncer/internal/format" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func QueryCommand() *cli.Command { + return &cli.Command{ + Name: "query", + Usage: "Query proxies", + Flags: clientFlag.ComposeFlags( + &cli.Int64SliceFlag{ + Name: "ids", + Usage: "use `IDS` as query filter", + }, + ), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + options := make([]client.QueryProxyOptionFunc, 0) + + rawIDs := ctx.StringSlice("ids") + if rawIDs != nil { + proxyIDs := func(ids []string) []store.ProxyID { + agentIDs := make([]store.ProxyID, len(ids)) + for i, id := range ids { + agentIDs[i] = store.ProxyID(id) + } + return agentIDs + }(rawIDs) + options = append(options, client.WithQueryProxyID(proxyIDs...)) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + proxies, err := client.QueryProxy(ctx.Context, options...) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := proxyHeaderHints(baseFlags.OutputMode) + + if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(proxies)...); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/client/proxy/root.go b/internal/command/client/proxy/root.go new file mode 100644 index 0000000..6219ebb --- /dev/null +++ b/internal/command/client/proxy/root.go @@ -0,0 +1,18 @@ +package proxy + +import ( + "github.com/urfave/cli/v2" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "proxy", + Usage: "Execute actions related to proxies", + Subcommands: []*cli.Command{ + GetCommand(), + CreateCommand(), + QueryCommand(), + DeleteCommand(), + }, + } +} diff --git a/internal/command/client/proxy/update.go b/internal/command/client/proxy/update.go new file mode 100644 index 0000000..e1cecf1 --- /dev/null +++ b/internal/command/client/proxy/update.go @@ -0,0 +1,72 @@ +package proxy + +// import ( +// "os" + +// "forge.cadoles.com/Cadoles/emissary/internal/client" +// agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag" +// "forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr" +// clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag" +// "forge.cadoles.com/Cadoles/emissary/internal/format" +// "github.com/pkg/errors" +// "github.com/urfave/cli/v2" +// ) + +// func UpdateCommand() *cli.Command { +// return &cli.Command{ +// Name: "update", +// Usage: "Updata agent", +// Flags: agentFlag.WithAgentFlags( +// &cli.IntFlag{ +// Name: "status", +// Usage: "Set `STATUS` to selected agent", +// Value: -1, +// }, +// &cli.StringFlag{ +// Name: "label", +// Usage: "Set `LABEL` to selected agent", +// Value: "", +// }, +// ), +// Action: func(ctx *cli.Context) error { +// baseFlags := clientFlag.GetBaseFlags(ctx) + +// token, err := clientFlag.GetToken(baseFlags) +// if err != nil { +// return errors.WithStack(apierr.Wrap(err)) +// } + +// agentID, err := agentFlag.AssertAgentID(ctx) +// if err != nil { +// return errors.WithStack(err) +// } + +// options := make([]client.UpdateAgentOptionFunc, 0) + +// status := ctx.Int("status") +// if status != -1 { +// options = append(options, client.WithAgentStatus(status)) +// } + +// label := ctx.String("label") +// if label != "" { +// options = append(options, client.WithAgentLabel(label)) +// } + +// client := client.New(baseFlags.ServerURL, client.WithToken(token)) + +// agent, err := client.UpdateAgent(ctx.Context, agentID, options...) +// if err != nil { +// return errors.WithStack(apierr.Wrap(err)) +// } + +// hints := agentHints(baseFlags.OutputMode) + +// if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil { +// return errors.WithStack(err) +// } + +// return nil +// }, +// } +// } diff --git a/internal/command/client/proxy/util.go b/internal/command/client/proxy/util.go new file mode 100644 index 0000000..72e5d13 --- /dev/null +++ b/internal/command/client/proxy/util.go @@ -0,0 +1,31 @@ +package proxy + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/format" + "forge.cadoles.com/cadoles/bouncer/internal/format/table" +) + +func proxyHeaderHints(outputMode format.OutputMode) format.Hints { + return format.Hints{ + OutputMode: outputMode, + Props: []format.Prop{ + format.NewProp("ID", "ID", table.WithCompactModeMaxColumnWidth(8)), + format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)), + format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)), + }, + } +} + +func proxyHints(outputMode format.OutputMode) format.Hints { + return format.Hints{ + OutputMode: outputMode, + Props: []format.Prop{ + format.NewProp("ID", "ID", table.WithCompactModeMaxColumnWidth(8)), + format.NewProp("From", "From"), + format.NewProp("To", "To"), + format.NewProp("Weight", "Weight"), + format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)), + format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)), + }, + } +} diff --git a/internal/command/client/root.go b/internal/command/client/root.go new file mode 100644 index 0000000..8d88bf4 --- /dev/null +++ b/internal/command/client/root.go @@ -0,0 +1,16 @@ +package client + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/command/client/proxy" + "github.com/urfave/cli/v2" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "client", + Usage: "Admin API related commands", + Subcommands: []*cli.Command{ + proxy.Root(), + }, + } +} diff --git a/internal/command/common/flags.go b/internal/command/common/flags.go new file mode 100644 index 0000000..7c25324 --- /dev/null +++ b/internal/command/common/flags.go @@ -0,0 +1,7 @@ +package common + +import "github.com/urfave/cli/v2" + +func Flags() []cli.Flag { + return []cli.Flag{} +} diff --git a/internal/command/common/load_config.go b/internal/command/common/load_config.go new file mode 100644 index 0000000..fc16461 --- /dev/null +++ b/internal/command/common/load_config.go @@ -0,0 +1,27 @@ +package common + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/config" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func LoadConfig(ctx *cli.Context) (*config.Config, error) { + configFile := ctx.String("config") + + var ( + conf *config.Config + err error + ) + + if configFile != "" { + conf, err = config.NewFromFile(configFile) + if err != nil { + return nil, errors.Wrapf(err, "Could not load config file '%s'", configFile) + } + } else { + conf = config.NewDefault() + } + + return conf, nil +} diff --git a/internal/command/config/dump.go b/internal/command/config/dump.go new file mode 100644 index 0000000..b14f63c --- /dev/null +++ b/internal/command/config/dump.go @@ -0,0 +1,36 @@ +package config + +import ( + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/command/common" + "forge.cadoles.com/cadoles/bouncer/internal/config" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" + "gitlab.com/wpetit/goweb/logger" +) + +func Dump() *cli.Command { + flags := common.Flags() + + return &cli.Command{ + Name: "dump", + Usage: "Dump the current configuration", + Flags: flags, + Action: func(ctx *cli.Context) error { + conf, err := common.LoadConfig(ctx) + if err != nil { + return errors.Wrap(err, "Could not load configuration") + } + + logger.SetFormat(logger.Format(conf.Logger.Format)) + logger.SetLevel(logger.Level(conf.Logger.Level)) + + if err := config.Dump(conf, os.Stdout); err != nil { + return errors.Wrap(err, "Could not dump configuration") + } + + return nil + }, + } +} diff --git a/internal/command/config/root.go b/internal/command/config/root.go new file mode 100644 index 0000000..e0042ee --- /dev/null +++ b/internal/command/config/root.go @@ -0,0 +1,13 @@ +package config + +import "github.com/urfave/cli/v2" + +func Root() *cli.Command { + return &cli.Command{ + Name: "config", + Usage: "Config related commands", + Subcommands: []*cli.Command{ + Dump(), + }, + } +} diff --git a/internal/command/main.go b/internal/command/main.go new file mode 100644 index 0000000..34583bd --- /dev/null +++ b/internal/command/main.go @@ -0,0 +1,107 @@ +package command + +import ( + "context" + "fmt" + "os" + "sort" + "time" + + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands ...*cli.Command) { + ctx := context.Background() + + compiled, err := time.Parse(time.RFC3339, buildDate) + if err != nil { + panic(errors.Wrapf(err, "could not parse build date '%s'", buildDate)) + } + + app := &cli.App{ + Version: fmt.Sprintf("%s (%s, %s)", projectVersion, gitRef, buildDate), + Compiled: compiled, + Name: "bouncer", + Usage: "reverse proxy server with dynamic queuing management", + Commands: commands, + Before: func(ctx *cli.Context) error { + workdir := ctx.String("workdir") + // Switch to new working directory if defined + if workdir != "" { + if err := os.Chdir(workdir); err != nil { + return errors.Wrap(err, "could not change working directory") + } + } + + if err := ctx.Set("projectVersion", projectVersion); err != nil { + return errors.WithStack(err) + } + + if err := ctx.Set("gitRef", gitRef); err != nil { + return errors.WithStack(err) + } + + if err := ctx.Set("buildDate", buildDate); err != nil { + return errors.WithStack(err) + } + + return nil + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "workdir", + Value: "", + Usage: "The working directory", + }, + &cli.StringFlag{ + Name: "projectVersion", + Value: "", + Hidden: true, + }, + &cli.StringFlag{ + Name: "gitRef", + Value: "", + Hidden: true, + }, + &cli.StringFlag{ + Name: "buildDate", + Value: "", + Hidden: true, + }, + &cli.BoolFlag{ + Name: "debug", + EnvVars: []string{"BOUNCER_DEBUG"}, + Value: false, + }, + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + EnvVars: []string{"BOUNCER_CONFIG"}, + Value: defaultConfigPath, + TakesFile: true, + }, + }, + } + + app.ExitErrHandler = func(ctx *cli.Context, err error) { + if err == nil { + return + } + + debug := ctx.Bool("debug") + + if !debug { + fmt.Printf("[ERROR] %v\n", err) + } else { + fmt.Printf("%+v", err) + } + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + + if err := app.RunContext(ctx, os.Args); err != nil { + os.Exit(1) + } +} diff --git a/internal/command/proxy/root.go b/internal/command/proxy/root.go new file mode 100644 index 0000000..0dc25db --- /dev/null +++ b/internal/command/proxy/root.go @@ -0,0 +1,15 @@ +package proxy + +import ( + "github.com/urfave/cli/v2" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "proxy", + Usage: "Proxy server related commands", + Subcommands: []*cli.Command{ + RunCommand(), + }, + } +} diff --git a/internal/command/proxy/run.go b/internal/command/proxy/run.go new file mode 100644 index 0000000..d956e81 --- /dev/null +++ b/internal/command/proxy/run.go @@ -0,0 +1,54 @@ +package proxy + +import ( + "fmt" + "strings" + + "forge.cadoles.com/cadoles/bouncer/internal/command/common" + "forge.cadoles.com/cadoles/bouncer/internal/proxy" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" + "gitlab.com/wpetit/goweb/logger" +) + +func RunCommand() *cli.Command { + flags := common.Flags() + + return &cli.Command{ + Name: "run", + Usage: "Run the proxy server", + Flags: flags, + Action: func(ctx *cli.Context) error { + conf, err := common.LoadConfig(ctx) + if err != nil { + return errors.Wrap(err, "could not load configuration") + } + + logger.SetFormat(logger.Format(conf.Logger.Format)) + logger.SetLevel(logger.Level(conf.Logger.Level)) + + srv := proxy.NewServer( + proxy.WithServerConfig(conf.Proxy), + proxy.WithRedisConfig(conf.Redis), + ) + + addrs, srvErrs := srv.Start(ctx.Context) + + select { + case addr := <-addrs: + url := fmt.Sprintf("http://%s", addr.String()) + url = strings.Replace(url, "0.0.0.0", "127.0.0.1", 1) + + logger.Info(ctx.Context, "listening", logger.F("url", url)) + case err = <-srvErrs: + return errors.WithStack(err) + } + + if err = <-srvErrs; err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/config/admin_server.go b/internal/config/admin_server.go new file mode 100644 index 0000000..faf1ffa --- /dev/null +++ b/internal/config/admin_server.go @@ -0,0 +1,27 @@ +package config + +type AdminServerConfig struct { + HTTP HTTPConfig `yaml:"http"` + CORS CORSConfig `yaml:"cors"` + Auth AuthConfig `yaml:"auth"` +} + +func NewDefaultAdminServerConfig() AdminServerConfig { + return AdminServerConfig{ + HTTP: NewHTTPConfig("127.0.0.1", 8081), + CORS: NewDefaultCORSConfig(), + Auth: NewDefaultAuthConfig(), + } +} + +type AuthConfig struct { + Issuer InterpolatedString `yaml:"issuer"` + PrivateKey InterpolatedString `yaml:"privateKey"` +} + +func NewDefaultAuthConfig() AuthConfig { + return AuthConfig{ + Issuer: "http://127.0.0.1:8081", + PrivateKey: "admin-key.json", + } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..2a5d087 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,64 @@ +package config + +import ( + "io" + "io/ioutil" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +// Config definition +type Config struct { + Admin AdminServerConfig `yaml:"admin"` + Proxy ProxyServerConfig `yaml:"proxy"` + Redis RedisConfig `yaml:"redis"` + Logger LoggerConfig `yaml:"logger"` +} + +// NewFromFile retrieves the configuration from the given file +func NewFromFile(path string) (*Config, error) { + config := NewDefault() + + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, errors.Wrapf(err, "could not read file '%s'", path) + } + + if err := yaml.Unmarshal(data, config); err != nil { + return nil, errors.Wrapf(err, "could not unmarshal configuration") + } + + return config, nil +} + +// NewDumpDefault dump the new default configuration +func NewDumpDefault() *Config { + config := NewDefault() + + return config +} + +// NewDefault return new default configuration +func NewDefault() *Config { + return &Config{ + Admin: NewDefaultAdminServerConfig(), + Proxy: NewDefaultProxyServerConfig(), + Logger: NewDefaultLoggerConfig(), + Redis: NewDefaultRedisConfig(), + } +} + +// Dump the given configuration in the given writer +func Dump(config *Config, w io.Writer) error { + data, err := yaml.Marshal(config) + if err != nil { + return errors.Wrap(err, "could not dump config") + } + + if _, err := w.Write(data); err != nil { + return errors.WithStack(err) + } + + return nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..5ddb43e --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,16 @@ +package config + +import ( + "testing" + + "github.com/pkg/errors" +) + +func TestConfigLoad(t *testing.T) { + filepath := "./testdata/config.yml" + + _, err := NewFromFile(filepath) + if err != nil { + t.Fatal(errors.WithStack(err)) + } +} diff --git a/internal/config/cors.go b/internal/config/cors.go new file mode 100644 index 0000000..7745728 --- /dev/null +++ b/internal/config/cors.go @@ -0,0 +1,20 @@ +package config + +type CORSConfig struct { + AllowedOrigins InterpolatedStringSlice `yaml:"allowedOrigins"` + AllowCredentials InterpolatedBool `yaml:"allowCredentials"` + AllowedMethods InterpolatedStringSlice `yaml:"allowMethods"` + AllowedHeaders InterpolatedStringSlice `yaml:"allowedHeaders"` + Debug InterpolatedBool `yaml:"debug"` +} + +// NewDefaultCorsConfig return the default CORS configuration. +func NewDefaultCORSConfig() CORSConfig { + return CORSConfig{ + AllowedOrigins: InterpolatedStringSlice{"http://localhost:3001"}, + AllowCredentials: true, + AllowedMethods: InterpolatedStringSlice{"POST", "GET", "PUT", "DELETE"}, + AllowedHeaders: InterpolatedStringSlice{"Origin", "Accept", "Content-Type", "Authorization", "Sentry-Trace"}, + Debug: false, + } +} diff --git a/internal/config/environment.go b/internal/config/environment.go new file mode 100644 index 0000000..3b1b3d2 --- /dev/null +++ b/internal/config/environment.go @@ -0,0 +1,125 @@ +package config + +import ( + "os" + "regexp" + "strconv" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +var reVar = regexp.MustCompile(`^\${(\w+)}$`) + +type InterpolatedString string + +func (is *InterpolatedString) UnmarshalYAML(value *yaml.Node) error { + var str string + + if err := value.Decode(&str); err != nil { + return errors.WithStack(err) + } + + if match := reVar.FindStringSubmatch(str); len(match) > 0 { + *is = InterpolatedString(os.Getenv(match[1])) + } else { + *is = InterpolatedString(str) + } + + return nil +} + +type InterpolatedInt int + +func (ii *InterpolatedInt) UnmarshalYAML(value *yaml.Node) error { + var str string + + if err := value.Decode(&str); err != nil { + return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line) + } + + if match := reVar.FindStringSubmatch(str); len(match) > 0 { + str = os.Getenv(match[1]) + } + + intVal, err := strconv.ParseInt(str, 10, 32) + if err != nil { + return errors.Wrapf(err, "could not parse int '%v', line '%d'", str, value.Line) + } + + *ii = InterpolatedInt(int(intVal)) + + return nil +} + +type InterpolatedBool bool + +func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error { + var str string + + if err := value.Decode(&str); err != nil { + return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line) + } + + if match := reVar.FindStringSubmatch(str); len(match) > 0 { + str = os.Getenv(match[1]) + } + + boolVal, err := strconv.ParseBool(str) + if err != nil { + return errors.Wrapf(err, "could not parse bool '%v', line '%d'", str, value.Line) + } + + *ib = InterpolatedBool(boolVal) + + return nil +} + +type InterpolatedMap map[string]interface{} + +func (im *InterpolatedMap) UnmarshalYAML(value *yaml.Node) error { + var data map[string]interface{} + + if err := value.Decode(&data); err != nil { + return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line) + } + + for key, value := range data { + strVal, ok := value.(string) + if !ok { + continue + } + + if match := reVar.FindStringSubmatch(strVal); len(match) > 0 { + strVal = os.Getenv(match[1]) + } + + data[key] = strVal + } + + *im = data + + return nil +} + +type InterpolatedStringSlice []string + +func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error { + var data []string + + if err := value.Decode(&data); err != nil { + return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line) + } + + for index, value := range data { + if match := reVar.FindStringSubmatch(value); len(match) > 0 { + value = os.Getenv(match[1]) + } + + data[index] = value + } + + *iss = data + + return nil +} diff --git a/internal/config/http.go b/internal/config/http.go new file mode 100644 index 0000000..d99bcca --- /dev/null +++ b/internal/config/http.go @@ -0,0 +1,13 @@ +package config + +type HTTPConfig struct { + Host InterpolatedString `yaml:"host"` + Port InterpolatedInt `yaml:"port"` +} + +func NewHTTPConfig(host string, port int) HTTPConfig { + return HTTPConfig{ + Host: InterpolatedString(host), + Port: InterpolatedInt(port), + } +} diff --git a/internal/config/logger.go b/internal/config/logger.go new file mode 100644 index 0000000..a524695 --- /dev/null +++ b/internal/config/logger.go @@ -0,0 +1,15 @@ +package config + +import "gitlab.com/wpetit/goweb/logger" + +type LoggerConfig struct { + Level InterpolatedInt `yaml:"level"` + Format InterpolatedString `yaml:"format"` +} + +func NewDefaultLoggerConfig() LoggerConfig { + return LoggerConfig{ + Level: InterpolatedInt(logger.LevelInfo), + Format: InterpolatedString(logger.FormatHuman), + } +} diff --git a/internal/config/proxy_server.go b/internal/config/proxy_server.go new file mode 100644 index 0000000..7bdc44c --- /dev/null +++ b/internal/config/proxy_server.go @@ -0,0 +1,11 @@ +package config + +type ProxyServerConfig struct { + HTTP HTTPConfig `yaml:"http"` +} + +func NewDefaultProxyServerConfig() ProxyServerConfig { + return ProxyServerConfig{ + HTTP: NewHTTPConfig("0.0.0.0", 8080), + } +} diff --git a/internal/config/redis.go b/internal/config/redis.go new file mode 100644 index 0000000..61faefd --- /dev/null +++ b/internal/config/redis.go @@ -0,0 +1,19 @@ +package config + +const ( + RedisModeSimple = "simple" + RedisModeSentinel = "sentinel" + RedisModeCluster = "cluster" +) + +type RedisConfig struct { + Adresses InterpolatedStringSlice `yaml:"addresses"` + Master InterpolatedString `yaml:"master"` +} + +func NewDefaultRedisConfig() RedisConfig { + return RedisConfig{ + Adresses: InterpolatedStringSlice{"localhost:6379"}, + Master: "", + } +} diff --git a/internal/config/testdata/config.yml b/internal/config/testdata/config.yml new file mode 100644 index 0000000..e06a61b --- /dev/null +++ b/internal/config/testdata/config.yml @@ -0,0 +1,6 @@ +logger: + level: 0 + format: human +http: + host: "0.0.0.0" + port: 3000 \ No newline at end of file diff --git a/internal/format/json/writer.go b/internal/format/json/writer.go new file mode 100644 index 0000000..020e02f --- /dev/null +++ b/internal/format/json/writer.go @@ -0,0 +1,38 @@ +package json + +import ( + "encoding/json" + "io" + + "forge.cadoles.com/cadoles/bouncer/internal/format" + "github.com/pkg/errors" +) + +const Format format.Format = "json" + +func init() { + format.Register(Format, NewWriter()) +} + +type Writer struct{} + +// Format implements format.Writer. +func (*Writer) Write(writer io.Writer, hints format.Hints, data ...any) error { + encoder := json.NewEncoder(writer) + + if hints.OutputMode == format.OutputModeWide { + encoder.SetIndent("", " ") + } + + if err := encoder.Encode(data); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func NewWriter() *Writer { + return &Writer{} +} + +var _ format.Writer = &Writer{} diff --git a/internal/format/prop.go b/internal/format/prop.go new file mode 100644 index 0000000..0881bd5 --- /dev/null +++ b/internal/format/prop.go @@ -0,0 +1,49 @@ +package format + +type PropHintName string + +type PropHintFunc func() (PropHintName, any) + +type Prop struct { + name string + label string + hints map[PropHintName]any +} + +func (p *Prop) Name() string { + return p.name +} + +func (p *Prop) Label() string { + return p.label +} + +func NewProp(name, label string, funcs ...PropHintFunc) Prop { + hints := make(map[PropHintName]any) + for _, fn := range funcs { + name, value := fn() + hints[name] = value + } + + return Prop{name, label, hints} +} + +func WithPropHint(name PropHintName, value any) PropHintFunc { + return func() (PropHintName, any) { + return name, value + } +} + +func PropHint[T any](p Prop, name PropHintName, defaultValue T) T { + rawValue, exists := p.hints[name] + if !exists { + return defaultValue + } + + value, ok := rawValue.(T) + if !ok { + return defaultValue + } + + return value +} diff --git a/internal/format/registry.go b/internal/format/registry.go new file mode 100644 index 0000000..bfe1e58 --- /dev/null +++ b/internal/format/registry.go @@ -0,0 +1,46 @@ +package format + +import ( + "io" + + "github.com/pkg/errors" +) + +type Format string + +type Registry map[Format]Writer + +var defaultRegistry = Registry{} + +var ErrUnknownFormat = errors.New("unknown format") + +func Write(format Format, writer io.Writer, hints Hints, data ...any) error { + formatWriter, exists := defaultRegistry[format] + if !exists { + return errors.WithStack(ErrUnknownFormat) + } + + if hints.OutputMode == "" { + hints.OutputMode = OutputModeCompact + } + + if err := formatWriter.Write(writer, hints, data...); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func Available() []Format { + formats := make([]Format, 0, len(defaultRegistry)) + + for f := range defaultRegistry { + formats = append(formats, f) + } + + return formats +} + +func Register(format Format, writer Writer) { + defaultRegistry[format] = writer +} diff --git a/internal/format/table/prop.go b/internal/format/table/prop.go new file mode 100644 index 0000000..26fc9e1 --- /dev/null +++ b/internal/format/table/prop.go @@ -0,0 +1,61 @@ +package table + +import ( + "encoding/json" + "fmt" + "reflect" + + "forge.cadoles.com/cadoles/bouncer/internal/format" + "github.com/pkg/errors" +) + +const ( + hintCompactModeMaxColumnWidth format.PropHintName = "compactModeMaxColumnWidth" +) + +func WithCompactModeMaxColumnWidth(max int) format.PropHintFunc { + return format.WithPropHint(hintCompactModeMaxColumnWidth, max) +} + +func getProps(d any) []format.Prop { + props := make([]format.Prop, 0) + + v := reflect.Indirect(reflect.ValueOf(d)) + typeOf := v.Type() + + for i := 0; i < v.NumField(); i++ { + name := typeOf.Field(i).Name + props = append(props, format.NewProp(name, name)) + } + + return props +} + +func getFieldValue(obj any, name string) string { + v := reflect.Indirect(reflect.ValueOf(obj)) + + fieldValue := v.FieldByName(name) + + if !fieldValue.IsValid() { + return "" + } + + switch fieldValue.Kind() { + case reflect.Map: + fallthrough + case reflect.Struct: + fallthrough + case reflect.Slice: + fallthrough + case reflect.Interface: + json, err := json.Marshal(fieldValue.Interface()) + if err != nil { + panic(errors.WithStack(err)) + } + + return string(json) + + default: + return fmt.Sprintf("%v", fieldValue) + } +} diff --git a/internal/format/table/writer.go b/internal/format/table/writer.go new file mode 100644 index 0000000..7575cae --- /dev/null +++ b/internal/format/table/writer.go @@ -0,0 +1,80 @@ +package table + +import ( + "io" + + "forge.cadoles.com/cadoles/bouncer/internal/format" + "github.com/jedib0t/go-pretty/v6/table" +) + +const Format format.Format = "table" + +const DefaultCompactModeMaxColumnWidth = 30 + +func init() { + format.Register(Format, NewWriter(DefaultCompactModeMaxColumnWidth)) +} + +type Writer struct { + compactModeMaxColumnWidth int +} + +// Write implements format.Writer. +func (w *Writer) Write(writer io.Writer, hints format.Hints, data ...any) error { + t := table.NewWriter() + + t.SetOutputMirror(writer) + + var props []format.Prop + + if hints.Props != nil { + props = hints.Props + } else { + if len(data) > 0 { + props = getProps(data[0]) + } else { + props = make([]format.Prop, 0) + } + } + + labels := table.Row{} + + for _, p := range props { + labels = append(labels, p.Label()) + } + + t.AppendHeader(labels) + + isCompactMode := hints.OutputMode == format.OutputModeCompact + + for _, d := range data { + row := table.Row{} + + for _, p := range props { + value := getFieldValue(d, p.Name()) + + compactModeMaxColumnWidth := format.PropHint(p, + hintCompactModeMaxColumnWidth, + w.compactModeMaxColumnWidth, + ) + + if isCompactMode && len(value) > compactModeMaxColumnWidth { + value = value[:compactModeMaxColumnWidth] + "..." + } + + row = append(row, value) + } + + t.AppendRow(row) + } + + t.Render() + + return nil +} + +func NewWriter(compactModeMaxColumnWidth int) *Writer { + return &Writer{compactModeMaxColumnWidth} +} + +var _ format.Writer = &Writer{} diff --git a/internal/format/table/writer_test.go b/internal/format/table/writer_test.go new file mode 100644 index 0000000..f91fac6 --- /dev/null +++ b/internal/format/table/writer_test.go @@ -0,0 +1,86 @@ +package table + +import ( + "bytes" + "strings" + "testing" + + "forge.cadoles.com/cadoles/bouncer/internal/format" + "github.com/pkg/errors" +) + +type dummyItem struct { + MyString string + MyInt int + MySub subItem +} + +type subItem struct { + MyBool bool +} + +var dummyItems = []any{ + dummyItem{ + MyString: "Foo", + MyInt: 1, + MySub: subItem{ + MyBool: false, + }, + }, + dummyItem{ + MyString: "Bar", + MyInt: 0, + MySub: subItem{ + MyBool: true, + }, + }, +} + +func TestWriterNoHints(t *testing.T) { + var buf bytes.Buffer + + writer := NewWriter(DefaultCompactModeMaxColumnWidth) + + if err := writer.Write(&buf, format.Hints{}, dummyItems...); err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + expected := `+----------+-------+------------------+ +| MYSTRING | MYINT | MYSUB | ++----------+-------+------------------+ +| Foo | 1 | {"MyBool":false} | +| Bar | 0 | {"MyBool":true} | ++----------+-------+------------------+` + + if e, g := strings.TrimSpace(expected), strings.TrimSpace(buf.String()); e != g { + t.Errorf("buf.String(): expected \n%v\ngot\n%v", e, g) + } +} + +func TestWriterWithPropHints(t *testing.T) { + var buf bytes.Buffer + + writer := NewWriter(DefaultCompactModeMaxColumnWidth) + + hints := format.Hints{ + Props: []format.Prop{ + format.NewProp("MyString", "MyString"), + format.NewProp("MyInt", "MyInt"), + }, + } + + if err := writer.Write(&buf, hints, dummyItems...); err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + expected := `+----------+-------+ +| MYSTRING | MYINT | ++----------+-------+ +| Foo | 1 | +| Bar | 0 | ++----------+-------+` + + if e, g := strings.TrimSpace(expected), strings.TrimSpace(buf.String()); e != g { + t.Errorf("buf.String(): expected \n%v\ngot\n%v", e, g) + } +} diff --git a/internal/format/writer.go b/internal/format/writer.go new file mode 100644 index 0000000..bfc214a --- /dev/null +++ b/internal/format/writer.go @@ -0,0 +1,19 @@ +package format + +import "io" + +type OutputMode string + +const ( + OutputModeWide OutputMode = "wide" + OutputModeCompact OutputMode = "compact" +) + +type Hints struct { + Props []Prop + OutputMode OutputMode +} + +type Writer interface { + Write(writer io.Writer, hints Hints, data ...any) error +} diff --git a/internal/imports/format/format_import.go b/internal/imports/format/format_import.go new file mode 100644 index 0000000..8e93391 --- /dev/null +++ b/internal/imports/format/format_import.go @@ -0,0 +1,6 @@ +package format + +import ( + _ "forge.cadoles.com/cadoles/bouncer/internal/format/json" + _ "forge.cadoles.com/cadoles/bouncer/internal/format/table" +) diff --git a/internal/jwk/jwk.go b/internal/jwk/jwk.go new file mode 100644 index 0000000..afa0bdf --- /dev/null +++ b/internal/jwk/jwk.go @@ -0,0 +1,140 @@ +package jwk + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/json" + "io/ioutil" + "os" + + "github.com/btcsuite/btcd/btcutil/base58" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jws" + + "github.com/pkg/errors" +) + +const DefaultKeySize = 2048 + +type ( + Key = jwk.Key + Set = jwk.Set + ParseOption = jwk.ParseOption +) + +var ( + FromRaw = jwk.FromRaw + NewSet = jwk.NewSet +) + +const AlgorithmKey = jwk.AlgorithmKey + +func Parse(src []byte, options ...jwk.ParseOption) (Set, error) { + return jwk.Parse(src, options...) +} + +func PublicKeySet(keys ...jwk.Key) (jwk.Set, error) { + set := jwk.NewSet() + + for _, k := range keys { + pubkey, err := k.PublicKey() + if err != nil { + return nil, errors.WithStack(err) + } + + if err := pubkey.Set(jwk.AlgorithmKey, jwa.RS256); err != nil { + return nil, errors.WithStack(err) + } + + if err := set.AddKey(pubkey); err != nil { + return nil, errors.WithStack(err) + } + } + + return set, nil +} + +func LoadOrGenerate(path string, size int) (jwk.Key, error) { + data, err := ioutil.ReadFile(path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, errors.WithStack(err) + } + + if errors.Is(err, os.ErrNotExist) { + key, err := Generate(size) + if err != nil { + return nil, errors.WithStack(err) + } + + data, err = json.Marshal(key) + if err != nil { + return nil, errors.WithStack(err) + } + + if err := ioutil.WriteFile(path, data, 0o640); err != nil { + return nil, errors.WithStack(err) + } + } + + key, err := jwk.ParseKey(data) + if err != nil { + return nil, errors.WithStack(err) + } + + return key, nil +} + +func Generate(size int) (jwk.Key, error) { + privKey, err := rsa.GenerateKey(rand.Reader, size) + if err != nil { + return nil, errors.WithStack(err) + } + + key, err := jwk.FromRaw(privKey) + if err != nil { + return nil, errors.WithStack(err) + } + + return key, nil +} + +func Sign(key jwk.Key, payload ...any) (string, error) { + json, err := json.Marshal(payload) + if err != nil { + return "", errors.WithStack(err) + } + + rawSignature, err := jws.Sign( + nil, + jws.WithKey(jwa.RS256, key), + jws.WithDetachedPayload(json), + ) + if err != nil { + return "", errors.WithStack(err) + } + + signature := base58.Encode(rawSignature) + + return signature, nil +} + +func Verify(jwks jwk.Set, signature string, payload ...any) (bool, error) { + json, err := json.Marshal(payload) + if err != nil { + return false, errors.WithStack(err) + } + + decoded := base58.Decode(signature) + + _, err = jws.Verify( + decoded, + jws.WithKeySet(jwks, jws.WithRequireKid(false)), + jws.WithDetachedPayload(json), + ) + if err != nil { + return false, errors.WithStack(err) + } + + return true, nil +} diff --git a/internal/jwk/jwk_test.go b/internal/jwk/jwk_test.go new file mode 100644 index 0000000..6a748e8 --- /dev/null +++ b/internal/jwk/jwk_test.go @@ -0,0 +1,40 @@ +package jwk + +import ( + "testing" + + "github.com/pkg/errors" +) + +func TestJWK(t *testing.T) { + privateKey, err := Generate(DefaultKeySize) + if err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + keySet, err := PublicKeySet(privateKey) + if err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + metadata := map[string]any{ + "Foo": "bar", + "Test": 1, + } + + signature, err := Sign(privateKey, metadata) + if err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + t.Logf("Signature: %s", signature) + + matches, err := Verify(keySet, signature, metadata) + if err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + if !matches { + t.Error("signature should match") + } +} diff --git a/internal/proxy/init.go b/internal/proxy/init.go new file mode 100644 index 0000000..ff4be33 --- /dev/null +++ b/internal/proxy/init.go @@ -0,0 +1,42 @@ +package proxy + +import ( + "context" + + "forge.cadoles.com/cadoles/bouncer/internal/setup" + "github.com/pkg/errors" +) + +func (s *Server) initRepositories(ctx context.Context) error { + if err := s.initQueueRepository(ctx); err != nil { + return errors.WithStack(err) + } + + if err := s.initProxyRepository(ctx); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (s *Server) initQueueRepository(ctx context.Context) error { + queueRepository, err := setup.NewQueueRepository(ctx, s.redisConfig) + if err != nil { + return errors.WithStack(err) + } + + s.queueRepository = queueRepository + + return nil +} + +func (s *Server) initProxyRepository(ctx context.Context) error { + proxyRepository, err := setup.NewProxyRepository(ctx, s.redisConfig) + if err != nil { + return errors.WithStack(err) + } + + s.proxyRepository = proxyRepository + + return nil +} diff --git a/internal/proxy/middleware/director/director.go b/internal/proxy/middleware/director/director.go new file mode 100644 index 0000000..78a4a0a --- /dev/null +++ b/internal/proxy/middleware/director/director.go @@ -0,0 +1,90 @@ +package director + +import ( + "context" + "net/http" + "net/url" + + "forge.cadoles.com/Cadoles/go-proxy" + "forge.cadoles.com/Cadoles/go-proxy/wildcard" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type Director struct { + repo store.ProxyRepository +} + +func (d *Director) rewriteRequest(r *http.Request) error { + ctx := r.Context() + + proxies, err := d.getProxies(ctx) + if err != nil { + return errors.WithStack(err) + } + + var match *url.URL + +MAIN: + for _, p := range proxies { + for _, from := range p.From { + if matches := wildcard.Match(r.Host, from); !matches { + continue + } + + match = p.To + break MAIN + } + } + + if match == nil { + return nil + } + + r.URL.Host = match.Host + r.URL.Scheme = match.Scheme + + return nil +} + +func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) { + headers, err := d.repo.QueryProxy(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + proxies := make([]*store.Proxy, len(headers)) + + for i, h := range headers { + proxy, err := d.repo.GetProxy(ctx, h.ID) + if err != nil { + return nil, errors.WithStack(err) + } + + proxies[i] = proxy + } + + return proxies, nil +} + +func (d *Director) Middleware() proxy.Middleware { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + if err := d.rewriteRequest(r); err != nil { + logger.Error(r.Context(), "could not rewrite request", logger.E(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + next.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} + +func New(repo store.ProxyRepository) *Director { + return &Director{repo} +} diff --git a/internal/proxy/option.go b/internal/proxy/option.go new file mode 100644 index 0000000..8f32e2f --- /dev/null +++ b/internal/proxy/option.go @@ -0,0 +1,31 @@ +package proxy + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/config" +) + +type Option struct { + ServerConfig config.ProxyServerConfig + RedisConfig config.RedisConfig +} + +type OptionFunc func(*Option) + +func defaultOption() *Option { + return &Option{ + ServerConfig: config.NewDefaultProxyServerConfig(), + RedisConfig: config.NewDefaultRedisConfig(), + } +} + +func WithServerConfig(conf config.ProxyServerConfig) OptionFunc { + return func(opt *Option) { + opt.ServerConfig = conf + } +} + +func WithRedisConfig(conf config.RedisConfig) OptionFunc { + return func(opt *Option) { + opt.RedisConfig = conf + } +} diff --git a/internal/proxy/server.go b/internal/proxy/server.go new file mode 100644 index 0000000..9928552 --- /dev/null +++ b/internal/proxy/server.go @@ -0,0 +1,110 @@ +package proxy + +import ( + "context" + "fmt" + "log" + "net" + "net/http" + + "forge.cadoles.com/Cadoles/go-proxy" + "forge.cadoles.com/cadoles/bouncer/internal/config" + "forge.cadoles.com/cadoles/bouncer/internal/proxy/middleware/director" + "forge.cadoles.com/cadoles/bouncer/internal/queue" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type Server struct { + serverConfig config.ProxyServerConfig + redisConfig config.RedisConfig + queueRepository queue.Repository + proxyRepository store.ProxyRepository +} + +func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) { + errs := make(chan error) + addrs := make(chan net.Addr) + + go s.run(ctx, addrs, errs) + + return addrs, errs +} + +func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan error) { + defer func() { + close(errs) + close(addrs) + }() + + ctx, cancel := context.WithCancel(parentCtx) + defer cancel() + + if err := s.initRepositories(ctx); err != nil { + errs <- errors.WithStack(err) + + return + } + + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.serverConfig.HTTP.Host, s.serverConfig.HTTP.Port)) + if err != nil { + errs <- errors.WithStack(err) + + return + } + + addrs <- listener.Addr() + + defer func() { + if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + errs <- errors.WithStack(err) + } + }() + + go func() { + <-ctx.Done() + + if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + log.Printf("%+v", errors.WithStack(err)) + } + }() + + router := chi.NewRouter() + + router.Use(middleware.Logger) + + logger.Info(ctx, "http server listening") + + queue := queue.New(s.queueRepository) + director := director.New(s.proxyRepository) + + handler := proxy.New( + proxy.WithMiddlewares( + director.Middleware(), + queue.Middleware(), + ), + ) + + router.Handle("/*", handler) + + if err := http.Serve(listener, router); err != nil && !errors.Is(err, net.ErrClosed) { + errs <- errors.WithStack(err) + } + + logger.Info(ctx, "http server exiting") +} + +func NewServer(funcs ...OptionFunc) *Server { + opt := defaultOption() + for _, fn := range funcs { + fn(opt) + } + + return &Server{ + serverConfig: opt.ServerConfig, + redisConfig: opt.RedisConfig, + } +} diff --git a/internal/queue/options.go b/internal/queue/options.go new file mode 100644 index 0000000..80426cb --- /dev/null +++ b/internal/queue/options.go @@ -0,0 +1,9 @@ +package queue + +type Options struct{} + +type OptionFunc func(*Options) + +func defaultOptions() *Options { + return &Options{} +} diff --git a/internal/queue/queue.go b/internal/queue/queue.go new file mode 100644 index 0000000..fc9ed78 --- /dev/null +++ b/internal/queue/queue.go @@ -0,0 +1,32 @@ +package queue + +import ( + "net/http" + + "forge.cadoles.com/Cadoles/go-proxy" +) + +type Queue struct { + repository Repository +} + +func (q *Queue) Middleware() proxy.Middleware { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} + +func New(repository Repository, funcs ...OptionFunc) *Queue { + opts := defaultOptions() + for _, fn := range funcs { + fn(opts) + } + + return &Queue{ + repository: repository, + } +} diff --git a/internal/queue/redis/repository.go b/internal/queue/redis/repository.go new file mode 100644 index 0000000..d157efa --- /dev/null +++ b/internal/queue/redis/repository.go @@ -0,0 +1,65 @@ +package redis + +import ( + "context" + + "forge.cadoles.com/cadoles/bouncer/internal/queue" + "github.com/redis/go-redis/v9" +) + +type Repository struct { + client redis.UniversalClient +} + +// GetQueue implements queue.Repository +func (*Repository) GetQueue(ctx context.Context, name string) (int, int, error) { + panic("unimplemented") +} + +// CreateQueue implements queue.Repository +func (*Repository) CreateQueue(ctx context.Context, name string, capacity int) error { + panic("unimplemented") +} + +// CreateToken implements queue.Repository +func (*Repository) CreateToken(ctx context.Context, name string) (string, int, error) { + panic("unimplemented") +} + +// DeleteQueue implements queue.Repository +func (*Repository) DeleteQueue(ctx context.Context, name string) error { + panic("unimplemented") +} + +// GetTokenPosition implements queue.Repository +func (*Repository) GetTokenPosition(ctx context.Context, name string, token string) (int, int, error) { + panic("unimplemented") +} + +// RefreshQueue implements queue.Repository +func (*Repository) RefreshQueue(ctx context.Context, name string) (int, int, error) { + panic("unimplemented") +} + +// RemoveToken implements queue.Repository +func (*Repository) RemoveToken(ctx context.Context, name string, token string) error { + panic("unimplemented") +} + +// TouchToken implements queue.Repository +func (*Repository) TouchToken(ctx context.Context, name string, token string) (int, error) { + panic("unimplemented") +} + +// UpdateQueue implements queue.Repository +func (*Repository) UpdateQueue(ctx context.Context, name string, capacity int) error { + panic("unimplemented") +} + +func NewRepository(client redis.UniversalClient) *Repository { + return &Repository{ + client: client, + } +} + +var _ queue.Repository = &Repository{} diff --git a/internal/queue/repository.go b/internal/queue/repository.go new file mode 100644 index 0000000..1adc9c2 --- /dev/null +++ b/internal/queue/repository.go @@ -0,0 +1,16 @@ +package queue + +import "context" + +type Repository interface { + CreateQueue(ctx context.Context, name string, capacity int) error + GetQueue(ctx context.Context, name string) (int, int, error) + UpdateQueue(ctx context.Context, name string, capacity int) error + DeleteQueue(ctx context.Context, name string) error + RefreshQueue(ctx context.Context, name string) (int, int, error) + + CreateToken(ctx context.Context, name string) (string, int, error) + GetTokenPosition(ctx context.Context, name string, token string) (int, int, error) + TouchToken(ctx context.Context, name string, token string) (int, error) + RemoveToken(ctx context.Context, name string, token string) error +} diff --git a/internal/setup/proxy_repository.go b/internal/setup/proxy_repository.go new file mode 100644 index 0000000..497dc8e --- /dev/null +++ b/internal/setup/proxy_repository.go @@ -0,0 +1,19 @@ +package setup + +import ( + "context" + + "forge.cadoles.com/cadoles/bouncer/internal/config" + "forge.cadoles.com/cadoles/bouncer/internal/store" + redisStore "forge.cadoles.com/cadoles/bouncer/internal/store/redis" + "github.com/redis/go-redis/v9" +) + +func NewProxyRepository(ctx context.Context, conf config.RedisConfig) (store.ProxyRepository, error) { + rdb := redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: conf.Adresses, + MasterName: string(conf.Master), + }) + + return redisStore.NewProxyRepository(rdb), nil +} diff --git a/internal/setup/queue_repository.go b/internal/setup/queue_repository.go new file mode 100644 index 0000000..157ca45 --- /dev/null +++ b/internal/setup/queue_repository.go @@ -0,0 +1,20 @@ +package setup + +import ( + "context" + + "forge.cadoles.com/cadoles/bouncer/internal/config" + "forge.cadoles.com/cadoles/bouncer/internal/queue" + "github.com/redis/go-redis/v9" + + queueRedis "forge.cadoles.com/cadoles/bouncer/internal/queue/redis" +) + +func NewQueueRepository(ctx context.Context, conf config.RedisConfig) (queue.Repository, error) { + rdb := redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: conf.Adresses, + MasterName: string(conf.Master), + }) + + return queueRedis.NewRepository(rdb), nil +} diff --git a/internal/store/error.go b/internal/store/error.go new file mode 100644 index 0000000..7ac4f93 --- /dev/null +++ b/internal/store/error.go @@ -0,0 +1,5 @@ +package store + +import "errors" + +var ErrNotFound = errors.New("not found") diff --git a/internal/store/id.go b/internal/store/id.go new file mode 100644 index 0000000..f74fe2c --- /dev/null +++ b/internal/store/id.go @@ -0,0 +1,19 @@ +package store + +import ( + "github.com/google/uuid" + "github.com/pkg/errors" +) + +func NewID() string { + return uuid.NewString() +} + +func ParseID[T ~string](raw string) (T, error) { + uuid, err := uuid.Parse(raw) + if err != nil { + return *new(T), errors.WithStack(err) + } + + return T(uuid.String()), nil +} diff --git a/internal/store/proxy.go b/internal/store/proxy.go new file mode 100644 index 0000000..9f53911 --- /dev/null +++ b/internal/store/proxy.go @@ -0,0 +1,27 @@ +package store + +import ( + "net/url" + "time" +) + +type ProxyID string + +func NewProxyID() ProxyID { + return ProxyID(NewID()) +} + +var ParseProxyID = ParseID[ProxyID] + +type ProxyHeader struct { + ID ProxyID + CreatedAt time.Time + UpdatedAt time.Time +} + +type Proxy struct { + ProxyHeader + To *url.URL + From []string + Weight int +} diff --git a/internal/store/proxy_repository.go b/internal/store/proxy_repository.go new file mode 100644 index 0000000..a921d15 --- /dev/null +++ b/internal/store/proxy_repository.go @@ -0,0 +1,73 @@ +package store + +import ( + "context" + "net/url" +) + +type ProxyRepository interface { + CreateProxy(ctx context.Context, to *url.URL, from ...string) (*Proxy, error) + UpdateProxy(ctx context.Context, id ProxyID, funcs ...UpdateProxyOptionFunc) (*Proxy, error) + QueryProxy(ctx context.Context, funcs ...QueryProxyOptionFunc) ([]*ProxyHeader, error) + GetProxy(ctx context.Context, id ProxyID) (*Proxy, error) + DeleteProxy(ctx context.Context, id ProxyID) error +} + +type UpdateProxyOptionFunc func(*UpdateProxyOptions) + +type UpdateProxyOptions struct { + To *url.URL + From []string +} + +func WithProxyUpdateTo(to *url.URL) UpdateProxyOptionFunc { + return func(o *UpdateProxyOptions) { + o.To = to + } +} + +func WithProxyUpdateFrom(from ...string) UpdateProxyOptionFunc { + return func(o *UpdateProxyOptions) { + o.From = from + } +} + +type QueryProxyOptionFunc func(*QueryProxyOptions) + +type QueryProxyOptions struct { + To *url.URL + IDs []ProxyID + From []string + Offset *int + Limit *int +} + +func WithProxyQueryOffset(offset int) QueryProxyOptionFunc { + return func(o *QueryProxyOptions) { + o.Offset = &offset + } +} + +func WithProxyQueryLimit(limit int) QueryProxyOptionFunc { + return func(o *QueryProxyOptions) { + o.Limit = &limit + } +} + +func WithProxyQueryTo(to *url.URL) QueryProxyOptionFunc { + return func(o *QueryProxyOptions) { + o.To = to + } +} + +func WithProxyQueryFrom(from ...string) QueryProxyOptionFunc { + return func(o *QueryProxyOptions) { + o.From = from + } +} + +func WithProxyQueryIDs(ids ...ProxyID) QueryProxyOptionFunc { + return func(o *QueryProxyOptions) { + o.IDs = ids + } +} diff --git a/internal/store/redis/proxy_repository.go b/internal/store/redis/proxy_repository.go new file mode 100644 index 0000000..12d5a7c --- /dev/null +++ b/internal/store/redis/proxy_repository.go @@ -0,0 +1,253 @@ +package redis + +import ( + "context" + "encoding/json" + "net/url" + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +const ( + keyID = "id" + keyFrom = "from" + keyTo = "to" + keyUpdatedAt = "updated_at" + keyCreatedAt = "created_at" + keyWeight = "weight" + keyPrefixProxy = "proxy:" +) + +type ProxyRepository struct { + client redis.UniversalClient +} + +// GetProxy implements store.ProxyRepository +func (r *ProxyRepository) GetProxy(ctx context.Context, id store.ProxyID) (*store.Proxy, error) { + var proxy store.Proxy + + key := proxyKey(id) + + cmd := r.client.HMGet(ctx, key, keyFrom, keyTo, keyWeight, keyCreatedAt, keyUpdatedAt) + + values, err := cmd.Result() + if err != nil { + return nil, errors.WithStack(err) + } + + if allNilValues(values) { + return nil, errors.WithStack(store.ErrNotFound) + } + + proxy.ID = id + + from, err := unwrap[[]string](values[0]) + if err != nil { + return nil, errors.WithStack(err) + } + + proxy.From = from + + rawTo, ok := values[1].(string) + if !ok { + return nil, errors.Errorf("unexpected 'to' value of type '%T'", values[1]) + } + + to, err := url.Parse(rawTo) + if err != nil { + return nil, errors.WithStack(err) + } + + proxy.To = to + + weight, err := unwrap[int](values[2]) + if err != nil { + return nil, errors.WithStack(err) + } + + proxy.Weight = weight + + createdAt, err := unwrap[time.Time](values[3]) + if err != nil { + return nil, errors.WithStack(err) + } + + proxy.CreatedAt = createdAt + + updatedAt, err := unwrap[time.Time](values[4]) + if err != nil { + return nil, errors.WithStack(err) + } + + proxy.UpdatedAt = updatedAt + + return &proxy, nil +} + +// CreateProxy implements store.ProxyRepository +func (r *ProxyRepository) CreateProxy(ctx context.Context, to *url.URL, from ...string) (*store.Proxy, error) { + id := store.NewProxyID() + now := time.Now().UTC() + + _, err := r.client.Pipelined(ctx, func(p redis.Pipeliner) error { + key := proxyKey(id) + + p.HMSet(ctx, key, keyID, string(id)) + p.HMSet(ctx, key, keyFrom, wrap(from)) + p.HMSet(ctx, key, keyTo, to.String()) + p.HMSet(ctx, key, keyWeight, wrap(0)) + p.HMSet(ctx, key, keyCreatedAt, wrap(now)) + p.HMSet(ctx, key, keyUpdatedAt, wrap(now)) + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + return &store.Proxy{ + ProxyHeader: store.ProxyHeader{ + ID: id, + CreatedAt: now, + UpdatedAt: now, + }, + To: to, + From: from, + }, nil +} + +// DeleteProxy implements store.ProxyRepository +func (r *ProxyRepository) DeleteProxy(ctx context.Context, id store.ProxyID) error { + key := proxyKey(id) + + if cmd := r.client.Del(ctx, key); cmd.Err() != nil { + return errors.WithStack(cmd.Err()) + } + + return nil +} + +// QueryProxy implements store.ProxyRepository +func (r *ProxyRepository) QueryProxy(ctx context.Context, funcs ...store.QueryProxyOptionFunc) ([]*store.ProxyHeader, error) { + iter := r.client.Scan(ctx, 0, keyPrefixProxy+"*", 0).Iterator() + + headers := make([]*store.ProxyHeader, 0) + + for iter.Next(ctx) { + key := iter.Val() + + cmd := r.client.HMGet(ctx, key, keyID, keyCreatedAt, keyUpdatedAt) + + values, err := cmd.Result() + if err != nil { + return nil, errors.WithStack(err) + } + + if allNilValues(values) { + continue + } + + id, ok := values[0].(string) + if !ok { + return nil, errors.Errorf("unexpected 'id' field value for key '%s': '%s'", key, values[0]) + } + + createdAt, err := unwrap[time.Time](values[1]) + if err != nil { + return nil, errors.WithStack(err) + } + + updatedAt, err := unwrap[time.Time](values[2]) + if err != nil { + return nil, errors.WithStack(err) + } + + h := &store.ProxyHeader{ + ID: store.ProxyID(id), + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + headers = append(headers, h) + } + + if err := iter.Err(); err != nil { + return nil, errors.WithStack(err) + } + + return headers, nil +} + +// UpdateProxy implements store.ProxyRepository +func (*ProxyRepository) UpdateProxy(ctx context.Context, id store.ProxyID, funcs ...store.UpdateProxyOptionFunc) (*store.Proxy, error) { + panic("unimplemented") +} + +func NewProxyRepository(client redis.UniversalClient) *ProxyRepository { + return &ProxyRepository{ + client: client, + } +} + +var _ store.ProxyRepository = &ProxyRepository{} + +func proxyKey(id store.ProxyID) string { + return keyPrefixProxy + string(id) +} + +type jsonWrapper[T any] struct { + value T +} + +func (w *jsonWrapper[T]) MarshalBinary() ([]byte, error) { + data, err := json.Marshal(w.value) + if err != nil { + return nil, errors.WithStack(err) + } + + return data, nil +} + +func (w *jsonWrapper[T]) UnmarshalBinary(data []byte) error { + if err := json.Unmarshal(data, &w.value); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (w *jsonWrapper[T]) Value() T { + return w.value +} + +func wrap[T any](v T) *jsonWrapper[T] { + return &jsonWrapper[T]{v} +} + +func unwrap[T any](v any) (T, error) { + str, ok := v.(string) + if !ok { + return *new(T), errors.Errorf("could not unwrap value of type '%T'", v) + } + + u := new(T) + + if err := json.Unmarshal([]byte(str), u); err != nil { + return *new(T), errors.WithStack(err) + } + + return *u, nil +} + +func allNilValues(values []any) bool { + for _, v := range values { + if v != nil { + return false + } + } + + return true +} diff --git a/internal/store/redis/proxy_repository_test.go b/internal/store/redis/proxy_repository_test.go new file mode 100644 index 0000000..c2232d5 --- /dev/null +++ b/internal/store/redis/proxy_repository_test.go @@ -0,0 +1,64 @@ +package redis + +import ( + "context" + "log" + "os" + "testing" + + "forge.cadoles.com/cadoles/bouncer/internal/store/testsuite" + "github.com/ory/dockertest/v3" + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +var client redis.UniversalClient + +func TestMain(m *testing.M) { + // uses a sensible default on windows (tcp/http) and linux/osx (socket) + pool, err := dockertest.NewPool("") + if err != nil { + log.Fatalf("%+v", errors.WithStack(err)) + } + + // uses pool to try to connect to Docker + err = pool.Client.Ping() + if err != nil { + log.Fatalf("%+v", errors.WithStack(err)) + } + + // pulls an image, creates a container based on it and runs it + resource, err := pool.Run("redis", "alpine3.17", []string{}) + if err != nil { + log.Fatalf("%+v", errors.WithStack(err)) + } + + if err := pool.Retry(func() error { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: []string{resource.GetHostPort("6379/tcp")}, + }) + + ctx := context.Background() + + if cmd := client.Ping(ctx); cmd.Err() != nil { + return errors.WithStack(err) + } + + return nil + }); err != nil { + log.Fatalf("%+v", errors.WithStack(err)) + } + + code := m.Run() + + if err := pool.Purge(resource); err != nil { + log.Fatalf("%+v", errors.WithStack(err)) + } + + os.Exit(code) +} + +func TestProxyRepository(t *testing.T) { + repository := NewProxyRepository(client) + testsuite.TestProxyRepository(t, repository) +} diff --git a/internal/store/testsuite/proxy_repository.go b/internal/store/testsuite/proxy_repository.go new file mode 100644 index 0000000..8d79c5f --- /dev/null +++ b/internal/store/testsuite/proxy_repository.go @@ -0,0 +1,191 @@ +package testsuite + +import ( + "context" + "net/url" + "reflect" + "testing" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +type proxyRepositoryTestCase struct { + Name string + Do func(repo store.ProxyRepository) error +} + +var proxyRepositoryTestCases = []proxyRepositoryTestCase{ + { + Name: "Create proxy", + Do: func(repo store.ProxyRepository) error { + ctx := context.Background() + + url, err := url.Parse("http://example.com") + if err != nil { + return errors.WithStack(err) + } + + proxy, err := repo.CreateProxy(ctx, url, "*:*") + if err != nil { + return errors.WithStack(err) + } + + if proxy == nil { + return errors.Errorf("proxy should not be nil") + } + + if proxy.ID == "" { + return errors.Errorf("proxy.ID should not be empty") + } + + if proxy.To == nil { + return errors.Errorf("proxy.To should not be nil") + } + + if e, g := url.String(), proxy.To.String(); e != g { + return errors.Errorf("proxy.URL.String(): expected '%v', got '%v'", url.String(), proxy.To.String()) + } + + if proxy.CreatedAt.IsZero() { + return errors.Errorf("proxy.CreatedAt should not be zero value") + } + + if proxy.UpdatedAt.IsZero() { + return errors.Errorf("proxy.UpdatedAt should not be zero value") + } + + return nil + }, + }, + { + Name: "Create then get proxy", + Do: func(repo store.ProxyRepository) error { + ctx := context.Background() + + url, err := url.Parse("http://example.com") + if err != nil { + return errors.WithStack(err) + } + + createdProxy, err := repo.CreateProxy(ctx, url, "127.0.0.1:*", "localhost:*") + if err != nil { + return errors.WithStack(err) + } + + foundProxy, err := repo.GetProxy(ctx, createdProxy.ID) + if err != nil { + return errors.WithStack(err) + } + + if e, g := createdProxy.ID, foundProxy.ID; e != g { + return errors.Errorf("foundProxy.ID: expected '%v', got '%v'", createdProxy.ID, foundProxy.ID) + } + + if e, g := createdProxy.From, foundProxy.From; !reflect.DeepEqual(e, g) { + return errors.Errorf("foundProxy.From: expected '%v', got '%v'", createdProxy.From, foundProxy.From) + } + + if e, g := createdProxy.To.String(), foundProxy.To.String(); e != g { + return errors.Errorf("foundProxy.To: expected '%v', got '%v'", createdProxy.To, foundProxy.To) + } + + if e, g := createdProxy.CreatedAt, foundProxy.CreatedAt; e != g { + return errors.Errorf("foundProxy.CreatedAt: expected '%v', got '%v'", createdProxy.CreatedAt, foundProxy.CreatedAt) + } + + if e, g := createdProxy.UpdatedAt, foundProxy.UpdatedAt; e != g { + return errors.Errorf("foundProxy.UpdatedAt: expected '%v', got '%v'", createdProxy.UpdatedAt, foundProxy.UpdatedAt) + } + + return nil + }, + }, + { + Name: "Create then delete proxy", + Do: func(repo store.ProxyRepository) error { + ctx := context.Background() + + url, err := url.Parse("http://example.com") + if err != nil { + return errors.WithStack(err) + } + + createdProxy, err := repo.CreateProxy(ctx, url, "127.0.0.1:*", "localhost:*") + if err != nil { + return errors.WithStack(err) + } + + if err := repo.DeleteProxy(ctx, createdProxy.ID); err != nil { + return errors.WithStack(err) + } + + foundProxy, err := repo.GetProxy(ctx, createdProxy.ID) + if err == nil { + return errors.New("err should not be nil") + } + + if !errors.Is(err, store.ErrNotFound) { + return errors.Errorf("err should be store.ErrNotFound, got '%+v'", err) + } + + if foundProxy != nil { + return errors.Errorf("foundProxy should be nil, got '%v'", foundProxy) + } + + return nil + }, + }, + { + Name: "Create then query", + Do: func(repo store.ProxyRepository) error { + ctx := context.Background() + + url, err := url.Parse("http://example.com") + if err != nil { + return errors.WithStack(err) + } + + createdProxy, err := repo.CreateProxy(ctx, url, "127.0.0.1:*", "localhost:*") + if err != nil { + return errors.WithStack(err) + } + + headers, err := repo.QueryProxy(ctx) + if err != nil { + return errors.WithStack(err) + } + + if len(headers) < 1 { + return errors.Errorf("len(headers): expected value > 1, got '%v'", len(headers)) + } + + found := false + + for _, h := range headers { + if h.ID == createdProxy.ID { + found = true + break + } + } + + if !found { + return errors.New("could not find created proxy in query results") + } + + return nil + }, + }, +} + +func TestProxyRepository(t *testing.T, repo store.ProxyRepository) { + for _, tc := range proxyRepositoryTestCases { + func(tc proxyRepositoryTestCase) { + t.Run(tc.Name, func(t *testing.T) { + if err := tc.Do(repo); err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + }) + }(tc) + } +} diff --git a/misc/jenkins/Dockerfile b/misc/jenkins/Dockerfile new file mode 100644 index 0000000..39f5646 --- /dev/null +++ b/misc/jenkins/Dockerfile @@ -0,0 +1,24 @@ +FROM reg.cadoles.com/proxy_cache/library/ubuntu:22.04 + +ARG HTTP_PROXY= +ARG HTTPS_PROXY= +ARG http_proxy= +ARG https_proxy= +ARG GO_VERSION=1.20.2 + +# Install dev environment dependencies +RUN export DEBIAN_FRONTEND=noninteractive &&\ + apt-get update -y &&\ + apt-get install -y --no-install-recommends curl ca-certificates build-essential wget unzip tar git jq + +# Install Go +RUN mkdir -p /tmp \ + && wget -O /tmp/go${GO_VERSION}.linux-amd64.tar.gz https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz \ + && rm -rf /usr/local/go \ + && mkdir -p /usr/local \ + && tar -C /usr/local -xzf /tmp/go${GO_VERSION}.linux-amd64.tar.gz + +ENV PATH="${PATH}:/usr/local/go/bin" + +# Add LetsEncrypt certificates +RUN curl -k https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/common/add-letsencrypt-ca.sh | bash \ No newline at end of file diff --git a/misc/logo/bouncer.svg b/misc/logo/bouncer.svg new file mode 100644 index 0000000..34dc8d2 --- /dev/null +++ b/misc/logo/bouncer.svg @@ -0,0 +1,39 @@ + + + + diff --git a/misc/packaging/common/config.yml b/misc/packaging/common/config.yml new file mode 100644 index 0000000..0f0a40d --- /dev/null +++ b/misc/packaging/common/config.yml @@ -0,0 +1,33 @@ +admin: + http: + host: 127.0.0.1 + port: 8081 + cors: + allowedOrigins: + - http://localhost:3001 + allowCredentials: true + allowMethods: + - POST + - GET + - PUT + - DELETE + allowedHeaders: + - Origin + - Accept + - Content-Type + - Authorization + - Sentry-Trace + debug: false + auth: + issuer: http://127.0.0.1:8081 + privateKey: admin-key.json +proxy: + http: + host: 0.0.0.0 + port: 8080 +database: + driver: sqlite + dsn: sqlite://bouncer.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=10000 +logger: + level: 1 + format: human \ No newline at end of file diff --git a/misc/packaging/common/postinstall-bouncer-admin.sh b/misc/packaging/common/postinstall-bouncer-admin.sh new file mode 100644 index 0000000..2e04d97 --- /dev/null +++ b/misc/packaging/common/postinstall-bouncer-admin.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +use_systemctl="True" +systemd_version=0 +if ! command -V systemctl >/dev/null 2>&1; then + use_systemctl="False" +else + systemd_version=$(systemctl --version | head -1 | cut -d ' ' -f 2) +fi + +service_name=bouncer-admin + +cleanup() { + if [ "${use_systemctl}" = "False" ]; then + rm -f /usr/lib/systemd/system/${service_name}.service + else + rm -f /etc/chkconfig/${service_name} + rm -f /etc/init.d/${service_name} + fi +} + +cleanInstall() { + printf "\033[32m Post Install of an clean install\033[0m\n" + if [ "${use_systemctl}" = "False" ]; then + if command -V chkconfig >/dev/null 2>&1; then + chkconfig --add ${service_name} + fi + + service ${service_name} restart || : + else + if [[ "${systemd_version}" -lt 231 ]]; then + printf "\033[31m systemd version %s is less then 231, fixing the service file \033[0m\n" "${systemd_version}" + sed -i "s/=+/=/g" /usr/lib/systemd/system/${service_name}.service + fi + printf "\033[32m Reload the service unit from disk\033[0m\n" + systemctl daemon-reload || : + printf "\033[32m Unmask the service\033[0m\n" + systemctl unmask ${service_name} || : + printf "\033[32m Set the preset flag for the service unit\033[0m\n" + systemctl preset ${service_name} || : + printf "\033[32m Set the enabled flag for the service unit\033[0m\n" + systemctl enable ${service_name} || : + systemctl restart ${service_name} || : + fi +} + +upgrade() { + printf "\033[32m Post Install of an upgrade\033[0m\n" +} + +# Step 2, check if this is a clean install or an upgrade +action="$1" +if [ "$1" = "configure" ] && [ -z "$2" ]; then + action="install" +elif [ "$1" = "configure" ] && [ -n "$2" ]; then + action="upgrade" +fi + +case "$action" in +"1" | "install") + cleanInstall + ;; +"2" | "upgrade") + printf "\033[32m Post Install of an upgrade\033[0m\n" + upgrade + ;; +*) + printf "\033[32m Alpine\033[0m" + cleanInstall + ;; +esac + +cleanup diff --git a/misc/packaging/common/postinstall-bouncer-proxy.sh b/misc/packaging/common/postinstall-bouncer-proxy.sh new file mode 100644 index 0000000..de03a0a --- /dev/null +++ b/misc/packaging/common/postinstall-bouncer-proxy.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +use_systemctl="True" +systemd_version=0 +if ! command -V systemctl >/dev/null 2>&1; then + use_systemctl="False" +else + systemd_version=$(systemctl --version | head -1 | cut -d ' ' -f 2) +fi + +service_name=bouncer-proxy + +cleanup() { + if [ "${use_systemctl}" = "False" ]; then + rm -f /usr/lib/systemd/system/${service_name}.service + else + rm -f /etc/chkconfig/${service_name} + rm -f /etc/init.d/${service_name} + fi +} + +cleanInstall() { + printf "\033[32m Post Install of an clean install\033[0m\n" + if [ "${use_systemctl}" = "False" ]; then + if command -V chkconfig >/dev/null 2>&1; then + chkconfig --add ${service_name} + fi + + service ${service_name} restart || : + else + if [[ "${systemd_version}" -lt 231 ]]; then + printf "\033[31m systemd version %s is less then 231, fixing the service file \033[0m\n" "${systemd_version}" + sed -i "s/=+/=/g" /usr/lib/systemd/system/${service_name}.service + fi + printf "\033[32m Reload the service unit from disk\033[0m\n" + systemctl daemon-reload || : + printf "\033[32m Unmask the service\033[0m\n" + systemctl unmask ${service_name} || : + printf "\033[32m Set the preset flag for the service unit\033[0m\n" + systemctl preset ${service_name} || : + printf "\033[32m Set the enabled flag for the service unit\033[0m\n" + systemctl enable ${service_name} || : + systemctl restart ${service_name} || : + fi +} + +upgrade() { + printf "\033[32m Post Install of an upgrade\033[0m\n" +} + +# Step 2, check if this is a clean install or an upgrade +action="$1" +if [ "$1" = "configure" ] && [ -z "$2" ]; then + action="install" +elif [ "$1" = "configure" ] && [ -n "$2" ]; then + action="upgrade" +fi + +case "$action" in +"1" | "install") + cleanInstall + ;; +"2" | "upgrade") + printf "\033[32m Post Install of an upgrade\033[0m\n" + upgrade + ;; +*) + printf "\033[32m Alpine\033[0m" + cleanInstall + ;; +esac + +cleanup diff --git a/misc/packaging/openrc/bouncer-admin.openrc.sh b/misc/packaging/openrc/bouncer-admin.openrc.sh new file mode 100644 index 0000000..6066be7 --- /dev/null +++ b/misc/packaging/openrc/bouncer-admin.openrc.sh @@ -0,0 +1,15 @@ +#!/sbin/openrc-run + +command="/usr/bin/bouncer" +command_args="--workdir /usr/share/bouncer --config /etc/bouncer/config.yml admin run" +supervisor=supervise-daemon +output_log="/var/log/bouncer/admin.log" +error_log="$output_log" + +start_pre() { + /usr/bin/bouncer --workdir /usr/share/bouncer --config /etc/bouncer/config.yml database migrate +} + +depend() { + need net +} \ No newline at end of file diff --git a/misc/packaging/openrc/bouncer-proxy.openrc.sh b/misc/packaging/openrc/bouncer-proxy.openrc.sh new file mode 100644 index 0000000..6e26981 --- /dev/null +++ b/misc/packaging/openrc/bouncer-proxy.openrc.sh @@ -0,0 +1,15 @@ +#!/sbin/openrc-run + +command="/usr/bin/bouncer" +command_args="--workdir /usr/share/bouncer --config /etc/bouncer/config.yml proxy run" +supervisor=supervise-daemon +output_log="/var/log/bouncer/proxy.log" +error_log="$output_log" + +start_pre() { + /usr/bin/bouncer --workdir /usr/share/bouncer --config /etc/bouncer/config.yml database migrate +} + +depend() { + need net +} \ No newline at end of file diff --git a/misc/packaging/systemd/bouncer-admin.systemd.service b/misc/packaging/systemd/bouncer-admin.systemd.service new file mode 100644 index 0000000..e34831a --- /dev/null +++ b/misc/packaging/systemd/bouncer-admin.systemd.service @@ -0,0 +1,12 @@ +[Unit] +Description=bouncer admin service +After=network.target + +[Service] +Type=simple +Restart=always +WorkingDirectory=/usr/share/bouncer +ExecStart=/usr/bin/bouncer --config /etc/bouncer/config.yml admin run + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/misc/packaging/systemd/bouncer-proxy.systemd.service b/misc/packaging/systemd/bouncer-proxy.systemd.service new file mode 100644 index 0000000..7664da2 --- /dev/null +++ b/misc/packaging/systemd/bouncer-proxy.systemd.service @@ -0,0 +1,12 @@ +[Unit] +Description=bouncer proxy service +After=network.target + +[Service] +Type=simple +Restart=always +WorkingDirectory=/usr/share/bouncer +ExecStart=/usr/bin/bouncer --config /etc/bouncer/config.yml proxy run + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/modd.conf b/modd.conf new file mode 100644 index 0000000..d7662b0 --- /dev/null +++ b/modd.conf @@ -0,0 +1,12 @@ +**/*.go +modd.conf +config.yml +.env { + prep: make RUN_INSTALL_TESTS=no GOTEST_ARGS="-short" test + prep: make build-bouncer + prep: make config.yml + prep: make .bouncer-token + daemon: make run BOUNCER_CMD="--config config.yml admin run" + daemon: make run BOUNCER_CMD="--config config.yml proxy run" + daemon +sigint: make run-redis +} \ No newline at end of file