kopia lustrzana https://codeberg.org/Codeberg/pages-server
Porównaj commity
65 Commity
Autor | SHA1 | Data |
---|---|---|
Dependency bot | d74f1fe8a4 | |
Dependency bot | adf13bfdbc | |
Dependency bot | 7c49c4b967 | |
Dependency bot | eb08c46dcd | |
crapStone | 56d44609ea | |
pat-s | ca9433e0ea | |
pat-s | d09c6e1218 | |
pat-s | 8cba7f9c8a | |
pat-s | f407fd3ae4 | |
Dependency bot | 89800d4f36 | |
crapStone | 418afb7357 | |
pat-s | e45a354eef | |
pat-s | 1a332c1d54 | |
pat-s | c14c5474b6 | |
pat-s | 7092883ebe | |
Dependency bot | 019e85a0d0 | |
Daniel Erat | 69fb22a9e7 | |
Moritz Marquardt | a986a52755 | |
Daniel Erat | 9ffdc9d4f9 | |
Jean-Marie 'Histausse' Mineau | 03881382a4 | |
caelandb | dd6d8bd60f | |
Hoernschen | a6e9510c07 | |
crapStone | 7e80ade24b | |
crapStone | c1fbe861fe | |
Panagiotis "Ivory" Vasilopoulos | a09bee68ad | |
Gusted | 15916444e1 | |
6543 | dd5124912e | |
crapStone | fffb8ffcb6 | |
crapStone | cbb2ce6d07 | |
crapStone | 7f0a4e5ca9 | |
crapStone | ea68a82cd2 | |
crapStone | 1e1c67be93 | |
6543 | be92f30e64 | |
crapStone | a8272f0ce9 | |
thepaperpilot | b6103c6a1b | |
Moritz Marquardt | ff3cd1ba35 | |
Moritz Marquardt | 56d3e291c4 | |
6543 | d720d25e42 | |
Gusted | 7f318f89a6 | |
video-prize-ranch | 974229681f | |
6543 | 970c13cf5c | |
6543 | 98d7a771be | |
Crystal | c40dddf471 | |
Crystal | 26d59b71f0 | |
6543 | c9050e5722 | |
deblan | 42d5802b9b | |
6543 | 0adac9a5b1 | |
6543 | 42b3f8d1b7 | |
6543 | 9a3d1c36dc | |
crystal | 46316f9e2f | |
6543 | 08d4e70cfd | |
6543 | 5753f7136d | |
6543 | fd643d15f0 | |
6543 | 272c7ca76f | |
6543 | d8d119b0b3 | |
6543 | 1b6ea4b6e1 | |
6543 | 7b35a192bf | |
foehammer | 7fce7cf68b | |
Gusted | 8b1f497bc4 | |
6543 | 2c2087953d | |
Gusted | 513e79832a | |
crapStone | bd538abd37 | |
Felipe Leopoldo Sologuren Gutiérrez | c286b3b1d0 | |
6543 | f7fad2a5ae | |
Gusted | 98d198d419 |
|
@ -0,0 +1,11 @@
|
|||
ACME_API=https://acme.mock.directory
|
||||
ACME_ACCEPT_TERMS=true
|
||||
PAGES_DOMAIN=localhost.mock.directory
|
||||
RAW_DOMAIN=raw.localhost.mock.directory
|
||||
PAGES_BRANCHES=pages,master,main
|
||||
GITEA_ROOT=https://codeberg.org
|
||||
PORT=4430
|
||||
HTTP_PORT=8880
|
||||
ENABLE_HTTP_SERVER=true
|
||||
LOG_LEVEL=trace
|
||||
ACME_ACCOUNT_CONFIG=integration/acme-account.json
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Codeberg Pages Usage Support
|
||||
url: https://codeberg.org/Codeberg/Community/issues/
|
||||
about: If you need help with configuring Codeberg Pages on codeberg.org, please go here.
|
|
@ -1,7 +1,10 @@
|
|||
.idea/
|
||||
.cache/
|
||||
*.iml
|
||||
key-database.pogreb/
|
||||
acme-account.json
|
||||
build/
|
||||
vendor/
|
||||
pages
|
||||
certs.sqlite
|
||||
.bash_history
|
||||
|
|
|
@ -12,9 +12,23 @@ linters-settings:
|
|||
- hugeParam
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- unconvert
|
||||
- gocritic
|
||||
- gofumpt
|
||||
- bidichk
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- whitespace
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf"
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch PagesServer",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"args": ["sqlite", "sqlite_unlock_notify", "netgo"],
|
||||
"envFile": "${workspaceFolder}/.env-dev"
|
||||
},
|
||||
{
|
||||
"name": "Launch PagesServer integration test",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/integration/main_test.go",
|
||||
"args": ["codeberg.org/codeberg/pages/integration/..."],
|
||||
"buildFlags": ["-tags", "'integration sqlite sqlite_unlock_notify netgo'"]
|
||||
}
|
||||
]
|
||||
}
|
111
.woodpecker.yml
111
.woodpecker.yml
|
@ -1,111 +0,0 @@
|
|||
pipeline:
|
||||
# use vendor to cache dependencies
|
||||
vendor:
|
||||
image: golang:1.18
|
||||
commands:
|
||||
- go mod vendor
|
||||
|
||||
lint:
|
||||
image: golangci/golangci-lint:latest
|
||||
group: compliant
|
||||
pull: true
|
||||
commands:
|
||||
- go version
|
||||
- go install mvdan.cc/gofumpt@latest
|
||||
- "[ $(gofumpt -extra -l . | wc -l) != 0 ] && { echo 'code not formated'; exit 1; }"
|
||||
- golangci-lint run --timeout 5m --build-tags integration
|
||||
|
||||
editor-config:
|
||||
group: compliant
|
||||
image: mstruebing/editorconfig-checker
|
||||
|
||||
build:
|
||||
group: compliant
|
||||
image: codeberg.org/6543/docker-images/golang_just
|
||||
commands:
|
||||
- go version
|
||||
- just build
|
||||
when:
|
||||
event: [ "pull_request", "push" ]
|
||||
|
||||
docker-dryrun:
|
||||
group: compliant
|
||||
image: plugins/kaniko
|
||||
settings:
|
||||
dockerfile: Dockerfile
|
||||
no_push: true
|
||||
tags: latest
|
||||
when:
|
||||
event: [ "pull_request", "push" ]
|
||||
path: Dockerfile
|
||||
|
||||
build-tag:
|
||||
group: compliant
|
||||
image: codeberg.org/6543/docker-images/golang_just
|
||||
commands:
|
||||
- go version
|
||||
- just build-tag ${CI_COMMIT_TAG##v}
|
||||
when:
|
||||
event: [ "tag" ]
|
||||
|
||||
test:
|
||||
group: test
|
||||
image: codeberg.org/6543/docker-images/golang_just
|
||||
commands:
|
||||
- just test
|
||||
|
||||
integration-tests:
|
||||
group: test
|
||||
image: codeberg.org/6543/docker-images/golang_just
|
||||
commands:
|
||||
- just integration
|
||||
environment:
|
||||
- ACME_API=https://acme.mock.directory
|
||||
- PAGES_DOMAIN=localhost.mock.directory
|
||||
- RAW_DOMAIN=raw.localhost.mock.directory
|
||||
- PORT=4430
|
||||
|
||||
release:
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
base_url: https://codeberg.org
|
||||
file_exists: overwrite
|
||||
files: build/codeberg-pages-server
|
||||
api_key:
|
||||
from_secret: bot_token
|
||||
environment:
|
||||
- DRONE_REPO_OWNER=${CI_REPO_OWNER}
|
||||
- DRONE_REPO_NAME=${CI_REPO_NAME}
|
||||
- DRONE_BUILD_EVENT=${CI_BUILD_EVENT}
|
||||
- DRONE_COMMIT_REF=${CI_COMMIT_REF}
|
||||
when:
|
||||
event: [ "tag" ]
|
||||
|
||||
docker-next:
|
||||
image: plugins/kaniko
|
||||
settings:
|
||||
registry: codeberg.org
|
||||
dockerfile: Dockerfile
|
||||
repo: codeberg.org/codeberg/pages-server
|
||||
tags: next
|
||||
username:
|
||||
from_secret: bot_user
|
||||
password:
|
||||
from_secret: bot_token
|
||||
when:
|
||||
event: [ "push" ]
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
||||
docker-tag:
|
||||
image: plugins/kaniko
|
||||
settings:
|
||||
registry: codeberg.org
|
||||
dockerfile: Dockerfile
|
||||
repo: codeberg.org/codeberg/pages-server
|
||||
tags: [ latest, "${CI_COMMIT_TAG}" ]
|
||||
username:
|
||||
from_secret: bot_user
|
||||
password:
|
||||
from_secret: bot_token
|
||||
when:
|
||||
event: [ "tag" ]
|
|
@ -0,0 +1,132 @@
|
|||
when:
|
||||
- event: [pull_request, tag, cron]
|
||||
- event: push
|
||||
branch:
|
||||
- ${CI_REPO_DEFAULT_BRANCH}
|
||||
- renovate/*
|
||||
|
||||
depends_on:
|
||||
- lint
|
||||
|
||||
steps:
|
||||
# use vendor to cache dependencies
|
||||
vendor:
|
||||
image: golang:1.22
|
||||
commands:
|
||||
- go mod vendor
|
||||
|
||||
build:
|
||||
depends_on: vendor
|
||||
image: codeberg.org/6543/docker-images/golang_just
|
||||
commands:
|
||||
- go version
|
||||
- just build
|
||||
when:
|
||||
- event: [push, pull_request]
|
||||
branch:
|
||||
- ${CI_REPO_DEFAULT_BRANCH}
|
||||
- renovate/*
|
||||
|
||||
docker-dryrun:
|
||||
depends_on: vendor
|
||||
image: woodpeckerci/plugin-docker-buildx:3.2.1
|
||||
settings:
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64
|
||||
dry-run: true
|
||||
tags: latest
|
||||
when:
|
||||
- event: [push, pull_request]
|
||||
branch:
|
||||
- ${CI_REPO_DEFAULT_BRANCH}
|
||||
- renovate/*
|
||||
path: Dockerfile
|
||||
|
||||
build-tag:
|
||||
depends_on: vendor
|
||||
image: codeberg.org/6543/docker-images/golang_just
|
||||
commands:
|
||||
- go version
|
||||
- just build-tag ${CI_COMMIT_TAG##v}
|
||||
when:
|
||||
- event: ['tag']
|
||||
branch:
|
||||
- ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
||||
test:
|
||||
depends_on: build
|
||||
image: codeberg.org/6543/docker-images/golang_just
|
||||
commands:
|
||||
- just test
|
||||
when:
|
||||
- event: pull_request
|
||||
- event: push
|
||||
branch: renovate/*
|
||||
|
||||
integration-tests:
|
||||
depends_on: build
|
||||
image: codeberg.org/6543/docker-images/golang_just
|
||||
commands:
|
||||
- just integration
|
||||
environment:
|
||||
- ACME_API=https://acme.mock.directory
|
||||
- PAGES_DOMAIN=localhost.mock.directory
|
||||
- RAW_DOMAIN=raw.localhost.mock.directory
|
||||
- PORT=4430
|
||||
when:
|
||||
- event: pull_request
|
||||
- event: push
|
||||
branch: renovate/*
|
||||
|
||||
release:
|
||||
depends_on: build
|
||||
image: plugins/gitea-release:1.1.0
|
||||
settings:
|
||||
base_url: https://codeberg.org
|
||||
file_exists: overwrite
|
||||
files: build/codeberg-pages-server
|
||||
api_key:
|
||||
from_secret: bot_token
|
||||
environment:
|
||||
- CI_REPO_OWNER=${CI_REPO_OWNER}
|
||||
- CI_REPO_NAME=${CI_REPO_NAME}
|
||||
- CI_BUILD_EVENT=${CI_BUILD_EVENT}
|
||||
- CI_COMMIT_REF=${CI_COMMIT_REF}
|
||||
when:
|
||||
- event: ['tag']
|
||||
branch:
|
||||
- ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
||||
docker-next:
|
||||
depends_on: vendor
|
||||
image: woodpeckerci/plugin-docker-buildx:3.2.1
|
||||
settings:
|
||||
registry: codeberg.org
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64,arm64
|
||||
repo: codeberg.org/codeberg/pages-server
|
||||
tags: next
|
||||
username:
|
||||
from_secret: bot_user
|
||||
password:
|
||||
from_secret: bot_token
|
||||
when:
|
||||
- event: ['push']
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
||||
docker-tag:
|
||||
depends_on: vendor
|
||||
image: woodpeckerci/plugin-docker-buildx:3.2.1
|
||||
settings:
|
||||
registry: codeberg.org
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64,arm64
|
||||
repo: codeberg.org/codeberg/pages-server
|
||||
tags: [latest, '${CI_COMMIT_TAG}']
|
||||
username:
|
||||
from_secret: bot_user
|
||||
password:
|
||||
from_secret: bot_token
|
||||
when:
|
||||
- event: ['push']
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
|
@ -0,0 +1,44 @@
|
|||
when:
|
||||
- event: pull_request
|
||||
- event: push
|
||||
branch:
|
||||
- ${CI_REPO_DEFAULT_BRANCH}
|
||||
- renovate/**
|
||||
|
||||
steps:
|
||||
lint:
|
||||
depends_on: []
|
||||
image: golangci/golangci-lint:v1.58.1
|
||||
commands:
|
||||
- go version
|
||||
- go install mvdan.cc/gofumpt@latest
|
||||
- "[ $(gofumpt -extra -l . | wc -l) != 0 ] && { echo 'code not formated'; exit 1; }"
|
||||
- golangci-lint run --timeout 5m --build-tags integration
|
||||
when:
|
||||
- event: pull_request
|
||||
- event: push
|
||||
branch: renovate/*
|
||||
|
||||
editor-config:
|
||||
depends_on: []
|
||||
image: mstruebing/editorconfig-checker:v3.0.1
|
||||
when:
|
||||
- event: pull_request
|
||||
- event: push
|
||||
branch: renovate/*
|
||||
|
||||
yamllint:
|
||||
image: pipelinecomponents/yamllint:0.31.1
|
||||
depends_on: []
|
||||
commands:
|
||||
- yamllint .
|
||||
when:
|
||||
- event: pull_request
|
||||
- event: push
|
||||
branch: renovate/*
|
||||
|
||||
prettier:
|
||||
image: docker.io/woodpeckerci/plugin-prettier:0.1.0
|
||||
depends_on: []
|
||||
settings:
|
||||
version: 3.2.5
|
|
@ -0,0 +1,19 @@
|
|||
extends: default
|
||||
|
||||
rules:
|
||||
comments:
|
||||
require-starting-space: false
|
||||
ignore-shebangs: true
|
||||
min-spaces-from-content: 1
|
||||
braces:
|
||||
min-spaces-inside: 1
|
||||
max-spaces-inside: 1
|
||||
document-start:
|
||||
present: false
|
||||
indentation:
|
||||
spaces: 2
|
||||
indent-sequences: true
|
||||
line-length:
|
||||
max: 256
|
||||
new-lines:
|
||||
type: unix
|
32
Dockerfile
32
Dockerfile
|
@ -1,15 +1,35 @@
|
|||
FROM golang:alpine as build
|
||||
# Set the default Go version as a build argument
|
||||
ARG XGO="go-1.21.x"
|
||||
|
||||
WORKDIR /workspace
|
||||
# Use xgo (a Go cross-compiler tool) as build image
|
||||
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:${XGO} as build
|
||||
|
||||
RUN apk add ca-certificates
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build .
|
||||
# Set the working directory and copy the source code
|
||||
WORKDIR /go/src/codeberg.org/codeberg/pages
|
||||
COPY . /go/src/codeberg.org/codeberg/pages
|
||||
|
||||
# Set the target architecture (can be set using --build-arg), buildx set it automatically
|
||||
ARG TARGETOS TARGETARCH
|
||||
|
||||
# Build the binary using xgo
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg \
|
||||
GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=1 \
|
||||
xgo -x -v --targets=${TARGETOS}/${TARGETARCH} -tags='sqlite sqlite_unlock_notify netgo' -ldflags='-s -w -extldflags "-static" -linkmode external' -out pages .
|
||||
|
||||
# Use a scratch image as the base image for the final container,
|
||||
# which will contain only the built binary and the CA certificates
|
||||
FROM scratch
|
||||
COPY --from=build /workspace/pages /pages
|
||||
|
||||
# Copy the built binary and the CA certificates from the build container to the final container
|
||||
COPY --from=build /go/src/codeberg.org/codeberg/pages/ /pages
|
||||
COPY --from=build \
|
||||
/etc/ssl/certs/ca-certificates.crt \
|
||||
/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Expose ports 80 and 443 for the built binary to listen on
|
||||
EXPOSE 80/tcp
|
||||
EXPOSE 443/tcp
|
||||
|
||||
# Set the entrypoint for the container to the built binary
|
||||
ENTRYPOINT ["/pages"]
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
# Features
|
||||
|
||||
## Custom domains
|
||||
|
||||
Custom domains can be used by creating a `.domains` file with the domain name, e.g.:
|
||||
|
||||
```text
|
||||
codeberg.page
|
||||
```
|
||||
|
||||
You also have to set some DNS records, see the [Codeberg Documentation](https://docs.codeberg.org/codeberg-pages/using-custom-domain/).
|
||||
|
||||
## Redirects
|
||||
|
||||
Redirects can be created with a `_redirects` file with the following format:
|
||||
|
||||
```text
|
||||
# Comment
|
||||
from to [status]
|
||||
```
|
||||
|
||||
- Lines starting with `#` are ignored
|
||||
- `from` - the path to redirect from (Note: repository and branch names are removed from request URLs)
|
||||
- `to` - the path or URL to redirect to
|
||||
- `status` - status code to use when redirecting (default 301)
|
||||
|
||||
### Status codes
|
||||
|
||||
- `200` - returns content from specified path (no external URLs) without changing the URL (rewrite)
|
||||
- `301` - Moved Permanently (Permanent redirect)
|
||||
- `302` - Found (Temporary redirect)
|
||||
|
||||
### Examples
|
||||
|
||||
#### SPA (single-page application) rewrite
|
||||
|
||||
Redirects all paths to `/index.html` for single-page apps.
|
||||
|
||||
```text
|
||||
/* /index.html 200
|
||||
```
|
||||
|
||||
#### Splats
|
||||
|
||||
Redirects every path under `/articles` to `/posts` while keeping the path.
|
||||
|
||||
```text
|
||||
/articles/* /posts/:splat 302
|
||||
```
|
||||
|
||||
Example: `/articles/2022/10/12/post-1/` -> `/posts/2022/10/12/post-1/`
|
34
Justfile
34
Justfile
|
@ -1,22 +1,21 @@
|
|||
dev:
|
||||
CGO_FLAGS := '-extldflags "-static" -linkmode external'
|
||||
TAGS := 'sqlite sqlite_unlock_notify netgo'
|
||||
|
||||
dev *FLAGS:
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
export ACME_API=https://acme.mock.directory
|
||||
export ACME_ACCEPT_TERMS=true
|
||||
export PAGES_DOMAIN=localhost.mock.directory
|
||||
export RAW_DOMAIN=raw.localhost.mock.directory
|
||||
export PORT=4430
|
||||
export LOG_LEVEL=trace
|
||||
go run .
|
||||
set -a # automatically export all variables
|
||||
source .env-dev
|
||||
set +a
|
||||
go run -tags '{{TAGS}}' . {{FLAGS}}
|
||||
|
||||
build:
|
||||
CGO_ENABLED=0 go build -ldflags '-s -w' -v -o build/codeberg-pages-server ./
|
||||
CGO_ENABLED=1 go build -tags '{{TAGS}}' -ldflags '-s -w {{CGO_FLAGS}}' -v -o build/codeberg-pages-server ./
|
||||
|
||||
build-tag VERSION:
|
||||
CGO_ENABLED=0 go build -ldflags '-s -w -X "codeberg.org/codeberg/pages/server/version.Version={{VERSION}}"' -v -o build/codeberg-pages-server ./
|
||||
CGO_ENABLED=1 go build -tags '{{TAGS}}' -ldflags '-s -w -X "codeberg.org/codeberg/pages/server/version.Version={{VERSION}}" {{CGO_FLAGS}}' -v -o build/codeberg-pages-server ./
|
||||
|
||||
lint: tool-golangci tool-gofumpt
|
||||
[ $(gofumpt -extra -l . | wc -l) != 0 ] && { echo 'code not formated'; exit 1; }; \
|
||||
golangci-lint run --timeout 5m --build-tags integration
|
||||
# TODO: run editorconfig-checker
|
||||
|
||||
|
@ -25,7 +24,7 @@ fmt: tool-gofumpt
|
|||
|
||||
clean:
|
||||
go clean ./...
|
||||
rm -rf build/
|
||||
rm -rf build/ integration/certs.sqlite integration/acme-account.json
|
||||
|
||||
tool-golangci:
|
||||
@hash golangci-lint> /dev/null 2>&1; if [ $? -ne 0 ]; then \
|
||||
|
@ -38,13 +37,16 @@ tool-gofumpt:
|
|||
fi
|
||||
|
||||
test:
|
||||
go test -race codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/
|
||||
go test -race -cover -tags '{{TAGS}}' codeberg.org/codeberg/pages/config/ codeberg.org/codeberg/pages/html/ codeberg.org/codeberg/pages/server/...
|
||||
|
||||
test-run TEST:
|
||||
go test -race -run "^{{TEST}}$" codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/
|
||||
go test -race -tags '{{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/config/ codeberg.org/codeberg/pages/html/ codeberg.org/codeberg/pages/server/...
|
||||
|
||||
integration:
|
||||
go test -race -tags integration codeberg.org/codeberg/pages/integration/...
|
||||
go test -race -tags 'integration {{TAGS}}' codeberg.org/codeberg/pages/integration/...
|
||||
|
||||
integration-run TEST:
|
||||
go test -race -tags integration -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/...
|
||||
go test -race -tags 'integration {{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/...
|
||||
|
||||
docker:
|
||||
docker run --rm -it --user $(id -u) -v $(pwd):/work --workdir /work -e HOME=/work codeberg.org/6543/docker-images/golang_just
|
||||
|
|
102
README.md
102
README.md
|
@ -1,5 +1,11 @@
|
|||
# Codeberg Pages
|
||||
|
||||
[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue)](https://opensource.org/license/eupl-1-2/)
|
||||
[![status-badge](https://ci.codeberg.org/api/badges/Codeberg/pages-server/status.svg)](https://ci.codeberg.org/Codeberg/pages-server)
|
||||
<a href="https://matrix.to/#/#gitea-pages-server:matrix.org" title="Join the Matrix room at https://matrix.to/#/#gitea-pages-server:matrix.org">
|
||||
<img src="https://img.shields.io/matrix/gitea-pages-server:matrix.org?label=matrix">
|
||||
</a>
|
||||
|
||||
Gitea lacks the ability to host static pages from Git.
|
||||
The Codeberg Pages Server addresses this lack by implementing a standalone service
|
||||
that connects to Gitea via API.
|
||||
|
@ -8,30 +14,37 @@ It is suitable to be deployed by other Gitea instances, too, to offer static pag
|
|||
**End user documentation** can mainly be found at the [Wiki](https://codeberg.org/Codeberg/pages-server/wiki/Overview)
|
||||
and the [Codeberg Documentation](https://docs.codeberg.org/codeberg-pages/).
|
||||
|
||||
<a href="https://codeberg.org/Codeberg/pages-server"> <img src="https://codeberg.org/Codeberg/GetItOnCodeberg/raw/branch/main/get-it-on-blue-on-white.svg" alt="Get It On Codeberg" width="250"/> </a>
|
||||
|
||||
## Quickstart
|
||||
|
||||
This is the new Codeberg Pages server, a solution for serving static pages from Gitea repositories.
|
||||
Mapping custom domains is not static anymore, but can be done with DNS:
|
||||
|
||||
1) add a `.domains` text file to your repository, containing the allowed domains, separated by new lines. The
|
||||
first line will be the canonical domain/URL; all other occurrences will be redirected to it.
|
||||
1. add a `.domains` text file to your repository, containing the allowed domains, separated by new lines. The
|
||||
first line will be the canonical domain/URL; all other occurrences will be redirected to it.
|
||||
|
||||
2) add a CNAME entry to your domain, pointing to `[[{branch}.]{repo}.]{owner}.codeberg.page` (repo defaults to
|
||||
"pages", "branch" defaults to the default branch if "repo" is "pages", or to "pages" if "repo" is something else.
|
||||
If the branch name contains slash characters, you need to replace "/" in the branch name to "~"):
|
||||
`www.example.org. IN CNAME main.pages.example.codeberg.page.`
|
||||
2. add a CNAME entry to your domain, pointing to `[[{branch}.]{repo}.]{owner}.codeberg.page` (repo defaults to
|
||||
"pages", "branch" defaults to the default branch if "repo" is "pages", or to "pages" if "repo" is something else.
|
||||
If the branch name contains slash characters, you need to replace "/" in the branch name to "~"):
|
||||
`www.example.org. IN CNAME main.pages.example.codeberg.page.`
|
||||
|
||||
3) if a CNAME is set for "www.example.org", you can redirect there from the naked domain by adding an ALIAS record
|
||||
for "example.org" (if your provider allows ALIAS or similar records, otherwise use A/AAAA), together with a TXT
|
||||
record that points to your repo (just like the CNAME record):
|
||||
`example.org IN ALIAS codeberg.page.`
|
||||
`example.org IN TXT main.pages.example.codeberg.page.`
|
||||
3. if a CNAME is set for "www.example.org", you can redirect there from the naked domain by adding an ALIAS record
|
||||
for "example.org" (if your provider allows ALIAS or similar records, otherwise use A/AAAA), together with a TXT
|
||||
record that points to your repo (just like the CNAME record):
|
||||
`example.org IN ALIAS codeberg.page.`
|
||||
`example.org IN TXT main.pages.example.codeberg.page.`
|
||||
|
||||
Certificates are generated, updated and cleaned up automatically via Let's Encrypt through a TLS challenge.
|
||||
|
||||
## Chat for admins & devs
|
||||
|
||||
[matrix: #gitea-pages-server:matrix.org](https://matrix.to/#/#gitea-pages-server:matrix.org)
|
||||
|
||||
## Deployment
|
||||
|
||||
**Warning: Some Caveats Apply**
|
||||
**Warning: Some Caveats Apply**
|
||||
|
||||
> Currently, the deployment requires you to have some knowledge of system administration as well as understanding and building code,
|
||||
> so you can eventually edit non-configurable and codeberg-specific settings.
|
||||
> In the future, we'll try to reduce these and make hosting Codeberg Pages as easy as setting up Gitea.
|
||||
|
@ -48,29 +61,29 @@ but if you want to run it on a shared IP address (and not a standalone),
|
|||
you'll need to configure your reverse proxy not to terminate the TLS connections,
|
||||
but forward the requests on the IP level to the Pages Server.
|
||||
|
||||
You can check out a proof of concept in the `haproxy-sni` folder,
|
||||
and especially have a look at [this section of the haproxy.cfg](https://codeberg.org/Codeberg/pages-server/src/branch/main/haproxy-sni/haproxy.cfg#L38).
|
||||
You can check out a proof of concept in the `examples/haproxy-sni` folder,
|
||||
and especially have a look at [this section of the haproxy.cfg](https://codeberg.org/Codeberg/pages-server/src/branch/main/examples/haproxy-sni/haproxy.cfg#L38).
|
||||
|
||||
### Environment
|
||||
### Environment Variables
|
||||
|
||||
- `HOST` & `PORT` (default: `[::]` & `443`): listen address.
|
||||
- `PAGES_DOMAIN` (default: `codeberg.page`): main domain for pages.
|
||||
- `RAW_DOMAIN` (default: `raw.codeberg.page`): domain for raw resources.
|
||||
- `RAW_DOMAIN` (default: `raw.codeberg.page`): domain for raw resources (must be subdomain of `PAGES_DOMAIN`).
|
||||
- `GITEA_ROOT` (default: `https://codeberg.org`): root of the upstream Gitea instance.
|
||||
- `GITEA_API_TOKEN` (default: empty): API token for the Gitea instance to access non-public (e.g. limited) repos.
|
||||
- `RAW_INFO_PAGE` (default: https://docs.codeberg.org/pages/raw-content/): info page for raw resources, shown if no resource is provided.
|
||||
- `ACME_API` (default: https://acme-v02.api.letsencrypt.org/directory): set this to https://acme.mock.director to use invalid certificates without any verification (great for debugging).
|
||||
- `RAW_INFO_PAGE` (default: <https://docs.codeberg.org/pages/raw-content/>): info page for raw resources, shown if no resource is provided.
|
||||
- `ACME_API` (default: <https://acme-v02.api.letsencrypt.org/directory>): set this to <https://acme.mock.director> to use invalid certificates without any verification (great for debugging).
|
||||
ZeroSSL might be better in the future as it doesn't have rate limits and doesn't clash with the official Codeberg certificates (which are using Let's Encrypt), but I couldn't get it to work yet.
|
||||
- `ACME_EMAIL` (default: `noreply@example.email`): Set this to "true" to accept the Terms of Service of your ACME provider.
|
||||
- `ACME_EAB_KID` & `ACME_EAB_HMAC` (default: don't use EAB): EAB credentials, for example for ZeroSSL.
|
||||
- `ACME_EMAIL` (default: `noreply@example.email`): Set the email sent to the ACME API server to receive, for example, renewal reminders.
|
||||
- `ACME_EAB_KID` & `ACME_EAB_HMAC` (default: don't use EAB): EAB credentials, for example for ZeroSSL.
|
||||
- `ACME_ACCEPT_TERMS` (default: use self-signed certificate): Set this to "true" to accept the Terms of Service of your ACME provider.
|
||||
- `ACME_USE_RATE_LIMITS` (default: true): Set this to false to disable rate limits, e.g. with ZeroSSL.
|
||||
- `ENABLE_HTTP_SERVER` (default: false): Set this to true to enable the HTTP-01 challenge and redirect all other HTTP requests to HTTPS. Currently only works with port 80.
|
||||
- `DNS_PROVIDER` (default: use self-signed certificate): Code of the ACME DNS provider for the main domain wildcard.
|
||||
See https://go-acme.github.io/lego/dns/ for available values & additional environment variables.
|
||||
See <https://go-acme.github.io/lego/dns/> for available values & additional environment variables.
|
||||
- `NO_DNS_01` (default: `false`): Disable the use of ACME DNS. This means that the wildcard certificate is self-signed and all domains and subdomains will have a distinct certificate. Because this may lead to a rate limit from the ACME provider, this option is not recommended for Gitea/Forgejo instances with open registrations or a great number of users/orgs.
|
||||
- `LOG_LEVEL` (default: warn): Set this to specify the level of logging.
|
||||
|
||||
|
||||
## Contributing to the development
|
||||
|
||||
The Codeberg team is very open to your contribution.
|
||||
|
@ -78,30 +91,51 @@ Since we are working nicely in a team, it might be hard at times to get started
|
|||
(still check out the issues, we always aim to have some things to get you started).
|
||||
|
||||
If you have any questions, want to work on a feature or could imagine collaborating with us for some time,
|
||||
feel free to ping us in an issue or in a general Matrix chatgroup.
|
||||
feel free to ping us in an issue or in a general [Matrix chat room](#chat-for-admins--devs).
|
||||
|
||||
You can also contact the maintainers of this project:
|
||||
You can also contact the maintainer(s) of this project:
|
||||
|
||||
- [crapStone](https://codeberg.org/crapStone) [(Matrix)](https://matrix.to/#/@crapstone:obermui.de)
|
||||
|
||||
Previous maintainers:
|
||||
|
||||
- [momar](https://codeberg.org/momar) [(Matrix)](https://matrix.to/#/@moritz:wuks.space)
|
||||
- [6543](https://codeberg.org/6543) [(Matrix)](https://matrix.to/#/@marddl:obermui.de)
|
||||
|
||||
### First steps
|
||||
|
||||
The code of this repository is split in several modules.
|
||||
While heavy refactoring work is currently undergo, you can easily understand the basic structure:
|
||||
The code of this repository is split in several modules.
|
||||
The [Architecture is explained](https://codeberg.org/Codeberg/pages-server/wiki/Architecture) in the wiki.
|
||||
|
||||
The `cmd` folder holds the data necessary for interacting with the service via the cli.
|
||||
If you are considering to deploy the service yourself, make sure to check it out.
|
||||
The heart of the software lives in the `server` folder and is split in several modules.
|
||||
After scanning the code, you should quickly be able to understand their function and start hacking on them.
|
||||
|
||||
Again: Feel free to get in touch with us for any questions that might arise.
|
||||
Thank you very much.
|
||||
|
||||
|
||||
### Test Server
|
||||
|
||||
run `just dev`
|
||||
now this pages should work:
|
||||
- https://magiclike.localhost.mock.directory:4430/
|
||||
- https://momar.localhost.mock.directory:4430/ci-testing/
|
||||
- https://momar.localhost.mock.directory:4430/pag/@master/
|
||||
Make sure you have [golang](https://go.dev) v1.21 or newer and [just](https://just.systems/man/en/) installed.
|
||||
|
||||
run `just dev`
|
||||
now these pages should work:
|
||||
|
||||
- <https://cb_pages_tests.localhost.mock.directory:4430/images/827679288a.jpg>
|
||||
- <https://momar.localhost.mock.directory:4430/ci-testing/>
|
||||
- <https://momar.localhost.mock.directory:4430/pag/@master/>
|
||||
- <https://mock-pages.codeberg-test.org:4430/README.md>
|
||||
|
||||
### Profiling
|
||||
|
||||
> This section is just a collection of commands for quick reference. If you want to learn more about profiling read [this](https://go.dev/doc/diagnostics) article or google `golang profiling`.
|
||||
|
||||
First enable profiling by supplying the cli arg `--enable-profiling` or using the environment variable `EENABLE_PROFILING`.
|
||||
|
||||
Get cpu and mem stats:
|
||||
|
||||
```bash
|
||||
go tool pprof -raw -output=cpu.txt 'http://localhost:9999/debug/pprof/profile?seconds=60' &
|
||||
curl -so mem.txt 'http://localhost:9999/debug/pprof/heap?seconds=60'
|
||||
```
|
||||
|
||||
More endpoints are documented here: <https://pkg.go.dev/net/http/pprof>
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/akrylysov/pogreb"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/database"
|
||||
)
|
||||
|
||||
var Certs = &cli.Command{
|
||||
|
@ -24,24 +22,26 @@ var Certs = &cli.Command{
|
|||
Action: removeCert,
|
||||
},
|
||||
},
|
||||
Flags: CertStorageFlags,
|
||||
}
|
||||
|
||||
func listCerts(ctx *cli.Context) error {
|
||||
// TODO: make "key-database.pogreb" set via flag
|
||||
keyDatabase, err := database.New("key-database.pogreb")
|
||||
certDB, closeFn, err := OpenCertDB(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create database: %v", err)
|
||||
return err
|
||||
}
|
||||
defer closeFn()
|
||||
|
||||
items, err := certDB.Items(0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
items := keyDatabase.Items()
|
||||
for domain, _, err := items.Next(); err != pogreb.ErrIterationDone; domain, _, err = items.Next() {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if domain[0] == '.' {
|
||||
fmt.Printf("*")
|
||||
}
|
||||
fmt.Printf("%s\n", domain)
|
||||
fmt.Printf("Domain\tValidTill\n\n")
|
||||
for _, cert := range items {
|
||||
fmt.Printf("%s\t%s\n",
|
||||
cert.Domain,
|
||||
time.Unix(cert.ValidTill, 0).Format(time.RFC3339))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -53,20 +53,17 @@ func removeCert(ctx *cli.Context) error {
|
|||
|
||||
domains := ctx.Args().Slice()
|
||||
|
||||
// TODO: make "key-database.pogreb" set via flag
|
||||
keyDatabase, err := database.New("key-database.pogreb")
|
||||
certDB, closeFn, err := OpenCertDB(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create database: %v", err)
|
||||
return err
|
||||
}
|
||||
defer closeFn()
|
||||
|
||||
for _, domain := range domains {
|
||||
fmt.Printf("Removing domain %s from the database...\n", domain)
|
||||
if err := keyDatabase.Delete(domain); err != nil {
|
||||
if err := certDB.Delete(domain); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := keyDatabase.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
CertStorageFlags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "db-type",
|
||||
Usage: "Specify the database driver. Valid options are \"sqlite3\", \"mysql\" and \"postgres\". Read more at https://xorm.io",
|
||||
Value: "sqlite3",
|
||||
EnvVars: []string{"DB_TYPE"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "db-conn",
|
||||
Usage: "Specify the database connection. For \"sqlite3\" it's the filepath. Read more at https://go.dev/doc/tutorial/database-access",
|
||||
Value: "certs.sqlite",
|
||||
EnvVars: []string{"DB_CONN"},
|
||||
},
|
||||
}
|
||||
|
||||
ServerFlags = append(CertStorageFlags, []cli.Flag{
|
||||
// #############
|
||||
// ### Gitea ###
|
||||
// #############
|
||||
// GiteaRoot specifies the root URL of the Gitea instance, without a trailing slash.
|
||||
&cli.StringFlag{
|
||||
Name: "gitea-root",
|
||||
Usage: "specifies the root URL of the Gitea instance, without a trailing slash.",
|
||||
EnvVars: []string{"GITEA_ROOT"},
|
||||
},
|
||||
// GiteaApiToken specifies an api token for the Gitea instance
|
||||
&cli.StringFlag{
|
||||
Name: "gitea-api-token",
|
||||
Usage: "specifies an api token for the Gitea instance",
|
||||
EnvVars: []string{"GITEA_API_TOKEN"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-lfs-support",
|
||||
Usage: "enable lfs support, require gitea >= v1.17.0 as backend",
|
||||
EnvVars: []string{"ENABLE_LFS_SUPPORT"},
|
||||
Value: false,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-symlink-support",
|
||||
Usage: "follow symlinks if enabled, require gitea >= v1.18.0 as backend",
|
||||
EnvVars: []string{"ENABLE_SYMLINK_SUPPORT"},
|
||||
Value: false,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "default-mime-type",
|
||||
Usage: "specifies the default mime type for files that don't have a specific mime type.",
|
||||
EnvVars: []string{"DEFAULT_MIME_TYPE"},
|
||||
Value: "application/octet-stream",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "forbidden-mime-types",
|
||||
Usage: "specifies the forbidden mime types. Use this flag multiple times for multiple mime types.",
|
||||
EnvVars: []string{"FORBIDDEN_MIME_TYPES"},
|
||||
},
|
||||
|
||||
// ###########################
|
||||
// ### Page Server Domains ###
|
||||
// ###########################
|
||||
// MainDomainSuffix specifies the main domain (starting with a dot) for which subdomains shall be served as static
|
||||
// pages, or used for comparison in CNAME lookups. Static pages can be accessed through
|
||||
// https://{owner}.{MainDomain}[/{repo}], with repo defaulting to "pages".
|
||||
&cli.StringFlag{
|
||||
Name: "pages-domain",
|
||||
Usage: "specifies the main domain (starting with a dot) for which subdomains shall be served as static pages",
|
||||
EnvVars: []string{"PAGES_DOMAIN"},
|
||||
},
|
||||
// RawDomain specifies the domain from which raw repository content shall be served in the following format:
|
||||
// https://{RawDomain}/{owner}/{repo}[/{branch|tag|commit}/{version}]/{filepath...}
|
||||
// (set to []byte(nil) to disable raw content hosting)
|
||||
&cli.StringFlag{
|
||||
Name: "raw-domain",
|
||||
Usage: "specifies the domain from which raw repository content shall be served, not set disable raw content hosting",
|
||||
EnvVars: []string{"RAW_DOMAIN"},
|
||||
},
|
||||
|
||||
// #########################
|
||||
// ### Page Server Setup ###
|
||||
// #########################
|
||||
&cli.StringFlag{
|
||||
Name: "host",
|
||||
Usage: "specifies host of listening address",
|
||||
EnvVars: []string{"HOST"},
|
||||
Value: "[::]",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "port",
|
||||
Usage: "specifies the https port to listen to ssl requests",
|
||||
EnvVars: []string{"PORT", "HTTPS_PORT"},
|
||||
Value: 443,
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "http-port",
|
||||
Usage: "specifies the http port, you also have to enable http server via ENABLE_HTTP_SERVER=true",
|
||||
EnvVars: []string{"HTTP_PORT"},
|
||||
Value: 80,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-http-server",
|
||||
Usage: "start a http server to redirect to https and respond to http acme challenges",
|
||||
EnvVars: []string{"ENABLE_HTTP_SERVER"},
|
||||
Value: false,
|
||||
},
|
||||
// Default branches to fetch assets from
|
||||
&cli.StringSliceFlag{
|
||||
Name: "pages-branch",
|
||||
Usage: "define a branch to fetch assets from. Use this flag multiple times for multiple branches.",
|
||||
EnvVars: []string{"PAGES_BRANCHES"},
|
||||
Value: cli.NewStringSlice("pages"),
|
||||
},
|
||||
|
||||
&cli.StringSliceFlag{
|
||||
Name: "allowed-cors-domains",
|
||||
Usage: "specify allowed CORS domains. Use this flag multiple times for multiple domains.",
|
||||
EnvVars: []string{"ALLOWED_CORS_DOMAINS"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "blacklisted-paths",
|
||||
Usage: "return an error on these url paths.Use this flag multiple times for multiple paths.",
|
||||
EnvVars: []string{"BLACKLISTED_PATHS"},
|
||||
},
|
||||
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Value: "warn",
|
||||
Usage: "specify at which log level should be logged. Possible options: info, warn, error, fatal",
|
||||
EnvVars: []string{"LOG_LEVEL"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config-file",
|
||||
Usage: "specify the location of the config file",
|
||||
Aliases: []string{"config"},
|
||||
EnvVars: []string{"CONFIG_FILE"},
|
||||
},
|
||||
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-profiling",
|
||||
Usage: "enables the go http profiling endpoints",
|
||||
EnvVars: []string{"ENABLE_PROFILING"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "profiling-address",
|
||||
Usage: "specify ip address and port the profiling server should listen on",
|
||||
EnvVars: []string{"PROFILING_ADDRESS"},
|
||||
Value: "localhost:9999",
|
||||
},
|
||||
|
||||
// ############################
|
||||
// ### ACME Client Settings ###
|
||||
// ############################
|
||||
&cli.StringFlag{
|
||||
Name: "acme-api-endpoint",
|
||||
EnvVars: []string{"ACME_API"},
|
||||
Value: "https://acme-v02.api.letsencrypt.org/directory",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "acme-email",
|
||||
EnvVars: []string{"ACME_EMAIL"},
|
||||
Value: "noreply@example.email",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "acme-use-rate-limits",
|
||||
// TODO: Usage
|
||||
EnvVars: []string{"ACME_USE_RATE_LIMITS"},
|
||||
Value: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "acme-accept-terms",
|
||||
Usage: "To accept the ACME ToS",
|
||||
EnvVars: []string{"ACME_ACCEPT_TERMS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "acme-eab-kid",
|
||||
Usage: "Register the current account to the ACME server with external binding.",
|
||||
EnvVars: []string{"ACME_EAB_KID"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "acme-eab-hmac",
|
||||
Usage: "Register the current account to the ACME server with external binding.",
|
||||
EnvVars: []string{"ACME_EAB_HMAC"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "dns-provider",
|
||||
Usage: "Use DNS-Challenge for main domain. Read more at: https://go-acme.github.io/lego/dns/",
|
||||
EnvVars: []string{"DNS_PROVIDER"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-dns-01",
|
||||
Usage: "Always use individual certificates instead of a DNS-01 wild card certificate",
|
||||
EnvVars: []string{"NO_DNS_01"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "acme-account-config",
|
||||
Usage: "json file of acme account",
|
||||
Value: "acme-account.json",
|
||||
EnvVars: []string{"ACME_ACCOUNT_CONFIG"},
|
||||
},
|
||||
}...)
|
||||
)
|
|
@ -0,0 +1,39 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/database"
|
||||
"codeberg.org/codeberg/pages/server/version"
|
||||
)
|
||||
|
||||
func CreatePagesApp() *cli.App {
|
||||
app := cli.NewApp()
|
||||
app.Name = "pages-server"
|
||||
app.Version = version.Version
|
||||
app.Usage = "pages server"
|
||||
app.Flags = ServerFlags
|
||||
app.Commands = []*cli.Command{
|
||||
Certs,
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func OpenCertDB(ctx *cli.Context) (certDB database.CertDB, closeFn func(), err error) {
|
||||
certDB, err = database.NewXormDB(ctx.String("db-type"), ctx.String("db-conn"))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not connect to database: %w", err)
|
||||
}
|
||||
|
||||
closeFn = func() {
|
||||
if err := certDB.Close(); err != nil {
|
||||
log.Error().Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
return certDB, closeFn, nil
|
||||
}
|
123
cmd/flags.go
123
cmd/flags.go
|
@ -1,123 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var ServeFlags = []cli.Flag{
|
||||
// MainDomainSuffix specifies the main domain (starting with a dot) for which subdomains shall be served as static
|
||||
// pages, or used for comparison in CNAME lookups. Static pages can be accessed through
|
||||
// https://{owner}.{MainDomain}[/{repo}], with repo defaulting to "pages".
|
||||
&cli.StringFlag{
|
||||
Name: "pages-domain",
|
||||
Usage: "specifies the main domain (starting with a dot) for which subdomains shall be served as static pages",
|
||||
EnvVars: []string{"PAGES_DOMAIN"},
|
||||
Value: "codeberg.page",
|
||||
},
|
||||
// GiteaRoot specifies the root URL of the Gitea instance, without a trailing slash.
|
||||
&cli.StringFlag{
|
||||
Name: "gitea-root",
|
||||
Usage: "specifies the root URL of the Gitea instance, without a trailing slash.",
|
||||
EnvVars: []string{"GITEA_ROOT"},
|
||||
Value: "https://codeberg.org",
|
||||
},
|
||||
// GiteaApiToken specifies an api token for the Gitea instance
|
||||
&cli.StringFlag{
|
||||
Name: "gitea-api-token",
|
||||
Usage: "specifies an api token for the Gitea instance",
|
||||
EnvVars: []string{"GITEA_API_TOKEN"},
|
||||
Value: "",
|
||||
},
|
||||
// RawDomain specifies the domain from which raw repository content shall be served in the following format:
|
||||
// https://{RawDomain}/{owner}/{repo}[/{branch|tag|commit}/{version}]/{filepath...}
|
||||
// (set to []byte(nil) to disable raw content hosting)
|
||||
&cli.StringFlag{
|
||||
Name: "raw-domain",
|
||||
Usage: "specifies the domain from which raw repository content shall be served, not set disable raw content hosting",
|
||||
EnvVars: []string{"RAW_DOMAIN"},
|
||||
Value: "raw.codeberg.page",
|
||||
},
|
||||
// RawInfoPage will be shown (with a redirect) when trying to access RawDomain directly (or without owner/repo/path).
|
||||
&cli.StringFlag{
|
||||
Name: "raw-info-page",
|
||||
Usage: "will be shown (with a redirect) when trying to access $RAW_DOMAIN directly (or without owner/repo/path)",
|
||||
EnvVars: []string{"RAW_INFO_PAGE"},
|
||||
Value: "https://docs.codeberg.org/codeberg-pages/raw-content/",
|
||||
},
|
||||
|
||||
// Server
|
||||
&cli.StringFlag{
|
||||
Name: "host",
|
||||
Usage: "specifies host of listening address",
|
||||
EnvVars: []string{"HOST"},
|
||||
Value: "[::]",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "port",
|
||||
Usage: "specifies port of listening address",
|
||||
EnvVars: []string{"PORT"},
|
||||
Value: "443",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-http-server",
|
||||
// TODO: desc
|
||||
EnvVars: []string{"ENABLE_HTTP_SERVER"},
|
||||
},
|
||||
// Server Options
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-lfs-support",
|
||||
Usage: "enable lfs support, require gitea v1.17.0 as backend",
|
||||
EnvVars: []string{"ENABLE_LFS_SUPPORT"},
|
||||
Value: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-symlink-support",
|
||||
Usage: "follow symlinks if enabled, require gitea v1.18.0 as backend",
|
||||
EnvVars: []string{"ENABLE_SYMLINK_SUPPORT"},
|
||||
Value: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Value: "warn",
|
||||
Usage: "specify at which log level should be logged. Possible options: info, warn, error, fatal",
|
||||
EnvVars: []string{"LOG_LEVEL"},
|
||||
},
|
||||
|
||||
// ACME
|
||||
&cli.StringFlag{
|
||||
Name: "acme-api-endpoint",
|
||||
EnvVars: []string{"ACME_API"},
|
||||
Value: "https://acme-v02.api.letsencrypt.org/directory",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "acme-email",
|
||||
EnvVars: []string{"ACME_EMAIL"},
|
||||
Value: "noreply@example.email",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "acme-use-rate-limits",
|
||||
// TODO: Usage
|
||||
EnvVars: []string{"ACME_USE_RATE_LIMITS"},
|
||||
Value: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "acme-accept-terms",
|
||||
// TODO: Usage
|
||||
EnvVars: []string{"ACME_ACCEPT_TERMS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "acme-eab-kid",
|
||||
// TODO: Usage
|
||||
EnvVars: []string{"ACME_EAB_KID"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "acme-eab-hmac",
|
||||
// TODO: Usage
|
||||
EnvVars: []string{"ACME_EAB_HMAC"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "dns-provider",
|
||||
// TODO: Usage
|
||||
EnvVars: []string{"DNS_PROVIDER"},
|
||||
},
|
||||
}
|
152
cmd/main.go
152
cmd/main.go
|
@ -1,152 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"codeberg.org/codeberg/pages/server"
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"codeberg.org/codeberg/pages/server/certificates"
|
||||
"codeberg.org/codeberg/pages/server/database"
|
||||
"codeberg.org/codeberg/pages/server/gitea"
|
||||
"codeberg.org/codeberg/pages/server/handler"
|
||||
)
|
||||
|
||||
// AllowedCorsDomains lists the domains for which Cross-Origin Resource Sharing is allowed.
|
||||
// TODO: make it a flag
|
||||
var AllowedCorsDomains = []string{
|
||||
"fonts.codeberg.org",
|
||||
"design.codeberg.org",
|
||||
}
|
||||
|
||||
// BlacklistedPaths specifies forbidden path prefixes for all Codeberg Pages.
|
||||
// TODO: Make it a flag too
|
||||
var BlacklistedPaths = []string{
|
||||
"/.well-known/acme-challenge/",
|
||||
}
|
||||
|
||||
// Serve sets up and starts the web server.
|
||||
func Serve(ctx *cli.Context) error {
|
||||
// Initalize the logger.
|
||||
logLevel, err := zerolog.ParseLevel(ctx.String("log-level"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger().Level(logLevel)
|
||||
|
||||
giteaRoot := strings.TrimSuffix(ctx.String("gitea-root"), "/")
|
||||
giteaAPIToken := ctx.String("gitea-api-token")
|
||||
rawDomain := ctx.String("raw-domain")
|
||||
mainDomainSuffix := ctx.String("pages-domain")
|
||||
rawInfoPage := ctx.String("raw-info-page")
|
||||
listeningAddress := fmt.Sprintf("%s:%s", ctx.String("host"), ctx.String("port"))
|
||||
enableHTTPServer := ctx.Bool("enable-http-server")
|
||||
|
||||
acmeAPI := ctx.String("acme-api-endpoint")
|
||||
acmeMail := ctx.String("acme-email")
|
||||
acmeUseRateLimits := ctx.Bool("acme-use-rate-limits")
|
||||
acmeAcceptTerms := ctx.Bool("acme-accept-terms")
|
||||
acmeEabKID := ctx.String("acme-eab-kid")
|
||||
acmeEabHmac := ctx.String("acme-eab-hmac")
|
||||
dnsProvider := ctx.String("dns-provider")
|
||||
if (!acmeAcceptTerms || dnsProvider == "") && acmeAPI != "https://acme.mock.directory" {
|
||||
return errors.New("you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER, unless $ACME_API is set to https://acme.mock.directory")
|
||||
}
|
||||
|
||||
allowedCorsDomains := AllowedCorsDomains
|
||||
if rawDomain != "" {
|
||||
allowedCorsDomains = append(allowedCorsDomains, rawDomain)
|
||||
}
|
||||
|
||||
// Make sure MainDomain has a trailing dot, and GiteaRoot has no trailing slash
|
||||
if !strings.HasPrefix(mainDomainSuffix, ".") {
|
||||
mainDomainSuffix = "." + mainDomainSuffix
|
||||
}
|
||||
|
||||
keyCache := cache.NewKeyValueCache()
|
||||
challengeCache := cache.NewKeyValueCache()
|
||||
// canonicalDomainCache stores canonical domains
|
||||
canonicalDomainCache := cache.NewKeyValueCache()
|
||||
// dnsLookupCache stores DNS lookups for custom domains
|
||||
dnsLookupCache := cache.NewKeyValueCache()
|
||||
// clientResponseCache stores responses from the Gitea server
|
||||
clientResponseCache := cache.NewKeyValueCache()
|
||||
|
||||
giteaClient, err := gitea.NewClient(giteaRoot, giteaAPIToken, clientResponseCache, ctx.Bool("enable-symlink-support"), ctx.Bool("enable-lfs-support"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create new gitea client: %v", err)
|
||||
}
|
||||
|
||||
// Create handler based on settings
|
||||
httpsHandler := handler.Handler(mainDomainSuffix, rawDomain,
|
||||
giteaClient,
|
||||
rawInfoPage,
|
||||
BlacklistedPaths, allowedCorsDomains,
|
||||
dnsLookupCache, canonicalDomainCache)
|
||||
|
||||
httpHandler := server.SetupHTTPACMEChallengeServer(challengeCache)
|
||||
|
||||
// Setup listener and TLS
|
||||
log.Info().Msgf("Listening on https://%s", listeningAddress)
|
||||
listener, err := net.Listen("tcp", listeningAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't create listener: %v", err)
|
||||
}
|
||||
|
||||
// TODO: make "key-database.pogreb" set via flag
|
||||
certDB, err := database.New("key-database.pogreb")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create database: %v", err)
|
||||
}
|
||||
defer certDB.Close() //nolint:errcheck // database has no close ... sync behave like it
|
||||
|
||||
listener = tls.NewListener(listener, certificates.TLSConfig(mainDomainSuffix,
|
||||
giteaClient,
|
||||
dnsProvider,
|
||||
acmeUseRateLimits,
|
||||
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache,
|
||||
certDB))
|
||||
|
||||
acmeConfig, err := certificates.SetupAcmeConfig(acmeAPI, acmeMail, acmeEabHmac, acmeEabKID, acmeAcceptTerms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := certificates.SetupCertificates(mainDomainSuffix, dnsProvider, acmeConfig, acmeUseRateLimits, enableHTTPServer, challengeCache, certDB); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interval := 12 * time.Hour
|
||||
certMaintainCtx, cancelCertMaintain := context.WithCancel(context.Background())
|
||||
defer cancelCertMaintain()
|
||||
go certificates.MaintainCertDB(certMaintainCtx, interval, mainDomainSuffix, dnsProvider, acmeUseRateLimits, certDB)
|
||||
|
||||
if enableHTTPServer {
|
||||
go func() {
|
||||
log.Info().Msg("Start HTTP server listening on :80")
|
||||
err := http.ListenAndServe("[::]:80", httpHandler)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("Couldn't start HTTP fastServer")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Start the web fastServer
|
||||
log.Info().Msgf("Start listening on %s", listener.Addr())
|
||||
if err := http.Serve(listener, httpsHandler); err != nil {
|
||||
log.Panic().Err(err).Msg("Couldn't start fastServer")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
logLevel = 'trace'
|
||||
|
||||
[server]
|
||||
host = '127.0.0.1'
|
||||
port = 443
|
||||
httpPort = 80
|
||||
httpServerEnabled = true
|
||||
mainDomain = 'codeberg.page'
|
||||
rawDomain = 'raw.codeberg.page'
|
||||
allowedCorsDomains = ['fonts.codeberg.org', 'design.codeberg.org']
|
||||
blacklistedPaths = ['do/not/use']
|
||||
|
||||
[gitea]
|
||||
root = 'codeberg.org'
|
||||
token = 'XXXXXXXX'
|
||||
lfsEnabled = true
|
||||
followSymlinks = true
|
||||
defaultMimeType = "application/wasm"
|
||||
forbiddenMimeTypes = ["text/html"]
|
||||
|
||||
[database]
|
||||
type = 'sqlite'
|
||||
conn = 'certs.sqlite'
|
||||
|
||||
[ACME]
|
||||
email = 'a@b.c'
|
||||
apiEndpoint = 'https://example.com'
|
||||
acceptTerms = false
|
||||
useRateLimits = true
|
||||
eab_hmac = 'asdf'
|
||||
eab_kid = 'qwer'
|
||||
dnsProvider = 'cloudflare.com'
|
||||
accountConfigFile = 'nope'
|
|
@ -0,0 +1,47 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
LogLevel string `default:"warn"`
|
||||
Server ServerConfig
|
||||
Gitea GiteaConfig
|
||||
Database DatabaseConfig
|
||||
ACME ACMEConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `default:"[::]"`
|
||||
Port uint16 `default:"443"`
|
||||
HttpPort uint16 `default:"80"`
|
||||
HttpServerEnabled bool `default:"true"`
|
||||
MainDomain string
|
||||
RawDomain string
|
||||
PagesBranches []string
|
||||
AllowedCorsDomains []string
|
||||
BlacklistedPaths []string
|
||||
}
|
||||
|
||||
type GiteaConfig struct {
|
||||
Root string
|
||||
Token string
|
||||
LFSEnabled bool `default:"false"`
|
||||
FollowSymlinks bool `default:"false"`
|
||||
DefaultMimeType string `default:"application/octet-stream"`
|
||||
ForbiddenMimeTypes []string
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type string `default:"sqlite3"`
|
||||
Conn string `default:"certs.sqlite"`
|
||||
}
|
||||
|
||||
type ACMEConfig struct {
|
||||
Email string
|
||||
APIEndpoint string `default:"https://acme-v02.api.letsencrypt.org/directory"`
|
||||
AcceptTerms bool `default:"false"`
|
||||
UseRateLimits bool `default:"true"`
|
||||
EAB_HMAC string
|
||||
EAB_KID string
|
||||
DNSProvider string
|
||||
NoDNS01 bool `default:"false"`
|
||||
AccountConfigFile string `default:"acme-account.json"`
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var ALWAYS_BLACKLISTED_PATHS = []string{
|
||||
"/.well-known/acme-challenge/",
|
||||
}
|
||||
|
||||
func NewDefaultConfig() Config {
|
||||
config := Config{}
|
||||
if err := defaults.Set(&config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// defaults does not support setting arrays from strings
|
||||
config.Server.PagesBranches = []string{"main", "master", "pages"}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func ReadConfig(ctx *cli.Context) (*Config, error) {
|
||||
config := NewDefaultConfig()
|
||||
// if config is not given as argument return empty config
|
||||
if !ctx.IsSet("config-file") {
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
configFile := path.Clean(ctx.String("config-file"))
|
||||
|
||||
log.Debug().Str("config-file", configFile).Msg("reading config file")
|
||||
content, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = toml.Unmarshal(content, &config)
|
||||
return &config, err
|
||||
}
|
||||
|
||||
func MergeConfig(ctx *cli.Context, config *Config) {
|
||||
if ctx.IsSet("log-level") {
|
||||
config.LogLevel = ctx.String("log-level")
|
||||
}
|
||||
|
||||
mergeServerConfig(ctx, &config.Server)
|
||||
mergeGiteaConfig(ctx, &config.Gitea)
|
||||
mergeDatabaseConfig(ctx, &config.Database)
|
||||
mergeACMEConfig(ctx, &config.ACME)
|
||||
}
|
||||
|
||||
func mergeServerConfig(ctx *cli.Context, config *ServerConfig) {
|
||||
if ctx.IsSet("host") {
|
||||
config.Host = ctx.String("host")
|
||||
}
|
||||
if ctx.IsSet("port") {
|
||||
config.Port = uint16(ctx.Uint("port"))
|
||||
}
|
||||
if ctx.IsSet("http-port") {
|
||||
config.HttpPort = uint16(ctx.Uint("http-port"))
|
||||
}
|
||||
if ctx.IsSet("enable-http-server") {
|
||||
config.HttpServerEnabled = ctx.Bool("enable-http-server")
|
||||
}
|
||||
if ctx.IsSet("pages-domain") {
|
||||
config.MainDomain = ctx.String("pages-domain")
|
||||
}
|
||||
if ctx.IsSet("raw-domain") {
|
||||
config.RawDomain = ctx.String("raw-domain")
|
||||
}
|
||||
if ctx.IsSet("pages-branch") {
|
||||
config.PagesBranches = ctx.StringSlice("pages-branch")
|
||||
}
|
||||
if ctx.IsSet("allowed-cors-domains") {
|
||||
config.AllowedCorsDomains = ctx.StringSlice("allowed-cors-domains")
|
||||
}
|
||||
if ctx.IsSet("blacklisted-paths") {
|
||||
config.BlacklistedPaths = ctx.StringSlice("blacklisted-paths")
|
||||
}
|
||||
|
||||
// add the paths that should always be blacklisted
|
||||
config.BlacklistedPaths = append(config.BlacklistedPaths, ALWAYS_BLACKLISTED_PATHS...)
|
||||
}
|
||||
|
||||
func mergeGiteaConfig(ctx *cli.Context, config *GiteaConfig) {
|
||||
if ctx.IsSet("gitea-root") {
|
||||
config.Root = ctx.String("gitea-root")
|
||||
}
|
||||
if ctx.IsSet("gitea-api-token") {
|
||||
config.Token = ctx.String("gitea-api-token")
|
||||
}
|
||||
if ctx.IsSet("enable-lfs-support") {
|
||||
config.LFSEnabled = ctx.Bool("enable-lfs-support")
|
||||
}
|
||||
if ctx.IsSet("enable-symlink-support") {
|
||||
config.FollowSymlinks = ctx.Bool("enable-symlink-support")
|
||||
}
|
||||
if ctx.IsSet("default-mime-type") {
|
||||
config.DefaultMimeType = ctx.String("default-mime-type")
|
||||
}
|
||||
if ctx.IsSet("forbidden-mime-types") {
|
||||
config.ForbiddenMimeTypes = ctx.StringSlice("forbidden-mime-types")
|
||||
}
|
||||
}
|
||||
|
||||
func mergeDatabaseConfig(ctx *cli.Context, config *DatabaseConfig) {
|
||||
if ctx.IsSet("db-type") {
|
||||
config.Type = ctx.String("db-type")
|
||||
}
|
||||
if ctx.IsSet("db-conn") {
|
||||
config.Conn = ctx.String("db-conn")
|
||||
}
|
||||
}
|
||||
|
||||
func mergeACMEConfig(ctx *cli.Context, config *ACMEConfig) {
|
||||
if ctx.IsSet("acme-email") {
|
||||
config.Email = ctx.String("acme-email")
|
||||
}
|
||||
if ctx.IsSet("acme-api-endpoint") {
|
||||
config.APIEndpoint = ctx.String("acme-api-endpoint")
|
||||
}
|
||||
if ctx.IsSet("acme-accept-terms") {
|
||||
config.AcceptTerms = ctx.Bool("acme-accept-terms")
|
||||
}
|
||||
if ctx.IsSet("acme-use-rate-limits") {
|
||||
config.UseRateLimits = ctx.Bool("acme-use-rate-limits")
|
||||
}
|
||||
if ctx.IsSet("acme-eab-hmac") {
|
||||
config.EAB_HMAC = ctx.String("acme-eab-hmac")
|
||||
}
|
||||
if ctx.IsSet("acme-eab-kid") {
|
||||
config.EAB_KID = ctx.String("acme-eab-kid")
|
||||
}
|
||||
if ctx.IsSet("dns-provider") {
|
||||
config.DNSProvider = ctx.String("dns-provider")
|
||||
}
|
||||
if ctx.IsSet("no-dns-01") {
|
||||
config.NoDNS01 = ctx.Bool("no-dns-01")
|
||||
}
|
||||
if ctx.IsSet("acme-account-config") {
|
||||
config.AccountConfigFile = ctx.String("acme-account-config")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,603 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
cmd "codeberg.org/codeberg/pages/cli"
|
||||
)
|
||||
|
||||
func runApp(t *testing.T, fn func(*cli.Context) error, args []string) {
|
||||
app := cmd.CreatePagesApp()
|
||||
app.Action = fn
|
||||
|
||||
appCtx, appCancel := context.WithCancel(context.Background())
|
||||
defer appCancel()
|
||||
|
||||
// os.Args always contains the binary name
|
||||
args = append([]string{"testing"}, args...)
|
||||
|
||||
err := app.RunContext(appCtx, args)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// fixArrayFromCtx fixes the number of "changed" strings in a string slice according to the number of values in the context.
|
||||
// This is a workaround because the cli library has a bug where the number of values in the context gets bigger the more tests are run.
|
||||
func fixArrayFromCtx(ctx *cli.Context, key string, expected []string) []string {
|
||||
if ctx.IsSet(key) {
|
||||
ctxSlice := ctx.StringSlice(key)
|
||||
|
||||
if len(ctxSlice) > 1 {
|
||||
for i := 1; i < len(ctxSlice); i++ {
|
||||
expected = append([]string{"changed"}, expected...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expected
|
||||
}
|
||||
|
||||
func readTestConfig() (*Config, error) {
|
||||
content, err := os.ReadFile("assets/test_config.toml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expectedConfig := NewDefaultConfig()
|
||||
err = toml.Unmarshal(content, &expectedConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &expectedConfig, nil
|
||||
}
|
||||
|
||||
func TestReadConfigShouldReturnEmptyConfigWhenConfigArgEmpty(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg, err := ReadConfig(ctx)
|
||||
expected := NewDefaultConfig()
|
||||
assert.Equal(t, &expected, cfg)
|
||||
|
||||
return err
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
}
|
||||
|
||||
func TestReadConfigShouldReturnConfigFromFileWhenConfigArgPresent(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg, err := ReadConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedConfig, err := readTestConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{"--config-file", "assets/test_config.toml"},
|
||||
)
|
||||
}
|
||||
|
||||
func TestValuesReadFromConfigFileShouldBeOverwrittenByArgs(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg, err := ReadConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
MergeConfig(ctx, cfg)
|
||||
|
||||
expectedConfig, err := readTestConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedConfig.LogLevel = "debug"
|
||||
expectedConfig.Gitea.Root = "not-codeberg.org"
|
||||
expectedConfig.ACME.AcceptTerms = true
|
||||
expectedConfig.Server.Host = "172.17.0.2"
|
||||
expectedConfig.Server.BlacklistedPaths = append(expectedConfig.Server.BlacklistedPaths, ALWAYS_BLACKLISTED_PATHS...)
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--config-file", "assets/test_config.toml",
|
||||
"--log-level", "debug",
|
||||
"--gitea-root", "not-codeberg.org",
|
||||
"--acme-accept-terms",
|
||||
"--host", "172.17.0.2",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &Config{
|
||||
LogLevel: "original",
|
||||
Server: ServerConfig{
|
||||
Host: "original",
|
||||
Port: 8080,
|
||||
HttpPort: 80,
|
||||
HttpServerEnabled: false,
|
||||
MainDomain: "original",
|
||||
RawDomain: "original",
|
||||
PagesBranches: []string{"original"},
|
||||
AllowedCorsDomains: []string{"original"},
|
||||
BlacklistedPaths: []string{"original"},
|
||||
},
|
||||
Gitea: GiteaConfig{
|
||||
Root: "original",
|
||||
Token: "original",
|
||||
LFSEnabled: false,
|
||||
FollowSymlinks: false,
|
||||
DefaultMimeType: "original",
|
||||
ForbiddenMimeTypes: []string{"original"},
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Type: "original",
|
||||
Conn: "original",
|
||||
},
|
||||
ACME: ACMEConfig{
|
||||
Email: "original",
|
||||
APIEndpoint: "original",
|
||||
AcceptTerms: false,
|
||||
UseRateLimits: false,
|
||||
EAB_HMAC: "original",
|
||||
EAB_KID: "original",
|
||||
DNSProvider: "original",
|
||||
NoDNS01: false,
|
||||
AccountConfigFile: "original",
|
||||
},
|
||||
}
|
||||
|
||||
MergeConfig(ctx, cfg)
|
||||
|
||||
expectedConfig := &Config{
|
||||
LogLevel: "changed",
|
||||
Server: ServerConfig{
|
||||
Host: "changed",
|
||||
Port: 8443,
|
||||
HttpPort: 443,
|
||||
HttpServerEnabled: true,
|
||||
MainDomain: "changed",
|
||||
RawDomain: "changed",
|
||||
PagesBranches: []string{"changed"},
|
||||
AllowedCorsDomains: []string{"changed"},
|
||||
BlacklistedPaths: append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...),
|
||||
},
|
||||
Gitea: GiteaConfig{
|
||||
Root: "changed",
|
||||
Token: "changed",
|
||||
LFSEnabled: true,
|
||||
FollowSymlinks: true,
|
||||
DefaultMimeType: "changed",
|
||||
ForbiddenMimeTypes: []string{"changed"},
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Type: "changed",
|
||||
Conn: "changed",
|
||||
},
|
||||
ACME: ACMEConfig{
|
||||
Email: "changed",
|
||||
APIEndpoint: "changed",
|
||||
AcceptTerms: true,
|
||||
UseRateLimits: true,
|
||||
EAB_HMAC: "changed",
|
||||
EAB_KID: "changed",
|
||||
DNSProvider: "changed",
|
||||
NoDNS01: true,
|
||||
AccountConfigFile: "changed",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--log-level", "changed",
|
||||
// Server
|
||||
"--pages-domain", "changed",
|
||||
"--raw-domain", "changed",
|
||||
"--allowed-cors-domains", "changed",
|
||||
"--blacklisted-paths", "changed",
|
||||
"--pages-branch", "changed",
|
||||
"--host", "changed",
|
||||
"--port", "8443",
|
||||
"--http-port", "443",
|
||||
"--enable-http-server",
|
||||
// Gitea
|
||||
"--gitea-root", "changed",
|
||||
"--gitea-api-token", "changed",
|
||||
"--enable-lfs-support",
|
||||
"--enable-symlink-support",
|
||||
"--default-mime-type", "changed",
|
||||
"--forbidden-mime-types", "changed",
|
||||
// Database
|
||||
"--db-type", "changed",
|
||||
"--db-conn", "changed",
|
||||
// ACME
|
||||
"--acme-email", "changed",
|
||||
"--acme-api-endpoint", "changed",
|
||||
"--acme-accept-terms",
|
||||
"--acme-use-rate-limits",
|
||||
"--acme-eab-hmac", "changed",
|
||||
"--acme-eab-kid", "changed",
|
||||
"--dns-provider", "changed",
|
||||
"--no-dns-01",
|
||||
"--acme-account-config", "changed",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeServerConfigShouldAddDefaultBlacklistedPathsToBlacklistedPaths(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &ServerConfig{}
|
||||
mergeServerConfig(ctx, cfg)
|
||||
|
||||
expected := ALWAYS_BLACKLISTED_PATHS
|
||||
assert.Equal(t, expected, cfg.BlacklistedPaths)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeServerConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
for range []uint8{0, 1} {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &ServerConfig{
|
||||
Host: "original",
|
||||
Port: 8080,
|
||||
HttpPort: 80,
|
||||
HttpServerEnabled: false,
|
||||
MainDomain: "original",
|
||||
RawDomain: "original",
|
||||
AllowedCorsDomains: []string{"original"},
|
||||
BlacklistedPaths: []string{"original"},
|
||||
}
|
||||
|
||||
mergeServerConfig(ctx, cfg)
|
||||
|
||||
expectedConfig := &ServerConfig{
|
||||
Host: "changed",
|
||||
Port: 8443,
|
||||
HttpPort: 443,
|
||||
HttpServerEnabled: true,
|
||||
MainDomain: "changed",
|
||||
RawDomain: "changed",
|
||||
AllowedCorsDomains: fixArrayFromCtx(ctx, "allowed-cors-domains", []string{"changed"}),
|
||||
BlacklistedPaths: fixArrayFromCtx(ctx, "blacklisted-paths", append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...)),
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--pages-domain", "changed",
|
||||
"--raw-domain", "changed",
|
||||
"--allowed-cors-domains", "changed",
|
||||
"--blacklisted-paths", "changed",
|
||||
"--host", "changed",
|
||||
"--port", "8443",
|
||||
"--http-port", "443",
|
||||
"--enable-http-server",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeServerConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
||||
type testValuePair struct {
|
||||
args []string
|
||||
callback func(*ServerConfig)
|
||||
}
|
||||
testValuePairs := []testValuePair{
|
||||
{args: []string{"--host", "changed"}, callback: func(sc *ServerConfig) { sc.Host = "changed" }},
|
||||
{args: []string{"--port", "8443"}, callback: func(sc *ServerConfig) { sc.Port = 8443 }},
|
||||
{args: []string{"--http-port", "443"}, callback: func(sc *ServerConfig) { sc.HttpPort = 443 }},
|
||||
{args: []string{"--enable-http-server"}, callback: func(sc *ServerConfig) { sc.HttpServerEnabled = true }},
|
||||
{args: []string{"--pages-domain", "changed"}, callback: func(sc *ServerConfig) { sc.MainDomain = "changed" }},
|
||||
{args: []string{"--raw-domain", "changed"}, callback: func(sc *ServerConfig) { sc.RawDomain = "changed" }},
|
||||
{args: []string{"--pages-branch", "changed"}, callback: func(sc *ServerConfig) { sc.PagesBranches = []string{"changed"} }},
|
||||
{args: []string{"--allowed-cors-domains", "changed"}, callback: func(sc *ServerConfig) { sc.AllowedCorsDomains = []string{"changed"} }},
|
||||
{args: []string{"--blacklisted-paths", "changed"}, callback: func(sc *ServerConfig) { sc.BlacklistedPaths = []string{"changed"} }},
|
||||
}
|
||||
|
||||
for _, pair := range testValuePairs {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := ServerConfig{
|
||||
Host: "original",
|
||||
Port: 8080,
|
||||
HttpPort: 80,
|
||||
HttpServerEnabled: false,
|
||||
MainDomain: "original",
|
||||
RawDomain: "original",
|
||||
PagesBranches: []string{"original"},
|
||||
AllowedCorsDomains: []string{"original"},
|
||||
BlacklistedPaths: []string{"original"},
|
||||
}
|
||||
|
||||
expectedConfig := cfg
|
||||
pair.callback(&expectedConfig)
|
||||
expectedConfig.BlacklistedPaths = append(expectedConfig.BlacklistedPaths, ALWAYS_BLACKLISTED_PATHS...)
|
||||
|
||||
expectedConfig.PagesBranches = fixArrayFromCtx(ctx, "pages-branch", expectedConfig.PagesBranches)
|
||||
expectedConfig.AllowedCorsDomains = fixArrayFromCtx(ctx, "allowed-cors-domains", expectedConfig.AllowedCorsDomains)
|
||||
expectedConfig.BlacklistedPaths = fixArrayFromCtx(ctx, "blacklisted-paths", expectedConfig.BlacklistedPaths)
|
||||
|
||||
mergeServerConfig(ctx, &cfg)
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
pair.args,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeGiteaConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &GiteaConfig{
|
||||
Root: "original",
|
||||
Token: "original",
|
||||
LFSEnabled: false,
|
||||
FollowSymlinks: false,
|
||||
DefaultMimeType: "original",
|
||||
ForbiddenMimeTypes: []string{"original"},
|
||||
}
|
||||
|
||||
mergeGiteaConfig(ctx, cfg)
|
||||
|
||||
expectedConfig := &GiteaConfig{
|
||||
Root: "changed",
|
||||
Token: "changed",
|
||||
LFSEnabled: true,
|
||||
FollowSymlinks: true,
|
||||
DefaultMimeType: "changed",
|
||||
ForbiddenMimeTypes: fixArrayFromCtx(ctx, "forbidden-mime-types", []string{"changed"}),
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--gitea-root", "changed",
|
||||
"--gitea-api-token", "changed",
|
||||
"--enable-lfs-support",
|
||||
"--enable-symlink-support",
|
||||
"--default-mime-type", "changed",
|
||||
"--forbidden-mime-types", "changed",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeGiteaConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
||||
type testValuePair struct {
|
||||
args []string
|
||||
callback func(*GiteaConfig)
|
||||
}
|
||||
testValuePairs := []testValuePair{
|
||||
{args: []string{"--gitea-root", "changed"}, callback: func(gc *GiteaConfig) { gc.Root = "changed" }},
|
||||
{args: []string{"--gitea-api-token", "changed"}, callback: func(gc *GiteaConfig) { gc.Token = "changed" }},
|
||||
{args: []string{"--enable-lfs-support"}, callback: func(gc *GiteaConfig) { gc.LFSEnabled = true }},
|
||||
{args: []string{"--enable-symlink-support"}, callback: func(gc *GiteaConfig) { gc.FollowSymlinks = true }},
|
||||
{args: []string{"--default-mime-type", "changed"}, callback: func(gc *GiteaConfig) { gc.DefaultMimeType = "changed" }},
|
||||
{args: []string{"--forbidden-mime-types", "changed"}, callback: func(gc *GiteaConfig) { gc.ForbiddenMimeTypes = []string{"changed"} }},
|
||||
}
|
||||
|
||||
for _, pair := range testValuePairs {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := GiteaConfig{
|
||||
Root: "original",
|
||||
Token: "original",
|
||||
LFSEnabled: false,
|
||||
FollowSymlinks: false,
|
||||
DefaultMimeType: "original",
|
||||
ForbiddenMimeTypes: []string{"original"},
|
||||
}
|
||||
|
||||
expectedConfig := cfg
|
||||
pair.callback(&expectedConfig)
|
||||
|
||||
mergeGiteaConfig(ctx, &cfg)
|
||||
|
||||
expectedConfig.ForbiddenMimeTypes = fixArrayFromCtx(ctx, "forbidden-mime-types", expectedConfig.ForbiddenMimeTypes)
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
pair.args,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeDatabaseConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &DatabaseConfig{
|
||||
Type: "original",
|
||||
Conn: "original",
|
||||
}
|
||||
|
||||
mergeDatabaseConfig(ctx, cfg)
|
||||
|
||||
expectedConfig := &DatabaseConfig{
|
||||
Type: "changed",
|
||||
Conn: "changed",
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--db-type", "changed",
|
||||
"--db-conn", "changed",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeDatabaseConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
||||
type testValuePair struct {
|
||||
args []string
|
||||
callback func(*DatabaseConfig)
|
||||
}
|
||||
testValuePairs := []testValuePair{
|
||||
{args: []string{"--db-type", "changed"}, callback: func(gc *DatabaseConfig) { gc.Type = "changed" }},
|
||||
{args: []string{"--db-conn", "changed"}, callback: func(gc *DatabaseConfig) { gc.Conn = "changed" }},
|
||||
}
|
||||
|
||||
for _, pair := range testValuePairs {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := DatabaseConfig{
|
||||
Type: "original",
|
||||
Conn: "original",
|
||||
}
|
||||
|
||||
expectedConfig := cfg
|
||||
pair.callback(&expectedConfig)
|
||||
|
||||
mergeDatabaseConfig(ctx, &cfg)
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
pair.args,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeACMEConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &ACMEConfig{
|
||||
Email: "original",
|
||||
APIEndpoint: "original",
|
||||
AcceptTerms: false,
|
||||
UseRateLimits: false,
|
||||
EAB_HMAC: "original",
|
||||
EAB_KID: "original",
|
||||
DNSProvider: "original",
|
||||
NoDNS01: false,
|
||||
AccountConfigFile: "original",
|
||||
}
|
||||
|
||||
mergeACMEConfig(ctx, cfg)
|
||||
|
||||
expectedConfig := &ACMEConfig{
|
||||
Email: "changed",
|
||||
APIEndpoint: "changed",
|
||||
AcceptTerms: true,
|
||||
UseRateLimits: true,
|
||||
EAB_HMAC: "changed",
|
||||
EAB_KID: "changed",
|
||||
DNSProvider: "changed",
|
||||
NoDNS01: true,
|
||||
AccountConfigFile: "changed",
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--acme-email", "changed",
|
||||
"--acme-api-endpoint", "changed",
|
||||
"--acme-accept-terms",
|
||||
"--acme-use-rate-limits",
|
||||
"--acme-eab-hmac", "changed",
|
||||
"--acme-eab-kid", "changed",
|
||||
"--dns-provider", "changed",
|
||||
"--no-dns-01",
|
||||
"--acme-account-config", "changed",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeACMEConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
||||
type testValuePair struct {
|
||||
args []string
|
||||
callback func(*ACMEConfig)
|
||||
}
|
||||
testValuePairs := []testValuePair{
|
||||
{args: []string{"--acme-email", "changed"}, callback: func(gc *ACMEConfig) { gc.Email = "changed" }},
|
||||
{args: []string{"--acme-api-endpoint", "changed"}, callback: func(gc *ACMEConfig) { gc.APIEndpoint = "changed" }},
|
||||
{args: []string{"--acme-accept-terms"}, callback: func(gc *ACMEConfig) { gc.AcceptTerms = true }},
|
||||
{args: []string{"--acme-use-rate-limits"}, callback: func(gc *ACMEConfig) { gc.UseRateLimits = true }},
|
||||
{args: []string{"--acme-eab-hmac", "changed"}, callback: func(gc *ACMEConfig) { gc.EAB_HMAC = "changed" }},
|
||||
{args: []string{"--acme-eab-kid", "changed"}, callback: func(gc *ACMEConfig) { gc.EAB_KID = "changed" }},
|
||||
{args: []string{"--dns-provider", "changed"}, callback: func(gc *ACMEConfig) { gc.DNSProvider = "changed" }},
|
||||
{args: []string{"--no-dns-01"}, callback: func(gc *ACMEConfig) { gc.NoDNS01 = true }},
|
||||
{args: []string{"--acme-account-config", "changed"}, callback: func(gc *ACMEConfig) { gc.AccountConfigFile = "changed" }},
|
||||
}
|
||||
|
||||
for _, pair := range testValuePairs {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := ACMEConfig{
|
||||
Email: "original",
|
||||
APIEndpoint: "original",
|
||||
AcceptTerms: false,
|
||||
UseRateLimits: false,
|
||||
EAB_HMAC: "original",
|
||||
EAB_KID: "original",
|
||||
DNSProvider: "original",
|
||||
AccountConfigFile: "original",
|
||||
}
|
||||
|
||||
expectedConfig := cfg
|
||||
pair.callback(&expectedConfig)
|
||||
|
||||
mergeACMEConfig(ctx, &cfg)
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
pair.args,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
logLevel = 'debug'
|
||||
|
||||
[server]
|
||||
host = '[::]'
|
||||
port = 443
|
||||
httpPort = 80
|
||||
httpServerEnabled = true
|
||||
mainDomain = 'codeberg.page'
|
||||
rawDomain = 'raw.codeberg.page'
|
||||
pagesBranches = ["pages"]
|
||||
allowedCorsDomains = []
|
||||
blacklistedPaths = []
|
||||
|
||||
[gitea]
|
||||
root = 'https://codeberg.org'
|
||||
token = 'ASDF1234'
|
||||
lfsEnabled = true
|
||||
followSymlinks = true
|
||||
|
||||
[database]
|
||||
type = 'sqlite'
|
||||
conn = 'certs.sqlite'
|
||||
|
||||
[ACME]
|
||||
email = 'noreply@example.email'
|
||||
apiEndpoint = 'https://acme-v02.api.letsencrypt.org/directory'
|
||||
acceptTerms = false
|
||||
useRateLimits = false
|
||||
eab_hmac = ''
|
||||
eab_kid = ''
|
||||
dnsProvider = ''
|
||||
accountConfigFile = 'acme-account.json'
|
|
@ -1,8 +1,9 @@
|
|||
# HAProxy with SNI & Host-based rules
|
||||
|
||||
This is a proof of concept, enabling HAProxy to use *either* SNI to redirect to backends with their own HTTPS certificates (which are then fully exposed to the client; HAProxy only proxies on a TCP level in that case), *as well as* to terminate HTTPS and use the Host header to redirect to backends that use HTTP (or a new HTTPS connection).
|
||||
This is a proof of concept, enabling HAProxy to use _either_ SNI to redirect to backends with their own HTTPS certificates (which are then fully exposed to the client; HAProxy only proxies on a TCP level in that case), _as well as_ to terminate HTTPS and use the Host header to redirect to backends that use HTTP (or a new HTTPS connection).
|
||||
|
||||
## How it works
|
||||
|
||||
1. The `http_redirect_frontend` is only there to listen on port 80 and redirect every request to HTTPS.
|
||||
2. The `https_sni_frontend` listens on port 443 and chooses a backend based on the SNI hostname of the TLS connection.
|
||||
3. The `https_termination_backend` passes all requests to a unix socket (using the plain TCP data).
|
||||
|
@ -11,6 +12,7 @@ This is a proof of concept, enabling HAProxy to use *either* SNI to redirect to
|
|||
In the example (see [haproxy.cfg](haproxy.cfg)), the `pages_backend` is listening via HTTPS and is providing its own HTTPS certificates, while the `gitea_backend` only provides HTTP.
|
||||
|
||||
## How to test
|
||||
|
||||
```bash
|
||||
docker-compose up &
|
||||
./test.sh
|
|
@ -0,0 +1,21 @@
|
|||
version: '3'
|
||||
services:
|
||||
haproxy:
|
||||
image: haproxy
|
||||
ports: ['443:443']
|
||||
volumes:
|
||||
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
||||
- ./dhparam.pem:/etc/ssl/dhparam.pem:ro
|
||||
- ./haproxy-certificates:/etc/ssl/private/haproxy:ro
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
gitea:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./gitea-www:/srv:ro
|
||||
- ./gitea.Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
pages:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./pages-www:/srv:ro
|
||||
- ./pages.Caddyfile:/etc/caddy/Caddyfile:ro
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "flake-utils",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1714030708,
|
||||
"narHash": "sha256-JOGPOxa8N6ySzB7SQBsh0OVz+UXZriyahgvfNHMIY0Y=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b0d52b31f7f4d80f8bf38f0253652125579c35ff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "systems",
|
||||
"type": "indirect"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
systems,
|
||||
}:
|
||||
flake-utils.lib.eachSystem (import systems)
|
||||
(system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in {
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
gcc
|
||||
go
|
||||
gofumpt
|
||||
gopls
|
||||
gotools
|
||||
go-tools
|
||||
sqlite-interactive
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
43
go.mod
43
go.mod
|
@ -1,17 +1,26 @@
|
|||
module codeberg.org/codeberg/pages
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.4
|
||||
|
||||
require (
|
||||
code.gitea.io/sdk/gitea v0.15.1-0.20220729105105-cc14c63cccfa
|
||||
code.gitea.io/sdk/gitea v0.17.1
|
||||
github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a
|
||||
github.com/akrylysov/pogreb v0.10.1
|
||||
github.com/creasty/defaults v1.7.0
|
||||
github.com/go-acme/lego/v4 v4.5.3
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/pelletier/go-toml/v2 v2.1.0
|
||||
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad
|
||||
github.com/rs/zerolog v1.27.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
|
||||
xorm.io/xorm v1.3.2
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -31,6 +40,7 @@ require (
|
|||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183 // indirect
|
||||
github.com/aws/aws-sdk-go v1.39.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.20.0 // indirect
|
||||
|
@ -47,14 +57,17 @@ require (
|
|||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect
|
||||
github.com/gofrs/uuid v3.2.0+incompatible // indirect
|
||||
github.com/goccy/go-json v0.8.1 // indirect
|
||||
github.com/gofrs/uuid v4.0.0+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
|
||||
github.com/gophercloud/gophercloud v0.16.0 // indirect
|
||||
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
|
@ -62,7 +75,7 @@ require (
|
|||
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
||||
github.com/jarcoal/httpmock v1.0.6 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.7 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
|
@ -78,7 +91,7 @@ require (
|
|||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/auroradns v1.0.1 // indirect
|
||||
github.com/nrdcg/desec v0.6.0 // indirect
|
||||
|
@ -102,17 +115,18 @@ require (
|
|||
github.com/softlayer/softlayer-go v1.0.3 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/transip/gotransip/v6 v6.6.1 // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14 // indirect
|
||||
github.com/vultr/govultr/v2 v2.7.1 // indirect
|
||||
go.opencensus.io v0.22.3 // indirect
|
||||
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect
|
||||
google.golang.org/api v0.20.0 // indirect
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
|
@ -123,5 +137,6 @@ require (
|
|||
gopkg.in/ns1/ns1-go.v2 v2.6.2 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
xorm.io/builder v0.3.12 // indirect
|
||||
)
|
||||
|
|
454
go.sum
454
go.sum
|
@ -22,9 +22,12 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
|
|||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
code.gitea.io/sdk/gitea v0.15.1-0.20220729105105-cc14c63cccfa h1:OVwgYrY6vr6gWZvgnmevFhtL0GVA4HKaFOhD+joPoNk=
|
||||
code.gitea.io/sdk/gitea v0.15.1-0.20220729105105-cc14c63cccfa/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE=
|
||||
code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8=
|
||||
code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
|
||||
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible h1:1JP8SKfroEakYiQU2ZyPDosh8w2Tg9UopKt88VyQPt4=
|
||||
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
|
@ -55,24 +58,38 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM
|
|||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24=
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
|
||||
github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a h1:Cf4CrDeyrIcuIiJZEZJAH5dapqQ6J3OmP/vHPbDjaFA=
|
||||
github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a/go.mod h1:ig6eVXkYn/9dz0Vm8UdLf+E0u1bE6kBSn3n2hqk6jas=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1 h1:bLzehmpyCwQiqCE1Qe9Ny6fbFqs7hPlmo9vKv2orUxs=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1/go.mod h1:kX6YddBkXqqywAe8c9LyvgTCyFuZCTMF4cRPQhc3Fy8=
|
||||
github.com/akrylysov/pogreb v0.10.1 h1:FqlR8VR7uCbJdfUob916tPM+idpKgeESDXOA1K0DK4w=
|
||||
github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183 h1:dkj8/dxOQ4L1XpwCzRLqukvUBbxuNdz3FeyvHFnRjmo=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.39.0 h1:74BBwkEmiqBbi2CGflEh34l0YNtIibTjZsibGarkNjo=
|
||||
github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
|
@ -81,27 +98,41 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
|||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/cloudflare-go v0.20.0 h1:y2a6KwYHTFxhw+8PLhz0q5hpTGj6Un3W1pbpQLhzFaE=
|
||||
github.com/cloudflare/cloudflare-go v0.20.0/go.mod h1:sPWL/lIC6biLEdyGZwBQ1rGQKF1FhM7N60fuNiFdYTI=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpu/goacmedns v0.1.1 h1:DM3H2NiN2oam7QljgGY5ygy4yDXhK5Z4JUnqaugs2C4=
|
||||
github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA=
|
||||
github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
|
@ -110,6 +141,7 @@ github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454Wv
|
|||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/deepmap/oapi-codegen v1.6.1 h1:2BvsmRb6pogGNtr8Ann+esAbSKFXx2CZN18VpAMecnw=
|
||||
github.com/deepmap/oapi-codegen v1.6.1/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
|
||||
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
|
@ -118,6 +150,13 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/
|
|||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/dnsimple/dnsimple-go v0.70.1 h1:cSZndVjttLpgplDuesY4LFIvfKf/zRA1J7mCATBbzSM=
|
||||
github.com/dnsimple/dnsimple-go v0.70.1/go.mod h1:F9WHww9cC76hrnwGFfAfrqdW99j3MOYasQcIwTS/aUk=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/exoscale/egoscale v0.67.0 h1:qgWh7T5IZGrNWtg6ib4dr+76WThvB+odTtGG+DGbXF8=
|
||||
|
@ -127,6 +166,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
|||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
|
@ -144,24 +185,37 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
|
|||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY=
|
||||
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
|
||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
|
||||
github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI=
|
||||
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
|
@ -186,6 +240,10 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
|||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
|
@ -194,8 +252,10 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
|
@ -208,8 +268,10 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf
|
|||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
|
@ -220,16 +282,25 @@ github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae h1:Hi3IgB9RQDE15
|
|||
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae/go.mod h1:wx8HMD8oQD0Ryhz6+6ykq75PJ79iPyEqYHfwZ4l7OsA=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
|
@ -246,7 +317,7 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
|
|||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
|
@ -258,12 +329,61 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m
|
|||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E=
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU=
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
|
||||
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
||||
github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
|
||||
github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
||||
github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=
|
||||
github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
github.com/jarcoal/httpmock v1.0.6 h1:e81vOSexXU3mJuJ4l//geOmKIt+Vkxerk1feQBC8D0g=
|
||||
github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
|
@ -277,8 +397,10 @@ github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
|
|||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
|
@ -286,6 +408,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
|||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b h1:DzHy0GlWeF0KAglaTMY7Q+khIFoG8toHP+wLFBVBQJc=
|
||||
|
@ -298,6 +422,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
|
|||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 h1:I7ITbmQPAVwrDdhd6dHKi+MYJTJqPCK0jE6YNBAevnk=
|
||||
|
@ -306,6 +431,15 @@ github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk=
|
|||
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
|
||||
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/linode/linodego v0.31.1 h1:dBtjKo7J9UhNFhTOclEXb12RRyQDaRBxISdONVuU+DA=
|
||||
github.com/linode/linodego v0.31.1/go.mod h1:BR0gVkCJffEdIGJSl6bHR80Ty+Uvg/2jkjmrWaFectM=
|
||||
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
||||
|
@ -315,19 +449,25 @@ github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAW
|
|||
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
|
||||
github.com/liquidweb/liquidweb-go v1.6.3 h1:NVHvcnX3eb3BltiIoA+gLYn15nOpkYkdizOEYGSKrk4=
|
||||
github.com/liquidweb/liquidweb-go v1.6.3/go.mod h1:SuXXp+thr28LnjEw18AYtWwIbWMHSUiajPQs8T9c/Rc=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
|
@ -337,9 +477,14 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
|||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
|
||||
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
|
@ -360,11 +505,19 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/nrdcg/auroradns v1.0.1 h1:m/kBq83Xvy3cU261MOknd8BdnOk12q4lAWM+kOdsC2Y=
|
||||
github.com/nrdcg/auroradns v1.0.1/go.mod h1:y4pc0i9QXYlFCWrhWrUSIETnZgrf4KuwjDIWmmXo3JI=
|
||||
|
@ -383,29 +536,51 @@ github.com/nrdcg/porkbun v0.1.1/go.mod h1:JWl/WKnguWos4mjfp4YizvvToigk9qpQwrodOk
|
|||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.14.0 h1:ep6kpPVwmr/nTbklSx2nrLNSIO62DoYAhnPNIMhK8gI=
|
||||
github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr97jXRSSN5MxTgG/zU=
|
||||
github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
||||
github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk=
|
||||
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
@ -414,27 +589,41 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
|
|||
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
|
||||
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad h1:WtSUHi5zthjudjIi3L6QmL/V9vpJPbc/j/F2u55d3fs=
|
||||
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad/go.mod h1:h0+DiDRe2Y+6iHTjIq/9HzUq7NII/Nffp0HkFrsAKq4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
|
@ -442,12 +631,18 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sacloud/libsacloud v1.36.2 h1:aosI7clbQ9IU0Hj+3rpk3SKJop5nLPpLThnWCivPqjI=
|
||||
github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f h1:WSnaD0/cvbKJgSTYbjAPf4RJXVvNNDAwVm+W8wEmnGE=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
|
@ -466,35 +661,52 @@ github.com/softlayer/softlayer-go v1.0.3/go.mod h1:6HepcfAXROz0Rf63krk5hPZyHT6qy
|
|||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ=
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/transip/gotransip/v6 v6.6.1 h1:nsCU1ErZS5G0FeOpgGXc4FsWvBff9GPswSMggsC4564=
|
||||
github.com/transip/gotransip/v6 v6.6.1/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g=
|
||||
github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo=
|
||||
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
|
@ -511,7 +723,14 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
|
|||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
|
@ -519,28 +738,42 @@ go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
|
|||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277 h1:d9qaMM+ODpCq+9We41//fu/sHsTnXcrqd1en3x+GKy4=
|
||||
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -551,6 +784,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
@ -571,6 +806,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -579,6 +817,7 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r
|
|||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
@ -589,6 +828,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
@ -604,8 +844,11 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -619,8 +862,10 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
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-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -629,9 +874,11 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -641,6 +888,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -648,6 +896,7 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -663,18 +912,29 @@ golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
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=
|
||||
|
@ -682,8 +942,13 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -692,6 +957,7 @@ golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxb
|
|||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs=
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
@ -700,14 +966,18 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
|
|||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
|
@ -716,6 +986,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
|
@ -725,12 +996,18 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK
|
|||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
|
@ -743,6 +1020,7 @@ google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/
|
|||
google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
|
@ -753,6 +1031,7 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn
|
|||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
|
@ -767,10 +1046,15 @@ google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfG
|
|||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171 h1:xes2Q2k+d/+YNXVw0FpZkIDJiaux4OVrRKXRAzH6A0U=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
|
||||
|
@ -789,10 +1073,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
|||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
@ -806,6 +1093,7 @@ gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI
|
|||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
@ -816,14 +1104,132 @@ 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.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
|
||||
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
|
||||
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
|
||||
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
|
||||
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
|
||||
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
|
||||
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
|
||||
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
|
||||
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
|
||||
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
|
||||
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
|
||||
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
|
||||
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
|
||||
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
|
||||
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
|
||||
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
|
||||
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
|
||||
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
|
||||
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
|
||||
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
|
||||
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
|
||||
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
|
||||
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
|
||||
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
|
||||
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
|
||||
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
|
||||
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
|
||||
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
|
||||
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
|
||||
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
|
||||
modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4=
|
||||
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
|
||||
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
|
||||
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
|
||||
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
|
||||
modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
|
||||
modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
|
||||
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
|
||||
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
|
||||
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
|
||||
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
|
||||
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
|
||||
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
|
||||
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
|
||||
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
|
||||
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
|
||||
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
|
||||
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
|
||||
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
|
||||
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
|
||||
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
|
||||
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
|
||||
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
|
||||
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
|
||||
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
|
||||
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
|
||||
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
|
||||
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
|
||||
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
|
||||
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
|
||||
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
|
||||
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
|
||||
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
|
||||
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
|
||||
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
|
||||
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
|
||||
modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
|
||||
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
|
||||
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
|
||||
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
|
||||
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
|
||||
modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE=
|
||||
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
|
||||
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
|
||||
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
|
||||
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
|
||||
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
|
||||
modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
|
||||
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
|
||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
|
||||
xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
|
||||
xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
version: "3"
|
||||
services:
|
||||
haproxy:
|
||||
image: haproxy
|
||||
ports: ["443:443"]
|
||||
volumes:
|
||||
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
||||
- ./dhparam.pem:/etc/ssl/dhparam.pem:ro
|
||||
- ./haproxy-certificates:/etc/ssl/private/haproxy:ro
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
gitea:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./gitea-www:/srv:ro
|
||||
- ./gitea.Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
pages:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./pages-www:/srv:ro
|
||||
- ./pages.Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<!doctype html>
|
||||
<html class="codeberg-design">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>%status%</title>
|
||||
|
||||
<link rel="stylesheet" href="https://design.codeberg.org/design-kit/codeberg.css" />
|
||||
<link href="https://fonts.codeberg.org/dist/inter/Inter%20Web/inter.css" rel="stylesheet" />
|
||||
<link href="https://fonts.codeberg.org/dist/fontawesome5/css/all.min.css" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0; padding: 1rem; box-sizing: border-box;
|
||||
width: 100%; min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<i class="fa fa-search text-primary" style="font-size: 96px;"></i>
|
||||
<h1 class="mb-0 text-primary">
|
||||
Page not found!
|
||||
</h1>
|
||||
<h5 class="text-center" style="max-width: 25em;">
|
||||
Sorry, but this page couldn't be found or is inaccessible (%status%).<br/>
|
||||
We hope this isn't a problem on our end ;) - Make sure to check the <a href="https://docs.codeberg.org/codeberg-pages/troubleshooting/" target="_blank">troubleshooting section in the Docs</a>!
|
||||
</h5>
|
||||
<small class="text-muted">
|
||||
<img src="https://design.codeberg.org/logo-kit/icon.svg" class="align-top">
|
||||
Static pages made easy - <a href="https://codeberg.page">Codeberg Pages</a>
|
||||
</small>
|
||||
</body>
|
||||
</html>
|
|
@ -1,50 +0,0 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/context"
|
||||
)
|
||||
|
||||
// ReturnErrorPage sets the response status code and writes NotFoundPage to the response body,
|
||||
// with "%status%" and %message% replaced with the provided statusCode and msg
|
||||
func ReturnErrorPage(ctx *context.Context, msg string, statusCode int) {
|
||||
ctx.RespWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
ctx.RespWriter.WriteHeader(statusCode)
|
||||
|
||||
msg = generateResponse(msg, statusCode)
|
||||
|
||||
_, _ = ctx.RespWriter.Write([]byte(msg))
|
||||
}
|
||||
|
||||
// TODO: use template engine
|
||||
func generateResponse(msg string, statusCode int) string {
|
||||
if msg == "" {
|
||||
msg = strings.ReplaceAll(NotFoundPage,
|
||||
"%status%",
|
||||
strconv.Itoa(statusCode)+" "+errorMessage(statusCode))
|
||||
} else {
|
||||
msg = strings.ReplaceAll(
|
||||
strings.ReplaceAll(ErrorPage, "%message%", template.HTMLEscapeString(msg)),
|
||||
"%status%",
|
||||
http.StatusText(statusCode))
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func errorMessage(statusCode int) string {
|
||||
message := http.StatusText(statusCode)
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusMisdirectedRequest:
|
||||
message += " - domain not specified in <code>.domains</code> file"
|
||||
case http.StatusFailedDependency:
|
||||
message += " - target repo/branch doesn't exist or is private"
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
<!doctype html>
|
||||
<html class="codeberg-design">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>%status%</title>
|
||||
|
||||
<link rel="stylesheet" href="https://design.codeberg.org/design-kit/codeberg.css" />
|
||||
<link href="https://fonts.codeberg.org/dist/inter/Inter%20Web/inter.css" rel="stylesheet" />
|
||||
<link href="https://fonts.codeberg.org/dist/fontawesome5/css/all.min.css" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0; padding: 1rem; box-sizing: border-box;
|
||||
width: 100%; min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<i class="fa fa-search text-primary" style="font-size: 96px;"></i>
|
||||
<h1 class="mb-0 text-primary">
|
||||
%status%!
|
||||
</h1>
|
||||
<h5 class="text-center" style="max-width: 25em;">
|
||||
Sorry, but this page couldn't be served.<br/>
|
||||
We got an <b>"%message%"</b><br/>
|
||||
We hope this isn't a problem on our end ;) - Make sure to check the <a href="https://docs.codeberg.org/codeberg-pages/troubleshooting/" target="_blank">troubleshooting section in the Docs</a>!
|
||||
</h5>
|
||||
<small class="text-muted">
|
||||
<img src="https://design.codeberg.org/logo-kit/icon.svg" class="align-top">
|
||||
Static pages made easy - <a href="https://codeberg.page">Codeberg Pages</a>
|
||||
</small>
|
||||
</body>
|
||||
</html>
|
|
@ -1,38 +0,0 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidMessage(t *testing.T) {
|
||||
testString := "requested blacklisted path"
|
||||
statusCode := http.StatusForbidden
|
||||
|
||||
expected := strings.ReplaceAll(
|
||||
strings.ReplaceAll(ErrorPage, "%message%", testString),
|
||||
"%status%",
|
||||
http.StatusText(statusCode))
|
||||
actual := generateResponse(testString, statusCode)
|
||||
|
||||
if expected != actual {
|
||||
t.Errorf("generated response did not match: expected: '%s', got: '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageWithHtml(t *testing.T) {
|
||||
testString := `abc<img src=1 onerror=alert("xss");`
|
||||
escapedString := "abc<img src=1 onerror=alert("xss");"
|
||||
statusCode := http.StatusNotFound
|
||||
|
||||
expected := strings.ReplaceAll(
|
||||
strings.ReplaceAll(ErrorPage, "%message%", escapedString),
|
||||
"%status%",
|
||||
http.StatusText(statusCode))
|
||||
actual := generateResponse(testString, statusCode)
|
||||
|
||||
if expected != actual {
|
||||
t.Errorf("generated response did not match: expected: '%s', got: '%s'", expected, actual)
|
||||
}
|
||||
}
|
54
html/html.go
54
html/html.go
|
@ -1,9 +1,53 @@
|
|||
package html
|
||||
|
||||
import _ "embed"
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"text/template" // do not use html/template here, we sanitize the message before passing it to the template
|
||||
|
||||
//go:embed 404.html
|
||||
var NotFoundPage string
|
||||
"codeberg.org/codeberg/pages/server/context"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
//go:embed error.html
|
||||
var ErrorPage string
|
||||
//go:embed templates/error.html
|
||||
var errorPage string
|
||||
|
||||
var (
|
||||
errorTemplate = template.Must(template.New("error").Parse(errorPage))
|
||||
sanitizer = createBlueMondayPolicy()
|
||||
)
|
||||
|
||||
type TemplateContext struct {
|
||||
StatusCode int
|
||||
StatusText string
|
||||
Message string
|
||||
}
|
||||
|
||||
// ReturnErrorPage sets the response status code and writes the error page to the response body.
|
||||
// The error page contains a sanitized version of the message and the statusCode both in text and numeric form.
|
||||
//
|
||||
// Currently, only the following html tags are supported: <code>
|
||||
func ReturnErrorPage(ctx *context.Context, msg string, statusCode int) {
|
||||
ctx.RespWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
ctx.RespWriter.WriteHeader(statusCode)
|
||||
|
||||
templateContext := TemplateContext{
|
||||
StatusCode: statusCode,
|
||||
StatusText: http.StatusText(statusCode),
|
||||
Message: sanitizer.Sanitize(msg),
|
||||
}
|
||||
|
||||
err := errorTemplate.Execute(ctx.RespWriter, templateContext)
|
||||
if err != nil {
|
||||
log.Err(err).Str("message", msg).Int("status", statusCode).Msg("could not write response")
|
||||
}
|
||||
}
|
||||
|
||||
func createBlueMondayPolicy() *bluemonday.Policy {
|
||||
p := bluemonday.NewPolicy()
|
||||
|
||||
p.AllowElements("code")
|
||||
|
||||
return p
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSanitizerSimpleString(t *testing.T) {
|
||||
str := "simple text message without any html elements"
|
||||
|
||||
assert.Equal(t, str, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithCodeTag(t *testing.T) {
|
||||
str := "simple text message with <code>html</code> tag"
|
||||
|
||||
assert.Equal(t, str, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithCodeTagWithAttribute(t *testing.T) {
|
||||
str := "simple text message with <code id=\"code\">html</code> tag"
|
||||
expected := "simple text message with <code>html</code> tag"
|
||||
|
||||
assert.Equal(t, expected, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithATag(t *testing.T) {
|
||||
str := "simple text message with <a>a link to another page</a>"
|
||||
expected := "simple text message with a link to another page"
|
||||
|
||||
assert.Equal(t, expected, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithATagAndHref(t *testing.T) {
|
||||
str := "simple text message with <a href=\"http://evil.site\">a link to another page</a>"
|
||||
expected := "simple text message with a link to another page"
|
||||
|
||||
assert.Equal(t, expected, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithImgTag(t *testing.T) {
|
||||
str := "simple text message with a <img alt=\"not found\" src=\"http://evil.site\">"
|
||||
expected := "simple text message with a "
|
||||
|
||||
assert.Equal(t, expected, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithImgTagAndOnerrorAttribute(t *testing.T) {
|
||||
str := "simple text message with a <img alt=\"not found\" src=\"http://evil.site\" onerror=\"alert(secret)\">"
|
||||
expected := "simple text message with a "
|
||||
|
||||
assert.Equal(t, expected, sanitizer.Sanitize(str))
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<!doctype html>
|
||||
<html class="codeberg-design">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{{.StatusText}}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://design.codeberg.org/design-kit/codeberg.css" />
|
||||
<link rel="stylesheet" href="https://fonts.codeberg.org/dist/inter/Inter%20Web/inter.css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
code {
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
background-color: silver;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="10em" viewBox="0 0 24 24" fill="var(--blue-color)">
|
||||
<path
|
||||
d="M 9 2 C 5.1458514 2 2 5.1458514 2 9 C 2 12.854149 5.1458514 16 9 16 C 10.747998 16 12.345009 15.348024 13.574219 14.28125 L 14 14.707031 L 14 16 L 19.585938 21.585938 C 20.137937 22.137937 21.033938 22.137938 21.585938 21.585938 C 22.137938 21.033938 22.137938 20.137938 21.585938 19.585938 L 16 14 L 14.707031 14 L 14.28125 13.574219 C 15.348024 12.345009 16 10.747998 16 9 C 16 5.1458514 12.854149 2 9 2 z M 9 4 C 11.773268 4 14 6.2267316 14 9 C 14 11.773268 11.773268 14 9 14 C 6.2267316 14 4 11.773268 4 9 C 4 6.2267316 6.2267316 4 9 4 z"
|
||||
/>
|
||||
</svg>
|
||||
<h1 class="mb-0 text-primary">{{.StatusText}} ({{.StatusCode}})!</h1>
|
||||
<h5 class="text-center" style="max-width: 25em">
|
||||
<p>Sorry, but this page couldn't be served.</p>
|
||||
<p><b>"{{.Message}}"</b></p>
|
||||
<p>
|
||||
We hope this isn't a problem on our end ;) - Make sure to check the
|
||||
<a href="https://docs.codeberg.org/codeberg-pages/troubleshooting/" target="_blank"
|
||||
>troubleshooting section in the Docs</a
|
||||
>!
|
||||
</p>
|
||||
</h5>
|
||||
<small class="text-muted">
|
||||
<img src="https://design.codeberg.org/logo-kit/icon.svg" class="align-top" />
|
||||
Static pages made easy -
|
||||
<a href="https://codeberg.page">Codeberg Pages</a>
|
||||
</small>
|
||||
</body>
|
||||
</html>
|
|
@ -20,7 +20,9 @@ func TestGetRedirect(t *testing.T) {
|
|||
log.Println("=== TestGetRedirect ===")
|
||||
// test custom domain redirect
|
||||
resp, err := getTestHTTPSClient().Get("https://calciumdibromid.localhost.mock.directory:4430")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.EqualValues(t, http.StatusTemporaryRedirect, resp.StatusCode) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
@ -31,7 +33,7 @@ func TestGetRedirect(t *testing.T) {
|
|||
func TestGetContent(t *testing.T) {
|
||||
log.Println("=== TestGetContent ===")
|
||||
// test get image
|
||||
resp, err := getTestHTTPSClient().Get("https://magiclike.localhost.mock.directory:4430/images/827679288a.jpg")
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/images/827679288a.jpg")
|
||||
assert.NoError(t, err)
|
||||
if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) {
|
||||
t.FailNow()
|
||||
|
@ -42,7 +44,7 @@ func TestGetContent(t *testing.T) {
|
|||
assert.Len(t, resp.Header.Get("ETag"), 42)
|
||||
|
||||
// specify branch
|
||||
resp, err = getTestHTTPSClient().Get("https://momar.localhost.mock.directory:4430/pag/@master/")
|
||||
resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pag/@master/")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
|
@ -53,7 +55,7 @@ func TestGetContent(t *testing.T) {
|
|||
assert.Len(t, resp.Header.Get("ETag"), 44)
|
||||
|
||||
// access branch name contains '/'
|
||||
resp, err = getTestHTTPSClient().Get("https://blumia.localhost.mock.directory:4430/pages-server-integration-tests/@docs~main/")
|
||||
resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/blumia/@docs~main/")
|
||||
assert.NoError(t, err)
|
||||
if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) {
|
||||
t.FailNow()
|
||||
|
@ -62,7 +64,7 @@ func TestGetContent(t *testing.T) {
|
|||
assert.True(t, getSize(resp.Body) > 100)
|
||||
assert.Len(t, resp.Header.Get("ETag"), 44)
|
||||
|
||||
// TODO: test get of non cachable content (content size > fileCacheSizeLimit)
|
||||
// TODO: test get of non cacheable content (content size > fileCacheSizeLimit)
|
||||
}
|
||||
|
||||
func TestCustomDomain(t *testing.T) {
|
||||
|
@ -78,10 +80,67 @@ func TestCustomDomain(t *testing.T) {
|
|||
assert.EqualValues(t, 106, getSize(resp.Body))
|
||||
}
|
||||
|
||||
func TestCustomDomainRedirects(t *testing.T) {
|
||||
log.Println("=== TestCustomDomainRedirects ===")
|
||||
// test redirect from default pages domain to custom domain
|
||||
resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/test_pages-server_custom-mock-domain/@main/README.md")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
// TODO: custom port is not evaluated (witch does hurt tests & dev env only)
|
||||
// assert.EqualValues(t, "https://mock-pages.codeberg-test.org:4430/@main/README.md", resp.Header.Get("Location"))
|
||||
assert.EqualValues(t, "https://mock-pages.codeberg-test.org/@main/README.md", resp.Header.Get("Location"))
|
||||
assert.EqualValues(t, `https:/codeberg.org/6543/test_pages-server_custom-mock-domain/src/branch/main/README.md; rel="canonical"; rel="canonical"`, resp.Header.Get("Link"))
|
||||
|
||||
// test redirect from an custom domain to the primary custom domain (www.example.com -> example.com)
|
||||
// regression test to https://codeberg.org/Codeberg/pages-server/issues/153
|
||||
resp, err = getTestHTTPSClient().Get("https://mock-pages-redirect.codeberg-test.org:4430/README.md")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
// TODO: custom port is not evaluated (witch does hurt tests & dev env only)
|
||||
// assert.EqualValues(t, "https://mock-pages.codeberg-test.org:4430/README.md", resp.Header.Get("Location"))
|
||||
assert.EqualValues(t, "https://mock-pages.codeberg-test.org/README.md", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestRawCustomDomain(t *testing.T) {
|
||||
log.Println("=== TestRawCustomDomain ===")
|
||||
// test raw domain response for custom domain branch
|
||||
resp, err := getTestHTTPSClient().Get("https://raw.localhost.mock.directory:4430/cb_pages_tests/raw-test/example") // need cb_pages_tests fork
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "76", resp.Header.Get("Content-Length"))
|
||||
assert.EqualValues(t, 76, getSize(resp.Body))
|
||||
}
|
||||
|
||||
func TestRawIndex(t *testing.T) {
|
||||
log.Println("=== TestRawIndex ===")
|
||||
// test raw domain response for index.html
|
||||
resp, err := getTestHTTPSClient().Get("https://raw.localhost.mock.directory:4430/cb_pages_tests/raw-test/@branch-test/index.html") // need cb_pages_tests fork
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "597", resp.Header.Get("Content-Length"))
|
||||
assert.EqualValues(t, 597, getSize(resp.Body))
|
||||
}
|
||||
|
||||
func TestGetNotFound(t *testing.T) {
|
||||
log.Println("=== TestGetNotFound ===")
|
||||
// test custom not found pages
|
||||
resp, err := getTestHTTPSClient().Get("https://crystal.localhost.mock.directory:4430/pages-404-demo/blah")
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pages-404-demo/blah")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
|
@ -92,10 +151,51 @@ func TestGetNotFound(t *testing.T) {
|
|||
assert.EqualValues(t, 37, getSize(resp.Body))
|
||||
}
|
||||
|
||||
func TestRedirect(t *testing.T) {
|
||||
log.Println("=== TestRedirect ===")
|
||||
// test redirects
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/some_redirects/redirect")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusMovedPermanently, resp.StatusCode)
|
||||
assert.EqualValues(t, "https://example.com/", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestSPARedirect(t *testing.T) {
|
||||
log.Println("=== TestSPARedirect ===")
|
||||
// test SPA redirects
|
||||
url := "https://cb_pages_tests.localhost.mock.directory:4430/some_redirects/app/aqdjw"
|
||||
resp, err := getTestHTTPSClient().Get(url)
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, url, resp.Request.URL.String())
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "258", resp.Header.Get("Content-Length"))
|
||||
assert.EqualValues(t, 258, getSize(resp.Body))
|
||||
}
|
||||
|
||||
func TestSplatRedirect(t *testing.T) {
|
||||
log.Println("=== TestSplatRedirect ===")
|
||||
// test splat redirects
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/some_redirects/articles/qfopefe")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusMovedPermanently, resp.StatusCode)
|
||||
assert.EqualValues(t, "/posts/qfopefe", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestFollowSymlink(t *testing.T) {
|
||||
log.Printf("=== TestFollowSymlink ===\n")
|
||||
|
||||
resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/link")
|
||||
// file symlink
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/link")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
|
@ -106,12 +206,22 @@ func TestFollowSymlink(t *testing.T) {
|
|||
body := getBytes(resp.Body)
|
||||
assert.EqualValues(t, 4, len(body))
|
||||
assert.EqualValues(t, "abc\n", string(body))
|
||||
|
||||
// relative file links (../index.html file in this case)
|
||||
resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/dir_aim/some/")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "an index\n", string(getBytes(resp.Body)))
|
||||
}
|
||||
|
||||
func TestLFSSupport(t *testing.T) {
|
||||
log.Printf("=== TestLFSSupport ===\n")
|
||||
|
||||
resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt")
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
|
@ -134,6 +244,18 @@ func TestGetOptions(t *testing.T) {
|
|||
assert.EqualValues(t, "GET, HEAD, OPTIONS", resp.Header.Get("Allow"))
|
||||
}
|
||||
|
||||
func TestHttpRedirect(t *testing.T) {
|
||||
log.Println("=== TestHttpRedirect ===")
|
||||
resp, err := getTestHTTPSClient().Get("http://mock-pages.codeberg-test.org:8880/README.md")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusMovedPermanently, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "https://mock-pages.codeberg-test.org:4430/README.md", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func getTestHTTPSClient() *http.Client {
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
return &http.Client{
|
||||
|
|
|
@ -10,9 +10,10 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"codeberg.org/codeberg/pages/cmd"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
cmd "codeberg.org/codeberg/pages/cli"
|
||||
"codeberg.org/codeberg/pages/server"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
@ -23,7 +24,7 @@ func TestMain(m *testing.M) {
|
|||
}
|
||||
defer func() {
|
||||
serverCancel()
|
||||
log.Println("=== TestMain: Server STOPED ===")
|
||||
log.Println("=== TestMain: Server STOPPED ===")
|
||||
}()
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
|
@ -32,19 +33,25 @@ func TestMain(m *testing.M) {
|
|||
}
|
||||
|
||||
func startServer(ctx context.Context) error {
|
||||
args := []string{
|
||||
"--verbose",
|
||||
"--acme-accept-terms", "true",
|
||||
}
|
||||
args := []string{"integration"}
|
||||
setEnvIfNotSet("ACME_API", "https://acme.mock.directory")
|
||||
setEnvIfNotSet("PAGES_DOMAIN", "localhost.mock.directory")
|
||||
setEnvIfNotSet("RAW_DOMAIN", "raw.localhost.mock.directory")
|
||||
setEnvIfNotSet("PAGES_BRANCHES", "pages,main,master")
|
||||
setEnvIfNotSet("PORT", "4430")
|
||||
setEnvIfNotSet("HTTP_PORT", "8880")
|
||||
setEnvIfNotSet("ENABLE_HTTP_SERVER", "true")
|
||||
setEnvIfNotSet("DB_TYPE", "sqlite3")
|
||||
setEnvIfNotSet("GITEA_ROOT", "https://codeberg.org")
|
||||
setEnvIfNotSet("LOG_LEVEL", "trace")
|
||||
setEnvIfNotSet("ENABLE_LFS_SUPPORT", "true")
|
||||
setEnvIfNotSet("ENABLE_SYMLINK_SUPPORT", "true")
|
||||
setEnvIfNotSet("ACME_ACCOUNT_CONFIG", "integration/acme-account.json")
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "pages-server"
|
||||
app.Action = cmd.Serve
|
||||
app.Flags = cmd.ServeFlags
|
||||
app.Action = server.Serve
|
||||
app.Flags = cmd.ServerFlags
|
||||
|
||||
go func() {
|
||||
if err := app.RunContext(ctx, args); err != nil {
|
||||
|
|
22
main.go
22
main.go
|
@ -1,31 +1,21 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"codeberg.org/codeberg/pages/cmd"
|
||||
"codeberg.org/codeberg/pages/cli"
|
||||
"codeberg.org/codeberg/pages/server"
|
||||
)
|
||||
|
||||
// can be changed with -X on compile
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "pages-server"
|
||||
app.Version = version
|
||||
app.Usage = "pages server"
|
||||
app.Action = cmd.Serve
|
||||
app.Flags = cmd.ServeFlags
|
||||
app.Commands = []*cli.Command{
|
||||
cmd.Certs,
|
||||
}
|
||||
app := cli.CreatePagesApp()
|
||||
app.Action = server.Serve
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
log.Error().Err(err).Msg("A fatal error occurred")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":maintainLockFilesWeekly",
|
||||
":enablePreCommit",
|
||||
"schedule:automergeDaily",
|
||||
"schedule:weekends"
|
||||
],
|
||||
"automergeType": "branch",
|
||||
"automergeMajor": false,
|
||||
"automerge": true,
|
||||
"prConcurrentLimit": 5,
|
||||
"labels": ["dependencies"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["gomod", "dockerfile"]
|
||||
},
|
||||
{
|
||||
"groupName": "golang deps non-major",
|
||||
"matchManagers": ["gomod"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"extends": ["schedule:daily"]
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["gomodTidy", "gomodUpdateImportPaths"]
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"codeberg.org/codeberg/pages/config"
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"codeberg.org/codeberg/pages/server/certificates"
|
||||
)
|
||||
|
||||
var ErrAcmeMissConfig = errors.New("ACME client has wrong config")
|
||||
|
||||
func CreateAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache cache.ICache) (*certificates.AcmeClient, error) {
|
||||
// check config
|
||||
if (!cfg.AcceptTerms || (cfg.DNSProvider == "" && !cfg.NoDNS01)) && cfg.APIEndpoint != "https://acme.mock.directory" {
|
||||
return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER or $NO_DNS_01, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig)
|
||||
}
|
||||
if cfg.EAB_HMAC != "" && cfg.EAB_KID == "" {
|
||||
return nil, fmt.Errorf("%w: ACME_EAB_HMAC also needs ACME_EAB_KID to be set", ErrAcmeMissConfig)
|
||||
} else if cfg.EAB_HMAC == "" && cfg.EAB_KID != "" {
|
||||
return nil, fmt.Errorf("%w: ACME_EAB_KID also needs ACME_EAB_HMAC to be set", ErrAcmeMissConfig)
|
||||
}
|
||||
|
||||
return certificates.NewAcmeClient(cfg, enableHTTPServer, challengeCache)
|
||||
}
|
|
@ -2,7 +2,8 @@ package cache
|
|||
|
||||
import "time"
|
||||
|
||||
type SetGetKey interface {
|
||||
// ICache is an interface that defines how the pages server interacts with the cache.
|
||||
type ICache interface {
|
||||
Set(key string, value interface{}, ttl time.Duration) error
|
||||
Get(key string) (interface{}, bool)
|
||||
Remove(key string)
|
||||
|
|
|
@ -2,6 +2,6 @@ package cache
|
|||
|
||||
import "github.com/OrlovEvgeny/go-mcache"
|
||||
|
||||
func NewKeyValueCache() SetGetKey {
|
||||
func NewInMemoryCache() ICache {
|
||||
return mcache.New()
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package certificates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/providers/dns"
|
||||
"github.com/reugn/equalizer"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"codeberg.org/codeberg/pages/config"
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
)
|
||||
|
||||
type AcmeClient struct {
|
||||
legoClient *lego.Client
|
||||
dnsChallengerLegoClient *lego.Client
|
||||
|
||||
obtainLocks sync.Map
|
||||
|
||||
acmeUseRateLimits bool
|
||||
|
||||
// limiter
|
||||
acmeClientOrderLimit *equalizer.TokenBucket
|
||||
acmeClientRequestLimit *equalizer.TokenBucket
|
||||
acmeClientFailLimit *equalizer.TokenBucket
|
||||
acmeClientCertificateLimitPerUser map[string]*equalizer.TokenBucket
|
||||
}
|
||||
|
||||
func NewAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache cache.ICache) (*AcmeClient, error) {
|
||||
acmeConfig, err := setupAcmeConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acmeClient, err := lego.NewClient(acmeConfig)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
err = acmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
|
||||
}
|
||||
if enableHTTPServer {
|
||||
err = acmeClient.Challenge.SetHTTP01Provider(AcmeHTTPChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create HTTP-01 provider")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainDomainAcmeClient, err := lego.NewClient(acmeConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
if cfg.DNSProvider == "" {
|
||||
// using mock wildcard certs
|
||||
mainDomainAcmeClient = nil
|
||||
} else {
|
||||
// use DNS-Challenge https://go-acme.github.io/lego/dns/
|
||||
provider, err := dns.NewDNSChallengeProviderByName(cfg.DNSProvider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not create DNS Challenge provider: %w", err)
|
||||
}
|
||||
if err := mainDomainAcmeClient.Challenge.SetDNS01Provider(provider); err != nil {
|
||||
return nil, fmt.Errorf("can not create DNS-01 provider: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &AcmeClient{
|
||||
legoClient: acmeClient,
|
||||
dnsChallengerLegoClient: mainDomainAcmeClient,
|
||||
|
||||
acmeUseRateLimits: cfg.UseRateLimits,
|
||||
|
||||
obtainLocks: sync.Map{},
|
||||
|
||||
// limiter
|
||||
|
||||
// rate limit is 300 / 3 hours, we want 200 / 2 hours but to refill more often, so that's 25 new domains every 15 minutes
|
||||
// TODO: when this is used a lot, we probably have to think of a somewhat better solution?
|
||||
acmeClientOrderLimit: equalizer.NewTokenBucket(25, 15*time.Minute),
|
||||
// rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests)
|
||||
acmeClientRequestLimit: equalizer.NewTokenBucket(5, 1*time.Second),
|
||||
// rate limit is 5 / hour https://letsencrypt.org/docs/failed-validation-limit/
|
||||
acmeClientFailLimit: equalizer.NewTokenBucket(5, 1*time.Hour),
|
||||
// checkUserLimit() use this to rate also per user
|
||||
acmeClientCertificateLimitPerUser: map[string]*equalizer.TokenBucket{},
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package certificates
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"codeberg.org/codeberg/pages/config"
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const challengePath = "/.well-known/acme-challenge/"
|
||||
|
||||
func setupAcmeConfig(cfg config.ACMEConfig) (*lego.Config, error) {
|
||||
var myAcmeAccount AcmeAccount
|
||||
var myAcmeConfig *lego.Config
|
||||
|
||||
if cfg.AccountConfigFile == "" {
|
||||
return nil, fmt.Errorf("invalid acme config file: '%s'", cfg.AccountConfigFile)
|
||||
}
|
||||
|
||||
if account, err := os.ReadFile(cfg.AccountConfigFile); err == nil {
|
||||
log.Info().Msgf("found existing acme account config file '%s'", cfg.AccountConfigFile)
|
||||
if err := json.Unmarshal(account, &myAcmeAccount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
myAcmeAccount.Key, err = certcrypto.ParsePEMPrivateKey([]byte(myAcmeAccount.KeyPEM))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
myAcmeConfig = lego.NewConfig(&myAcmeAccount)
|
||||
myAcmeConfig.CADirURL = cfg.APIEndpoint
|
||||
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
|
||||
|
||||
// Validate Config
|
||||
_, err := lego.NewClient(myAcmeConfig)
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("config validation failed, you might just delete the config file and let it recreate")
|
||||
return nil, fmt.Errorf("acme config validation failed: %w", err)
|
||||
}
|
||||
|
||||
return myAcmeConfig, nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info().Msgf("no existing acme account config found, try to create a new one")
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myAcmeAccount = AcmeAccount{
|
||||
Email: cfg.Email,
|
||||
Key: privateKey,
|
||||
KeyPEM: string(certcrypto.PEMEncode(privateKey)),
|
||||
}
|
||||
myAcmeConfig = lego.NewConfig(&myAcmeAccount)
|
||||
myAcmeConfig.CADirURL = cfg.APIEndpoint
|
||||
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
|
||||
tempClient, err := lego.NewClient(myAcmeConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
// accept terms & log in to EAB
|
||||
if cfg.EAB_KID == "" || cfg.EAB_HMAC == "" {
|
||||
reg, err := tempClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: cfg.AcceptTerms})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
|
||||
} else {
|
||||
myAcmeAccount.Registration = reg
|
||||
}
|
||||
} else {
|
||||
reg, err := tempClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: cfg.AcceptTerms,
|
||||
Kid: cfg.EAB_KID,
|
||||
HmacEncoded: cfg.EAB_HMAC,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
|
||||
} else {
|
||||
myAcmeAccount.Registration = reg
|
||||
}
|
||||
}
|
||||
|
||||
if myAcmeAccount.Registration != nil {
|
||||
acmeAccountJSON, err := json.Marshal(myAcmeAccount)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("json.Marshalfailed, waiting for manual restart to avoid rate limits")
|
||||
select {}
|
||||
}
|
||||
log.Info().Msgf("new acme account created. write to config file '%s'", cfg.AccountConfigFile)
|
||||
err = os.WriteFile(cfg.AccountConfigFile, acmeAccountJSON, 0o600)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("os.WriteFile failed, waiting for manual restart to avoid rate limits")
|
||||
select {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return myAcmeConfig, nil
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package certificates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"codeberg.org/codeberg/pages/server/context"
|
||||
)
|
||||
|
||||
type AcmeTLSChallengeProvider struct {
|
||||
challengeCache cache.ICache
|
||||
}
|
||||
|
||||
// make sure AcmeTLSChallengeProvider match Provider interface
|
||||
var _ challenge.Provider = AcmeTLSChallengeProvider{}
|
||||
|
||||
func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error {
|
||||
return a.challengeCache.Set(domain, keyAuth, 1*time.Hour)
|
||||
}
|
||||
|
||||
func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error {
|
||||
a.challengeCache.Remove(domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
type AcmeHTTPChallengeProvider struct {
|
||||
challengeCache cache.ICache
|
||||
}
|
||||
|
||||
// make sure AcmeHTTPChallengeProvider match Provider interface
|
||||
var _ challenge.Provider = AcmeHTTPChallengeProvider{}
|
||||
|
||||
func (a AcmeHTTPChallengeProvider) Present(domain, token, keyAuth string) error {
|
||||
return a.challengeCache.Set(domain+"/"+token, keyAuth, 1*time.Hour)
|
||||
}
|
||||
|
||||
func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error {
|
||||
a.challengeCache.Remove(domain + "/" + token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetupHTTPACMEChallengeServer(challengeCache cache.ICache, sslPort uint) http.HandlerFunc {
|
||||
// handle custom-ssl-ports to be added on https redirects
|
||||
portPart := ""
|
||||
if sslPort != 443 {
|
||||
portPart = fmt.Sprintf(":%d", sslPort)
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := context.New(w, req)
|
||||
domain := ctx.TrimHostPort()
|
||||
|
||||
// it's an acme request
|
||||
if strings.HasPrefix(ctx.Path(), challengePath) {
|
||||
challenge, ok := challengeCache.Get(domain + "/" + strings.TrimPrefix(ctx.Path(), challengePath))
|
||||
if !ok || challenge == nil {
|
||||
log.Info().Msgf("HTTP-ACME challenge for '%s' failed: token not found", domain)
|
||||
ctx.String("no challenge for this token", http.StatusNotFound)
|
||||
}
|
||||
log.Info().Msgf("HTTP-ACME challenge for '%s' succeeded", domain)
|
||||
ctx.String(challenge.(string))
|
||||
return
|
||||
}
|
||||
|
||||
// it's a normal http request that needs to be redirected
|
||||
u, err := url.Parse(fmt.Sprintf("https://%s%s%s", domain, portPart, ctx.Path()))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("could not craft http to https redirect")
|
||||
ctx.String("", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
newURL := u.String()
|
||||
log.Debug().Msgf("redirect http to https: %s", newURL)
|
||||
ctx.Redirect(newURL, http.StatusMovedPermanently)
|
||||
}
|
||||
}
|
|
@ -1,30 +1,19 @@
|
|||
package certificates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/providers/dns"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/reugn/equalizer"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
|
@ -35,33 +24,39 @@ import (
|
|||
"codeberg.org/codeberg/pages/server/upstream"
|
||||
)
|
||||
|
||||
var ErrUserRateLimitExceeded = errors.New("rate limit exceeded: 10 certificates per user per 24 hours")
|
||||
|
||||
// TLSConfig returns the configuration for generating, serving and cleaning up Let's Encrypt certificates.
|
||||
func TLSConfig(mainDomainSuffix string,
|
||||
giteaClient *gitea.Client,
|
||||
dnsProvider string,
|
||||
acmeUseRateLimits bool,
|
||||
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache cache.SetGetKey,
|
||||
acmeClient *AcmeClient,
|
||||
firstDefaultBranch string,
|
||||
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache cache.ICache,
|
||||
certDB database.CertDB,
|
||||
noDNS01 bool,
|
||||
rawDomain string,
|
||||
) *tls.Config {
|
||||
return &tls.Config{
|
||||
// check DNS name & get certificate from Let's Encrypt
|
||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
sni := strings.ToLower(strings.TrimSpace(info.ServerName))
|
||||
if len(sni) < 1 {
|
||||
return nil, errors.New("missing sni")
|
||||
domain := strings.ToLower(strings.TrimSpace(info.ServerName))
|
||||
if len(domain) < 1 {
|
||||
return nil, errors.New("missing domain info via SNI (RFC 4366, Section 3.1)")
|
||||
}
|
||||
|
||||
// https request init is actually a acme challenge
|
||||
if info.SupportedProtos != nil {
|
||||
for _, proto := range info.SupportedProtos {
|
||||
if proto != tlsalpn01.ACMETLS1Protocol {
|
||||
continue
|
||||
}
|
||||
log.Info().Msgf("Detect ACME-TLS1 challenge for '%s'", domain)
|
||||
|
||||
challenge, ok := challengeCache.Get(sni)
|
||||
challenge, ok := challengeCache.Get(domain)
|
||||
if !ok {
|
||||
return nil, errors.New("no challenge for this domain")
|
||||
}
|
||||
cert, err := tlsalpn01.ChallengeCert(sni, challenge.(string))
|
||||
cert, err := tlsalpn01.ChallengeCert(domain, challenge.(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -70,54 +65,78 @@ func TLSConfig(mainDomainSuffix string,
|
|||
}
|
||||
|
||||
targetOwner := ""
|
||||
if strings.HasSuffix(sni, mainDomainSuffix) || strings.EqualFold(sni, mainDomainSuffix[1:]) {
|
||||
// deliver default certificate for the main domain (*.codeberg.page)
|
||||
sni = mainDomainSuffix
|
||||
mayObtainCert := true
|
||||
|
||||
if strings.HasSuffix(domain, mainDomainSuffix) || strings.EqualFold(domain, mainDomainSuffix[1:]) {
|
||||
if noDNS01 {
|
||||
// Limit the domains allowed to request a certificate to pages-server domains
|
||||
// and domains for an existing user of org
|
||||
if !strings.EqualFold(domain, mainDomainSuffix[1:]) && !strings.EqualFold(domain, rawDomain) {
|
||||
targetOwner := strings.TrimSuffix(domain, mainDomainSuffix)
|
||||
owner_exist, err := giteaClient.GiteaCheckIfOwnerExists(targetOwner)
|
||||
mayObtainCert = owner_exist
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to check '%s' existence on the forge: %s", targetOwner, err)
|
||||
mayObtainCert = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// deliver default certificate for the main domain (*.codeberg.page)
|
||||
domain = mainDomainSuffix
|
||||
}
|
||||
} else {
|
||||
var targetRepo, targetBranch string
|
||||
targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(sni, mainDomainSuffix, dnsLookupCache)
|
||||
targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
|
||||
if targetOwner == "" {
|
||||
// DNS not set up, return main certificate to redirect to the docs
|
||||
sni = mainDomainSuffix
|
||||
domain = mainDomainSuffix
|
||||
} else {
|
||||
targetOpt := &upstream.Options{
|
||||
TargetOwner: targetOwner,
|
||||
TargetRepo: targetRepo,
|
||||
TargetBranch: targetBranch,
|
||||
}
|
||||
_, valid := targetOpt.CheckCanonicalDomain(giteaClient, sni, mainDomainSuffix, canonicalDomainCache)
|
||||
_, valid := targetOpt.CheckCanonicalDomain(giteaClient, domain, mainDomainSuffix, canonicalDomainCache)
|
||||
if !valid {
|
||||
sni = mainDomainSuffix
|
||||
// We shouldn't obtain a certificate when we cannot check if the
|
||||
// repository has specified this domain in the `.domains` file.
|
||||
mayObtainCert = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tlsCertificate, ok := keyCache.Get(sni); ok {
|
||||
if tlsCertificate, ok := keyCache.Get(domain); ok {
|
||||
// we can use an existing certificate object
|
||||
return tlsCertificate.(*tls.Certificate), nil
|
||||
}
|
||||
|
||||
var tlsCertificate tls.Certificate
|
||||
var tlsCertificate *tls.Certificate
|
||||
var err error
|
||||
var ok bool
|
||||
if tlsCertificate, ok = retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider, acmeUseRateLimits, certDB); !ok {
|
||||
// request a new certificate
|
||||
if strings.EqualFold(sni, mainDomainSuffix) {
|
||||
if tlsCertificate, err = acmeClient.retrieveCertFromDB(domain, mainDomainSuffix, false, certDB); err != nil {
|
||||
if !errors.Is(err, database.ErrNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
// we could not find a cert in db, request a new certificate
|
||||
|
||||
// first check if we are allowed to obtain a cert for this domain
|
||||
if strings.EqualFold(domain, mainDomainSuffix) {
|
||||
return nil, errors.New("won't request certificate for main domain, something really bad has happened")
|
||||
}
|
||||
if !mayObtainCert {
|
||||
return nil, fmt.Errorf("won't request certificate for %q", domain)
|
||||
}
|
||||
|
||||
tlsCertificate, err = obtainCert(acmeClient, []string{sni}, nil, targetOwner, dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
|
||||
tlsCertificate, err = acmeClient.obtainCert(acmeClient.legoClient, []string{domain}, nil, targetOwner, false, mainDomainSuffix, certDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := keyCache.Set(sni, &tlsCertificate, 15*time.Minute); err != nil {
|
||||
if err := keyCache.Set(domain, tlsCertificate, 15*time.Minute); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tlsCertificate, nil
|
||||
return tlsCertificate, nil
|
||||
},
|
||||
PreferServerCipherSuites: true,
|
||||
NextProtos: []string{
|
||||
"h2",
|
||||
"http/1.1",
|
||||
|
@ -138,159 +157,116 @@ func TLSConfig(mainDomainSuffix string,
|
|||
}
|
||||
}
|
||||
|
||||
func checkUserLimit(user string) error {
|
||||
userLimit, ok := acmeClientCertificateLimitPerUser[user]
|
||||
func (c *AcmeClient) checkUserLimit(user string) error {
|
||||
userLimit, ok := c.acmeClientCertificateLimitPerUser[user]
|
||||
if !ok {
|
||||
// Each Codeberg user can only add 10 new domains per day.
|
||||
// Each user can only add 10 new domains per day.
|
||||
userLimit = equalizer.NewTokenBucket(10, time.Hour*24)
|
||||
acmeClientCertificateLimitPerUser[user] = userLimit
|
||||
c.acmeClientCertificateLimitPerUser[user] = userLimit
|
||||
}
|
||||
if !userLimit.Ask() {
|
||||
return errors.New("rate limit exceeded: 10 certificates per user per 24 hours")
|
||||
return fmt.Errorf("user '%s' error: %w", user, ErrUserRateLimitExceeded)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
acmeClient, mainDomainAcmeClient *lego.Client
|
||||
acmeClientCertificateLimitPerUser = map[string]*equalizer.TokenBucket{}
|
||||
)
|
||||
|
||||
// rate limit is 300 / 3 hours, we want 200 / 2 hours but to refill more often, so that's 25 new domains every 15 minutes
|
||||
// TODO: when this is used a lot, we probably have to think of a somewhat better solution?
|
||||
var acmeClientOrderLimit = equalizer.NewTokenBucket(25, 15*time.Minute)
|
||||
|
||||
// rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests)
|
||||
var acmeClientRequestLimit = equalizer.NewTokenBucket(5, 1*time.Second)
|
||||
|
||||
type AcmeTLSChallengeProvider struct {
|
||||
challengeCache cache.SetGetKey
|
||||
}
|
||||
|
||||
// make sure AcmeTLSChallengeProvider match Provider interface
|
||||
var _ challenge.Provider = AcmeTLSChallengeProvider{}
|
||||
|
||||
func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error {
|
||||
return a.challengeCache.Set(domain, keyAuth, 1*time.Hour)
|
||||
}
|
||||
|
||||
func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error {
|
||||
a.challengeCache.Remove(domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
type AcmeHTTPChallengeProvider struct {
|
||||
challengeCache cache.SetGetKey
|
||||
}
|
||||
|
||||
// make sure AcmeHTTPChallengeProvider match Provider interface
|
||||
var _ challenge.Provider = AcmeHTTPChallengeProvider{}
|
||||
|
||||
func (a AcmeHTTPChallengeProvider) Present(domain, token, keyAuth string) error {
|
||||
return a.challengeCache.Set(domain+"/"+token, keyAuth, 1*time.Hour)
|
||||
}
|
||||
|
||||
func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error {
|
||||
a.challengeCache.Remove(domain + "/" + token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) (tls.Certificate, bool) {
|
||||
func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProvider bool, certDB database.CertDB) (*tls.Certificate, error) {
|
||||
// parse certificate from database
|
||||
res, err := certDB.Get(sni)
|
||||
if err != nil {
|
||||
panic(err) // TODO: no panic
|
||||
}
|
||||
if res == nil {
|
||||
return tls.Certificate{}, false
|
||||
return nil, err
|
||||
} else if res == nil {
|
||||
return nil, database.ErrNotFound
|
||||
}
|
||||
|
||||
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: document & put into own function
|
||||
if !strings.EqualFold(sni, mainDomainSuffix) {
|
||||
tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, fmt.Errorf("error parsing leaf tlsCert: %w", err)
|
||||
}
|
||||
|
||||
// renew certificates 7 days before they expire
|
||||
if tlsCertificate.Leaf.NotAfter.Before(time.Now().Add(7 * 24 * time.Hour)) {
|
||||
// TODO: add ValidUntil to custom res struct
|
||||
// TODO: use ValidTill of custom cert struct
|
||||
if res.CSR != nil && len(res.CSR) > 0 {
|
||||
// CSR stores the time when the renewal shall be tried again
|
||||
nextTryUnix, err := strconv.ParseInt(string(res.CSR), 10, 64)
|
||||
if err == nil && time.Now().Before(time.Unix(nextTryUnix, 0)) {
|
||||
return tlsCertificate, true
|
||||
return &tlsCertificate, nil
|
||||
}
|
||||
}
|
||||
// TODO: make a queue ?
|
||||
go (func() {
|
||||
res.CSR = nil // acme client doesn't like CSR to be set
|
||||
tlsCertificate, err = obtainCert(acmeClient, []string{sni}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
|
||||
if err != nil {
|
||||
if _, err := c.obtainCert(c.legoClient, []string{sni}, res, "", useDnsProvider, mainDomainSuffix, certDB); err != nil {
|
||||
log.Error().Msgf("Couldn't renew certificate for %s: %v", sni, err)
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
return tlsCertificate, true
|
||||
return &tlsCertificate, nil
|
||||
}
|
||||
|
||||
var obtainLocks = sync.Map{}
|
||||
|
||||
func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user, dnsProvider, mainDomainSuffix string, acmeUseRateLimits bool, keyDatabase database.CertDB) (tls.Certificate, error) {
|
||||
func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user string, useDnsProvider bool, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) {
|
||||
name := strings.TrimPrefix(domains[0], "*")
|
||||
if dnsProvider == "" && len(domains[0]) > 0 && domains[0][0] == '*' {
|
||||
domains = domains[1:]
|
||||
}
|
||||
|
||||
// lock to avoid simultaneous requests
|
||||
_, working := obtainLocks.LoadOrStore(name, struct{}{})
|
||||
_, working := c.obtainLocks.LoadOrStore(name, struct{}{})
|
||||
if working {
|
||||
for working {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, working = obtainLocks.Load(name)
|
||||
_, working = c.obtainLocks.Load(name)
|
||||
}
|
||||
cert, ok := retrieveCertFromDB(name, mainDomainSuffix, dnsProvider, acmeUseRateLimits, keyDatabase)
|
||||
if !ok {
|
||||
return tls.Certificate{}, errors.New("certificate failed in synchronous request")
|
||||
cert, err := c.retrieveCertFromDB(name, mainDomainSuffix, useDnsProvider, keyDatabase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certificate failed in synchronous request: %w", err)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
defer obtainLocks.Delete(name)
|
||||
defer c.obtainLocks.Delete(name)
|
||||
|
||||
if acmeClient == nil {
|
||||
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase), nil
|
||||
if useDnsProvider {
|
||||
return mockCert(domains[0], "DNS ACME client is not defined", mainDomainSuffix, keyDatabase)
|
||||
} else {
|
||||
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
// request actual cert
|
||||
var res *certificate.Resource
|
||||
var err error
|
||||
if renew != nil && renew.CertURL != "" {
|
||||
if acmeUseRateLimits {
|
||||
acmeClientRequestLimit.Take()
|
||||
if c.acmeUseRateLimits {
|
||||
c.acmeClientRequestLimit.Take()
|
||||
}
|
||||
log.Debug().Msgf("Renewing certificate for: %v", domains)
|
||||
res, err = acmeClient.Certificate.Renew(*renew, true, false, "")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Couldn't renew certificate for %v, trying to request a new one", domains)
|
||||
if c.acmeUseRateLimits {
|
||||
c.acmeClientFailLimit.Take()
|
||||
}
|
||||
res = nil
|
||||
}
|
||||
}
|
||||
if res == nil {
|
||||
if user != "" {
|
||||
if err := checkUserLimit(user); err != nil {
|
||||
return tls.Certificate{}, err
|
||||
if err := c.checkUserLimit(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if acmeUseRateLimits {
|
||||
acmeClientOrderLimit.Take()
|
||||
acmeClientRequestLimit.Take()
|
||||
if c.acmeUseRateLimits {
|
||||
c.acmeClientOrderLimit.Take()
|
||||
c.acmeClientRequestLimit.Take()
|
||||
}
|
||||
log.Debug().Msgf("Re-requesting new certificate for %v", domains)
|
||||
res, err = acmeClient.Certificate.Obtain(certificate.ObtainRequest{
|
||||
|
@ -298,163 +274,58 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
|
|||
Bundle: true,
|
||||
MustStaple: false,
|
||||
})
|
||||
if c.acmeUseRateLimits && err != nil {
|
||||
c.acmeClientFailLimit.Take()
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Couldn't obtain again a certificate or %v", domains)
|
||||
if renew != nil && renew.CertURL != "" {
|
||||
tlsCertificate, err := tls.X509KeyPair(renew.Certificate, renew.PrivateKey)
|
||||
if err == nil && tlsCertificate.Leaf.NotAfter.After(time.Now()) {
|
||||
if err != nil {
|
||||
mockC, err2 := mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase)
|
||||
if err2 != nil {
|
||||
return nil, errors.Join(err, err2)
|
||||
}
|
||||
return mockC, err
|
||||
}
|
||||
leaf, err := leaf(&tlsCertificate)
|
||||
if err == nil && leaf.NotAfter.After(time.Now()) {
|
||||
// avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at
|
||||
renew.CSR = []byte(strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10))
|
||||
if err := keyDatabase.Put(name, renew); err != nil {
|
||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err
|
||||
mockC, err2 := mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase)
|
||||
if err2 != nil {
|
||||
return nil, errors.Join(err, err2)
|
||||
}
|
||||
return mockC, err
|
||||
}
|
||||
return tlsCertificate, nil
|
||||
return &tlsCertificate, nil
|
||||
}
|
||||
}
|
||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err
|
||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase)
|
||||
}
|
||||
log.Debug().Msgf("Obtained certificate for %v", domains)
|
||||
|
||||
if err := keyDatabase.Put(name, res); err != nil {
|
||||
return tls.Certificate{}, err
|
||||
return nil, err
|
||||
}
|
||||
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
return tlsCertificate, nil
|
||||
}
|
||||
|
||||
func SetupAcmeConfig(acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcceptTerms bool) (*lego.Config, error) {
|
||||
const configFile = "acme-account.json"
|
||||
var myAcmeAccount AcmeAccount
|
||||
var myAcmeConfig *lego.Config
|
||||
|
||||
if account, err := os.ReadFile(configFile); err == nil {
|
||||
if err := json.Unmarshal(account, &myAcmeAccount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myAcmeAccount.Key, err = certcrypto.ParsePEMPrivateKey([]byte(myAcmeAccount.KeyPEM))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myAcmeConfig = lego.NewConfig(&myAcmeAccount)
|
||||
myAcmeConfig.CADirURL = acmeAPI
|
||||
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
|
||||
|
||||
// Validate Config
|
||||
_, err := lego.NewClient(myAcmeConfig)
|
||||
if err != nil {
|
||||
// TODO: should we fail hard instead?
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
}
|
||||
return myAcmeConfig, nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myAcmeAccount = AcmeAccount{
|
||||
Email: acmeMail,
|
||||
Key: privateKey,
|
||||
KeyPEM: string(certcrypto.PEMEncode(privateKey)),
|
||||
}
|
||||
myAcmeConfig = lego.NewConfig(&myAcmeAccount)
|
||||
myAcmeConfig.CADirURL = acmeAPI
|
||||
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
|
||||
tempClient, err := lego.NewClient(myAcmeConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
// accept terms & log in to EAB
|
||||
if acmeEabKID == "" || acmeEabHmac == "" {
|
||||
reg, err := tempClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: acmeAcceptTerms})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
|
||||
} else {
|
||||
myAcmeAccount.Registration = reg
|
||||
}
|
||||
} else {
|
||||
reg, err := tempClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: acmeAcceptTerms,
|
||||
Kid: acmeEabKID,
|
||||
HmacEncoded: acmeEabHmac,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
|
||||
} else {
|
||||
myAcmeAccount.Registration = reg
|
||||
}
|
||||
}
|
||||
|
||||
if myAcmeAccount.Registration != nil {
|
||||
acmeAccountJSON, err := json.Marshal(myAcmeAccount)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("json.Marshalfailed, waiting for manual restart to avoid rate limits")
|
||||
select {}
|
||||
}
|
||||
err = os.WriteFile(configFile, acmeAccountJSON, 0o600)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("os.WriteFile failed, waiting for manual restart to avoid rate limits")
|
||||
select {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return myAcmeConfig, nil
|
||||
return &tlsCertificate, nil
|
||||
}
|
||||
|
||||
func SetupCertificates(mainDomainSuffix, dnsProvider string, acmeConfig *lego.Config, acmeUseRateLimits, enableHTTPServer bool, challengeCache cache.SetGetKey, certDB database.CertDB) error {
|
||||
func SetupMainDomainCertificates(mainDomainSuffix string, acmeClient *AcmeClient, certDB database.CertDB) error {
|
||||
// getting main cert before ACME account so that we can fail here without hitting rate limits
|
||||
mainCertBytes, err := certDB.Get(mainDomainSuffix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cert database is not working")
|
||||
}
|
||||
|
||||
acmeClient, err = lego.NewClient(acmeConfig)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
err = acmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
|
||||
}
|
||||
if enableHTTPServer {
|
||||
err = acmeClient.Challenge.SetHTTP01Provider(AcmeHTTPChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create HTTP-01 provider")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainDomainAcmeClient, err = lego.NewClient(acmeConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
if dnsProvider == "" {
|
||||
// using mock server, don't use wildcard certs
|
||||
err := mainDomainAcmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
|
||||
}
|
||||
} else {
|
||||
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create DNS Challenge provider")
|
||||
}
|
||||
err = mainDomainAcmeClient.Challenge.SetDNS01Provider(provider)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create DNS-01 provider")
|
||||
}
|
||||
}
|
||||
if err != nil && !errors.Is(err, database.ErrNotFound) {
|
||||
return fmt.Errorf("cert database is not working: %w", err)
|
||||
}
|
||||
|
||||
if mainCertBytes == nil {
|
||||
_, err = obtainCert(mainDomainAcmeClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, nil, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
|
||||
_, err = acmeClient.obtainCert(acmeClient.dnsChallengerLegoClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, nil, "", true, mainDomainSuffix, certDB)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Couldn't renew main domain certificate, continuing with mock certs only")
|
||||
}
|
||||
|
@ -463,43 +334,29 @@ func SetupCertificates(mainDomainSuffix, dnsProvider string, acmeConfig *lego.Co
|
|||
return nil
|
||||
}
|
||||
|
||||
func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffix, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) {
|
||||
func MaintainCertDB(ctx context.Context, interval time.Duration, acmeClient *AcmeClient, mainDomainSuffix string, certDB database.CertDB) {
|
||||
for {
|
||||
// clean up expired certs
|
||||
now := time.Now()
|
||||
// delete expired certs that will be invalid until next clean up
|
||||
threshold := time.Now().Add(interval)
|
||||
expiredCertCount := 0
|
||||
keyDatabaseIterator := certDB.Items()
|
||||
key, resBytes, err := keyDatabaseIterator.Next()
|
||||
for err == nil {
|
||||
if !strings.EqualFold(string(key), mainDomainSuffix) {
|
||||
resGob := bytes.NewBuffer(resBytes)
|
||||
resDec := gob.NewDecoder(resGob)
|
||||
res := &certificate.Resource{}
|
||||
err = resDec.Decode(res)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate)
|
||||
if err != nil || tlsCertificates[0].NotAfter.Before(now) {
|
||||
err := certDB.Delete(string(key))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Deleting expired certificate for %q failed", string(key))
|
||||
} else {
|
||||
expiredCertCount++
|
||||
certs, err := certDB.Items(0, 0)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("could not get certs from list")
|
||||
} else {
|
||||
for _, cert := range certs {
|
||||
if !strings.EqualFold(cert.Domain, strings.TrimPrefix(mainDomainSuffix, ".")) {
|
||||
if time.Unix(cert.ValidTill, 0).Before(threshold) {
|
||||
err := certDB.Delete(cert.Domain)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Deleting expired certificate for %q failed", cert.Domain)
|
||||
} else {
|
||||
expiredCertCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
key, resBytes, err = keyDatabaseIterator.Next()
|
||||
}
|
||||
log.Debug().Msgf("Removed %d expired certificates from the database", expiredCertCount)
|
||||
|
||||
// compact the database
|
||||
msg, err := certDB.Compact()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Compacting key database failed")
|
||||
} else {
|
||||
log.Debug().Msgf("Compacted key database: %s", msg)
|
||||
log.Debug().Msgf("Removed %d expired certificates from the database", expiredCertCount)
|
||||
}
|
||||
|
||||
// update main cert
|
||||
|
@ -510,11 +367,12 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi
|
|||
log.Error().Msgf("Couldn't renew certificate for main domain %q expected main domain cert to exist, but it's missing - seems like the database is corrupted", mainDomainSuffix)
|
||||
} else {
|
||||
tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate)
|
||||
|
||||
// renew main certificate 30 days before it expires
|
||||
if tlsCertificates[0].NotAfter.Before(time.Now().Add(30 * 24 * time.Hour)) {
|
||||
if err != nil {
|
||||
log.Error().Err(fmt.Errorf("could not parse cert for mainDomainSuffix: %w", err))
|
||||
} else if tlsCertificates[0].NotAfter.Before(time.Now().Add(30 * 24 * time.Hour)) {
|
||||
// renew main certificate 30 days before it expires
|
||||
go (func() {
|
||||
_, err = obtainCert(mainDomainAcmeClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
|
||||
_, err = acmeClient.obtainCert(acmeClient.dnsChallengerLegoClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, res, "", true, mainDomainSuffix, certDB)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Couldn't renew certificate for main domain")
|
||||
}
|
||||
|
@ -529,3 +387,12 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// leaf returns the parsed leaf certificate, either from c.leaf or by parsing
|
||||
// the corresponding c.Certificate[0].
|
||||
func leaf(c *tls.Certificate) (*x509.Certificate, error) {
|
||||
if c.Leaf != nil {
|
||||
return c.Leaf, nil
|
||||
}
|
||||
return x509.ParseCertificate(c.Certificate[0])
|
||||
}
|
||||
|
|
|
@ -13,14 +13,15 @@ import (
|
|||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/database"
|
||||
)
|
||||
|
||||
func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) tls.Certificate {
|
||||
func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) {
|
||||
key, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
|
@ -52,7 +53,7 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB)
|
|||
key,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
|
@ -61,7 +62,7 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB)
|
|||
Type: "CERTIFICATE",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
outBytes := out.Bytes()
|
||||
res := &certificate.Resource{
|
||||
|
@ -75,12 +76,12 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB)
|
|||
databaseName = mainDomainSuffix
|
||||
}
|
||||
if err := keyDatabase.Put(databaseName, res); err != nil {
|
||||
panic(err)
|
||||
log.Error().Err(err)
|
||||
}
|
||||
|
||||
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
return tlsCertificate
|
||||
return &tlsCertificate, nil
|
||||
}
|
||||
|
|
|
@ -3,14 +3,18 @@ package certificates
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/database"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/database"
|
||||
)
|
||||
|
||||
func TestMockCert(t *testing.T) {
|
||||
db, err := database.NewTmpDB()
|
||||
db := database.NewMockCertDB(t)
|
||||
db.Mock.On("Put", mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
cert, err := mockCert("example.com", "some error msg", "codeberg.page", db)
|
||||
assert.NoError(t, err)
|
||||
cert := mockCert("example.com", "some error msg", "codeberg.page", db)
|
||||
if assert.NotEmpty(t, cert) {
|
||||
assert.NotEmpty(t, cert.Certificate)
|
||||
}
|
||||
|
|
|
@ -48,11 +48,9 @@ func (c *Context) Redirect(uri string, statusCode int) {
|
|||
http.Redirect(c.RespWriter, c.Req, uri, statusCode)
|
||||
}
|
||||
|
||||
// Path returns requested path.
|
||||
//
|
||||
// The returned bytes are valid until your request handler returns.
|
||||
// Path returns the cleaned requested path.
|
||||
func (c *Context) Path() string {
|
||||
return c.Req.URL.Path
|
||||
return utils.CleanPath(c.Req.URL.Path)
|
||||
}
|
||||
|
||||
func (c *Context) Host() string {
|
||||
|
|
|
@ -1,15 +1,78 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"github.com/akrylysov/pogreb"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
//go:generate go install github.com/vektra/mockery/v2@latest
|
||||
//go:generate mockery --name CertDB --output . --filename mock.go --inpackage --case underscore
|
||||
|
||||
type CertDB interface {
|
||||
Close() error
|
||||
Put(name string, cert *certificate.Resource) error
|
||||
Get(name string) (*certificate.Resource, error)
|
||||
Delete(key string) error
|
||||
Compact() (string, error)
|
||||
Items() *pogreb.ItemIterator
|
||||
Items(page, pageSize int) ([]*Cert, error)
|
||||
}
|
||||
|
||||
type Cert struct {
|
||||
Domain string `xorm:"pk NOT NULL UNIQUE 'domain'"`
|
||||
Created int64 `xorm:"created NOT NULL DEFAULT 0 'created'"`
|
||||
Updated int64 `xorm:"updated NOT NULL DEFAULT 0 'updated'"`
|
||||
ValidTill int64 `xorm:" NOT NULL DEFAULT 0 'valid_till'"`
|
||||
// certificate.Resource
|
||||
CertURL string `xorm:"'cert_url'"`
|
||||
CertStableURL string `xorm:"'cert_stable_url'"`
|
||||
PrivateKey []byte `xorm:"'private_key'"`
|
||||
Certificate []byte `xorm:"'certificate'"`
|
||||
IssuerCertificate []byte `xorm:"'issuer_certificate'"`
|
||||
}
|
||||
|
||||
func (c Cert) Raw() *certificate.Resource {
|
||||
return &certificate.Resource{
|
||||
Domain: c.Domain,
|
||||
CertURL: c.CertURL,
|
||||
CertStableURL: c.CertStableURL,
|
||||
PrivateKey: c.PrivateKey,
|
||||
Certificate: c.Certificate,
|
||||
IssuerCertificate: c.IssuerCertificate,
|
||||
}
|
||||
}
|
||||
|
||||
func toCert(name string, c *certificate.Resource) (*Cert, error) {
|
||||
tlsCertificates, err := certcrypto.ParsePEMBundle(c.Certificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tlsCertificates) == 0 || tlsCertificates[0] == nil {
|
||||
err := fmt.Errorf("parsed cert resource has no cert")
|
||||
log.Error().Err(err).Str("domain", c.Domain).Msgf("cert: %v", c)
|
||||
return nil, err
|
||||
}
|
||||
validTill := tlsCertificates[0].NotAfter.Unix()
|
||||
|
||||
// handle wildcard certs
|
||||
if name[:1] == "." {
|
||||
name = "*" + name
|
||||
}
|
||||
if name != c.Domain {
|
||||
err := fmt.Errorf("domain key '%s' and cert domain '%s' not equal", name, c.Domain)
|
||||
log.Error().Err(err).Msg("toCert conversion did discover mismatch")
|
||||
// TODO: fail hard: return nil, err
|
||||
}
|
||||
|
||||
return &Cert{
|
||||
Domain: c.Domain,
|
||||
ValidTill: validTill,
|
||||
|
||||
CertURL: c.CertURL,
|
||||
CertStableURL: c.CertStableURL,
|
||||
PrivateKey: c.PrivateKey,
|
||||
Certificate: c.Certificate,
|
||||
IssuerCertificate: c.IssuerCertificate,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -1,55 +1,122 @@
|
|||
// Code generated by mockery v2.20.0. DO NOT EDIT.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/OrlovEvgeny/go-mcache"
|
||||
"github.com/akrylysov/pogreb"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
certificate "github.com/go-acme/lego/v4/certificate"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
var _ CertDB = tmpDB{}
|
||||
|
||||
type tmpDB struct {
|
||||
intern *mcache.CacheDriver
|
||||
ttl time.Duration
|
||||
// MockCertDB is an autogenerated mock type for the CertDB type
|
||||
type MockCertDB struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (p tmpDB) Close() error {
|
||||
_ = p.intern.Close()
|
||||
return nil
|
||||
}
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *MockCertDB) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
func (p tmpDB) Put(name string, cert *certificate.Resource) error {
|
||||
return p.intern.Set(name, cert, p.ttl)
|
||||
}
|
||||
|
||||
func (p tmpDB) Get(name string) (*certificate.Resource, error) {
|
||||
cert, has := p.intern.Get(name)
|
||||
if !has {
|
||||
return nil, fmt.Errorf("cert for %q not found", name)
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return cert.(*certificate.Resource), nil
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
func (p tmpDB) Delete(key string) error {
|
||||
p.intern.Remove(key)
|
||||
return nil
|
||||
// Delete provides a mock function with given fields: key
|
||||
func (_m *MockCertDB) Delete(key string) error {
|
||||
ret := _m.Called(key)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(key)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
func (p tmpDB) Compact() (string, error) {
|
||||
p.intern.Truncate()
|
||||
return "Truncate done", nil
|
||||
// Get provides a mock function with given fields: name
|
||||
func (_m *MockCertDB) Get(name string) (*certificate.Resource, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *certificate.Resource
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (*certificate.Resource, error)); ok {
|
||||
return rf(name)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) *certificate.Resource); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*certificate.Resource)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (p tmpDB) Items() *pogreb.ItemIterator {
|
||||
panic("ItemIterator not implemented for tmpDB")
|
||||
// Items provides a mock function with given fields: page, pageSize
|
||||
func (_m *MockCertDB) Items(page int, pageSize int) ([]*Cert, error) {
|
||||
ret := _m.Called(page, pageSize)
|
||||
|
||||
var r0 []*Cert
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(int, int) ([]*Cert, error)); ok {
|
||||
return rf(page, pageSize)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(int, int) []*Cert); ok {
|
||||
r0 = rf(page, pageSize)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*Cert)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(int, int) error); ok {
|
||||
r1 = rf(page, pageSize)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func NewTmpDB() (CertDB, error) {
|
||||
return &tmpDB{
|
||||
intern: mcache.New(),
|
||||
ttl: time.Minute,
|
||||
}, nil
|
||||
// Put provides a mock function with given fields: name, cert
|
||||
func (_m *MockCertDB) Put(name string, cert *certificate.Resource) error {
|
||||
ret := _m.Called(name, cert)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, *certificate.Resource) error); ok {
|
||||
r0 = rf(name, cert)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewMockCertDB interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewMockCertDB creates a new instance of MockCertDB. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockCertDB(t mockConstructorTestingTNewMockCertDB) *MockCertDB {
|
||||
mock := &MockCertDB{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/akrylysov/pogreb"
|
||||
"github.com/akrylysov/pogreb/fs"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var _ CertDB = aDB{}
|
||||
|
||||
type aDB struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
intern *pogreb.DB
|
||||
syncInterval time.Duration
|
||||
}
|
||||
|
||||
func (p aDB) Close() error {
|
||||
p.cancel()
|
||||
return p.intern.Sync()
|
||||
}
|
||||
|
||||
func (p aDB) Put(name string, cert *certificate.Resource) error {
|
||||
var resGob bytes.Buffer
|
||||
if err := gob.NewEncoder(&resGob).Encode(cert); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.intern.Put([]byte(name), resGob.Bytes())
|
||||
}
|
||||
|
||||
func (p aDB) Get(name string) (*certificate.Resource, error) {
|
||||
cert := &certificate.Resource{}
|
||||
resBytes, err := p.intern.Get([]byte(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resBytes == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := gob.NewDecoder(bytes.NewBuffer(resBytes)).Decode(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (p aDB) Delete(key string) error {
|
||||
return p.intern.Delete([]byte(key))
|
||||
}
|
||||
|
||||
func (p aDB) Compact() (string, error) {
|
||||
result, err := p.intern.Compact()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%+v", result), nil
|
||||
}
|
||||
|
||||
func (p aDB) Items() *pogreb.ItemIterator {
|
||||
return p.intern.Items()
|
||||
}
|
||||
|
||||
var _ CertDB = &aDB{}
|
||||
|
||||
func (p aDB) sync() {
|
||||
for {
|
||||
err := p.intern.Sync()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Syncing cert database failed")
|
||||
}
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
case <-time.After(p.syncInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func New(path string) (CertDB, error) {
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("path not set")
|
||||
}
|
||||
db, err := pogreb.Open(path, &pogreb.Options{
|
||||
BackgroundSyncInterval: 30 * time.Second,
|
||||
BackgroundCompactionInterval: 6 * time.Hour,
|
||||
FileSystem: fs.OSMMap,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
result := &aDB{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
intern: db,
|
||||
syncInterval: 5 * time.Minute,
|
||||
}
|
||||
|
||||
go result.sync()
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"xorm.io/xorm"
|
||||
|
||||
// register sql driver
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var _ CertDB = xDB{}
|
||||
|
||||
var ErrNotFound = errors.New("entry not found")
|
||||
|
||||
type xDB struct {
|
||||
engine *xorm.Engine
|
||||
}
|
||||
|
||||
func NewXormDB(dbType, dbConn string) (CertDB, error) {
|
||||
if !supportedDriver(dbType) {
|
||||
return nil, fmt.Errorf("not supported db type '%s'", dbType)
|
||||
}
|
||||
if dbConn == "" {
|
||||
return nil, fmt.Errorf("no db connection provided")
|
||||
}
|
||||
|
||||
e, err := xorm.NewEngine(dbType, dbConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := e.Sync2(new(Cert)); err != nil {
|
||||
return nil, fmt.Errorf("could not sync db model :%w", err)
|
||||
}
|
||||
|
||||
return &xDB{
|
||||
engine: e,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (x xDB) Close() error {
|
||||
return x.engine.Close()
|
||||
}
|
||||
|
||||
func (x xDB) Put(domain string, cert *certificate.Resource) error {
|
||||
log.Trace().Str("domain", cert.Domain).Msg("inserting cert to db")
|
||||
|
||||
c, err := toCert(domain, cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := x.engine.NewSession()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer sess.Close()
|
||||
|
||||
if exist, _ := sess.ID(c.Domain).Exist(new(Cert)); exist {
|
||||
if _, err := sess.ID(c.Domain).Update(c); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err = sess.Insert(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func (x xDB) Get(domain string) (*certificate.Resource, error) {
|
||||
// handle wildcard certs
|
||||
if domain[:1] == "." {
|
||||
domain = "*" + domain
|
||||
}
|
||||
|
||||
cert := new(Cert)
|
||||
log.Trace().Str("domain", domain).Msg("get cert from db")
|
||||
if found, err := x.engine.ID(domain).Get(cert); err != nil {
|
||||
return nil, err
|
||||
} else if !found {
|
||||
return nil, fmt.Errorf("%w: name='%s'", ErrNotFound, domain)
|
||||
}
|
||||
return cert.Raw(), nil
|
||||
}
|
||||
|
||||
func (x xDB) Delete(domain string) error {
|
||||
// handle wildcard certs
|
||||
if domain[:1] == "." {
|
||||
domain = "*" + domain
|
||||
}
|
||||
|
||||
log.Trace().Str("domain", domain).Msg("delete cert from db")
|
||||
_, err := x.engine.ID(domain).Delete(new(Cert))
|
||||
return err
|
||||
}
|
||||
|
||||
// Items return al certs from db, if pageSize is 0 it does not use limit
|
||||
func (x xDB) Items(page, pageSize int) ([]*Cert, error) {
|
||||
// paginated return
|
||||
if pageSize > 0 {
|
||||
certs := make([]*Cert, 0, pageSize)
|
||||
if page >= 0 {
|
||||
page = 1
|
||||
}
|
||||
err := x.engine.Limit(pageSize, (page-1)*pageSize).Find(&certs)
|
||||
return certs, err
|
||||
}
|
||||
|
||||
// return all
|
||||
certs := make([]*Cert, 0, 64)
|
||||
err := x.engine.Find(&certs)
|
||||
return certs, err
|
||||
}
|
||||
|
||||
// Supported database drivers
|
||||
const (
|
||||
DriverSqlite = "sqlite3"
|
||||
DriverMysql = "mysql"
|
||||
DriverPostgres = "postgres"
|
||||
)
|
||||
|
||||
func supportedDriver(driver string) bool {
|
||||
switch driver {
|
||||
case DriverMysql, DriverPostgres, DriverSqlite:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func newTestDB(t *testing.T) *xDB {
|
||||
e, err := xorm.NewEngine("sqlite3", ":memory:")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, e.Sync2(new(Cert)))
|
||||
return &xDB{engine: e}
|
||||
}
|
||||
|
||||
func TestSanitizeWildcardCerts(t *testing.T) {
|
||||
certDB := newTestDB(t)
|
||||
|
||||
_, err := certDB.Get(".not.found")
|
||||
assert.True(t, errors.Is(err, ErrNotFound))
|
||||
|
||||
// TODO: cert key and domain mismatch are don not fail hard jet
|
||||
// https://codeberg.org/Codeberg/pages-server/src/commit/d8595cee882e53d7f44f1ddc4ef8a1f7b8f31d8d/server/database/interface.go#L64
|
||||
//
|
||||
// assert.Error(t, certDB.Put(".wildcard.de", &certificate.Resource{
|
||||
// Domain: "*.localhost.mock.directory",
|
||||
// Certificate: localhost_mock_directory_certificate,
|
||||
// }))
|
||||
|
||||
// insert new wildcard cert
|
||||
assert.NoError(t, certDB.Put(".wildcard.de", &certificate.Resource{
|
||||
Domain: "*.wildcard.de",
|
||||
Certificate: localhost_mock_directory_certificate,
|
||||
}))
|
||||
|
||||
// update existing cert
|
||||
assert.NoError(t, certDB.Put(".wildcard.de", &certificate.Resource{
|
||||
Domain: "*.wildcard.de",
|
||||
Certificate: localhost_mock_directory_certificate,
|
||||
}))
|
||||
|
||||
c1, err := certDB.Get(".wildcard.de")
|
||||
assert.NoError(t, err)
|
||||
c2, err := certDB.Get("*.wildcard.de")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, c1, c2)
|
||||
}
|
||||
|
||||
var localhost_mock_directory_certificate = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDczCCAlugAwIBAgIIJyBaXHmLk6gwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
|
||||
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA0OWE0ZmIwHhcNMjMwMjEwMDEwOTA2
|
||||
WhcNMjgwMjEwMDEwOTA2WjAjMSEwHwYDVQQDExhsb2NhbGhvc3QubW9jay5kaXJl
|
||||
Y3RvcnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIU/CjzS7t62Gj
|
||||
neEMqvP7sn99ULT7AEUzEfWL05fWG2z714qcUg1hXkZLgdVDgmsCpplyddip7+2t
|
||||
ZH/9rLPLMqJphzvOL4CF6jDLbeifETtKyjnt9vUZFnnNWcP3tu8lo8iYSl08qsUI
|
||||
Pp/hiEriAQzCDjTbR5m9xUPNPYqxzcS4ALzmmCX9Qfc4CuuhMkdv2G4TT7rylWrA
|
||||
SCSRPnGjeA7pCByfNrO/uXbxmzl3sMO3k5sqgMkx1QIHEN412V8+vtx88mt2sM6k
|
||||
xjzGZWWKXlRq+oufIKX9KPplhsCjMH6E3VNAzgOPYDqXagtUcGmLWghURltO8Mt2
|
||||
zwM6OgjjAgMBAAGjgaUwgaIwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG
|
||||
AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSMQvlJ1755
|
||||
sarf8i1KNqj7s5o/aDAfBgNVHSMEGDAWgBTcZcxJMhWdP7MecHCCpNkFURC/YzAj
|
||||
BgNVHREEHDAaghhsb2NhbGhvc3QubW9jay5kaXJlY3RvcnkwDQYJKoZIhvcNAQEL
|
||||
BQADggEBACcd7TT28OWwzQN2PcH0aG38JX5Wp2iOS/unDCfWjNAztXHW7nBDMxza
|
||||
VtyebkJfccexpuVuOsjOX+bww0vtEYIvKX3/GbkhogksBrNkE0sJZtMnZWMR33wa
|
||||
YxAy/kJBTmLi02r8fX9ZhwjldStHKBav4USuP7DXZjrgX7LFQhR4LIDrPaYqQRZ8
|
||||
ltC3mM9LDQ9rQyIFP5cSBMO3RUAm4I8JyLoOdb/9G2uxjHr7r6eG1g8DmLYSKBsQ
|
||||
mWGQDOYgR3cGltDe2yMxM++yHY+b1uhxGOWMrDA1+1k7yI19LL8Ifi2FMovDfu/X
|
||||
JxYk1NNNtdctwaYJFenmGQvDaIq1KgE=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDUDCCAjigAwIBAgIIKBJ7IIA6W1swDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
|
||||
AxMVUGViYmxlIFJvb3QgQ0EgNTdmZjE2MCAXDTIzMDIwOTA1MzMxMloYDzIwNTMw
|
||||
MjA5MDUzMzEyWjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDQ5
|
||||
YTRmYjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOvlqRx8SXQFWo2
|
||||
gFCiXxls53eENcyr8+meFyjgnS853eEvplaPxoa2MREKd+ZYxM8EMMfj2XGvR3UI
|
||||
aqR5QyLQ9ihuRqvQo4fG91usBHgH+vDbGPdMX8gDmm9HgnmtOVhSKJU+M2jfE1SW
|
||||
UuWB9xOa3LMreTXbTNfZEMoXf+GcWZMbx5WPgEga3DvfmV+RsfNvB55eD7YAyZgF
|
||||
ZnQ3Dskmnxxlkz0EGgd7rqhFHHNB9jARlL22gITADwoWZidlr3ciM9DISymRKQ0c
|
||||
mRN15fQjNWdtuREgJlpXecbYQMGhdTOmFrqdHkveD1o63rGSC4z+s/APV6xIbcRp
|
||||
aNpO7L8CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB
|
||||
BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNxlzEky
|
||||
FZ0/sx5wcIKk2QVREL9jMB8GA1UdIwQYMBaAFOqfkm9rebIz4z0SDIKW5edLg5JM
|
||||
MA0GCSqGSIb3DQEBCwUAA4IBAQBRG9AHEnyj2fKzVDDbQaKHjAF5jh0gwyHoIeRK
|
||||
FkP9mQNSWxhvPWI0tK/E49LopzmVuzSbDd5kZsaii73rAs6f6Rf9W5veo3AFSEad
|
||||
stM+Zv0f2vWB38nuvkoCRLXMX+QUeuL65rKxdEpyArBju4L3/PqAZRgMLcrH+ak8
|
||||
nvw5RdAq+Km/ZWyJgGikK6cfMmh91YALCDFnoWUWrCjkBaBFKrG59ONV9f0IQX07
|
||||
aNfFXFCF5l466xw9dHjw5iaFib10cpY3iq4kyPYIMs6uaewkCtxWKKjiozM4g4w3
|
||||
HqwyUyZ52WUJOJ/6G9DJLDtN3fgGR+IAp8BhYd5CqOscnt3h
|
||||
-----END CERTIFICATE-----`)
|
|
@ -11,9 +11,11 @@ import (
|
|||
// lookupCacheTimeout specifies the timeout for the DNS lookup cache.
|
||||
var lookupCacheTimeout = 15 * time.Minute
|
||||
|
||||
var defaultPagesRepo = "pages"
|
||||
|
||||
// GetTargetFromDNS searches for CNAME or TXT entries on the request domain ending with MainDomainSuffix.
|
||||
// If everything is fine, it returns the target data.
|
||||
func GetTargetFromDNS(domain, mainDomainSuffix string, dnsLookupCache cache.SetGetKey) (targetOwner, targetRepo, targetBranch string) {
|
||||
func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLookupCache cache.ICache) (targetOwner, targetRepo, targetBranch string) {
|
||||
// Get CNAME or TXT
|
||||
var cname string
|
||||
var err error
|
||||
|
@ -28,7 +30,7 @@ func GetTargetFromDNS(domain, mainDomainSuffix string, dnsLookupCache cache.SetG
|
|||
names, err := net.LookupTXT(domain)
|
||||
if err == nil {
|
||||
for _, name := range names {
|
||||
name = strings.TrimSuffix(name, ".")
|
||||
name = strings.TrimSuffix(strings.TrimSpace(name), ".")
|
||||
if strings.HasSuffix(name, mainDomainSuffix) {
|
||||
cname = name
|
||||
break
|
||||
|
@ -50,10 +52,10 @@ func GetTargetFromDNS(domain, mainDomainSuffix string, dnsLookupCache cache.SetG
|
|||
targetBranch = cnameParts[len(cnameParts)-3]
|
||||
}
|
||||
if targetRepo == "" {
|
||||
targetRepo = "pages"
|
||||
targetRepo = defaultPagesRepo
|
||||
}
|
||||
if targetBranch == "" && targetRepo != "pages" {
|
||||
targetBranch = "pages"
|
||||
if targetBranch == "" && targetRepo != defaultPagesRepo {
|
||||
targetBranch = firstDefaultBranch
|
||||
}
|
||||
// if targetBranch is still empty, the caller must find the default branch
|
||||
return
|
||||
|
|
|
@ -26,6 +26,9 @@ const (
|
|||
// TODO: move as option into cache interface
|
||||
fileCacheTimeout = 5 * time.Minute
|
||||
|
||||
// ownerExistenceCacheTimeout specifies the timeout for the existence of a repo/org
|
||||
ownerExistenceCacheTimeout = 5 * time.Minute
|
||||
|
||||
// fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default.
|
||||
fileCacheSizeLimit = int64(1000 * 1000)
|
||||
)
|
||||
|
@ -39,7 +42,7 @@ type FileResponse struct {
|
|||
}
|
||||
|
||||
func (f FileResponse) IsEmpty() bool {
|
||||
return len(f.Body) != 0
|
||||
return len(f.Body) == 0
|
||||
}
|
||||
|
||||
func (f FileResponse) createHttpResponse(cacheKey string) (header http.Header, statusCode int) {
|
||||
|
@ -72,15 +75,16 @@ type BranchTimestamp struct {
|
|||
type writeCacheReader struct {
|
||||
originalReader io.ReadCloser
|
||||
buffer *bytes.Buffer
|
||||
rileResponse *FileResponse
|
||||
fileResponse *FileResponse
|
||||
cacheKey string
|
||||
cache cache.SetGetKey
|
||||
cache cache.ICache
|
||||
hasError bool
|
||||
}
|
||||
|
||||
func (t *writeCacheReader) Read(p []byte) (n int, err error) {
|
||||
log.Trace().Msgf("[cache] read %q", t.cacheKey)
|
||||
n, err = t.originalReader.Read(p)
|
||||
if err != nil {
|
||||
if err != nil && err != io.EOF {
|
||||
log.Trace().Err(err).Msgf("[cache] original reader for %q has returned an error", t.cacheKey)
|
||||
t.hasError = true
|
||||
} else if n > 0 {
|
||||
|
@ -90,16 +94,24 @@ func (t *writeCacheReader) Read(p []byte) (n int, err error) {
|
|||
}
|
||||
|
||||
func (t *writeCacheReader) Close() error {
|
||||
if !t.hasError {
|
||||
fc := *t.rileResponse
|
||||
fc.Body = t.buffer.Bytes()
|
||||
_ = t.cache.Set(t.cacheKey, fc, fileCacheTimeout)
|
||||
doWrite := !t.hasError
|
||||
fc := *t.fileResponse
|
||||
fc.Body = t.buffer.Bytes()
|
||||
if fc.IsEmpty() {
|
||||
log.Trace().Msg("[cache] file response is empty")
|
||||
doWrite = false
|
||||
}
|
||||
log.Trace().Msgf("cacheReader for %q saved=%t closed", t.cacheKey, !t.hasError)
|
||||
if doWrite {
|
||||
err := t.cache.Set(t.cacheKey, fc, fileCacheTimeout)
|
||||
if err != nil {
|
||||
log.Trace().Err(err).Msgf("[cache] writer for %q has returned an error", t.cacheKey)
|
||||
}
|
||||
}
|
||||
log.Trace().Msgf("cacheReader for %q saved=%t closed", t.cacheKey, doWrite)
|
||||
return t.originalReader.Close()
|
||||
}
|
||||
|
||||
func (f FileResponse) CreateCacheReader(r io.ReadCloser, cache cache.SetGetKey, cacheKey string) io.ReadCloser {
|
||||
func (f FileResponse) CreateCacheReader(r io.ReadCloser, cache cache.ICache, cacheKey string) io.ReadCloser {
|
||||
if r == nil || cache == nil || cacheKey == "" {
|
||||
log.Error().Msg("could not create CacheReader")
|
||||
return nil
|
||||
|
@ -108,7 +120,7 @@ func (f FileResponse) CreateCacheReader(r io.ReadCloser, cache cache.SetGetKey,
|
|||
return &writeCacheReader{
|
||||
originalReader: r,
|
||||
buffer: bytes.NewBuffer(make([]byte, 0)),
|
||||
rileResponse: &f,
|
||||
fileResponse: &f,
|
||||
cache: cache,
|
||||
cacheKey: cacheKey,
|
||||
}
|
||||
|
|
|
@ -16,16 +16,19 @@ import (
|
|||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"codeberg.org/codeberg/pages/config"
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"codeberg.org/codeberg/pages/server/version"
|
||||
)
|
||||
|
||||
var ErrorNotFound = errors.New("not found")
|
||||
|
||||
const (
|
||||
// cache key prefixe
|
||||
// cache key prefixes
|
||||
branchTimestampCacheKeyPrefix = "branchTime"
|
||||
defaultBranchCacheKeyPrefix = "defaultBranch"
|
||||
rawContentCacheKeyPrefix = "rawContent"
|
||||
ownerExistenceKeyPrefix = "ownerExist"
|
||||
|
||||
// pages server
|
||||
PagesCacheIndicatorHeader = "X-Pages-Cache"
|
||||
|
@ -43,7 +46,7 @@ const (
|
|||
|
||||
type Client struct {
|
||||
sdkClient *gitea.Client
|
||||
responseCache cache.SetGetKey
|
||||
responseCache cache.ICache
|
||||
|
||||
giteaRoot string
|
||||
|
||||
|
@ -54,37 +57,40 @@ type Client struct {
|
|||
defaultMimeType string
|
||||
}
|
||||
|
||||
func NewClient(giteaRoot, giteaAPIToken string, respCache cache.SetGetKey, followSymlinks, supportLFS bool) (*Client, error) {
|
||||
rootURL, err := url.Parse(giteaRoot)
|
||||
func NewClient(cfg config.GiteaConfig, respCache cache.ICache) (*Client, error) {
|
||||
rootURL, err := url.Parse(cfg.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
giteaRoot = strings.Trim(rootURL.String(), "/")
|
||||
giteaRoot := strings.Trim(rootURL.String(), "/")
|
||||
|
||||
stdClient := http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
// TODO: pass down
|
||||
var (
|
||||
forbiddenMimeTypes map[string]bool
|
||||
defaultMimeType string
|
||||
)
|
||||
|
||||
if forbiddenMimeTypes == nil {
|
||||
forbiddenMimeTypes = make(map[string]bool)
|
||||
forbiddenMimeTypes := make(map[string]bool, len(cfg.ForbiddenMimeTypes))
|
||||
for _, mimeType := range cfg.ForbiddenMimeTypes {
|
||||
forbiddenMimeTypes[mimeType] = true
|
||||
}
|
||||
|
||||
defaultMimeType := cfg.DefaultMimeType
|
||||
if defaultMimeType == "" {
|
||||
defaultMimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
sdk, err := gitea.NewClient(giteaRoot, gitea.SetHTTPClient(&stdClient), gitea.SetToken(giteaAPIToken))
|
||||
sdk, err := gitea.NewClient(
|
||||
giteaRoot,
|
||||
gitea.SetHTTPClient(&stdClient),
|
||||
gitea.SetToken(cfg.Token),
|
||||
gitea.SetUserAgent("pages-server/"+version.Version),
|
||||
)
|
||||
|
||||
return &Client{
|
||||
sdkClient: sdk,
|
||||
responseCache: respCache,
|
||||
|
||||
giteaRoot: giteaRoot,
|
||||
|
||||
followSymlinks: followSymlinks,
|
||||
supportLFS: supportLFS,
|
||||
followSymlinks: cfg.FollowSymlinks,
|
||||
supportLFS: cfg.LFSEnabled,
|
||||
|
||||
forbiddenMimeTypes: forbiddenMimeTypes,
|
||||
defaultMimeType: defaultMimeType,
|
||||
|
@ -107,26 +113,27 @@ func (client *Client) GiteaRawContent(targetOwner, targetRepo, ref, resource str
|
|||
func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource string) (io.ReadCloser, http.Header, int, error) {
|
||||
cacheKey := fmt.Sprintf("%s/%s/%s|%s|%s", rawContentCacheKeyPrefix, targetOwner, targetRepo, ref, resource)
|
||||
log := log.With().Str("cache_key", cacheKey).Logger()
|
||||
|
||||
log.Trace().Msg("try file in cache")
|
||||
// handle if cache entry exist
|
||||
if cache, ok := client.responseCache.Get(cacheKey); ok {
|
||||
cache := cache.(FileResponse)
|
||||
cachedHeader, cachedStatusCode := cache.createHttpResponse(cacheKey)
|
||||
// TODO: check against some timestamp missmatch?!?
|
||||
// TODO: check against some timestamp mismatch?!?
|
||||
if cache.Exists {
|
||||
log.Debug().Msg("[cache] exists")
|
||||
if cache.IsSymlink {
|
||||
linkDest := string(cache.Body)
|
||||
log.Debug().Msgf("[cache] follow symlink from %q to %q", resource, linkDest)
|
||||
return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest)
|
||||
} else {
|
||||
log.Debug().Msg("[cache] return bytes")
|
||||
} else if !cache.IsEmpty() {
|
||||
log.Debug().Msgf("[cache] return %d bytes", len(cache.Body))
|
||||
return io.NopCloser(bytes.NewReader(cache.Body)), cachedHeader, cachedStatusCode, nil
|
||||
} else if cache.IsEmpty() {
|
||||
log.Debug().Msg("[cache] is empty")
|
||||
}
|
||||
} else {
|
||||
return nil, cachedHeader, cachedStatusCode, ErrorNotFound
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace().Msg("file not in cache")
|
||||
// not in cache, open reader via gitea api
|
||||
reader, resp, err := client.sdkClient.GetFileReader(targetOwner, targetRepo, ref, resource, client.supportLFS)
|
||||
if resp != nil {
|
||||
|
@ -145,13 +152,19 @@ func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource str
|
|||
}
|
||||
linkDest := strings.TrimSpace(string(linkDestBytes))
|
||||
|
||||
// handle relative links
|
||||
// we first remove the link from the path, and make a relative join (resolve parent paths like "/../" too)
|
||||
linkDest = path.Join(path.Dir(resource), linkDest)
|
||||
|
||||
// we store symlink not content to reduce duplicates in cache
|
||||
if err := client.responseCache.Set(cacheKey, FileResponse{
|
||||
fileResponse := FileResponse{
|
||||
Exists: true,
|
||||
IsSymlink: true,
|
||||
Body: []byte(linkDest),
|
||||
ETag: resp.Header.Get(ETagHeader),
|
||||
}, fileCacheTimeout); err != nil {
|
||||
}
|
||||
log.Trace().Msgf("file response has %d bytes", len(fileResponse.Body))
|
||||
if err := client.responseCache.Set(cacheKey, fileResponse, fileCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
|
||||
|
@ -168,7 +181,7 @@ func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource str
|
|||
return reader, resp.Response.Header, resp.StatusCode, err
|
||||
}
|
||||
|
||||
// now we write to cache and respond at the sime time
|
||||
// now we write to cache and respond at the same time
|
||||
fileResp := FileResponse{
|
||||
Exists: true,
|
||||
ETag: resp.Header.Get(ETagHeader),
|
||||
|
@ -254,6 +267,38 @@ func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (str
|
|||
return branch, nil
|
||||
}
|
||||
|
||||
func (client *Client) GiteaCheckIfOwnerExists(owner string) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("%s/%s", ownerExistenceKeyPrefix, owner)
|
||||
|
||||
if exist, ok := client.responseCache.Get(cacheKey); ok && exist != nil {
|
||||
return exist.(bool), nil
|
||||
}
|
||||
|
||||
_, resp, err := client.sdkClient.GetUserInfo(owner)
|
||||
if resp.StatusCode == http.StatusOK && err == nil {
|
||||
if err := client.responseCache.Set(cacheKey, true, ownerExistenceCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
return true, nil
|
||||
} else if resp.StatusCode != http.StatusNotFound {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, resp, err = client.sdkClient.GetOrg(owner)
|
||||
if resp.StatusCode == http.StatusOK && err == nil {
|
||||
if err := client.responseCache.Set(cacheKey, true, ownerExistenceCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
return true, nil
|
||||
} else if resp.StatusCode != http.StatusNotFound {
|
||||
return false, err
|
||||
}
|
||||
if err := client.responseCache.Set(cacheKey, false, ownerExistenceCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (client *Client) getMimeTypeByExtension(resource string) string {
|
||||
mimeType := mime.TypeByExtension(path.Ext(resource))
|
||||
mimeTypeSplit := strings.SplitN(mimeType, ";", 2)
|
||||
|
@ -274,11 +319,11 @@ func shouldRespBeSavedToCache(resp *http.Response) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
contentLeng, err := strconv.ParseInt(contentLengthRaw, 10, 64)
|
||||
contentLength, err := strconv.ParseInt(contentLengthRaw, 10, 64)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("could not parse content length")
|
||||
}
|
||||
|
||||
// if content to big or could not be determined we not cache it
|
||||
return contentLeng > 0 && contentLeng < fileCacheSizeLimit
|
||||
return contentLength > 0 && contentLength < fileCacheSizeLimit
|
||||
}
|
||||
|
|
|
@ -6,32 +6,31 @@ import (
|
|||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"codeberg.org/codeberg/pages/config"
|
||||
"codeberg.org/codeberg/pages/html"
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"codeberg.org/codeberg/pages/server/context"
|
||||
"codeberg.org/codeberg/pages/server/gitea"
|
||||
"codeberg.org/codeberg/pages/server/version"
|
||||
)
|
||||
|
||||
const (
|
||||
headerAccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
||||
headerAccessControlAllowMethods = "Access-Control-Allow-Methods"
|
||||
defaultPagesRepo = "pages"
|
||||
defaultPagesBranch = "pages"
|
||||
)
|
||||
|
||||
// Handler handles a single HTTP request to the web server.
|
||||
func Handler(mainDomainSuffix, rawDomain string,
|
||||
func Handler(
|
||||
cfg config.ServerConfig,
|
||||
giteaClient *gitea.Client,
|
||||
rawInfoPage string,
|
||||
blacklistedPaths, allowedCorsDomains []string,
|
||||
dnsLookupCache, canonicalDomainCache cache.SetGetKey,
|
||||
dnsLookupCache, canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
log.Debug().Msg("\n----------------------------------------------------------")
|
||||
log := log.With().Strs("Handler", []string{req.Host, req.RequestURI}).Logger()
|
||||
ctx := context.New(w, req)
|
||||
|
||||
ctx.RespWriter.Header().Set("Server", "CodebergPages/"+version.Version)
|
||||
ctx.RespWriter.Header().Set("Server", "pages-server")
|
||||
|
||||
// Force new default from specification (since November 2020) - see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#strict-origin-when-cross-origin
|
||||
ctx.RespWriter.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
|
@ -41,8 +40,8 @@ func Handler(mainDomainSuffix, rawDomain string,
|
|||
|
||||
trimmedHost := ctx.TrimHostPort()
|
||||
|
||||
// Add HSTS for RawDomain and MainDomainSuffix
|
||||
if hsts := getHSTSHeader(trimmedHost, mainDomainSuffix, rawDomain); hsts != "" {
|
||||
// Add HSTS for RawDomain and MainDomain
|
||||
if hsts := getHSTSHeader(trimmedHost, cfg.MainDomain, cfg.RawDomain); hsts != "" {
|
||||
ctx.RespWriter.Header().Set("Strict-Transport-Security", hsts)
|
||||
}
|
||||
|
||||
|
@ -64,16 +63,16 @@ func Handler(mainDomainSuffix, rawDomain string,
|
|||
}
|
||||
|
||||
// Block blacklisted paths (like ACME challenges)
|
||||
for _, blacklistedPath := range blacklistedPaths {
|
||||
for _, blacklistedPath := range cfg.BlacklistedPaths {
|
||||
if strings.HasPrefix(ctx.Path(), blacklistedPath) {
|
||||
html.ReturnErrorPage(ctx, "requested blacklisted path", http.StatusForbidden)
|
||||
html.ReturnErrorPage(ctx, "requested path is blacklisted", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Allow CORS for specified domains
|
||||
allowCors := false
|
||||
for _, allowedCorsDomain := range allowedCorsDomains {
|
||||
for _, allowedCorsDomain := range cfg.AllowedCorsDomains {
|
||||
if strings.EqualFold(trimmedHost, allowedCorsDomain) {
|
||||
allowCors = true
|
||||
break
|
||||
|
@ -87,27 +86,29 @@ func Handler(mainDomainSuffix, rawDomain string,
|
|||
// Prepare request information to Gitea
|
||||
pathElements := strings.Split(strings.Trim(ctx.Path(), "/"), "/")
|
||||
|
||||
if rawDomain != "" && strings.EqualFold(trimmedHost, rawDomain) {
|
||||
log.Debug().Msg("raw domain request detecded")
|
||||
if cfg.RawDomain != "" && strings.EqualFold(trimmedHost, cfg.RawDomain) {
|
||||
log.Debug().Msg("raw domain request detected")
|
||||
handleRaw(log, ctx, giteaClient,
|
||||
mainDomainSuffix, rawInfoPage,
|
||||
cfg.MainDomain,
|
||||
trimmedHost,
|
||||
pathElements,
|
||||
canonicalDomainCache)
|
||||
} else if strings.HasSuffix(trimmedHost, mainDomainSuffix) {
|
||||
log.Debug().Msg("subdomain request detecded")
|
||||
canonicalDomainCache, redirectsCache)
|
||||
} else if strings.HasSuffix(trimmedHost, cfg.MainDomain) {
|
||||
log.Debug().Msg("subdomain request detected")
|
||||
handleSubDomain(log, ctx, giteaClient,
|
||||
mainDomainSuffix,
|
||||
cfg.MainDomain,
|
||||
cfg.PagesBranches,
|
||||
trimmedHost,
|
||||
pathElements,
|
||||
canonicalDomainCache)
|
||||
canonicalDomainCache, redirectsCache)
|
||||
} else {
|
||||
log.Debug().Msg("custom domain request detecded")
|
||||
log.Debug().Msg("custom domain request detected")
|
||||
handleCustomDomain(log, ctx, giteaClient,
|
||||
mainDomainSuffix,
|
||||
cfg.MainDomain,
|
||||
trimmedHost,
|
||||
pathElements,
|
||||
dnsLookupCache, canonicalDomainCache)
|
||||
cfg.PagesBranches[0],
|
||||
dnsLookupCache, canonicalDomainCache, redirectsCache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,11 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
|
|||
mainDomainSuffix string,
|
||||
trimmedHost string,
|
||||
pathElements []string,
|
||||
dnsLookupCache, canonicalDomainCache cache.SetGetKey,
|
||||
firstDefaultBranch string,
|
||||
dnsLookupCache, canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) {
|
||||
// Serve pages from custom domains
|
||||
targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, dnsLookupCache)
|
||||
targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
|
||||
if targetOwner == "" {
|
||||
html.ReturnErrorPage(ctx,
|
||||
"could not obtain repo owner from custom domain",
|
||||
|
@ -52,9 +53,9 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
|
|||
return
|
||||
} else if canonicalDomain != trimmedHost {
|
||||
// only redirect if the target is also a codeberg page!
|
||||
targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, dnsLookupCache)
|
||||
targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
|
||||
if targetOwner != "" {
|
||||
ctx.Redirect("https://"+canonicalDomain+targetOpt.TargetPath, http.StatusTemporaryRedirect)
|
||||
ctx.Redirect("https://"+canonicalDomain+"/"+targetOpt.TargetPath, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -63,7 +64,7 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
|
|||
}
|
||||
|
||||
log.Debug().Msg("tryBranch, now trying upstream 7")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -16,17 +16,21 @@ import (
|
|||
)
|
||||
|
||||
func handleRaw(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client,
|
||||
mainDomainSuffix, rawInfoPage string,
|
||||
mainDomainSuffix string,
|
||||
trimmedHost string,
|
||||
pathElements []string,
|
||||
canonicalDomainCache cache.SetGetKey,
|
||||
canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) {
|
||||
// Serve raw content from RawDomain
|
||||
log.Debug().Msg("raw domain")
|
||||
|
||||
if len(pathElements) < 2 {
|
||||
// https://{RawDomain}/{owner}/{repo}[/@{branch}]/{path} is required
|
||||
ctx.Redirect(rawInfoPage, http.StatusTemporaryRedirect)
|
||||
html.ReturnErrorPage(
|
||||
ctx,
|
||||
"a url in the form of <code>https://{domain}/{owner}/{repo}[/@{branch}]/{path}</code> is required",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -41,7 +45,7 @@ func handleRaw(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Clie
|
|||
TargetPath: path.Join(pathElements[3:]...),
|
||||
}, true); works {
|
||||
log.Trace().Msg("tryUpstream: serve raw domain with specified branch")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
log.Debug().Msg("missing branch info")
|
||||
|
@ -58,10 +62,10 @@ func handleRaw(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Clie
|
|||
TargetPath: path.Join(pathElements[2:]...),
|
||||
}, true); works {
|
||||
log.Trace().Msg("tryUpstream: serve raw domain with default branch")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
} else {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("raw domain could not find repo '%s/%s' or repo is empty", targetOpt.TargetOwner, targetOpt.TargetRepo),
|
||||
fmt.Sprintf("raw domain could not find repo <code>%s/%s</code> or repo is empty", targetOpt.TargetOwner, targetOpt.TargetRepo),
|
||||
http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"codeberg.org/codeberg/pages/html"
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
|
@ -17,9 +18,10 @@ import (
|
|||
|
||||
func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client,
|
||||
mainDomainSuffix string,
|
||||
defaultPagesBranches []string,
|
||||
trimmedHost string,
|
||||
pathElements []string,
|
||||
canonicalDomainCache cache.SetGetKey,
|
||||
canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) {
|
||||
// Serve pages from subdomains of MainDomainSuffix
|
||||
log.Debug().Msg("main domain suffix")
|
||||
|
@ -51,11 +53,13 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite
|
|||
TargetPath: path.Join(pathElements[2:]...),
|
||||
}, true); works {
|
||||
log.Trace().Msg("tryUpstream: serve with specified repo and branch")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
} else {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("explizite set branch %q do not exist at '%s/%s'", targetOpt.TargetBranch, targetOpt.TargetOwner, targetOpt.TargetRepo),
|
||||
http.StatusFailedDependency)
|
||||
html.ReturnErrorPage(
|
||||
ctx,
|
||||
formatSetBranchNotFoundMessage(pathElements[1][1:], targetOwner, pathElements[0]),
|
||||
http.StatusFailedDependency,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -63,38 +67,66 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite
|
|||
// Check if the first directory is a branch for the defaultPagesRepo
|
||||
// example.codeberg.page/@main/index.html
|
||||
if strings.HasPrefix(pathElements[0], "@") {
|
||||
targetBranch := pathElements[0][1:]
|
||||
|
||||
// if the default pages branch can be determined exactly, it does not need to be set
|
||||
if len(defaultPagesBranches) == 1 && slices.Contains(defaultPagesBranches, targetBranch) {
|
||||
// example.codeberg.org/@pages/... redirects to example.codeberg.org/...
|
||||
ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msg("main domain preparations, now trying with specified branch")
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
TryIndexPages: true,
|
||||
TargetOwner: targetOwner,
|
||||
TargetRepo: defaultPagesRepo,
|
||||
TargetBranch: pathElements[0][1:],
|
||||
TargetBranch: targetBranch,
|
||||
TargetPath: path.Join(pathElements[1:]...),
|
||||
}, true); works {
|
||||
log.Trace().Msg("tryUpstream: serve default pages repo with specified branch")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
} else {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("explizite set branch %q do not exist at '%s/%s'", targetOpt.TargetBranch, targetOpt.TargetOwner, targetOpt.TargetRepo),
|
||||
http.StatusFailedDependency)
|
||||
html.ReturnErrorPage(
|
||||
ctx,
|
||||
formatSetBranchNotFoundMessage(targetBranch, targetOwner, defaultPagesRepo),
|
||||
http.StatusFailedDependency,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the first directory is a repo with a defaultPagesRepo branch
|
||||
// example.codeberg.page/myrepo/index.html
|
||||
// example.codeberg.page/pages/... is not allowed here.
|
||||
log.Debug().Msg("main domain preparations, now trying with specified repo")
|
||||
if pathElements[0] != defaultPagesRepo {
|
||||
for _, defaultPagesBranch := range defaultPagesBranches {
|
||||
// Check if the first directory is a repo with a default pages branch
|
||||
// example.codeberg.page/myrepo/index.html
|
||||
// example.codeberg.page/{PAGES_BRANCHE}/... is not allowed here.
|
||||
log.Debug().Msg("main domain preparations, now trying with specified repo")
|
||||
if pathElements[0] != defaultPagesBranch {
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
TryIndexPages: true,
|
||||
TargetOwner: targetOwner,
|
||||
TargetRepo: pathElements[0],
|
||||
TargetBranch: defaultPagesBranch,
|
||||
TargetPath: path.Join(pathElements[1:]...),
|
||||
}, false); works {
|
||||
log.Debug().Msg("tryBranch, now trying upstream 5")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use the defaultPagesRepo on an default pages branch
|
||||
// example.codeberg.page/index.html
|
||||
log.Debug().Msg("main domain preparations, now trying with default repo")
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
TryIndexPages: true,
|
||||
TargetOwner: targetOwner,
|
||||
TargetRepo: pathElements[0],
|
||||
TargetRepo: defaultPagesRepo,
|
||||
TargetBranch: defaultPagesBranch,
|
||||
TargetPath: path.Join(pathElements[1:]...),
|
||||
TargetPath: path.Join(pathElements...),
|
||||
}, false); works {
|
||||
log.Debug().Msg("tryBranch, now trying upstream 5")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache)
|
||||
log.Debug().Msg("tryBranch, now trying upstream 6")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -109,12 +141,16 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite
|
|||
TargetPath: path.Join(pathElements...),
|
||||
}, false); works {
|
||||
log.Debug().Msg("tryBranch, now trying upstream 6")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
|
||||
// Couldn't find a valid repo/branch
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("could not find a valid repository[%s]", targetRepo),
|
||||
fmt.Sprintf("could not find a valid repository or branch for repository: <code>%s</code>", targetRepo),
|
||||
http.StatusNotFound)
|
||||
}
|
||||
|
||||
func formatSetBranchNotFoundMessage(branch, owner, repo string) string {
|
||||
return fmt.Sprintf("explicitly set branch <code>%q</code> does not exist at <code>%s/%s</code>", branch, owner, repo)
|
||||
}
|
||||
|
|
|
@ -1,30 +1,39 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"codeberg.org/codeberg/pages/config"
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"codeberg.org/codeberg/pages/server/gitea"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func TestHandlerPerformance(t *testing.T) {
|
||||
giteaClient, _ := gitea.NewClient("https://codeberg.org", "", cache.NewKeyValueCache(), false, false)
|
||||
testHandler := Handler(
|
||||
"codeberg.page", "raw.codeberg.org",
|
||||
giteaClient,
|
||||
"https://docs.codeberg.org/pages/raw-content/",
|
||||
[]string{"/.well-known/acme-challenge/"},
|
||||
[]string{"raw.codeberg.org", "fonts.codeberg.org", "design.codeberg.org"},
|
||||
cache.NewKeyValueCache(),
|
||||
cache.NewKeyValueCache(),
|
||||
)
|
||||
cfg := config.GiteaConfig{
|
||||
Root: "https://codeberg.org",
|
||||
Token: "",
|
||||
LFSEnabled: false,
|
||||
FollowSymlinks: false,
|
||||
}
|
||||
giteaClient, _ := gitea.NewClient(cfg, cache.NewInMemoryCache())
|
||||
serverCfg := config.ServerConfig{
|
||||
MainDomain: "codeberg.page",
|
||||
RawDomain: "raw.codeberg.page",
|
||||
BlacklistedPaths: []string{
|
||||
"/.well-known/acme-challenge/",
|
||||
},
|
||||
AllowedCorsDomains: []string{"raw.codeberg.org", "fonts.codeberg.org", "design.codeberg.org"},
|
||||
PagesBranches: []string{"pages"},
|
||||
}
|
||||
testHandler := Handler(serverCfg, giteaClient, cache.NewInMemoryCache(), cache.NewInMemoryCache(), cache.NewInMemoryCache())
|
||||
|
||||
testCase := func(uri string, status int) {
|
||||
t.Run(uri, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", uri, nil)
|
||||
req := httptest.NewRequest("GET", uri, http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
log.Printf("Start: %v\n", time.Now())
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
@ -17,10 +18,11 @@ import (
|
|||
func tryUpstream(ctx *context.Context, giteaClient *gitea.Client,
|
||||
mainDomainSuffix, trimmedHost string,
|
||||
options *upstream.Options,
|
||||
canonicalDomainCache cache.SetGetKey,
|
||||
canonicalDomainCache cache.ICache,
|
||||
redirectsCache cache.ICache,
|
||||
) {
|
||||
// check if a canonical domain exists on a request on MainDomain
|
||||
if strings.HasSuffix(trimmedHost, mainDomainSuffix) {
|
||||
if strings.HasSuffix(trimmedHost, mainDomainSuffix) && !options.ServeRaw {
|
||||
canonicalDomain, _ := options.CheckCanonicalDomain(giteaClient, "", mainDomainSuffix, canonicalDomainCache)
|
||||
if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix) {
|
||||
canonicalPath := ctx.Req.RequestURI
|
||||
|
@ -39,8 +41,8 @@ func tryUpstream(ctx *context.Context, giteaClient *gitea.Client,
|
|||
options.Host = trimmedHost
|
||||
|
||||
// Try to request the file from the Gitea API
|
||||
if !options.Upstream(ctx, giteaClient) {
|
||||
html.ReturnErrorPage(ctx, "", ctx.StatusCode)
|
||||
if !options.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
html.ReturnErrorPage(ctx, fmt.Sprintf("Forge returned %d %s", ctx.StatusCode, http.StatusText(ctx.StatusCode)), ctx.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func StartProfilingServer(listeningAddress string) {
|
||||
server := &http.Server{
|
||||
Addr: listeningAddress,
|
||||
Handler: http.DefaultServeMux,
|
||||
}
|
||||
|
||||
log.Info().Msgf("Starting debug server on %s", listeningAddress)
|
||||
|
||||
go func() {
|
||||
log.Fatal().Err(server.ListenAndServe()).Msg("Failed to start debug server")
|
||||
}()
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"codeberg.org/codeberg/pages/server/context"
|
||||
"codeberg.org/codeberg/pages/server/utils"
|
||||
)
|
||||
|
||||
func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey) http.HandlerFunc {
|
||||
challengePath := "/.well-known/acme-challenge/"
|
||||
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := context.New(w, req)
|
||||
if strings.HasPrefix(ctx.Path(), challengePath) {
|
||||
challenge, ok := challengeCache.Get(utils.TrimHostPort(ctx.Host()) + "/" + strings.TrimPrefix(ctx.Path(), challengePath))
|
||||
if !ok || challenge == nil {
|
||||
ctx.String("no challenge for this token", http.StatusNotFound)
|
||||
}
|
||||
ctx.String(challenge.(string))
|
||||
} else {
|
||||
ctx.Redirect("https://"+ctx.Host()+ctx.Path(), http.StatusMovedPermanently)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
cmd "codeberg.org/codeberg/pages/cli"
|
||||
"codeberg.org/codeberg/pages/config"
|
||||
"codeberg.org/codeberg/pages/server/acme"
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"codeberg.org/codeberg/pages/server/certificates"
|
||||
"codeberg.org/codeberg/pages/server/gitea"
|
||||
"codeberg.org/codeberg/pages/server/handler"
|
||||
)
|
||||
|
||||
// Serve sets up and starts the web server.
|
||||
func Serve(ctx *cli.Context) error {
|
||||
// initialize logger with Trace, overridden later with actual level
|
||||
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger().Level(zerolog.TraceLevel)
|
||||
|
||||
cfg, err := config.ReadConfig(ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("could not read config")
|
||||
}
|
||||
|
||||
config.MergeConfig(ctx, cfg)
|
||||
|
||||
// Initialize the logger.
|
||||
logLevel, err := zerolog.ParseLevel(cfg.LogLevel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger().Level(logLevel)
|
||||
|
||||
listeningSSLAddress := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
listeningHTTPAddress := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.HttpPort)
|
||||
|
||||
if cfg.Server.RawDomain != "" {
|
||||
cfg.Server.AllowedCorsDomains = append(cfg.Server.AllowedCorsDomains, cfg.Server.RawDomain)
|
||||
}
|
||||
|
||||
// Make sure MainDomain has a leading dot
|
||||
if !strings.HasPrefix(cfg.Server.MainDomain, ".") {
|
||||
// TODO make this better
|
||||
cfg.Server.MainDomain = "." + cfg.Server.MainDomain
|
||||
}
|
||||
|
||||
if len(cfg.Server.PagesBranches) == 0 {
|
||||
return fmt.Errorf("no default branches set (PAGES_BRANCHES)")
|
||||
}
|
||||
|
||||
// Init ssl cert database
|
||||
certDB, closeFn, err := cmd.OpenCertDB(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeFn()
|
||||
|
||||
keyCache := cache.NewInMemoryCache()
|
||||
challengeCache := cache.NewInMemoryCache()
|
||||
// canonicalDomainCache stores canonical domains
|
||||
canonicalDomainCache := cache.NewInMemoryCache()
|
||||
// dnsLookupCache stores DNS lookups for custom domains
|
||||
dnsLookupCache := cache.NewInMemoryCache()
|
||||
// redirectsCache stores redirects in _redirects files
|
||||
redirectsCache := cache.NewInMemoryCache()
|
||||
// clientResponseCache stores responses from the Gitea server
|
||||
clientResponseCache := cache.NewInMemoryCache()
|
||||
|
||||
giteaClient, err := gitea.NewClient(cfg.Gitea, clientResponseCache)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create new gitea client: %v", err)
|
||||
}
|
||||
|
||||
acmeClient, err := acme.CreateAcmeClient(cfg.ACME, cfg.Server.HttpServerEnabled, challengeCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := certificates.SetupMainDomainCertificates(cfg.Server.MainDomain, acmeClient, certDB); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create listener for SSL connections
|
||||
log.Info().Msgf("Create TCP listener for SSL on %s", listeningSSLAddress)
|
||||
listener, err := net.Listen("tcp", listeningSSLAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't create listener: %v", err)
|
||||
}
|
||||
|
||||
// Setup listener for SSL connections
|
||||
listener = tls.NewListener(listener, certificates.TLSConfig(
|
||||
cfg.Server.MainDomain,
|
||||
giteaClient,
|
||||
acmeClient,
|
||||
cfg.Server.PagesBranches[0],
|
||||
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache,
|
||||
certDB,
|
||||
cfg.ACME.NoDNS01,
|
||||
cfg.Server.RawDomain,
|
||||
))
|
||||
|
||||
interval := 12 * time.Hour
|
||||
certMaintainCtx, cancelCertMaintain := context.WithCancel(context.Background())
|
||||
defer cancelCertMaintain()
|
||||
go certificates.MaintainCertDB(certMaintainCtx, interval, acmeClient, cfg.Server.MainDomain, certDB)
|
||||
|
||||
if cfg.Server.HttpServerEnabled {
|
||||
// Create handler for http->https redirect and http acme challenges
|
||||
httpHandler := certificates.SetupHTTPACMEChallengeServer(challengeCache, uint(cfg.Server.Port))
|
||||
|
||||
// Create listener for http and start listening
|
||||
go func() {
|
||||
log.Info().Msgf("Start HTTP server listening on %s", listeningHTTPAddress)
|
||||
err := http.ListenAndServe(listeningHTTPAddress, httpHandler)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Couldn't start HTTP server")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if ctx.IsSet("enable-profiling") {
|
||||
StartProfilingServer(ctx.String("profiling-address"))
|
||||
}
|
||||
|
||||
// Create ssl handler based on settings
|
||||
sslHandler := handler.Handler(cfg.Server, giteaClient, dnsLookupCache, canonicalDomainCache, redirectsCache)
|
||||
|
||||
// Start the ssl listener
|
||||
log.Info().Msgf("Start SSL server using TCP listener on %s", listener.Addr())
|
||||
|
||||
return http.Serve(listener, sslHandler)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -16,45 +17,54 @@ var canonicalDomainCacheTimeout = 15 * time.Minute
|
|||
const canonicalDomainConfig = ".domains"
|
||||
|
||||
// CheckCanonicalDomain returns the canonical domain specified in the repo (using the `.domains` file).
|
||||
func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.SetGetKey) (string, bool) {
|
||||
var (
|
||||
domains []string
|
||||
valid bool
|
||||
)
|
||||
func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.ICache) (domain string, valid bool) {
|
||||
// Check if this request is cached.
|
||||
if cachedValue, ok := canonicalDomainCache.Get(o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch); ok {
|
||||
domains = cachedValue.([]string)
|
||||
domains := cachedValue.([]string)
|
||||
for _, domain := range domains {
|
||||
if domain == actualDomain {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig)
|
||||
if err == nil {
|
||||
for _, domain := range strings.Split(string(body), "\n") {
|
||||
domain = strings.ToLower(domain)
|
||||
domain = strings.TrimSpace(domain)
|
||||
domain = strings.TrimPrefix(domain, "http://")
|
||||
domain = strings.TrimPrefix(domain, "https://")
|
||||
if len(domain) > 0 && !strings.HasPrefix(domain, "#") && !strings.ContainsAny(domain, "\t /") && strings.ContainsRune(domain, '.') {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
if domain == actualDomain {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Info().Err(err).Msgf("could not read %s of %s/%s", canonicalDomainConfig, o.TargetOwner, o.TargetRepo)
|
||||
return domains[0], valid
|
||||
}
|
||||
|
||||
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig)
|
||||
if err != nil && !errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Error().Err(err).Msgf("could not read %s of %s/%s", canonicalDomainConfig, o.TargetOwner, o.TargetRepo)
|
||||
}
|
||||
|
||||
var domains []string
|
||||
for _, domain := range strings.Split(string(body), "\n") {
|
||||
domain = strings.ToLower(domain)
|
||||
domain = strings.TrimSpace(domain)
|
||||
domain = strings.TrimPrefix(domain, "http://")
|
||||
domain = strings.TrimPrefix(domain, "https://")
|
||||
if domain != "" && !strings.HasPrefix(domain, "#") && !strings.ContainsAny(domain, "\t /") && strings.ContainsRune(domain, '.') {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
domains = append(domains, o.TargetOwner+mainDomainSuffix)
|
||||
if domains[len(domains)-1] == actualDomain {
|
||||
if domain == actualDomain {
|
||||
valid = true
|
||||
}
|
||||
if o.TargetRepo != "" && o.TargetRepo != "pages" {
|
||||
domains[len(domains)-1] += "/" + o.TargetRepo
|
||||
}
|
||||
_ = canonicalDomainCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, domains, canonicalDomainCacheTimeout)
|
||||
}
|
||||
|
||||
// Add [owner].[pages-domain] as valid domain.
|
||||
domains = append(domains, o.TargetOwner+mainDomainSuffix)
|
||||
if domains[len(domains)-1] == actualDomain {
|
||||
valid = true
|
||||
}
|
||||
|
||||
// If the target repository isn't called pages, add `/[repository]` to the
|
||||
// previous valid domain.
|
||||
if o.TargetRepo != "" && o.TargetRepo != "pages" {
|
||||
domains[len(domains)-1] += "/" + o.TargetRepo
|
||||
}
|
||||
|
||||
// Add result to cache.
|
||||
_ = canonicalDomainCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, domains, canonicalDomainCacheTimeout)
|
||||
|
||||
// Return the first domain from the list and return if any of the domains
|
||||
// matched the requested domain.
|
||||
return domains[0], valid
|
||||
}
|
||||
|
|
|
@ -17,17 +17,17 @@ func (o *Options) GetBranchTimestamp(giteaClient *gitea.Client) (bool, error) {
|
|||
// Get default branch
|
||||
defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(o.TargetOwner, o.TargetRepo)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Could't fetch default branch from repository")
|
||||
log.Err(err).Msg("Couldn't fetch default branch from repository")
|
||||
return false, err
|
||||
}
|
||||
log.Debug().Msgf("Succesfully fetched default branch %q from Gitea", defaultBranch)
|
||||
log.Debug().Msgf("Successfully fetched default branch %q from Gitea", defaultBranch)
|
||||
o.TargetBranch = defaultBranch
|
||||
}
|
||||
|
||||
timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(o.TargetOwner, o.TargetRepo, o.TargetBranch)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Error().Err(err).Msg("Could not get latest commit's timestamp from branch")
|
||||
log.Error().Err(err).Msg("Could not get latest commit timestamp from branch")
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ func (o *Options) GetBranchTimestamp(giteaClient *gitea.Client) (bool, error) {
|
|||
return false, fmt.Errorf("empty response")
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Succesfully fetched latest commit's timestamp from branch: %#v", timestamp)
|
||||
log.Debug().Msgf("Successfully fetched latest commit timestamp from branch: %#v", timestamp)
|
||||
o.BranchTimestamp = timestamp.Timestamp
|
||||
o.TargetBranch = timestamp.Branch
|
||||
return true, nil
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"codeberg.org/codeberg/pages/server/context"
|
||||
"codeberg.org/codeberg/pages/server/gitea"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Redirect struct {
|
||||
From string
|
||||
To string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
// rewriteURL returns the destination URL and true if r matches reqURL.
|
||||
func (r *Redirect) rewriteURL(reqURL string) (dstURL string, ok bool) {
|
||||
// check if from url matches request url
|
||||
if strings.TrimSuffix(r.From, "/") == strings.TrimSuffix(reqURL, "/") {
|
||||
return r.To, true
|
||||
}
|
||||
// handle wildcard redirects
|
||||
if strings.HasSuffix(r.From, "/*") {
|
||||
trimmedFromURL := strings.TrimSuffix(r.From, "/*")
|
||||
if reqURL == trimmedFromURL || strings.HasPrefix(reqURL, trimmedFromURL+"/") {
|
||||
if strings.Contains(r.To, ":splat") {
|
||||
matched := strings.TrimPrefix(reqURL, trimmedFromURL)
|
||||
matched = strings.TrimPrefix(matched, "/")
|
||||
return strings.ReplaceAll(r.To, ":splat", matched), true
|
||||
}
|
||||
return r.To, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// redirectsCacheTimeout specifies the timeout for the redirects cache.
|
||||
var redirectsCacheTimeout = 10 * time.Minute
|
||||
|
||||
const redirectsConfig = "_redirects"
|
||||
|
||||
// getRedirects returns redirects specified in the _redirects file.
|
||||
func (o *Options) getRedirects(giteaClient *gitea.Client, redirectsCache cache.ICache) []Redirect {
|
||||
var redirects []Redirect
|
||||
cacheKey := o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch
|
||||
|
||||
// Check for cached redirects
|
||||
if cachedValue, ok := redirectsCache.Get(cacheKey); ok {
|
||||
redirects = cachedValue.([]Redirect)
|
||||
} else {
|
||||
// Get _redirects file and parse
|
||||
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, redirectsConfig)
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(string(body), "\n") {
|
||||
redirectArr := strings.Fields(line)
|
||||
|
||||
// Ignore comments and invalid lines
|
||||
if strings.HasPrefix(line, "#") || len(redirectArr) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get redirect status code
|
||||
statusCode := 301
|
||||
if len(redirectArr) == 3 {
|
||||
statusCode, err = strconv.Atoi(redirectArr[2])
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msgf("could not read %s of %s/%s", redirectsConfig, o.TargetOwner, o.TargetRepo)
|
||||
}
|
||||
}
|
||||
|
||||
redirects = append(redirects, Redirect{
|
||||
From: redirectArr[0],
|
||||
To: redirectArr[1],
|
||||
StatusCode: statusCode,
|
||||
})
|
||||
}
|
||||
}
|
||||
_ = redirectsCache.Set(cacheKey, redirects, redirectsCacheTimeout)
|
||||
}
|
||||
return redirects
|
||||
}
|
||||
|
||||
func (o *Options) matchRedirects(ctx *context.Context, giteaClient *gitea.Client, redirects []Redirect, redirectsCache cache.ICache) (final bool) {
|
||||
reqURL := ctx.Req.RequestURI
|
||||
// remove repo and branch from request url
|
||||
reqURL = strings.TrimPrefix(reqURL, "/"+o.TargetRepo)
|
||||
reqURL = strings.TrimPrefix(reqURL, "/@"+o.TargetBranch)
|
||||
|
||||
for _, redirect := range redirects {
|
||||
if dstURL, ok := redirect.rewriteURL(reqURL); ok {
|
||||
// do rewrite if status code is 200
|
||||
if redirect.StatusCode == 200 {
|
||||
o.TargetPath = dstURL
|
||||
o.Upstream(ctx, giteaClient, redirectsCache)
|
||||
} else {
|
||||
ctx.Redirect(dstURL, redirect.StatusCode)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedirect_rewriteURL(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
redirect Redirect
|
||||
reqURL string
|
||||
wantDstURL string
|
||||
wantOk bool
|
||||
}{
|
||||
{Redirect{"/", "/dst", 200}, "/", "/dst", true},
|
||||
{Redirect{"/", "/dst", 200}, "/foo", "", false},
|
||||
{Redirect{"/src", "/dst", 200}, "/src", "/dst", true},
|
||||
{Redirect{"/src", "/dst", 200}, "/foo", "", false},
|
||||
{Redirect{"/src", "/dst", 200}, "/src/foo", "", false},
|
||||
{Redirect{"/*", "/dst", 200}, "/", "/dst", true},
|
||||
{Redirect{"/*", "/dst", 200}, "/src", "/dst", true},
|
||||
{Redirect{"/src/*", "/dst/:splat", 200}, "/src", "/dst/", true},
|
||||
{Redirect{"/src/*", "/dst/:splat", 200}, "/src/", "/dst/", true},
|
||||
{Redirect{"/src/*", "/dst/:splat", 200}, "/src/foo", "/dst/foo", true},
|
||||
{Redirect{"/src/*", "/dst/:splat", 200}, "/src/foo/bar", "/dst/foo/bar", true},
|
||||
{Redirect{"/src/*", "/dst/:splatsuffix", 200}, "/src/foo", "/dst/foosuffix", true},
|
||||
{Redirect{"/src/*", "/dst:splat", 200}, "/src/foo", "/dstfoo", true},
|
||||
{Redirect{"/src/*", "/dst", 200}, "/srcfoo", "", false},
|
||||
// This is the example from FEATURES.md:
|
||||
{Redirect{"/articles/*", "/posts/:splat", 302}, "/articles/2022/10/12/post-1/", "/posts/2022/10/12/post-1/", true},
|
||||
} {
|
||||
if dstURL, ok := tc.redirect.rewriteURL(tc.reqURL); dstURL != tc.wantDstURL || ok != tc.wantOk {
|
||||
t.Errorf("%#v.rewriteURL(%q) = %q, %v; want %q, %v",
|
||||
tc.redirect, tc.reqURL, dstURL, ok, tc.wantDstURL, tc.wantOk)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
|
||||
"codeberg.org/codeberg/pages/html"
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"codeberg.org/codeberg/pages/server/context"
|
||||
"codeberg.org/codeberg/pages/server/gitea"
|
||||
)
|
||||
|
@ -52,11 +53,13 @@ type Options struct {
|
|||
}
|
||||
|
||||
// Upstream requests a file from the Gitea API at GiteaRoot and writes it to the request context.
|
||||
func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (final bool) {
|
||||
func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client, redirectsCache cache.ICache) bool {
|
||||
log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger()
|
||||
|
||||
log.Debug().Msg("Start")
|
||||
|
||||
if o.TargetOwner == "" || o.TargetRepo == "" {
|
||||
html.ReturnErrorPage(ctx, "either repo owner or name info is missing", http.StatusBadRequest)
|
||||
html.ReturnErrorPage(ctx, "forge client: either repo owner or name info is missing", http.StatusBadRequest)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -66,7 +69,7 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (fin
|
|||
// handle 404
|
||||
if err != nil && errors.Is(err, gitea.ErrorNotFound) || !branchExist {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("branch %q for '%s/%s' not found", o.TargetBranch, o.TargetOwner, o.TargetRepo),
|
||||
fmt.Sprintf("branch <code>%q</code> for <code>%s/%s</code> not found", o.TargetBranch, o.TargetOwner, o.TargetRepo),
|
||||
http.StatusNotFound)
|
||||
return true
|
||||
}
|
||||
|
@ -74,7 +77,7 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (fin
|
|||
// handle unexpected errors
|
||||
if err != nil {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("could not get timestamp of branch %q: %v", o.TargetBranch, err),
|
||||
fmt.Sprintf("could not get timestamp of branch <code>%q</code>: '%v'", o.TargetBranch, err),
|
||||
http.StatusFailedDependency)
|
||||
return true
|
||||
}
|
||||
|
@ -103,39 +106,54 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (fin
|
|||
|
||||
// Handle not found error
|
||||
if err != nil && errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Debug().Msg("Handling not found error")
|
||||
// Get and match redirects
|
||||
redirects := o.getRedirects(giteaClient, redirectsCache)
|
||||
if o.matchRedirects(ctx, giteaClient, redirects, redirectsCache) {
|
||||
log.Trace().Msg("redirect")
|
||||
return true
|
||||
}
|
||||
|
||||
if o.TryIndexPages {
|
||||
log.Trace().Msg("try index page")
|
||||
// copy the o struct & try if an index page exists
|
||||
optionsForIndexPages := *o
|
||||
optionsForIndexPages.TryIndexPages = false
|
||||
optionsForIndexPages.appendTrailingSlash = true
|
||||
for _, indexPage := range upstreamIndexPages {
|
||||
optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient) {
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("try html file with path name")
|
||||
// compatibility fix for GitHub Pages (/example → /example.html)
|
||||
optionsForIndexPages.appendTrailingSlash = false
|
||||
optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Path(), "/") + ".html"
|
||||
optionsForIndexPages.TargetPath = o.TargetPath + ".html"
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient) {
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace().Msg("not found")
|
||||
|
||||
ctx.StatusCode = http.StatusNotFound
|
||||
if o.TryIndexPages {
|
||||
log.Trace().Msg("try not found page")
|
||||
// copy the o struct & try if a not found page exists
|
||||
optionsForNotFoundPages := *o
|
||||
optionsForNotFoundPages.TryIndexPages = false
|
||||
optionsForNotFoundPages.appendTrailingSlash = false
|
||||
for _, notFoundPage := range upstreamNotFoundPages {
|
||||
optionsForNotFoundPages.TargetPath = "/" + notFoundPage
|
||||
if optionsForNotFoundPages.Upstream(ctx, giteaClient) {
|
||||
if optionsForNotFoundPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("not found page missing")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -145,16 +163,16 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (fin
|
|||
var msg string
|
||||
|
||||
if err != nil {
|
||||
msg = "gitea client returned unexpected error"
|
||||
msg = "forge client: returned unexpected error"
|
||||
log.Error().Err(err).Msg(msg)
|
||||
msg = fmt.Sprintf("%s: %v", msg, err)
|
||||
msg = fmt.Sprintf("%s: '%v'", msg, err)
|
||||
}
|
||||
if reader == nil {
|
||||
msg = "gitea client returned no reader"
|
||||
msg = "forge client: returned no reader"
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
msg = fmt.Sprintf("Couldn't fetch contents (status code %d)", statusCode)
|
||||
msg = fmt.Sprintf("forge client: couldn't fetch contents: <code>%d - %s</code>", statusCode, http.StatusText(statusCode))
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
|
||||
|
@ -165,10 +183,12 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (fin
|
|||
// Append trailing slash if missing (for index files), and redirect to fix filenames in general
|
||||
// o.appendTrailingSlash is only true when looking for index pages
|
||||
if o.appendTrailingSlash && !strings.HasSuffix(ctx.Path(), "/") {
|
||||
log.Trace().Msg("append trailing slash and redirect")
|
||||
ctx.Redirect(ctx.Path()+"/", http.StatusTemporaryRedirect)
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(ctx.Path(), "/index.html") {
|
||||
if strings.HasSuffix(ctx.Path(), "/index.html") && !o.ServeRaw {
|
||||
log.Trace().Msg("remove index.html from path and redirect")
|
||||
ctx.Redirect(strings.TrimSuffix(ctx.Path(), "index.html"), http.StatusTemporaryRedirect)
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -11,3 +13,15 @@ func TrimHostPort(host string) string {
|
|||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func CleanPath(uriPath string) string {
|
||||
unescapedPath, _ := url.PathUnescape(uriPath)
|
||||
cleanedPath := path.Join("/", unescapedPath)
|
||||
|
||||
// If the path refers to a directory, add a trailing slash.
|
||||
if !strings.HasSuffix(cleanedPath, "/") && (strings.HasSuffix(unescapedPath, "/") || strings.HasSuffix(unescapedPath, "/.") || strings.HasSuffix(unescapedPath, "/..")) {
|
||||
cleanedPath += "/"
|
||||
}
|
||||
|
||||
return cleanedPath
|
||||
}
|
||||
|
|
|
@ -11,3 +11,59 @@ func TestTrimHostPort(t *testing.T) {
|
|||
assert.EqualValues(t, "", TrimHostPort(":"))
|
||||
assert.EqualValues(t, "example.com", TrimHostPort("example.com:80"))
|
||||
}
|
||||
|
||||
// TestCleanPath is mostly copied from fasthttp, to keep the behaviour we had before migrating away from it.
|
||||
// Source (MIT licensed): https://github.com/valyala/fasthttp/blob/v1.48.0/uri_test.go#L154
|
||||
// Copyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors
|
||||
func TestCleanPath(t *testing.T) {
|
||||
// double slash
|
||||
testURIPathNormalize(t, "/aa//bb", "/aa/bb")
|
||||
|
||||
// triple slash
|
||||
testURIPathNormalize(t, "/x///y/", "/x/y/")
|
||||
|
||||
// multi slashes
|
||||
testURIPathNormalize(t, "/abc//de///fg////", "/abc/de/fg/")
|
||||
|
||||
// encoded slashes
|
||||
testURIPathNormalize(t, "/xxxx%2fyyy%2f%2F%2F", "/xxxx/yyy/")
|
||||
|
||||
// dotdot
|
||||
testURIPathNormalize(t, "/aaa/..", "/")
|
||||
|
||||
// dotdot with trailing slash
|
||||
testURIPathNormalize(t, "/xxx/yyy/../", "/xxx/")
|
||||
|
||||
// multi dotdots
|
||||
testURIPathNormalize(t, "/aaa/bbb/ccc/../../ddd", "/aaa/ddd")
|
||||
|
||||
// dotdots separated by other data
|
||||
testURIPathNormalize(t, "/a/b/../c/d/../e/..", "/a/c/")
|
||||
|
||||
// too many dotdots
|
||||
testURIPathNormalize(t, "/aaa/../../../../xxx", "/xxx")
|
||||
testURIPathNormalize(t, "/../../../../../..", "/")
|
||||
testURIPathNormalize(t, "/../../../../../../", "/")
|
||||
|
||||
// encoded dotdots
|
||||
testURIPathNormalize(t, "/aaa%2Fbbb%2F%2E.%2Fxxx", "/aaa/xxx")
|
||||
|
||||
// double slash with dotdots
|
||||
testURIPathNormalize(t, "/aaa////..//b", "/b")
|
||||
|
||||
// fake dotdot
|
||||
testURIPathNormalize(t, "/aaa/..bbb/ccc/..", "/aaa/..bbb/")
|
||||
|
||||
// single dot
|
||||
testURIPathNormalize(t, "/a/./b/././c/./d.html", "/a/b/c/d.html")
|
||||
testURIPathNormalize(t, "./foo/", "/foo/")
|
||||
testURIPathNormalize(t, "./../.././../../aaa/bbb/../../../././../", "/")
|
||||
testURIPathNormalize(t, "./a/./.././../b/./foo.html", "/b/foo.html")
|
||||
}
|
||||
|
||||
func testURIPathNormalize(t *testing.T, requestURI, expectedPath string) {
|
||||
cleanedPath := CleanPath(requestURI)
|
||||
if cleanedPath != expectedPath {
|
||||
t.Fatalf("Unexpected path %q. Expected %q. requestURI=%q", cleanedPath, expectedPath, requestURI)
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue