From e66938f1d3bc2b52c83c928f8ace1e582e12d3e6 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 | 136 +++ Dockerfile | 30 + Jenkinsfile | 60 ++ Makefile | 142 +++ README.md | 15 + cmd/bouncer/main.go | 31 + commitlint.config.js | 1 + doc/README.md | 13 + go.mod | 92 ++ go.sum | 903 ++++++++++++++++++ internal/admin/authz.go | 101 ++ internal/admin/error.go | 31 + internal/admin/init.go | 42 + internal/admin/layer_route.go | 302 ++++++ internal/admin/option.go | 31 + internal/admin/proxy_route.go | 312 ++++++ internal/admin/server.go | 145 +++ internal/auth/jwt/authenticator.go | 72 ++ internal/auth/jwt/jwt.go | 62 ++ internal/auth/jwt/user.go | 32 + internal/auth/middleware.go | 79 ++ internal/chi/log_formatter.go | 53 + internal/client/client.go | 144 +++ internal/client/create_layer.go | 32 + internal/client/create_proxy.go | 30 + internal/client/delete_layer.go | 26 + internal/client/delete_proxy.go | 26 + internal/client/get_layer.go | 26 + internal/client/get_proxy.go | 26 + internal/client/options.go | 24 + internal/client/query_layer.go | 75 ++ internal/client/query_proxy.go | 75 ++ internal/client/update_layer.go | 28 + internal/client/update_proxy.go | 28 + internal/command/admin/apierr/wrap.go | 91 ++ internal/command/admin/flag/flag.go | 98 ++ internal/command/admin/flag/util.go | 11 + internal/command/admin/layer/create.go | 72 ++ internal/command/admin/layer/delete.go | 62 ++ internal/command/admin/layer/flag/flag.go | 76 ++ internal/command/admin/layer/get.go | 55 ++ internal/command/admin/layer/query.go | 69 ++ internal/command/admin/layer/root.go | 19 + internal/command/admin/layer/update.go | 91 ++ internal/command/admin/layer/util.go | 33 + internal/command/admin/proxy/create.go | 69 ++ internal/command/admin/proxy/delete.go | 56 ++ internal/command/admin/proxy/flag/flag.go | 36 + internal/command/admin/proxy/get.go | 49 + internal/command/admin/proxy/query.go | 63 ++ internal/command/admin/proxy/root.go | 19 + internal/command/admin/proxy/update.go | 93 ++ internal/command/admin/proxy/util.go | 32 + internal/command/admin/root.go | 18 + internal/command/auth/create_token.go | 54 ++ internal/command/auth/root.go | 15 + 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/server/admin/root.go | 15 + internal/command/server/admin/run.go | 54 ++ internal/command/server/proxy/root.go | 15 + internal/command/server/proxy/run.go | 87 ++ internal/command/server/root.go | 18 + 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/director/context.go | 60 ++ internal/proxy/director/director.go | 231 +++++ internal/proxy/director/layer_registry.go | 104 ++ internal/proxy/director/util.go | 18 + internal/proxy/init.go | 42 + internal/proxy/option.go | 40 + internal/proxy/server.go | 118 +++ internal/queue/adapter.go | 16 + internal/queue/layer_options.go | 48 + internal/queue/options.go | 9 + internal/queue/queue.go | 143 +++ internal/queue/redis/adapter.go | 167 ++++ internal/queue/schema.go | 20 + internal/queue/schema/layer-options.json | 21 + internal/schema/error.go | 33 + internal/schema/load.go | 17 + internal/schema/registry.go | 56 ++ internal/setup/proxy_repository.go | 28 + internal/setup/queue_repository.go | 20 + internal/store/error.go | 8 + internal/store/layer.go | 25 + internal/store/layer_repository.go | 78 ++ internal/store/name.go | 14 + internal/store/proxy.go | 22 + internal/store/proxy_repository.go | 91 ++ internal/store/redis/helper.go | 93 ++ internal/store/redis/layer_item.go | 62 ++ internal/store/redis/layer_repository.go | 256 +++++ internal/store/redis/layer_repository_test.go | 12 + internal/store/redis/main_test.go | 58 ++ internal/store/redis/proxy_item.go | 60 ++ internal/store/redis/proxy_repository.go | 254 +++++ internal/store/redis/proxy_repository_test.go | 12 + internal/store/sort.go | 13 + internal/store/testsuite/layer_repository.go | 66 ++ internal/store/testsuite/proxy_repository.go | 204 ++++ misc/jenkins/Dockerfile | 25 + 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 | 16 + 134 files changed, 8507 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/error.go create mode 100644 internal/admin/init.go create mode 100644 internal/admin/layer_route.go create mode 100644 internal/admin/option.go create mode 100644 internal/admin/proxy_route.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/chi/log_formatter.go create mode 100644 internal/client/client.go create mode 100644 internal/client/create_layer.go create mode 100644 internal/client/create_proxy.go create mode 100644 internal/client/delete_layer.go create mode 100644 internal/client/delete_proxy.go create mode 100644 internal/client/get_layer.go create mode 100644 internal/client/get_proxy.go create mode 100644 internal/client/options.go create mode 100644 internal/client/query_layer.go create mode 100644 internal/client/query_proxy.go create mode 100644 internal/client/update_layer.go create mode 100644 internal/client/update_proxy.go create mode 100644 internal/command/admin/apierr/wrap.go create mode 100644 internal/command/admin/flag/flag.go create mode 100644 internal/command/admin/flag/util.go create mode 100644 internal/command/admin/layer/create.go create mode 100644 internal/command/admin/layer/delete.go create mode 100644 internal/command/admin/layer/flag/flag.go create mode 100644 internal/command/admin/layer/get.go create mode 100644 internal/command/admin/layer/query.go create mode 100644 internal/command/admin/layer/root.go create mode 100644 internal/command/admin/layer/update.go create mode 100644 internal/command/admin/layer/util.go create mode 100644 internal/command/admin/proxy/create.go create mode 100644 internal/command/admin/proxy/delete.go create mode 100644 internal/command/admin/proxy/flag/flag.go create mode 100644 internal/command/admin/proxy/get.go create mode 100644 internal/command/admin/proxy/query.go create mode 100644 internal/command/admin/proxy/root.go create mode 100644 internal/command/admin/proxy/update.go create mode 100644 internal/command/admin/proxy/util.go create mode 100644 internal/command/admin/root.go create mode 100644 internal/command/auth/create_token.go create mode 100644 internal/command/auth/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/server/admin/root.go create mode 100644 internal/command/server/admin/run.go create mode 100644 internal/command/server/proxy/root.go create mode 100644 internal/command/server/proxy/run.go create mode 100644 internal/command/server/root.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/director/context.go create mode 100644 internal/proxy/director/director.go create mode 100644 internal/proxy/director/layer_registry.go create mode 100644 internal/proxy/director/util.go create mode 100644 internal/proxy/init.go create mode 100644 internal/proxy/option.go create mode 100644 internal/proxy/server.go create mode 100644 internal/queue/adapter.go create mode 100644 internal/queue/layer_options.go create mode 100644 internal/queue/options.go create mode 100644 internal/queue/queue.go create mode 100644 internal/queue/redis/adapter.go create mode 100644 internal/queue/schema.go create mode 100644 internal/queue/schema/layer-options.json create mode 100644 internal/schema/error.go create mode 100644 internal/schema/load.go create mode 100644 internal/schema/registry.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/layer.go create mode 100644 internal/store/layer_repository.go create mode 100644 internal/store/name.go create mode 100644 internal/store/proxy.go create mode 100644 internal/store/proxy_repository.go create mode 100644 internal/store/redis/helper.go create mode 100644 internal/store/redis/layer_item.go create mode 100644 internal/store/redis/layer_repository.go create mode 100644 internal/store/redis/layer_repository_test.go create mode 100644 internal/store/redis/main_test.go create mode 100644 internal/store/redis/proxy_item.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/sort.go create mode 100644 internal/store/testsuite/layer_repository.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..e6b0bcb --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,136 @@ +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: /usr/share/bouncer + type: dir + file_info: + mode: 0700 + - 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: /usr/share/bouncer + type: dir + file_info: + mode: 0700 + - 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..868ed14 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,60 @@ +@Library('cadoles') _ + +pipeline { + agent { + dockerfile { + label 'docker' + filename 'Dockerfile' + dir 'misc/jenkins' + args '-v /var/run/docker.sock:/var/run/docker.sock --network host' + } + } + + 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..3dbe31b --- /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 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..030b424 --- /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/auth" + "forge.cadoles.com/cadoles/bouncer/internal/command/config" + "forge.cadoles.com/cadoles/bouncer/internal/command/server" + + _ "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, + server.Root(), + auth.Root(), + admin.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..6217e6b --- /dev/null +++ b/go.mod @@ -0,0 +1,92 @@ +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/davecgh/go-spew v1.1.1 + 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/qri-io/jsonpointer v0.1.1 // indirect + github.com/qri-io/jsonschema v0.2.1 // 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..4b7f089 --- /dev/null +++ b/go.sum @@ -0,0 +1,903 @@ +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/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA= +github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64= +github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0= +github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI= +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/error.go b/internal/admin/error.go new file mode 100644 index 0000000..223ed5e --- /dev/null +++ b/internal/admin/error.go @@ -0,0 +1,31 @@ +package admin + +import ( + "fmt" + "net/http" + + "forge.cadoles.com/cadoles/bouncer/internal/schema" + "gitlab.com/wpetit/goweb/api" +) + +const ErrCodeAlreadyExist api.ErrorCode = "already-exist" + +func invalidDataErrorResponse(w http.ResponseWriter, r *http.Request, err *schema.InvalidDataError) { + keyErrors := err.KeyErrors() + + message := "" + for idx, err := range keyErrors { + if idx != 0 { + message += ", " + } + message += fmt.Sprintf("Path [%s]: %s", err.PropertyPath, err.Message) + } + + api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, &struct { + Message string `json:"message"` + }{ + Message: message, + }) + + return +} diff --git a/internal/admin/init.go b/internal/admin/init.go new file mode 100644 index 0000000..871a396 --- /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.initLayerRepository(ctx); err != nil { + return errors.WithStack(err) + } + + if err := s.initProxyRepository(ctx); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (s *Server) initLayerRepository(ctx context.Context) error { + layerRepository, err := setup.NewLayerRepository(ctx, s.redisConfig) + if err != nil { + return errors.WithStack(err) + } + + s.layerRepository = layerRepository + + 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/layer_route.go b/internal/admin/layer_route.go new file mode 100644 index 0000000..df39163 --- /dev/null +++ b/internal/admin/layer_route.go @@ -0,0 +1,302 @@ +package admin + +import ( + "net/http" + "sort" + + "forge.cadoles.com/cadoles/bouncer/internal/schema" + "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 QueryLayerResponse struct { + Layers []*store.LayerHeader `json:"layers"` +} + +func (s *Server) queryLayer(w http.ResponseWriter, r *http.Request) { + proxyName, ok := getProxyName(w, r) + if !ok { + return + } + + options := []store.QueryLayerOptionFunc{} + + name := r.URL.Query().Get("name") + if name != "" { + options = append(options, store.WithLayerQueryName(store.LayerName(name))) + } + + ctx := r.Context() + + layers, err := s.layerRepository.QueryLayers( + ctx, + proxyName, + options..., + ) + if err != nil { + logger.Error(ctx, "could not list layers", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + sort.Sort(store.ByLayerWeight(layers)) + + api.DataResponse(w, http.StatusOK, QueryLayerResponse{ + Layers: layers, + }) +} + +func validateLayerName(v string) (store.LayerName, error) { + name, err := store.ValidateName(v) + if err != nil { + return "", errors.WithStack(err) + } + + return store.LayerName(name), nil +} + +type GetLayerResponse struct { + Layer *store.Layer `json:"layer"` +} + +func (s *Server) getLayer(w http.ResponseWriter, r *http.Request) { + proxyName, ok := getProxyName(w, r) + if !ok { + return + } + + layerName, ok := getLayerName(w, r) + if !ok { + return + } + + ctx := r.Context() + + layer, err := s.layerRepository.GetLayer(ctx, proxyName, layerName) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil) + + return + } + + logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + api.DataResponse(w, http.StatusOK, GetLayerResponse{ + Layer: layer, + }) +} + +type DeleteLayerResponse struct { + LayerName store.LayerName `json:"layerName"` +} + +func (s *Server) deleteLayer(w http.ResponseWriter, r *http.Request) { + proxyName, ok := getProxyName(w, r) + if !ok { + return + } + + layerName, ok := getLayerName(w, r) + if !ok { + return + } + + ctx := r.Context() + + if err := s.layerRepository.DeleteLayer(ctx, proxyName, layerName); err != nil { + if errors.Is(err, store.ErrNotFound) { + api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil) + + return + } + + logger.Error(ctx, "could not delete layer", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + api.DataResponse(w, http.StatusOK, DeleteLayerResponse{ + LayerName: layerName, + }) +} + +type CreateLayerRequest struct { + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required"` + Options map[string]any `json:"options"` +} + +type CreateLayerResponse struct { + Layer *store.Layer `json:"layer"` +} + +func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + proxyName, ok := getProxyName(w, r) + if !ok { + return + } + + createLayerReq := &CreateLayerRequest{} + if ok := api.Bind(w, r, createLayerReq); !ok { + return + } + + layerName, err := store.ValidateName(createLayerReq.Name) + if err != nil { + logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) + + return + } + + layer, err := s.layerRepository.CreateLayer(ctx, proxyName, store.LayerName(layerName), store.LayerType(createLayerReq.Type), createLayerReq.Options) + if err != nil { + if errors.Is(err, store.ErrAlreadyExist) { + api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyExist, nil) + + return + } + + logger.Error(ctx, "could not create layer", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + api.DataResponse(w, http.StatusOK, struct { + Layer *store.Layer `json:"layer"` + }{ + Layer: layer, + }) +} + +type UpdateLayerRequest struct { + Enabled *bool `json:"enabled"` + Weight *int `json:"weight"` + Options *store.LayerOptions `json:"options"` +} + +type UpdateLayerResponse struct { + Layer *store.Layer `json:"layer"` +} + +func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + proxyName, ok := getProxyName(w, r) + if !ok { + return + } + + layerName, ok := getLayerName(w, r) + if !ok { + return + } + + layer, err := s.layerRepository.GetLayer(ctx, proxyName, layerName) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil) + + return + } + + logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + updateLayerReq := &UpdateLayerRequest{} + if ok := api.Bind(w, r, updateLayerReq); !ok { + return + } + + options := make([]store.UpdateLayerOptionFunc, 0) + + if updateLayerReq.Enabled != nil { + options = append(options, store.WithLayerUpdateEnabled(*updateLayerReq.Enabled)) + } + + if updateLayerReq.Weight != nil { + options = append(options, store.WithLayerUpdateWeight(*updateLayerReq.Weight)) + } + + if updateLayerReq.Options != nil { + if err := schema.ValidateLayerOptions(ctx, layer.Type, updateLayerReq.Options); err != nil { + logger.Error(r.Context(), "could not validate layer options", logger.E(errors.WithStack(err))) + + var invalidDataErr *schema.InvalidDataError + if errors.As(err, &invalidDataErr) { + invalidDataErrorResponse(w, r, invalidDataErr) + + return + } + + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + options = append(options, store.WithLayerUpdateOptions(*updateLayerReq.Options)) + } + + layer, err = s.layerRepository.UpdateLayer( + ctx, proxyName, layerName, + options..., + ) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil) + + return + } + + logger.Error(ctx, "could not update layer", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + api.DataResponse(w, http.StatusOK, UpdateLayerResponse{Layer: layer}) +} + +func getLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool) { + rawLayerName := chi.URLParam(r, "layerName") + + name, err := store.ValidateName(rawLayerName) + if err != nil { + logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) + + return "", false + } + + return store.LayerName(name), true +} + +func geLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool) { + rawLayerName := chi.URLParam(r, "layerName") + + name, err := store.ValidateName(rawLayerName) + if err != nil { + logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) + + return "", false + } + + return store.LayerName(name), true +} 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_route.go b/internal/admin/proxy_route.go new file mode 100644 index 0000000..816d802 --- /dev/null +++ b/internal/admin/proxy_route.go @@ -0,0 +1,312 @@ +package admin + +import ( + "net/http" + "net/url" + "sort" + "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) { + options := []store.QueryProxyOptionFunc{} + + names, ok := getStringableSliceValues(w, r, "names", nil, validateProxyName) + if !ok { + return + } + + if names != nil { + options = append(options, store.WithProxyQueryNames(names...)) + } + + 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 + } + + sort.Sort(store.ByProxyWeight(proxies)) + + api.DataResponse(w, http.StatusOK, QueryProxyResponse{ + Proxies: proxies, + }) +} + +func validateProxyName(v string) (store.ProxyName, error) { + name, err := store.ValidateName(v) + if err != nil { + return "", errors.WithStack(err) + } + + return store.ProxyName(name), nil +} + +type GetProxyResponse struct { + Proxy *store.Proxy `json:"proxy"` +} + +func (s *Server) getProxy(w http.ResponseWriter, r *http.Request) { + proxyName, ok := getProxyName(w, r) + if !ok { + return + } + + ctx := r.Context() + + proxy, err := s.proxyRepository.GetProxy(ctx, proxyName) + 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 { + ProxyName store.ProxyName `json:"proxyName"` +} + +func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) { + proxyName, ok := getProxyName(w, r) + if !ok { + return + } + + ctx := r.Context() + + if err := s.proxyRepository.DeleteProxy(ctx, proxyName); 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{ + ProxyName: proxyName, + }) +} + +type CreateProxyRequest struct { + Name string `json:"name" validate:"required"` + 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 + } + + name, err := store.ValidateName(createProxyReq.Name) + if err != nil { + logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) + + return + } + + if _, err := url.Parse(createProxyReq.To); 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, store.ProxyName(name), createProxyReq.To, createProxyReq.From...) + if err != nil { + if errors.Is(err, store.ErrAlreadyExist) { + api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyExist, nil) + + return + } + + logger.Error(ctx, "could not create proxy", 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 { + Enabled *bool `json:"enabled"` + Weight *int `json:"weight"` + To *string `json:"to"` + From []string `json:"from"` +} + +type UpdateProxyResponse struct { + Proxy *store.Proxy `json:"proxy"` +} + +func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + proxyName, ok := getProxyName(w, r) + if !ok { + return + } + + updateProxyReq := &UpdateProxyRequest{} + if ok := api.Bind(w, r, updateProxyReq); !ok { + return + } + + options := make([]store.UpdateProxyOptionFunc, 0) + + if updateProxyReq.Enabled != nil { + options = append(options, store.WithProxyUpdateEnabled(*updateProxyReq.Enabled)) + } + + if updateProxyReq.To != nil { + _, err := url.Parse(*updateProxyReq.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 + } + + options = append(options, store.WithProxyUpdateTo(*updateProxyReq.To)) + } + + if updateProxyReq.From != nil { + options = append(options, store.WithProxyUpdateFrom(updateProxyReq.From...)) + } + + if updateProxyReq.Weight != nil { + options = append(options, store.WithProxyUpdateWeight(*updateProxyReq.Weight)) + } + + proxy, err := s.proxyRepository.UpdateProxy( + ctx, proxyName, + options..., + ) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil) + + return + } + + logger.Error(ctx, "could not update proxy", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) + + return + } + + api.DataResponse(w, http.StatusOK, UpdateProxyResponse{Proxy: proxy}) +} + +func getProxyName(w http.ResponseWriter, r *http.Request) (store.ProxyName, bool) { + rawProxyName := chi.URLParam(r, "proxyName") + + name, err := store.ValidateName(rawProxyName) + if err != nil { + logger.Error(r.Context(), "could not parse proxy name", logger.E(errors.WithStack(err))) + api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) + + return "", false + } + + return store.ProxyName(name), 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, validate func(string) (T, error)) ([]T, bool) { + rawValue := r.URL.Query().Get(param) + + if rawValue != "" { + rawValues := strings.Split(rawValue, ",") + values := make([]T, 0, len(rawValues)) + + for _, rv := range rawValues { + v, err := validate(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 + } + + values = append(values, v) + } + + return values, true + } + + return defaultValue, true +} diff --git a/internal/admin/server.go b/internal/admin/server.go new file mode 100644 index 0000000..e312c94 --- /dev/null +++ b/internal/admin/server.go @@ -0,0 +1,145 @@ +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/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 + proxyRepository store.ProxyRepository + layerRepository store.LayerRepository +} + +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("/{proxyName}", s.getProxy) + r.With(assertWriteAccess).Put("/{proxyName}", s.updateProxy) + r.With(assertWriteAccess).Delete("/{proxyName}", s.deleteProxy) + + r.With(assertReadAccess).Get("/{proxyName}/layers", s.queryLayer) + r.With(assertWriteAccess).Post("/{proxyName}/layers", s.createLayer) + r.With(assertReadAccess).Get("/{proxyName}/layers/{layerName}", s.getLayer) + r.With(assertWriteAccess).Put("/{proxyName}/layers/{layerName}", s.updateLayer) + r.With(assertWriteAccess).Delete("/{proxyName}/layers/{layerName}", s.deleteLayer) + }) + }) + }) + + 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/chi/log_formatter.go b/internal/chi/log_formatter.go new file mode 100644 index 0000000..c3d604f --- /dev/null +++ b/internal/chi/log_formatter.go @@ -0,0 +1,53 @@ +package chi + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/go-chi/chi/v5/middleware" + "gitlab.com/wpetit/goweb/logger" +) + +type LogFormatter struct{} + +// NewLogEntry implements middleware.LogFormatter +func (*LogFormatter) NewLogEntry(r *http.Request) middleware.LogEntry { + return &LogEntry{ + method: r.Method, + path: r.URL.Path, + ctx: r.Context(), + } +} + +func NewLogFormatter() *LogFormatter { + return &LogFormatter{} +} + +var _ middleware.LogFormatter = &LogFormatter{} + +type LogEntry struct { + method string + path string + ctx context.Context +} + +// Panic implements middleware.LogEntry +func (e *LogEntry) Panic(v interface{}, stack []byte) { + logger.Error(e.ctx, fmt.Sprintf("%s %s", e.method, e.path), logger.F("stack", string(stack))) +} + +// Write implements middleware.LogEntry +func (e *LogEntry) Write(status int, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { + logger.Info(e.ctx, fmt.Sprintf("%s %s - %d", e.method, e.path, status), + logger.F("status", status), + logger.F("bytes", bytes), + logger.F("elapsed", elapsed), + logger.F("method", e.method), + logger.F("path", e.path), + logger.F("extra", extra), + ) +} + +var _ middleware.LogEntry = &LogEntry{} 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_layer.go b/internal/client/create_layer.go new file mode 100644 index 0000000..94aae18 --- /dev/null +++ b/internal/client/create_layer.go @@ -0,0 +1,32 @@ +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) CreateLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, layerType store.LayerType, options store.LayerOptions, funcs ...OptionFunc) (*store.Layer, error) { + request := admin.CreateLayerRequest{ + Name: string(layerName), + Type: string(layerType), + Options: options, + } + + response := withResponse[admin.CreateLayerResponse]() + + path := fmt.Sprintf("/api/v1/proxies/%s/layers", proxyName) + + if err := c.apiPost(ctx, path, request, &response); err != nil { + return nil, errors.WithStack(err) + } + + if response.Error != nil { + return nil, errors.WithStack(response.Error) + } + + return response.Data.Layer, nil +} diff --git a/internal/client/create_proxy.go b/internal/client/create_proxy.go new file mode 100644 index 0000000..3944f97 --- /dev/null +++ b/internal/client/create_proxy.go @@ -0,0 +1,30 @@ +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, name store.ProxyName, to *url.URL, from []string, funcs ...OptionFunc) (*store.Proxy, error) { + request := admin.CreateProxyRequest{ + Name: string(name), + 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_layer.go b/internal/client/delete_layer.go new file mode 100644 index 0000000..9d18cb8 --- /dev/null +++ b/internal/client/delete_layer.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) DeleteLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, funcs ...OptionFunc) (store.LayerName, error) { + response := withResponse[admin.DeleteLayerResponse]() + + path := fmt.Sprintf("/api/v1/proxies/%s/layers/%s", proxyName, layerName) + + 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.LayerName, nil +} diff --git a/internal/client/delete_proxy.go b/internal/client/delete_proxy.go new file mode 100644 index 0000000..9c889c7 --- /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, proxyName store.ProxyName, funcs ...OptionFunc) (store.ProxyName, error) { + response := withResponse[admin.DeleteProxyResponse]() + + path := fmt.Sprintf("/api/v1/proxies/%s", proxyName) + + 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.ProxyName, nil +} diff --git a/internal/client/get_layer.go b/internal/client/get_layer.go new file mode 100644 index 0000000..e5549b8 --- /dev/null +++ b/internal/client/get_layer.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) GetLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, funcs ...OptionFunc) (*store.Layer, error) { + response := withResponse[admin.GetLayerResponse]() + + path := fmt.Sprintf("/api/v1/proxies/%s/layers/%s", proxyName, layerName) + + 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.Layer, nil +} diff --git a/internal/client/get_proxy.go b/internal/client/get_proxy.go new file mode 100644 index 0000000..c495608 --- /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, proxyName store.ProxyName, funcs ...OptionFunc) (*store.Proxy, error) { + response := withResponse[admin.GetProxyResponse]() + + path := fmt.Sprintf("/api/v1/proxies/%s", proxyName) + + 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_layer.go b/internal/client/query_layer.go new file mode 100644 index 0000000..1c1a1e8 --- /dev/null +++ b/internal/client/query_layer.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 QueryLayerOptionFunc func(*QueryLayerOptions) + +type QueryLayerOptions struct { + Options []OptionFunc + Offset *int + Limit *int + Names []store.LayerName +} + +func WithQueryLayerOptions(funcs ...OptionFunc) QueryLayerOptionFunc { + return func(opts *QueryLayerOptions) { + opts.Options = funcs + } +} + +func WithQueryLayerLimit(limit int) QueryLayerOptionFunc { + return func(opts *QueryLayerOptions) { + opts.Limit = &limit + } +} + +func WithQueryLayerOffset(offset int) QueryLayerOptionFunc { + return func(opts *QueryLayerOptions) { + opts.Offset = &offset + } +} + +func WithQueryLayerNames(names ...store.LayerName) QueryLayerOptionFunc { + return func(opts *QueryLayerOptions) { + opts.Names = names + } +} + +func (c *Client) QueryLayer(ctx context.Context, proxyName store.ProxyName, funcs ...QueryLayerOptionFunc) ([]*store.LayerHeader, error) { + options := &QueryLayerOptions{} + for _, fn := range funcs { + fn(options) + } + + query := url.Values{} + + if options.Names != nil && len(options.Names) > 0 { + query.Set("names", joinSlice(options.Names)) + } + + path := fmt.Sprintf("/api/v1/proxies/%s/layers?%s", proxyName, query.Encode()) + + response := withResponse[admin.QueryLayerResponse]() + + 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.Layers, nil +} diff --git a/internal/client/query_proxy.go b/internal/client/query_proxy.go new file mode 100644 index 0000000..06ea42d --- /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 + Names []store.ProxyName +} + +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 WithQueryProxyNames(names ...store.ProxyName) QueryProxyOptionFunc { + return func(opts *QueryProxyOptions) { + opts.Names = names + } +} + +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.Names != nil && len(options.Names) > 0 { + query.Set("names", joinSlice(options.Names)) + } + + 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_layer.go b/internal/client/update_layer.go new file mode 100644 index 0000000..6be6cec --- /dev/null +++ b/internal/client/update_layer.go @@ -0,0 +1,28 @@ +package client + +import ( + "context" + "fmt" + + "forge.cadoles.com/cadoles/bouncer/internal/admin" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +type UpdateLayerOptions = admin.UpdateLayerRequest + +func (c *Client) UpdateLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, opts *UpdateLayerOptions, funcs ...OptionFunc) (*store.Layer, error) { + response := withResponse[admin.UpdateLayerResponse]() + + path := fmt.Sprintf("/api/v1/proxies/%s/layers/%s", proxyName, layerName) + + if err := c.apiPut(ctx, path, opts, &response); err != nil { + return nil, errors.WithStack(err) + } + + if response.Error != nil { + return nil, errors.WithStack(response.Error) + } + + return response.Data.Layer, nil +} diff --git a/internal/client/update_proxy.go b/internal/client/update_proxy.go new file mode 100644 index 0000000..9f1a642 --- /dev/null +++ b/internal/client/update_proxy.go @@ -0,0 +1,28 @@ +package client + +import ( + "context" + "fmt" + + "forge.cadoles.com/cadoles/bouncer/internal/admin" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +type UpdateProxyOptions = admin.UpdateProxyRequest + +func (c *Client) UpdateProxy(ctx context.Context, proxyName store.ProxyName, opts *UpdateProxyOptions, funcs ...OptionFunc) (*store.Proxy, error) { + response := withResponse[admin.UpdateProxyResponse]() + + path := fmt.Sprintf("/api/v1/proxies/%s", proxyName) + + if err := c.apiPut(ctx, path, opts, &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/apierr/wrap.go b/internal/command/admin/apierr/wrap.go new file mode 100644 index 0000000..952c2c3 --- /dev/null +++ b/internal/command/admin/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/admin/flag/flag.go b/internal/command/admin/flag/flag.go new file mode 100644 index 0000000..4a2ec3b --- /dev/null +++ b/internal/command/admin/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/admin/flag/util.go b/internal/command/admin/flag/util.go new file mode 100644 index 0000000..de2af79 --- /dev/null +++ b/internal/command/admin/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/admin/layer/create.go b/internal/command/admin/layer/create.go new file mode 100644 index 0000000..34a243d --- /dev/null +++ b/internal/command/admin/layer/create.go @@ -0,0 +1,72 @@ +package layer + +import ( + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag" + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer/flag" + layerFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/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 layer", + Flags: layerFlag.WithLayerCreateFlags(), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + layerName, err := flag.AssertLayerName(ctx) + if err != nil { + return errors.WithStack(err) + } + + proxyName, err := proxyFlag.AssertProxyName(ctx) + if err != nil { + return errors.WithStack(err) + } + + layerType, err := flag.AssertLayerType(ctx) + if err != nil { + return errors.WithStack(err) + } + + layerOptions, err := flag.AssertLayerOptions(ctx) + if err != nil { + return errors.WithStack(err) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + layer, err := client.CreateLayer( + ctx.Context, + proxyName, + layerName, + layerType, + layerOptions, + ) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := layerHints(baseFlags.OutputMode) + + if err := format.Write(baseFlags.Format, os.Stdout, hints, layer); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/admin/layer/delete.go b/internal/command/admin/layer/delete.go new file mode 100644 index 0000000..ccc33ea --- /dev/null +++ b/internal/command/admin/layer/delete.go @@ -0,0 +1,62 @@ +package layer + +import ( + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag" + layerFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/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 layer", + Flags: layerFlag.WithLayerFlags(), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + proxyName, err := proxyFlag.AssertProxyName(ctx) + if err != nil { + return errors.WithStack(err) + } + + layerName, err := layerFlag.AssertLayerName(ctx) + if err != nil { + return errors.WithStack(err) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + layerName, err = client.DeleteLayer(ctx.Context, proxyName, layerName) + 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 { + Name store.LayerName `json:"id"` + }{ + Name: layerName, + }); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/admin/layer/flag/flag.go b/internal/command/admin/layer/flag/flag.go new file mode 100644 index 0000000..61a5a5a --- /dev/null +++ b/internal/command/admin/layer/flag/flag.go @@ -0,0 +1,76 @@ +package flag + +import ( + "encoding/json" + + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +const ( + FlagLayerName = "layer-name" + FlagLayerType = "layer-type" + FlagLayerOptions = "layer-options" +) + +func WithLayerFlags(flags ...cli.Flag) []cli.Flag { + baseFlags := proxyFlag.WithProxyFlags( + &cli.StringFlag{ + Name: FlagLayerName, + Usage: "use `LAYER_NAME` as targeted layer", + Value: "", + Required: true, + }, + ) + + flags = append(flags, baseFlags...) + + return flags +} + +func WithLayerCreateFlags(flags ...cli.Flag) []cli.Flag { + return WithLayerFlags( + &cli.StringFlag{ + Name: FlagLayerType, + Usage: "Set `LAYER_TYPE` as layer's type", + Value: "", + Required: true, + }, + &cli.StringFlag{ + Name: FlagLayerOptions, + Usage: "Set `LAYER_OPTIONS` as layer's options", + Value: "{}", + }, + ) +} + +func AssertLayerName(ctx *cli.Context) (store.LayerName, error) { + rawLayerName := ctx.String(FlagLayerName) + + name, err := store.ValidateName(rawLayerName) + if err != nil { + return "", errors.WithStack(err) + } + + return store.LayerName(name), nil +} + +func AssertLayerType(ctx *cli.Context) (store.LayerType, error) { + rawLayerType := ctx.String(FlagLayerType) + + return store.LayerType(rawLayerType), nil +} + +func AssertLayerOptions(ctx *cli.Context) (store.LayerOptions, error) { + rawLayerOptions := ctx.String(FlagLayerOptions) + + layerOptions := store.LayerOptions{} + + if err := json.Unmarshal([]byte(rawLayerOptions), &layerOptions); err != nil { + return nil, errors.WithStack(err) + } + + return layerOptions, nil +} diff --git a/internal/command/admin/layer/get.go b/internal/command/admin/layer/get.go new file mode 100644 index 0000000..124f1bc --- /dev/null +++ b/internal/command/admin/layer/get.go @@ -0,0 +1,55 @@ +package layer + +import ( + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag" + layerFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/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 layer", + Flags: layerFlag.WithLayerFlags(), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + proxyName, err := proxyFlag.AssertProxyName(ctx) + if err != nil { + return errors.WithStack(err) + } + + layerName, err := layerFlag.AssertLayerName(ctx) + if err != nil { + return errors.WithStack(err) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + layer, err := client.GetLayer(ctx.Context, proxyName, layerName) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := layerHints(baseFlags.OutputMode) + + if err := format.Write(baseFlags.Format, os.Stdout, hints, layer); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/admin/layer/query.go b/internal/command/admin/layer/query.go new file mode 100644 index 0000000..1296649 --- /dev/null +++ b/internal/command/admin/layer/query.go @@ -0,0 +1,69 @@ +package layer + +import ( + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/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 QueryCommand() *cli.Command { + return &cli.Command{ + Name: "query", + Usage: "Query layers", + Flags: proxyFlag.WithProxyFlags( + &cli.StringSliceFlag{ + Name: "with-name", + Usage: "use `WITH_NAME` 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)) + } + + proxyName, err := proxyFlag.AssertProxyName(ctx) + if err != nil { + return errors.WithStack(err) + } + + options := make([]client.QueryLayerOptionFunc, 0) + + rawNames := ctx.StringSlice("with-name") + if rawNames != nil { + layerNames := func(names []string) []store.LayerName { + layerNames := make([]store.LayerName, len(names)) + for i, name := range names { + layerNames[i] = store.LayerName(name) + } + return layerNames + }(rawNames) + options = append(options, client.WithQueryLayerNames(layerNames...)) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + proxies, err := client.QueryLayer(ctx.Context, proxyName, options...) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := layerHeaderHints(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/admin/layer/root.go b/internal/command/admin/layer/root.go new file mode 100644 index 0000000..78598ff --- /dev/null +++ b/internal/command/admin/layer/root.go @@ -0,0 +1,19 @@ +package layer + +import ( + "github.com/urfave/cli/v2" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "layer", + Usage: "Execute actions related to layers", + Subcommands: []*cli.Command{ + CreateCommand(), + GetCommand(), + QueryCommand(), + UpdateCommand(), + DeleteCommand(), + }, + } +} diff --git a/internal/command/admin/layer/update.go b/internal/command/admin/layer/update.go new file mode 100644 index 0000000..0bf438c --- /dev/null +++ b/internal/command/admin/layer/update.go @@ -0,0 +1,91 @@ +package layer + +import ( + "encoding/json" + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag" + layerFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/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 UpdateCommand() *cli.Command { + return &cli.Command{ + Name: "update", + Usage: "Update layer", + Flags: layerFlag.WithLayerFlags( + &cli.BoolFlag{ + Name: "enabled", + Usage: "Enable or disable proxy", + }, + &cli.IntFlag{ + Name: "weight", + Usage: "Set `WEIGHT` as proxy's weight", + }, + &cli.StringFlag{ + Name: "options", + Usage: "Set `OPTIONS` as proxy's options", + }, + ), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + proxyName, err := proxyFlag.AssertProxyName(ctx) + if err != nil { + return errors.WithStack(err) + } + + layerName, err := layerFlag.AssertLayerName(ctx) + if err != nil { + return errors.WithStack(err) + } + + opts := &client.UpdateLayerOptions{} + + if ctx.IsSet("options") { + var options store.LayerOptions + if err := json.Unmarshal([]byte(ctx.String("options")), &options); err != nil { + return errors.Wrap(err, "could not parse options") + } + + opts.Options = &options + } + + if ctx.IsSet("weight") { + weight := ctx.Int("weight") + opts.Weight = &weight + } + + if ctx.IsSet("enabled") { + enabled := ctx.Bool("enabled") + opts.Enabled = &enabled + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + proxy, err := client.UpdateLayer(ctx.Context, proxyName, layerName, opts) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := layerHints(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/admin/layer/util.go b/internal/command/admin/layer/util.go new file mode 100644 index 0000000..dc8d0d6 --- /dev/null +++ b/internal/command/admin/layer/util.go @@ -0,0 +1,33 @@ +package layer + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/format" + "forge.cadoles.com/cadoles/bouncer/internal/format/table" +) + +func layerHeaderHints(outputMode format.OutputMode) format.Hints { + return format.Hints{ + OutputMode: outputMode, + Props: []format.Prop{ + format.NewProp("Name", "Name"), + format.NewProp("Type", "Type"), + format.NewProp("Enabled", "Enabled"), + format.NewProp("Weight", "Weight"), + }, + } +} + +func layerHints(outputMode format.OutputMode) format.Hints { + return format.Hints{ + OutputMode: outputMode, + Props: []format.Prop{ + format.NewProp("Name", "Name"), + format.NewProp("Type", "Type"), + format.NewProp("Enabled", "Enabled"), + format.NewProp("Weight", "Weight"), + format.NewProp("Options", "Options"), + format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)), + format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)), + }, + } +} diff --git a/internal/command/admin/proxy/create.go b/internal/command/admin/proxy/create.go new file mode 100644 index 0000000..32b25eb --- /dev/null +++ b/internal/command/admin/proxy/create.go @@ -0,0 +1,69 @@ +package proxy + +import ( + "net/url" + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/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: proxyFlag.WithProxyFlags( + &cli.StringFlag{ + Name: "to", + Usage: "Set `TO` as proxy's destination url", + Value: "", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "from", + Usage: "Set `FROM` as proxy's 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)) + } + + proxyName, err := proxyFlag.AssertProxyName(ctx) + if err != nil { + return errors.Wrap(err, "'to' parameter should be a valid url") + } + + 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, proxyName, 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/admin/proxy/delete.go b/internal/command/admin/proxy/delete.go new file mode 100644 index 0000000..46dd3a6 --- /dev/null +++ b/internal/command/admin/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/admin/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/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)) + } + + proxyName, err := proxyFlag.AssertProxyName(ctx) + if err != nil { + return errors.WithStack(err) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + proxyName, err = client.DeleteProxy(ctx.Context, proxyName) + 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 { + Name store.ProxyName `json:"id"` + }{ + Name: proxyName, + }); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/admin/proxy/flag/flag.go b/internal/command/admin/proxy/flag/flag.go new file mode 100644 index 0000000..f6f562f --- /dev/null +++ b/internal/command/admin/proxy/flag/flag.go @@ -0,0 +1,36 @@ +package flag + +import ( + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +const FlagProxyName = "proxy-name" + +func WithProxyFlags(flags ...cli.Flag) []cli.Flag { + baseFlags := clientFlag.ComposeFlags( + &cli.StringFlag{ + Name: FlagProxyName, + Usage: "use `PROXY_NAME` as targeted proxy", + Value: "", + Required: true, + }, + ) + + flags = append(flags, baseFlags...) + + return flags +} + +func AssertProxyName(ctx *cli.Context) (store.ProxyName, error) { + rawProxyName := ctx.String(FlagProxyName) + + name, err := store.ValidateName(rawProxyName) + if err != nil { + return "", errors.WithStack(err) + } + + return store.ProxyName(name), nil +} diff --git a/internal/command/admin/proxy/get.go b/internal/command/admin/proxy/get.go new file mode 100644 index 0000000..b991aea --- /dev/null +++ b/internal/command/admin/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/admin/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/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)) + } + + proxyName, err := proxyFlag.AssertProxyName(ctx) + if err != nil { + return errors.WithStack(err) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + proxy, err := client.GetProxy(ctx.Context, proxyName) + 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/admin/proxy/query.go b/internal/command/admin/proxy/query.go new file mode 100644 index 0000000..2be76f1 --- /dev/null +++ b/internal/command/admin/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/admin/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/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) + + rawNames := ctx.StringSlice("ids") + if rawNames != nil { + proxyNames := func(names []string) []store.ProxyName { + proxyNames := make([]store.ProxyName, len(names)) + for i, name := range names { + proxyNames[i] = store.ProxyName(name) + } + return proxyNames + }(rawNames) + options = append(options, client.WithQueryProxyNames(proxyNames...)) + } + + 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/admin/proxy/root.go b/internal/command/admin/proxy/root.go new file mode 100644 index 0000000..5e37c53 --- /dev/null +++ b/internal/command/admin/proxy/root.go @@ -0,0 +1,19 @@ +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(), + UpdateCommand(), + }, + } +} diff --git a/internal/command/admin/proxy/update.go b/internal/command/admin/proxy/update.go new file mode 100644 index 0000000..ff03532 --- /dev/null +++ b/internal/command/admin/proxy/update.go @@ -0,0 +1,93 @@ +package proxy + +import ( + "net/url" + "os" + + "forge.cadoles.com/cadoles/bouncer/internal/client" + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" + clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag" + proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag" + "forge.cadoles.com/cadoles/bouncer/internal/format" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func UpdateCommand() *cli.Command { + return &cli.Command{ + Name: "update", + Usage: "Update proxy", + Flags: proxyFlag.WithProxyFlags( + &cli.StringFlag{ + Name: "to", + Usage: "Set `TO` as proxy's destination url", + }, + &cli.StringSliceFlag{ + Name: "from", + Usage: "Set `FROM` as proxy's patterns to match incoming requests", + }, + &cli.BoolFlag{ + Name: "enabled", + Usage: "Enable or disable proxy", + }, + &cli.IntFlag{ + Name: "weight", + Usage: "Set `WEIGHT` as proxy's weight", + }, + ), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + proxyName, err := proxyFlag.AssertProxyName(ctx) + if err != nil { + return errors.WithStack(err) + } + + opts := &client.UpdateProxyOptions{} + + if ctx.IsSet("to") { + to := ctx.String("to") + if _, err := url.Parse(to); err != nil { + return errors.Wrap(err, "'to' parameter should be a valid url") + } + + opts.To = &to + } + + from := ctx.StringSlice("from") + if from != nil { + opts.From = from + } + + if ctx.IsSet("weight") { + weight := ctx.Int("weight") + opts.Weight = &weight + } + + if ctx.IsSet("enabled") { + enabled := ctx.Bool("enabled") + opts.Enabled = &enabled + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + proxy, err := client.UpdateProxy(ctx.Context, proxyName, opts) + 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/admin/proxy/util.go b/internal/command/admin/proxy/util.go new file mode 100644 index 0000000..0a9d6ec --- /dev/null +++ b/internal/command/admin/proxy/util.go @@ -0,0 +1,32 @@ +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("Name", "Name"), + format.NewProp("Enabled", "Enabled"), + format.NewProp("Weight", "Weight"), + }, + } +} + +func proxyHints(outputMode format.OutputMode) format.Hints { + return format.Hints{ + OutputMode: outputMode, + Props: []format.Prop{ + format.NewProp("Name", "Name"), + format.NewProp("From", "From"), + format.NewProp("To", "To"), + format.NewProp("Enabled", "Enabled"), + format.NewProp("Weight", "Weight"), + format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)), + format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)), + }, + } +} diff --git a/internal/command/admin/root.go b/internal/command/admin/root.go new file mode 100644 index 0000000..17874ba --- /dev/null +++ b/internal/command/admin/root.go @@ -0,0 +1,18 @@ +package admin + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer" + "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy" + "github.com/urfave/cli/v2" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "admin", + Usage: "Admin related commands", + Subcommands: []*cli.Command{ + proxy.Root(), + layer.Root(), + }, + } +} diff --git a/internal/command/auth/create_token.go b/internal/command/auth/create_token.go new file mode 100644 index 0000000..d639ac9 --- /dev/null +++ b/internal/command/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/auth/root.go b/internal/command/auth/root.go new file mode 100644 index 0000000..ccb554a --- /dev/null +++ b/internal/command/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/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/server/admin/root.go b/internal/command/server/admin/root.go new file mode 100644 index 0000000..8998d46 --- /dev/null +++ b/internal/command/server/admin/root.go @@ -0,0 +1,15 @@ +package admin + +import ( + "github.com/urfave/cli/v2" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "admin", + Usage: "Admin server related commands", + Subcommands: []*cli.Command{ + RunCommand(), + }, + } +} diff --git a/internal/command/server/admin/run.go b/internal/command/server/admin/run.go new file mode 100644 index 0000000..99b568e --- /dev/null +++ b/internal/command/server/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/server/proxy/root.go b/internal/command/server/proxy/root.go new file mode 100644 index 0000000..0dc25db --- /dev/null +++ b/internal/command/server/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/server/proxy/run.go b/internal/command/server/proxy/run.go new file mode 100644 index 0000000..82ad72b --- /dev/null +++ b/internal/command/server/proxy/run.go @@ -0,0 +1,87 @@ +package proxy + +import ( + "context" + "fmt" + "strings" + + "forge.cadoles.com/cadoles/bouncer/internal/command/common" + "forge.cadoles.com/cadoles/bouncer/internal/config" + "forge.cadoles.com/cadoles/bouncer/internal/proxy" + "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" + "forge.cadoles.com/cadoles/bouncer/internal/queue" + "forge.cadoles.com/cadoles/bouncer/internal/setup" + "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)) + + layers, err := initDirectorLayers(ctx.Context, conf) + if err != nil { + return errors.Wrap(err, "could not initialize director layers") + } + + srv := proxy.NewServer( + proxy.WithServerConfig(conf.Proxy), + proxy.WithRedisConfig(conf.Redis), + proxy.WithDirectorLayers(layers...), + ) + + 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 + }, + } +} + +func initDirectorLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) { + layers := make([]director.Layer, 0) + + queue, err := initQueueLayer(ctx, conf) + if err != nil { + return nil, errors.Wrap(err, "could not initialize queue layer") + } + + layers = append(layers, queue) + + return layers, nil +} + +func initQueueLayer(ctx context.Context, conf *config.Config) (*queue.Queue, error) { + adapter, err := setup.NewQueueAdapter(ctx, conf.Redis) + if err != nil { + return nil, errors.WithStack(err) + } + + return queue.New(adapter), nil +} diff --git a/internal/command/server/root.go b/internal/command/server/root.go new file mode 100644 index 0000000..2d5d5ce --- /dev/null +++ b/internal/command/server/root.go @@ -0,0 +1,18 @@ +package server + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/command/server/admin" + "forge.cadoles.com/cadoles/bouncer/internal/command/server/proxy" + "github.com/urfave/cli/v2" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "server", + Usage: "Server related commands", + Subcommands: []*cli.Command{ + proxy.Root(), + admin.Root(), + }, + } +} 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/director/context.go b/internal/proxy/director/context.go new file mode 100644 index 0000000..1b00a5e --- /dev/null +++ b/internal/proxy/director/context.go @@ -0,0 +1,60 @@ +package director + +import ( + "context" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +type contextKey string + +const ( + contextKeyProxy contextKey = "proxy" + contextKeyLayers contextKey = "layers" +) + +var ( + errContextKeyNotFound = errors.New("context key not found") + errUnexpectedContextValue = errors.New("unexpected context value") +) + +func withProxy(ctx context.Context, proxy *store.Proxy) context.Context { + return context.WithValue(ctx, contextKeyProxy, proxy) +} + +func ctxProxy(ctx context.Context) (*store.Proxy, error) { + proxy, err := ctxValue[*store.Proxy](ctx, contextKeyProxy) + if err != nil { + return nil, errors.WithStack(err) + } + + return proxy, nil +} + +func withLayers(ctx context.Context, layers []*store.Layer) context.Context { + return context.WithValue(ctx, contextKeyLayers, layers) +} + +func ctxLayers(ctx context.Context) ([]*store.Layer, error) { + layers, err := ctxValue[[]*store.Layer](ctx, contextKeyLayers) + if err != nil { + return nil, errors.WithStack(err) + } + + return layers, nil +} + +func ctxValue[T any](ctx context.Context, key contextKey) (T, error) { + raw := ctx.Value(key) + if raw == nil { + return *new(T), errors.WithStack(errContextKeyNotFound) + } + + value, ok := raw.(T) + if !ok { + return *new(T), errors.WithStack(errUnexpectedContextValue) + } + + return value, nil +} diff --git a/internal/proxy/director/director.go b/internal/proxy/director/director.go new file mode 100644 index 0000000..6d5749d --- /dev/null +++ b/internal/proxy/director/director.go @@ -0,0 +1,231 @@ +package director + +import ( + "context" + "net/http" + "net/url" + "sort" + + "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 { + proxyRepository store.ProxyRepository + layerRepository store.LayerRepository + layerRegistry *LayerRegistry +} + +func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) { + ctx := r.Context() + + proxies, err := d.getProxies(ctx) + if err != nil { + return r, errors.WithStack(err) + } + + var match *store.Proxy + +MAIN: + for _, p := range proxies { + for _, from := range p.From { + if matches := wildcard.Match(r.Host, from); !matches { + continue + } + + match = p + break MAIN + } + } + + if match == nil { + return r, nil + } + + toURL, err := url.Parse(match.To) + if err != nil { + return r, errors.WithStack(err) + } + + r.URL.Host = toURL.Host + r.URL.Scheme = toURL.Scheme + + ctx = logger.With(ctx, + logger.F("proxy", match.Name), + logger.F("host", r.Host), + logger.F("remoteAddr", r.RemoteAddr), + ) + + ctx = withProxy(ctx, match) + + layers, err := d.getLayers(ctx, match.Name) + if err != nil { + return r, errors.WithStack(err) + } + + ctx = withLayers(ctx, layers) + r = r.WithContext(ctx) + + return r, nil +} + +func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) { + headers, err := d.proxyRepository.QueryProxy(ctx, store.WithProxyQueryEnabled(true)) + if err != nil { + return nil, errors.WithStack(err) + } + + sort.Sort(store.ByProxyWeight(headers)) + + proxies := make([]*store.Proxy, 0, len(headers)) + + for _, h := range headers { + if !h.Enabled { + continue + } + + proxy, err := d.proxyRepository.GetProxy(ctx, h.Name) + if err != nil { + return nil, errors.WithStack(err) + } + + proxies = append(proxies, proxy) + } + + return proxies, nil +} + +func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]*store.Layer, error) { + headers, err := d.layerRepository.QueryLayers(ctx, proxyName, store.WithLayerQueryEnabled(true)) + if err != nil { + return nil, errors.WithStack(err) + } + + sort.Sort(store.ByLayerWeight(headers)) + + layers := make([]*store.Layer, 0, len(headers)) + + for _, h := range headers { + if !h.Enabled { + continue + } + + layer, err := d.layerRepository.GetLayer(ctx, proxyName, h.Name) + if err != nil { + return nil, errors.WithStack(err) + } + + layers = append(layers, layer) + } + + return layers, nil +} + +func (d *Director) RequestTransformer() proxy.RequestTransformer { + return func(r *http.Request) { + ctx := r.Context() + layers, err := ctxLayers(ctx) + if err != nil { + if errors.Is(err, errContextKeyNotFound) { + return + } + + logger.Error(ctx, "could not retrieve layers from context", logger.E(errors.WithStack(err))) + + return + } + + for _, layer := range layers { + transformerLayer, ok := d.layerRegistry.GetRequestTransformer(layer.Type) + if !ok { + continue + } + + transformer := transformerLayer.RequestTransformer(layer) + + transformer(r) + } + } +} + +func (d *Director) ResponseTransformer() proxy.ResponseTransformer { + return func(r *http.Response) error { + ctx := r.Request.Context() + layers, err := ctxLayers(ctx) + if err != nil { + if errors.Is(err, errContextKeyNotFound) { + return nil + } + + return errors.WithStack(err) + } + + for _, layer := range layers { + transformerLayer, ok := d.layerRegistry.GetResponseTransformer(layer.Type) + if !ok { + continue + } + + transformer := transformerLayer.ResponseTransformer(layer) + + if err := transformer(r); err != nil { + return errors.WithStack(err) + } + } + + return nil + } +} + +func (d *Director) Middleware() proxy.Middleware { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + r, err := d.rewriteRequest(r) + if 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 + } + + ctx := r.Context() + + layers, err := ctxLayers(ctx) + if err != nil { + if errors.Is(err, errContextKeyNotFound) { + return + } + + logger.Error(ctx, "could not retrieve proxy and layers from context", logger.E(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + httpMiddlewares := make([]proxy.Middleware, 0) + for _, layer := range layers { + middleware, ok := d.layerRegistry.GetMiddleware(layer.Type) + if !ok { + continue + } + + httpMiddlewares = append(httpMiddlewares, middleware.Middleware(layer)) + } + + handler := createMiddlewareChain(next, httpMiddlewares) + + handler.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} + +func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, layers ...Layer) *Director { + registry := NewLayerRegistry(layers...) + + return &Director{proxyRepository, layerRepository, registry} +} diff --git a/internal/proxy/director/layer_registry.go b/internal/proxy/director/layer_registry.go new file mode 100644 index 0000000..b7081c7 --- /dev/null +++ b/internal/proxy/director/layer_registry.go @@ -0,0 +1,104 @@ +package director + +import ( + "forge.cadoles.com/Cadoles/go-proxy" + "forge.cadoles.com/cadoles/bouncer/internal/store" +) + +type Layer interface { + LayerType() store.LayerType +} + +type MiddlewareLayer interface { + Layer + Middleware(layer *store.Layer) proxy.Middleware +} + +type RequestTransformerLayer interface { + Layer + RequestTransformer(layer *store.Layer) proxy.RequestTransformer +} + +type ResponseTransformerLayer interface { + Layer + ResponseTransformer(layer *store.Layer) proxy.ResponseTransformer +} + +type LayerRegistry struct { + index map[store.LayerType]Layer +} + +func (r *LayerRegistry) GetLayer(layerType store.LayerType) (Layer, bool) { + layer, exists := r.index[layerType] + if !exists { + return nil, false + } + + return layer, true +} + +func (r *LayerRegistry) getLayerAsAny(layerType store.LayerType) (any, bool) { + return r.GetLayer(layerType) +} + +func (r *LayerRegistry) GetMiddleware(layerType store.LayerType) (MiddlewareLayer, bool) { + layer, exists := r.getLayerAsAny(layerType) + if !exists { + return nil, false + } + + middleware, ok := layer.(MiddlewareLayer) + if !ok { + return nil, false + } + + return middleware, true +} + +func (r *LayerRegistry) GetResponseTransformer(layerType store.LayerType) (ResponseTransformerLayer, bool) { + layer, exists := r.getLayerAsAny(layerType) + if !exists { + return nil, false + } + + transformer, ok := layer.(ResponseTransformerLayer) + if !ok { + return nil, false + } + + return transformer, true +} + +func (r *LayerRegistry) GetRequestTransformer(layerType store.LayerType) (RequestTransformerLayer, bool) { + layer, exists := r.getLayerAsAny(layerType) + if !exists { + return nil, false + } + + transformer, ok := layer.(RequestTransformerLayer) + if !ok { + return nil, false + } + + return transformer, true +} + +func (r *LayerRegistry) Load(layers ...Layer) { + index := make(map[store.LayerType]Layer) + + for _, l := range layers { + layerType := l.LayerType() + index[layerType] = l + } + + r.index = index +} + +func NewLayerRegistry(layers ...Layer) *LayerRegistry { + registry := &LayerRegistry{ + index: make(map[store.LayerType]Layer), + } + registry.Load(layers...) + + return registry +} diff --git a/internal/proxy/director/util.go b/internal/proxy/director/util.go new file mode 100644 index 0000000..ca0f30d --- /dev/null +++ b/internal/proxy/director/util.go @@ -0,0 +1,18 @@ +package director + +import ( + "net/http" + + "forge.cadoles.com/Cadoles/go-proxy" + "forge.cadoles.com/Cadoles/go-proxy/util" +) + +func createMiddlewareChain(handler http.Handler, middlewares []proxy.Middleware) http.Handler { + util.Reverse(middlewares) + + for _, m := range middlewares { + handler = m(handler) + } + + return handler +} diff --git a/internal/proxy/init.go b/internal/proxy/init.go new file mode 100644 index 0000000..099606f --- /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.initProxyRepository(ctx); err != nil { + return errors.WithStack(err) + } + + if err := s.initLayerRepository(ctx); err != nil { + return errors.WithStack(err) + } + + 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 +} + +func (s *Server) initLayerRepository(ctx context.Context) error { + layerRepository, err := setup.NewLayerRepository(ctx, s.redisConfig) + if err != nil { + return errors.WithStack(err) + } + + s.layerRepository = layerRepository + + return nil +} diff --git a/internal/proxy/option.go b/internal/proxy/option.go new file mode 100644 index 0000000..e9b434b --- /dev/null +++ b/internal/proxy/option.go @@ -0,0 +1,40 @@ +package proxy + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/config" + "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" +) + +type Option struct { + ServerConfig config.ProxyServerConfig + RedisConfig config.RedisConfig + DirectorLayers []director.Layer +} + +type OptionFunc func(*Option) + +func defaultOption() *Option { + return &Option{ + ServerConfig: config.NewDefaultProxyServerConfig(), + RedisConfig: config.NewDefaultRedisConfig(), + DirectorLayers: make([]director.Layer, 0), + } +} + +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 + } +} + +func WithDirectorLayers(layers ...director.Layer) OptionFunc { + return func(opt *Option) { + opt.DirectorLayers = layers + } +} diff --git a/internal/proxy/server.go b/internal/proxy/server.go new file mode 100644 index 0000000..98c2ef6 --- /dev/null +++ b/internal/proxy/server.go @@ -0,0 +1,118 @@ +package proxy + +import ( + "context" + "fmt" + "log" + "net" + "net/http" + + "forge.cadoles.com/Cadoles/go-proxy" + bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi" + "forge.cadoles.com/cadoles/bouncer/internal/config" + "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" + "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 + directorLayers []director.Layer + proxyRepository store.ProxyRepository + layerRepository store.LayerRepository +} + +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() + + logger.Info(ctx, "http server listening") + + director := director.New( + s.proxyRepository, + s.layerRepository, + s.directorLayers..., + ) + + router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter())) + router.Use(director.Middleware()) + + handler := proxy.New( + proxy.WithRequestTransformers( + director.RequestTransformer(), + ), + proxy.WithResponseTransformers( + director.ResponseTransformer(), + ), + ) + + 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, + directorLayers: opt.DirectorLayers, + } +} diff --git a/internal/queue/adapter.go b/internal/queue/adapter.go new file mode 100644 index 0000000..748694e --- /dev/null +++ b/internal/queue/adapter.go @@ -0,0 +1,16 @@ +package queue + +import ( + "context" + "time" +) + +type Status struct { + Sessions int64 +} + +type Adapter interface { + Touch(ctx context.Context, queueName string, sessionId string) (int64, error) + Status(ctx context.Context, queueName string) (*Status, error) + Refresh(ctx context.Context, queueName string, keepAlive time.Duration) error +} diff --git a/internal/queue/layer_options.go b/internal/queue/layer_options.go new file mode 100644 index 0000000..5cc9d9e --- /dev/null +++ b/internal/queue/layer_options.go @@ -0,0 +1,48 @@ +package queue + +import ( + "reflect" + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +type LayerOptions struct { + Capacity int64 `mapstructure:"capacity"` + Matchers []string `mapstructure:"matchers"` + KeepAlive time.Duration `mapstructure:"keepAlive"` +} + +func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) { + layerOptions := LayerOptions{ + Capacity: 1000, + Matchers: []string{"*"}, + KeepAlive: 30 * time.Second, + } + + config := mapstructure.DecoderConfig{ + DecodeHook: stringToDurationHook, + Result: &layerOptions, + } + + decoder, err := mapstructure.NewDecoder(&config) + if err != nil { + return nil, err + } + + if err := decoder.Decode(storeOptions); err != nil { + return nil, errors.WithStack(err) + } + + return &layerOptions, nil +} + +func stringToDurationHook(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { + if t == reflect.TypeOf(*new(time.Duration)) && f == reflect.TypeOf("") { + return time.ParseDuration(data.(string)) + } + + return data, nil +} 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..47e1c4c --- /dev/null +++ b/internal/queue/queue.go @@ -0,0 +1,143 @@ +package queue + +import ( + "context" + "fmt" + "net/http" + "sync/atomic" + "time" + + "forge.cadoles.com/Cadoles/go-proxy" + "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/google/uuid" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +const LayerType store.LayerType = "queue" + +type Queue struct { + adapter Adapter + refreshJobRunning uint32 +} + +// LayerType implements director.MiddlewareLayer +func (q *Queue) LayerType() store.LayerType { + return LayerType +} + +// Middleware implements director.MiddlewareLayer +func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware { + return func(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + options, err := fromStoreOptions(layer.Options) + if err != nil { + logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + cookieName := q.getCookieName(layer.Name) + + cookie, err := r.Cookie(cookieName) + if err != nil && !errors.Is(err, http.ErrNoCookie) { + logger.Error(ctx, "could not retrieve cookie", logger.E(errors.WithStack(err))) + } + + if cookie == nil { + cookie = &http.Cookie{ + Name: cookieName, + Value: uuid.NewString(), + Path: "/", + } + + w.Header().Add("Set-Cookie", cookie.String()) + } + + sessionID := cookie.Value + queueName := string(layer.Name) + + q.refreshQueue(queueName, options.KeepAlive) + + rank, err := q.adapter.Touch(ctx, queueName, sessionID) + if err != nil { + logger.Error(ctx, "could not retrieve session rank", logger.E(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + if rank >= options.Capacity { + q.renderQueuePage(w, r, queueName, options, rank) + + return + } + + ctx = logger.With(ctx, + logger.F("queueSessionId", sessionID), + logger.F("queueName", queueName), + logger.F("queueSessionRank", rank), + ) + r = r.WithContext(ctx) + + h.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} + +func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueName string, options *LayerOptions, rank int64) { + ctx := r.Context() + + status, err := q.adapter.Status(ctx, queueName) + if err != nil { + logger.Error(ctx, "could not retrieve queue status", logger.E(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + http.Error(w, fmt.Sprintf("queued (rank: %d, status: %d/%d)", rank+1, status.Sessions, options.Capacity), http.StatusServiceUnavailable) +} + +func (q *Queue) refreshQueue(queueName string, keepAlive time.Duration) { + if !atomic.CompareAndSwapUint32(&q.refreshJobRunning, 0, 1) { + return + } + + go func() { + defer atomic.StoreUint32(&q.refreshJobRunning, 0) + + ctx, cancel := context.WithTimeout(context.Background(), keepAlive*2) + defer cancel() + + if err := q.adapter.Refresh(ctx, queueName, keepAlive); err != nil { + logger.Error(ctx, "could not refresh queue", + logger.E(errors.WithStack(err)), + logger.F("queue", queueName), + ) + } + }() +} + +func (q *Queue) getCookieName(layerName store.LayerName) string { + return fmt.Sprintf("_%s_%s", LayerType, layerName) +} + +func New(adapter Adapter, funcs ...OptionFunc) *Queue { + opts := defaultOptions() + for _, fn := range funcs { + fn(opts) + } + + return &Queue{ + adapter: adapter, + } +} + +var _ director.MiddlewareLayer = &Queue{} diff --git a/internal/queue/redis/adapter.go b/internal/queue/redis/adapter.go new file mode 100644 index 0000000..7bb282e --- /dev/null +++ b/internal/queue/redis/adapter.go @@ -0,0 +1,167 @@ +package redis + +import ( + "context" + "strconv" + "strings" + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/queue" + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +const ( + keyPrefixQueue = "queue" +) + +type Adapter struct { + client redis.UniversalClient + txMaxRetry int +} + +// Refresh implements queue.Adapter +func (a *Adapter) Refresh(ctx context.Context, queueName string, keepAlive time.Duration) error { + lastSeenKey := lastSeenKey(queueName) + rankKey := rankKey(queueName) + + err := withTx(ctx, a.client, func(ctx context.Context, tx *redis.Tx) error { + expires := time.Now().UTC().Add(-keepAlive) + + cmd := tx.ZRangeByScore(ctx, lastSeenKey, &redis.ZRangeBy{ + Min: "0", + Max: strconv.FormatInt(expires.Unix(), 10), + }) + + members, err := cmd.Result() + if err != nil { + return errors.WithStack(err) + } + + if len(members) == 0 { + return nil + } + + anyMembers := make([]any, len(members)) + for i, m := range members { + anyMembers[i] = m + } + + if err := tx.ZRem(ctx, rankKey, anyMembers...).Err(); err != nil { + return errors.WithStack(err) + } + + if err := tx.ZRem(ctx, lastSeenKey, anyMembers...).Err(); err != nil { + return errors.WithStack(err) + } + + return nil + }, rankKey, lastSeenKey) + if err != nil { + return errors.WithStack(err) + } + + return nil +} + +// Touch implements queue.Adapter +func (a *Adapter) Touch(ctx context.Context, queueName string, sessionId string) (int64, error) { + lastSeenKey := lastSeenKey(queueName) + rankKey := rankKey(queueName) + + var rank int64 + + retry := a.txMaxRetry + + for retry > 0 { + err := withTx(ctx, a.client, func(ctx context.Context, tx *redis.Tx) error { + now := time.Now().UTC().Unix() + + err := tx.ZAddNX(ctx, rankKey, redis.Z{Score: float64(now), Member: sessionId}).Err() + if err != nil { + return errors.WithStack(err) + } + + err = tx.ZAdd(ctx, lastSeenKey, redis.Z{Score: float64(now), Member: sessionId}).Err() + if err != nil { + return errors.WithStack(err) + } + + val, err := tx.ZRank(ctx, rankKey, sessionId).Result() + if err != nil { + return errors.WithStack(err) + } + + rank = val + + return nil + }, rankKey, lastSeenKey) + if err != nil { + if errors.Is(err, redis.Nil) && retry > 0 { + retry-- + + continue + } + + return 0, errors.WithStack(err) + } + + break + } + + return rank, nil +} + +// Status implements queue.Adapter +func (a *Adapter) Status(ctx context.Context, queueName string) (*queue.Status, error) { + rankKey := rankKey(queueName) + + status := &queue.Status{} + + cmd := a.client.ZCard(ctx, rankKey) + if err := cmd.Err(); err != nil { + return nil, errors.WithStack(err) + } + + status.Sessions = cmd.Val() + + return status, nil +} + +func NewAdapter(client redis.UniversalClient, txMaxRetry int) *Adapter { + return &Adapter{ + client: client, + txMaxRetry: txMaxRetry, + } +} + +var _ queue.Adapter = &Adapter{} + +func key(parts ...string) string { + return strings.Join(parts, ":") +} + +func rankKey(queueName string) string { + return key(keyPrefixQueue, queueName, "rank") +} + +func lastSeenKey(queueName string) string { + return key(keyPrefixQueue, queueName, "last_seen") +} + +func withTx(ctx context.Context, client redis.UniversalClient, fn func(ctx context.Context, tx *redis.Tx) error, keys ...string) error { + txf := func(tx *redis.Tx) error { + if err := fn(ctx, tx); err != nil { + return errors.WithStack(err) + } + + return nil + } + + err := client.Watch(ctx, txf, keys...) + if err != nil { + return errors.WithStack(err) + } + + return nil +} diff --git a/internal/queue/schema.go b/internal/queue/schema.go new file mode 100644 index 0000000..941a8ba --- /dev/null +++ b/internal/queue/schema.go @@ -0,0 +1,20 @@ +package queue + +import ( + _ "embed" + + "forge.cadoles.com/cadoles/bouncer/internal/schema" + "github.com/pkg/errors" +) + +//go:embed schema/layer-options.json +var rawLayerOptionsSchema []byte + +func init() { + layerOptionsSchema, err := schema.Parse(rawLayerOptionsSchema) + if err != nil { + panic(errors.Wrap(err, "could not parse queue layer options schema")) + } + + schema.RegisterLayerOptionsSchema(LayerType, layerOptionsSchema) +} diff --git a/internal/queue/schema/layer-options.json b/internal/queue/schema/layer-options.json new file mode 100644 index 0000000..5f20bf7 --- /dev/null +++ b/internal/queue/schema/layer-options.json @@ -0,0 +1,21 @@ +{ + "$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/queue-layer-options", + "title": "Queue layer options", + "type": "object", + "properties": { + "capacity": { + "type": "number", + "minimum": 0 + }, + "matchers": { + "type": "array", + "items": { + "type": "string" + } + }, + "keepAlive": { + "type": "string" + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/internal/schema/error.go b/internal/schema/error.go new file mode 100644 index 0000000..d1a1fee --- /dev/null +++ b/internal/schema/error.go @@ -0,0 +1,33 @@ +package schema + +import ( + "errors" + "fmt" + + "github.com/qri-io/jsonschema" +) + +var ( + ErrSchemaNotFound = errors.New("schema not found") + ErrInvalidData = errors.New("invalid data") +) + +type InvalidDataError struct { + keyErrors []jsonschema.KeyError +} + +func (e *InvalidDataError) Is(err error) bool { + return err == ErrInvalidData +} + +func (e *InvalidDataError) Error() string { + return fmt.Sprintf("%s: %s", ErrInvalidData.Error(), e.keyErrors) +} + +func (e *InvalidDataError) KeyErrors() []jsonschema.KeyError { + return e.keyErrors +} + +func NewInvalidDataError(keyErrors ...jsonschema.KeyError) *InvalidDataError { + return &InvalidDataError{keyErrors} +} diff --git a/internal/schema/load.go b/internal/schema/load.go new file mode 100644 index 0000000..31b924e --- /dev/null +++ b/internal/schema/load.go @@ -0,0 +1,17 @@ +package schema + +import ( + "encoding/json" + + "github.com/pkg/errors" + "github.com/qri-io/jsonschema" +) + +func Parse(data []byte) (*jsonschema.Schema, error) { + var schema jsonschema.Schema + if err := json.Unmarshal(data, &schema); err != nil { + return nil, errors.WithStack(err) + } + + return &schema, nil +} diff --git a/internal/schema/registry.go b/internal/schema/registry.go new file mode 100644 index 0000000..b243b41 --- /dev/null +++ b/internal/schema/registry.go @@ -0,0 +1,56 @@ +package schema + +import ( + "context" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" + "github.com/qri-io/jsonschema" +) + +var defaultRegistry = NewRegistry() + +func RegisterLayerOptionsSchema(layerType store.LayerType, schema *jsonschema.Schema) { + defaultRegistry.RegisterLayerOptionsSchema(layerType, schema) +} + +func ValidateLayerOptions(ctx context.Context, layerType store.LayerType, options *store.LayerOptions) error { + if err := defaultRegistry.ValidateLayerOptions(ctx, layerType, options); err != nil { + return errors.WithStack(err) + } + + return nil +} + +type Registry struct { + layerOptionSchemas map[store.LayerType]*jsonschema.Schema +} + +func (r *Registry) RegisterLayerOptionsSchema(layerType store.LayerType, schema *jsonschema.Schema) { + r.layerOptionSchemas[layerType] = schema +} + +func (r *Registry) ValidateLayerOptions(ctx context.Context, layerType store.LayerType, options *store.LayerOptions) error { + schema, exists := r.layerOptionSchemas[layerType] + if !exists { + return errors.WithStack(ErrSchemaNotFound) + } + + rawOptions := func(opts *store.LayerOptions) map[string]any { + return *opts + }(options) + + state := schema.Validate(ctx, rawOptions) + + if len(*state.Errs) > 0 { + return errors.WithStack(NewInvalidDataError(*state.Errs...)) + } + + return nil +} + +func NewRegistry() *Registry { + return &Registry{ + layerOptionSchemas: make(map[store.LayerType]*jsonschema.Schema), + } +} diff --git a/internal/setup/proxy_repository.go b/internal/setup/proxy_repository.go new file mode 100644 index 0000000..3ebc6f5 --- /dev/null +++ b/internal/setup/proxy_repository.go @@ -0,0 +1,28 @@ +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 +} + +func NewLayerRepository(ctx context.Context, conf config.RedisConfig) (store.LayerRepository, error) { + rdb := redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: conf.Adresses, + MasterName: string(conf.Master), + }) + + return redisStore.NewLayerRepository(rdb), nil +} diff --git a/internal/setup/queue_repository.go b/internal/setup/queue_repository.go new file mode 100644 index 0000000..51a73b9 --- /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 NewQueueAdapter(ctx context.Context, conf config.RedisConfig) (queue.Adapter, error) { + rdb := redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: conf.Adresses, + MasterName: string(conf.Master), + }) + + return queueRedis.NewAdapter(rdb, 2), nil +} diff --git a/internal/store/error.go b/internal/store/error.go new file mode 100644 index 0000000..a0e186b --- /dev/null +++ b/internal/store/error.go @@ -0,0 +1,8 @@ +package store + +import "errors" + +var ( + ErrAlreadyExist = errors.New("already exist") + ErrNotFound = errors.New("not found") +) diff --git a/internal/store/layer.go b/internal/store/layer.go new file mode 100644 index 0000000..e08a613 --- /dev/null +++ b/internal/store/layer.go @@ -0,0 +1,25 @@ +package store + +import "time" + +type ( + LayerName Name + LayerType string +) + +type LayerHeader struct { + Proxy ProxyName `json:"proxy"` + Name LayerName `json:"name"` + Type LayerType `json:"type"` + + Weight int `json:"weight"` + Enabled bool `json:"enabled"` +} + +type Layer struct { + LayerHeader + + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Options LayerOptions `json:"options"` +} diff --git a/internal/store/layer_repository.go b/internal/store/layer_repository.go new file mode 100644 index 0000000..1cd75ad --- /dev/null +++ b/internal/store/layer_repository.go @@ -0,0 +1,78 @@ +package store + +import ( + "context" +) + +type LayerOptions map[string]any + +type LayerRepository interface { + CreateLayer(ctx context.Context, proxyName ProxyName, layerName LayerName, layerType LayerType, options LayerOptions) (*Layer, error) + UpdateLayer(ctx context.Context, proxyName ProxyName, layerName LayerName, funcs ...UpdateLayerOptionFunc) (*Layer, error) + DeleteLayer(ctx context.Context, proxyName ProxyName, layerName LayerName) error + GetLayer(ctx context.Context, proxyName ProxyName, layerName LayerName) (*Layer, error) + QueryLayers(ctx context.Context, proxyName ProxyName, funcs ...QueryLayerOptionFunc) ([]*LayerHeader, error) +} + +type QueryLayerOptionFunc func(*QueryLayerOptions) + +type QueryLayerOptions struct { + Type *LayerType + Name *LayerName + Enabled *bool +} + +func DefaultQueryLayerOptions() *QueryLayerOptions { + funcs := []QueryLayerOptionFunc{} + + opts := &QueryLayerOptions{} + for _, fn := range funcs { + fn(opts) + } + + return opts +} + +func WithLayerQueryType(layerType LayerType) QueryLayerOptionFunc { + return func(o *QueryLayerOptions) { + o.Type = &layerType + } +} + +func WithLayerQueryName(layerName LayerName) QueryLayerOptionFunc { + return func(o *QueryLayerOptions) { + o.Name = &layerName + } +} + +func WithLayerQueryEnabled(enabled bool) QueryLayerOptionFunc { + return func(o *QueryLayerOptions) { + o.Enabled = &enabled + } +} + +type UpdateLayerOptionFunc func(*UpdateLayerOptions) + +type UpdateLayerOptions struct { + Enabled *bool + Weight *int + Options *LayerOptions +} + +func WithLayerUpdateEnabled(enabled bool) UpdateLayerOptionFunc { + return func(o *UpdateLayerOptions) { + o.Enabled = &enabled + } +} + +func WithLayerUpdateWeight(weight int) UpdateLayerOptionFunc { + return func(o *UpdateLayerOptions) { + o.Weight = &weight + } +} + +func WithLayerUpdateOptions(options LayerOptions) UpdateLayerOptionFunc { + return func(o *UpdateLayerOptions) { + o.Options = &options + } +} diff --git a/internal/store/name.go b/internal/store/name.go new file mode 100644 index 0000000..6c314c2 --- /dev/null +++ b/internal/store/name.go @@ -0,0 +1,14 @@ +package store + +import "github.com/pkg/errors" + +type Name string + +var ErrEmptyName = errors.New("name cannot be empty") + +func ValidateName(name string) (Name, error) { + if name == "" { + return "", errors.WithStack(ErrEmptyName) + } + return Name(name), nil +} diff --git a/internal/store/proxy.go b/internal/store/proxy.go new file mode 100644 index 0000000..3ed85f9 --- /dev/null +++ b/internal/store/proxy.go @@ -0,0 +1,22 @@ +package store + +import ( + "time" +) + +type ProxyName Name + +type ProxyHeader struct { + Name ProxyName `json:"name"` + + Weight int `json:"weight"` + Enabled bool `json:"enabled"` +} + +type Proxy struct { + ProxyHeader + To string `json:"to"` + From []string `json:"from"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/internal/store/proxy_repository.go b/internal/store/proxy_repository.go new file mode 100644 index 0000000..b4fe691 --- /dev/null +++ b/internal/store/proxy_repository.go @@ -0,0 +1,91 @@ +package store + +import ( + "context" + "net/url" +) + +type ProxyRepository interface { + CreateProxy(ctx context.Context, name ProxyName, to string, from ...string) (*Proxy, error) + UpdateProxy(ctx context.Context, name ProxyName, funcs ...UpdateProxyOptionFunc) (*Proxy, error) + QueryProxy(ctx context.Context, funcs ...QueryProxyOptionFunc) ([]*ProxyHeader, error) + GetProxy(ctx context.Context, name ProxyName) (*Proxy, error) + DeleteProxy(ctx context.Context, name ProxyName) error +} + +type UpdateProxyOptionFunc func(*UpdateProxyOptions) + +type UpdateProxyOptions struct { + To *string + From []string + Enabled *bool + Weight *int +} + +func WithProxyUpdateEnabled(enabled bool) UpdateProxyOptionFunc { + return func(o *UpdateProxyOptions) { + o.Enabled = &enabled + } +} + +func WithProxyUpdateWeight(weight int) UpdateProxyOptionFunc { + return func(o *UpdateProxyOptions) { + o.Weight = &weight + } +} + +func WithProxyUpdateTo(to string) 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 + Names []ProxyName + Enabled *bool + From []string +} + +func DefaultQueryProxyOptions() *QueryProxyOptions { + funcs := []QueryProxyOptionFunc{} + + opts := &QueryProxyOptions{} + for _, fn := range funcs { + fn(opts) + } + + return opts +} + +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 WithProxyQueryNames(names ...ProxyName) QueryProxyOptionFunc { + return func(o *QueryProxyOptions) { + o.Names = names + } +} + +func WithProxyQueryEnabled(enabled bool) QueryProxyOptionFunc { + return func(o *QueryProxyOptions) { + o.Enabled = &enabled + } +} diff --git a/internal/store/redis/helper.go b/internal/store/redis/helper.go new file mode 100644 index 0000000..627a767 --- /dev/null +++ b/internal/store/redis/helper.go @@ -0,0 +1,93 @@ +package redis + +import ( + "context" + "encoding/json" + "strings" + + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +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]) UnmarshalText(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 key(parts ...string) string { + return strings.Join(parts, ":") +} + +func WithTx(ctx context.Context, client redis.UniversalClient, key string, fn func(ctx context.Context, tx *redis.Tx) error) error { + txf := func(tx *redis.Tx) error { + if err := fn(ctx, tx); err != nil { + return errors.WithStack(err) + } + + return nil + } + + err := client.Watch(ctx, txf, key) + if err != nil { + return errors.WithStack(err) + } + + return nil +} + +func contains[T ~string](values []T, v T) bool { + for _, vv := range values { + if vv == v { + return true + } + } + + return false +} diff --git a/internal/store/redis/layer_item.go b/internal/store/redis/layer_item.go new file mode 100644 index 0000000..68a5269 --- /dev/null +++ b/internal/store/redis/layer_item.go @@ -0,0 +1,62 @@ +package redis + +import ( + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +type layerHeaderItem struct { + Proxy string `redis:"proxy"` + Name string `redis:"name"` + Type string `redis:"type"` + + Weight int `redis:"weight"` + Enabled bool `redis:"enabled"` +} + +func (i *layerHeaderItem) ToLayerHeader() (*store.LayerHeader, error) { + layerHeader := &store.LayerHeader{ + Proxy: store.ProxyName(i.Proxy), + Name: store.LayerName(i.Name), + Type: store.LayerType(i.Type), + Weight: i.Weight, + Enabled: i.Enabled, + } + + return layerHeader, nil +} + +type layerItem struct { + layerHeaderItem + Options *jsonWrapper[store.LayerOptions] `redis:"options"` + + CreatedAt *jsonWrapper[time.Time] `redis:"created_at"` + UpdatedAt *jsonWrapper[time.Time] `redis:"updated_at"` +} + +func (i *layerItem) ToLayer() (*store.Layer, error) { + layerHeader, err := i.layerHeaderItem.ToLayerHeader() + if err != nil { + return nil, errors.WithStack(err) + } + + layer := &store.Layer{ + LayerHeader: *layerHeader, + } + + if i.Options != nil { + layer.Options = i.Options.Value() + } + + if i.CreatedAt != nil { + layer.CreatedAt = i.CreatedAt.Value() + } + + if i.UpdatedAt != nil { + layer.UpdatedAt = i.UpdatedAt.Value() + } + + return layer, nil +} diff --git a/internal/store/redis/layer_repository.go b/internal/store/redis/layer_repository.go new file mode 100644 index 0000000..1b94db7 --- /dev/null +++ b/internal/store/redis/layer_repository.go @@ -0,0 +1,256 @@ +package redis + +import ( + "context" + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +const ( + keyPrefixLayer = "layer" +) + +type LayerRepository struct { + client redis.UniversalClient +} + +// CreateLayer implements store.LayerRepository +func (r *LayerRepository) CreateLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, layerType store.LayerType, options store.LayerOptions) (*store.Layer, error) { + now := time.Now().UTC() + key := layerKey(proxyName, layerName) + + layerItem := &layerItem{ + layerHeaderItem: layerHeaderItem{ + Proxy: string(proxyName), + Name: string(layerName), + Type: string(layerType), + Weight: 0, + Enabled: false, + }, + + CreatedAt: wrap(now), + UpdatedAt: wrap(now), + Options: wrap(store.LayerOptions{}), + } + + txf := func(tx *redis.Tx) error { + exists, err := tx.Exists(ctx, key).Uint64() + if err != nil { + return errors.WithStack(err) + } + + if exists > 0 { + return errors.WithStack(store.ErrAlreadyExist) + } + + _, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error { + p.HMSet(ctx, key, &layerItem.layerHeaderItem) + p.HMSet(ctx, key, layerItem) + + return nil + }) + + if err != nil { + return errors.WithStack(err) + } + + return nil + } + + err := r.client.Watch(ctx, txf, key) + if err != nil { + return nil, errors.WithStack(err) + } + + return &store.Layer{ + LayerHeader: store.LayerHeader{ + Name: layerName, + Proxy: proxyName, + Type: layerType, + Weight: 0, + Enabled: false, + }, + + CreatedAt: now, + UpdatedAt: now, + Options: store.LayerOptions{}, + }, nil +} + +// DeleteLayer implements store.LayerRepository +func (r *LayerRepository) DeleteLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName) error { + key := layerKey(proxyName, layerName) + + if cmd := r.client.Del(ctx, key); cmd.Err() != nil { + return errors.WithStack(cmd.Err()) + } + + return nil +} + +// GetLayer implements store.LayerRepository +func (r *LayerRepository) GetLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName) (*store.Layer, error) { + key := layerKey(proxyName, layerName) + var layerItem *layerItem + + err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error { + pItem, err := r.txGetLayerItem(ctx, tx, proxyName, layerName) + if err != nil { + return errors.WithStack(err) + } + + layerItem = pItem + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + layer, err := layerItem.ToLayer() + if err != nil { + return nil, errors.WithStack(err) + } + + return layer, nil +} + +func (r *LayerRepository) txGetLayerItem(ctx context.Context, tx *redis.Tx, proxyName store.ProxyName, layerName store.LayerName) (*layerItem, error) { + layerItem := layerItem{} + key := layerKey(proxyName, layerName) + + exists, err := tx.Exists(ctx, key).Uint64() + if err != nil { + return nil, errors.WithStack(err) + } + + if exists == 0 { + return nil, errors.WithStack(store.ErrNotFound) + } + + if err := tx.HGetAll(ctx, key).Scan(&layerItem.layerHeaderItem); err != nil { + return nil, errors.WithStack(err) + } + + if err := tx.HGetAll(ctx, key).Scan(&layerItem); err != nil { + return nil, errors.WithStack(err) + } + + return &layerItem, nil +} + +// QueryLayers implements store.LayerRepository +func (r *LayerRepository) QueryLayers(ctx context.Context, proxyName store.ProxyName, funcs ...store.QueryLayerOptionFunc) ([]*store.LayerHeader, error) { + opts := store.DefaultQueryLayerOptions() + for _, fn := range funcs { + fn(opts) + } + + keyParts := []string{keyPrefixLayer, string(proxyName)} + + if opts.Name != nil { + keyParts = append(keyParts, string(*opts.Name)) + } else { + keyParts = append(keyParts, "*") + } + + key := key(keyParts...) + + iter := r.client.Scan(ctx, 0, key, 0).Iterator() + + headers := make([]*store.LayerHeader, 0) + + for iter.Next(ctx) { + key := iter.Val() + + layerHeaderItem := &layerHeaderItem{} + + if err := r.client.HGetAll(ctx, key).Scan(layerHeaderItem); err != nil { + return nil, errors.WithStack(err) + } + + layerHeader, err := layerHeaderItem.ToLayerHeader() + if err != nil { + return nil, errors.WithStack(err) + } + + headers = append(headers, layerHeader) + } + + if err := iter.Err(); err != nil { + return nil, errors.WithStack(err) + } + + return headers, nil +} + +// UpdateLayer implements store.LayerRepository +func (r *LayerRepository) UpdateLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, funcs ...store.UpdateLayerOptionFunc) (*store.Layer, error) { + opts := &store.UpdateLayerOptions{} + for _, fn := range funcs { + fn(opts) + } + + key := layerKey(proxyName, layerName) + var layerItem layerItem + + err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error { + item, err := r.txGetLayerItem(ctx, tx, proxyName, layerName) + if err != nil { + return errors.WithStack(err) + } + + if opts.Enabled != nil { + item.Enabled = *opts.Enabled + } + + if opts.Weight != nil { + item.Weight = *opts.Weight + } + + if opts.Options != nil { + item.Options = wrap(*opts.Options) + } + + item.UpdatedAt = wrap(time.Now().UTC()) + + _, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error { + p.HMSet(ctx, key, item.layerHeaderItem) + p.HMSet(ctx, key, item) + + return nil + }) + if err != nil { + return errors.WithStack(err) + } + + layerItem = *item + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + layer, err := layerItem.ToLayer() + if err != nil { + return nil, errors.WithStack(err) + } + + return layer, nil +} + +func NewLayerRepository(client redis.UniversalClient) *LayerRepository { + return &LayerRepository{ + client: client, + } +} + +var _ store.LayerRepository = &LayerRepository{} + +func layerKey(proxyName store.ProxyName, layerName store.LayerName) string { + return key(keyPrefixLayer, string(proxyName), string(layerName)) +} diff --git a/internal/store/redis/layer_repository_test.go b/internal/store/redis/layer_repository_test.go new file mode 100644 index 0000000..939a45f --- /dev/null +++ b/internal/store/redis/layer_repository_test.go @@ -0,0 +1,12 @@ +package redis + +import ( + "testing" + + "forge.cadoles.com/cadoles/bouncer/internal/store/testsuite" +) + +func TestLayerRepository(t *testing.T) { + repository := NewLayerRepository(client) + testsuite.TestLayerRepository(t, repository) +} diff --git a/internal/store/redis/main_test.go b/internal/store/redis/main_test.go new file mode 100644 index 0000000..0939b93 --- /dev/null +++ b/internal/store/redis/main_test.go @@ -0,0 +1,58 @@ +package redis + +import ( + "context" + "log" + "os" + "testing" + + "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) +} diff --git a/internal/store/redis/proxy_item.go b/internal/store/redis/proxy_item.go new file mode 100644 index 0000000..5d21654 --- /dev/null +++ b/internal/store/redis/proxy_item.go @@ -0,0 +1,60 @@ +package redis + +import ( + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +type proxyHeaderItem struct { + Name string `redis:"name"` + + Weight int `redis:"weight"` + Enabled bool `redis:"enabled"` + + CreatedAt *jsonWrapper[time.Time] `redis:"created_at"` + UpdatedAt *jsonWrapper[time.Time] `redis:"updated_at"` +} + +func (i *proxyHeaderItem) ToProxyHeader() (*store.ProxyHeader, error) { + proxyHeader := &store.ProxyHeader{ + Name: store.ProxyName(i.Name), + Weight: i.Weight, + Enabled: i.Enabled, + } + + return proxyHeader, nil +} + +type proxyItem struct { + proxyHeaderItem + To string `redis:"to"` + From *jsonWrapper[[]string] `redis:"from"` +} + +func (i *proxyItem) ToProxy() (*store.Proxy, error) { + proxyHeader, err := i.proxyHeaderItem.ToProxyHeader() + if err != nil { + return nil, errors.WithStack(err) + } + + proxy := &store.Proxy{ + ProxyHeader: *proxyHeader, + To: i.To, + } + + if i.CreatedAt != nil { + proxy.CreatedAt = i.CreatedAt.Value() + } + + if i.UpdatedAt != nil { + proxy.UpdatedAt = i.UpdatedAt.Value() + } + + if i.From != nil { + proxy.From = i.From.Value() + } + + return proxy, nil +} diff --git a/internal/store/redis/proxy_repository.go b/internal/store/redis/proxy_repository.go new file mode 100644 index 0000000..aa5f6b4 --- /dev/null +++ b/internal/store/redis/proxy_repository.go @@ -0,0 +1,254 @@ +package redis + +import ( + "context" + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +const ( + keyPrefixProxy = "proxy" +) + +type ProxyRepository struct { + client redis.UniversalClient +} + +// GetProxy implements store.ProxyRepository +func (r *ProxyRepository) GetProxy(ctx context.Context, name store.ProxyName) (*store.Proxy, error) { + key := proxyKey(name) + var proxyItem *proxyItem + + err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error { + pItem, err := r.txGetProxyItem(ctx, tx, name) + if err != nil { + return errors.WithStack(err) + } + + proxyItem = pItem + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + proxy, err := proxyItem.ToProxy() + if err != nil { + return nil, errors.WithStack(err) + } + + return proxy, nil +} + +func (r *ProxyRepository) txGetProxyItem(ctx context.Context, tx *redis.Tx, name store.ProxyName) (*proxyItem, error) { + proxyItem := proxyItem{} + key := proxyKey(name) + + exists, err := tx.Exists(ctx, key).Uint64() + if err != nil { + return nil, errors.WithStack(err) + } + + if exists == 0 { + return nil, errors.WithStack(store.ErrNotFound) + } + + if err := tx.HGetAll(ctx, key).Scan(&proxyItem.proxyHeaderItem); err != nil { + return nil, errors.WithStack(err) + } + + if err := tx.HGetAll(ctx, key).Scan(&proxyItem); err != nil { + return nil, errors.WithStack(err) + } + + return &proxyItem, nil +} + +// CreateProxy implements store.ProxyRepository +func (r *ProxyRepository) CreateProxy(ctx context.Context, name store.ProxyName, to string, from ...string) (*store.Proxy, error) { + now := time.Now().UTC() + key := proxyKey(name) + + txf := func(tx *redis.Tx) error { + exists, err := tx.Exists(ctx, key).Uint64() + if err != nil { + return errors.WithStack(err) + } + + if exists > 0 { + return errors.WithStack(store.ErrAlreadyExist) + } + + proxyItem := &proxyItem{ + proxyHeaderItem: proxyHeaderItem{ + Name: string(name), + CreatedAt: wrap(now), + UpdatedAt: wrap(now), + Weight: 0, + Enabled: false, + }, + To: to, + From: wrap(from), + } + + _, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error { + p.HMSet(ctx, key, proxyItem.proxyHeaderItem) + p.HMSet(ctx, key, proxyItem) + + return nil + }) + + if err != nil { + return errors.WithStack(err) + } + + return nil + } + + err := r.client.Watch(ctx, txf, key) + if err != nil { + return nil, errors.WithStack(err) + } + + return &store.Proxy{ + ProxyHeader: store.ProxyHeader{ + Name: name, + + Weight: 0, + Enabled: false, + }, + To: to, + From: from, + CreatedAt: now, + UpdatedAt: now, + }, nil +} + +// DeleteProxy implements store.ProxyRepository +func (r *ProxyRepository) DeleteProxy(ctx context.Context, name store.ProxyName) error { + key := proxyKey(name) + + 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) { + opts := store.DefaultQueryProxyOptions() + for _, fn := range funcs { + fn(opts) + } + + iter := r.client.Scan(ctx, 0, keyPrefixProxy+"*", 0).Iterator() + + headers := make([]*store.ProxyHeader, 0) + + for iter.Next(ctx) { + key := iter.Val() + + proxyHeaderItem := &proxyHeaderItem{} + if err := r.client.HGetAll(ctx, key).Scan(proxyHeaderItem); err != nil { + return nil, errors.WithStack(err) + } + + proxyHeader, err := proxyHeaderItem.ToProxyHeader() + if err != nil { + return nil, errors.WithStack(err) + } + + if opts.Enabled != nil && proxyHeader.Enabled != *opts.Enabled { + continue + } + + if opts.Names != nil && !contains(opts.Names, proxyHeader.Name) { + continue + } + + headers = append(headers, proxyHeader) + } + + if err := iter.Err(); err != nil { + return nil, errors.WithStack(err) + } + + return headers, nil +} + +// UpdateProxy implements store.ProxyRepository +func (r *ProxyRepository) UpdateProxy(ctx context.Context, name store.ProxyName, funcs ...store.UpdateProxyOptionFunc) (*store.Proxy, error) { + opts := &store.UpdateProxyOptions{} + for _, fn := range funcs { + fn(opts) + } + + key := proxyKey(name) + var proxyItem proxyItem + + err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error { + item, err := r.txGetProxyItem(ctx, tx, name) + if err != nil { + return errors.WithStack(err) + } + + if opts.Enabled != nil { + item.Enabled = *opts.Enabled + } + + if opts.From != nil { + item.From = wrap(opts.From) + } + + if opts.Weight != nil { + item.Weight = *opts.Weight + } + + if opts.To != nil { + item.To = *opts.To + } + + item.UpdatedAt = wrap(time.Now().UTC()) + + _, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error { + p.HMSet(ctx, key, item.proxyHeaderItem) + p.HMSet(ctx, key, item) + + return nil + }) + if err != nil { + return errors.WithStack(err) + } + + proxyItem = *item + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + proxy, err := proxyItem.ToProxy() + if err != nil { + return nil, errors.WithStack(err) + } + + return proxy, nil +} + +func NewProxyRepository(client redis.UniversalClient) *ProxyRepository { + return &ProxyRepository{ + client: client, + } +} + +var _ store.ProxyRepository = &ProxyRepository{} + +func proxyKey(name store.ProxyName) string { + return key(keyPrefixProxy, string(name)) +} diff --git a/internal/store/redis/proxy_repository_test.go b/internal/store/redis/proxy_repository_test.go new file mode 100644 index 0000000..73701f0 --- /dev/null +++ b/internal/store/redis/proxy_repository_test.go @@ -0,0 +1,12 @@ +package redis + +import ( + "testing" + + "forge.cadoles.com/cadoles/bouncer/internal/store/testsuite" +) + +func TestProxyRepository(t *testing.T) { + repository := NewProxyRepository(client) + testsuite.TestProxyRepository(t, repository) +} diff --git a/internal/store/sort.go b/internal/store/sort.go new file mode 100644 index 0000000..d522d00 --- /dev/null +++ b/internal/store/sort.go @@ -0,0 +1,13 @@ +package store + +type ByProxyWeight []*ProxyHeader + +func (s ByProxyWeight) Len() int { return len(s) } +func (s ByProxyWeight) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s ByProxyWeight) Less(i, j int) bool { return s[i].Weight > s[j].Weight } + +type ByLayerWeight []*LayerHeader + +func (s ByLayerWeight) Len() int { return len(s) } +func (s ByLayerWeight) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s ByLayerWeight) Less(i, j int) bool { return s[i].Weight > s[j].Weight } diff --git a/internal/store/testsuite/layer_repository.go b/internal/store/testsuite/layer_repository.go new file mode 100644 index 0000000..a70ddf6 --- /dev/null +++ b/internal/store/testsuite/layer_repository.go @@ -0,0 +1,66 @@ +package testsuite + +import ( + "context" + "testing" + + "forge.cadoles.com/cadoles/bouncer/internal/store" + "github.com/pkg/errors" +) + +type layerRepositoryTestCase struct { + Name string + Do func(repo store.LayerRepository) error +} + +const layerType store.LayerType = "test" + +var layerRepositoryTestCases = []layerRepositoryTestCase{ + { + Name: "Create layer", + Do: func(repo store.LayerRepository) error { + ctx := context.Background() + + options := map[string]any{} + + layer, err := repo.CreateLayer(ctx, "create_layer_proxy", "create_layer", layerType, options) + if err != nil { + return errors.WithStack(err) + } + + if layer == nil { + return errors.Errorf("layer should not be nil") + } + + if layer.Name == "" { + return errors.Errorf("layer.Name should not be empty") + } + + if layer.Proxy == "" { + return errors.Errorf("layer.Proxy should not be empty") + } + + if layer.CreatedAt.IsZero() { + return errors.Errorf("layer.CreatedAt should not be zero value") + } + + if layer.UpdatedAt.IsZero() { + return errors.Errorf("layer.UpdatedAt should not be zero value") + } + + return nil + }, + }, +} + +func TestLayerRepository(t *testing.T, repo store.LayerRepository) { + for _, tc := range layerRepositoryTestCases { + func(tc layerRepositoryTestCase) { + 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/internal/store/testsuite/proxy_repository.go b/internal/store/testsuite/proxy_repository.go new file mode 100644 index 0000000..58ffa4f --- /dev/null +++ b/internal/store/testsuite/proxy_repository.go @@ -0,0 +1,204 @@ +package testsuite + +import ( + "context" + "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() + + to := "http://example.com" + + proxy, err := repo.CreateProxy(ctx, "create_proxy", to, "*:*") + if err != nil { + return errors.WithStack(err) + } + + if proxy == nil { + return errors.Errorf("proxy should not be nil") + } + + if proxy.Name == "" { + return errors.Errorf("proxy.Name should not be empty") + } + + if proxy.To == "" { + return errors.Errorf("proxy.To should not be empty") + } + + if e, g := to, proxy.To; e != g { + return errors.Errorf("proxy.To: expected '%v', got '%v'", to, proxy.To) + } + + 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() + + to := "http://example.com" + + createdProxy, err := repo.CreateProxy(ctx, "create_then_get_proxy", to, "127.0.0.1:*", "localhost:*") + if err != nil { + return errors.WithStack(err) + } + + foundProxy, err := repo.GetProxy(ctx, createdProxy.Name) + if err != nil { + return errors.WithStack(err) + } + + if e, g := createdProxy.Name, foundProxy.Name; e != g { + return errors.Errorf("foundProxy.Name: expected '%v', got '%v'", createdProxy.Name, foundProxy.Name) + } + + 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, foundProxy.To; 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() + + to := "http://example.com" + + createdProxy, err := repo.CreateProxy(ctx, "create_then_delete_proxy", to, "127.0.0.1:*", "localhost:*") + if err != nil { + return errors.WithStack(err) + } + + if err := repo.DeleteProxy(ctx, createdProxy.Name); err != nil { + return errors.WithStack(err) + } + + foundProxy, err := repo.GetProxy(ctx, createdProxy.Name) + 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() + + to := "http://example.com" + + createdProxy, err := repo.CreateProxy(ctx, "create_then_query", to, "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.Name == createdProxy.Name { + found = true + break + } + } + + if !found { + return errors.New("could not find created proxy in query results") + } + + return nil + }, + }, + { + Name: "Create already existing proxy", + Do: func(repo store.ProxyRepository) error { + ctx := context.Background() + + to := "http://example.com" + + var name store.ProxyName = "create_already_existing_proxy" + + _, err := repo.CreateProxy(ctx, name, to, "127.0.0.1:*", "localhost:*") + if err != nil { + return errors.WithStack(err) + } + + _, err = repo.CreateProxy(ctx, name, to, "127.0.0.1:*") + if err == nil { + return errors.New("err should not be nil") + } + + if !errors.Is(err, store.ErrAlreadyExist) { + return errors.Errorf("err: expected store.ErrAlreadyExists, got '%+v'", err) + } + + 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..b520dfd --- /dev/null +++ b/misc/jenkins/Dockerfile @@ -0,0 +1,25 @@ +FROM reg.cadoles.com/proxy_cache/library/ubuntu:22.04 + +ARG HTTP_PROXY= +ARG HTTPS_PROXY= +ARG http_proxy= +ARG https_proxy= + +# 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 + +# Add LetsEncrypt certificates +RUN curl -k https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/common/add-letsencrypt-ca.sh | bash + +ARG GO_VERSION=1.20.4 + +# 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" \ 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..9cb78ae --- /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 server 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..9beaf87 --- /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 server 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..d90651d --- /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 server 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..8b83fdb --- /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 server 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..04bebf0 --- /dev/null +++ b/modd.conf @@ -0,0 +1,16 @@ +**/*.go +internal/**/*.json +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 server admin run" + daemon: make run BOUNCER_CMD="--config config.yml server proxy run" +} + +{ + daemon +sigint: make run-redis +} \ No newline at end of file