commit e66938f1d3bc2b52c83c928f8ace1e582e12d3e6 Author: William Petit Date: Mon Apr 24 20:52:12 2023 +0200 feat: initial commit 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