mirror of
https://codeberg.org/Codeberg/pages-server
synced 2024-09-19 23:28:30 +01:00
Compare commits
38 Commits
ac1b9520a5
...
e82f48b636
Author | SHA1 | Date | |
---|---|---|---|
|
e82f48b636 | ||
|
c043a887c2 | ||
|
adcd973ade | ||
|
2c1701a657 | ||
|
e61ab7075b | ||
|
4543033874 | ||
|
05413e50c6 | ||
|
3928cd7aff | ||
|
74cdf78044 | ||
|
2c41e11f2f | ||
|
b9a9467dba | ||
|
77a8439ea7 | ||
|
eea009c7fe | ||
|
885cfac2ec | ||
|
69361c69c1 | ||
|
b54cd38d0b | ||
|
c1df2f068b | ||
|
d74f1fe8a4 | ||
|
adf13bfdbc | ||
|
7c49c4b967 | ||
|
eb08c46dcd | ||
|
56d44609ea | ||
|
ca9433e0ea | ||
|
d09c6e1218 | ||
|
8cba7f9c8a | ||
|
f407fd3ae4 | ||
|
89800d4f36 | ||
|
418afb7357 | ||
|
e45a354eef | ||
|
1a332c1d54 | ||
|
c14c5474b6 | ||
|
7092883ebe | ||
|
019e85a0d0 | ||
|
69fb22a9e7 | ||
|
a986a52755 | ||
|
9ffdc9d4f9 | ||
|
03881382a4 | ||
|
dd6d8bd60f |
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf"
|
||||
}
|
114
.woodpecker.yml
114
.woodpecker.yml
@ -1,114 +0,0 @@
|
||||
when:
|
||||
branch: main
|
||||
|
||||
steps:
|
||||
# use vendor to cache dependencies
|
||||
vendor:
|
||||
image: golang:1.21
|
||||
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" ]
|
149
.woodpecker/build.yml
Normal file
149
.woodpecker/build.yml
Normal file
@ -0,0 +1,149 @@
|
||||
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:4.0.0
|
||||
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:4.0.0
|
||||
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}
|
||||
|
||||
'Publish PR image':
|
||||
image: woodpeckerci/plugin-docker-buildx:3.2.1
|
||||
depends_on: test
|
||||
settings:
|
||||
registry: codeberg.org
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64
|
||||
repo: codeberg.org/codeberg/pages-server
|
||||
tags: next
|
||||
username:
|
||||
from_secret: bot_user
|
||||
password:
|
||||
from_secret: bot_token
|
||||
when:
|
||||
evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_image"'
|
||||
event: pull_request
|
||||
|
||||
docker-tag:
|
||||
depends_on: vendor
|
||||
image: woodpeckerci/plugin-docker-buildx:4.0.0
|
||||
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}
|
44
.woodpecker/lint.yml
Normal file
44
.woodpecker/lint.yml
Normal file
@ -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.59.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.2
|
||||
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
|
19
.yamllint.yaml
Normal file
19
.yamllint.yaml
Normal file
@ -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,14 +1,36 @@
|
||||
FROM techknowlogick/xgo 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
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=1 go build -tags 'sqlite sqlite_unlock_notify netgo' -ldflags '-s -w -extldflags "-static" -linkmode external' .
|
||||
# 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 .
|
||||
RUN mv -vf /build/pages-* /go/src/codeberg.org/codeberg/pages/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 /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"]
|
||||
|
14
FEATURES.md
14
FEATURES.md
@ -19,16 +19,16 @@ Redirects can be created with a `_redirects` file with the following format:
|
||||
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)
|
||||
- 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)
|
||||
- `200` - returns content from specified path (no external URLs) without changing the URL (rewrite)
|
||||
- `301` - Moved Permanently (Permanent redirect)
|
||||
- `302` - Found (Temporary redirect)
|
||||
|
||||
### Examples
|
||||
|
||||
|
4
Justfile
4
Justfile
@ -1,13 +1,13 @@
|
||||
CGO_FLAGS := '-extldflags "-static" -linkmode external'
|
||||
TAGS := 'sqlite sqlite_unlock_notify netgo'
|
||||
|
||||
dev:
|
||||
dev *FLAGS:
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
set -a # automatically export all variables
|
||||
source .env-dev
|
||||
set +a
|
||||
go run -tags '{{TAGS}}' .
|
||||
go run -tags '{{TAGS}}' . {{FLAGS}}
|
||||
|
||||
build:
|
||||
CGO_ENABLED=1 go build -tags '{{TAGS}}' -ldflags '-s -w {{CGO_FLAGS}}' -v -o build/codeberg-pages-server ./
|
||||
|
75
README.md
75
README.md
@ -3,7 +3,7 @@
|
||||
[![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">
|
||||
<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.
|
||||
@ -21,19 +21,19 @@ and the [Codeberg Documentation](https://docs.codeberg.org/codeberg-pages/).
|
||||
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.
|
||||
|
||||
@ -43,7 +43,8 @@ Certificates are generated, updated and cleaned up automatically via Let's Encry
|
||||
|
||||
## 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.
|
||||
@ -63,24 +64,25 @@ but forward the requests on the IP level to the Pages Server.
|
||||
You can check out a proof of concept in the `examples/haproxy-sni` folder,
|
||||
and especially have a look at [this section of the haproxy.cfg](https://codeberg.org/Codeberg/pages-server/src/branch/main/examples/haproxy-sni/haproxy.cfg#L38).
|
||||
|
||||
If you want to test a change, you can open a PR and ask for the label `build_pr_image` to be added.
|
||||
This will trigger a build of the PR which will build a docker image to be used for testing.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `ACME_ACCEPT_TERMS` (default: use self-signed certificate): Set this to "true" to accept the Terms of Service of your ACME provider.
|
||||
- `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_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_USE_RATE_LIMITS` (default: true): Set this to false to disable rate limits, e.g. with ZeroSSL.
|
||||
- `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.
|
||||
- `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.
|
||||
- `GITEA_API_TOKEN` (default: empty): API token for the Gitea instance to access non-public (e.g. limited) repos.
|
||||
- `GITEA_ROOT` (default: `https://codeberg.org`): root of the upstream Gitea instance.
|
||||
- `HOST` & `PORT` (default: `[::]` & `443`): listen address.
|
||||
- `LOG_LEVEL` (default: warn): Set this to specify the level of logging.
|
||||
- `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.
|
||||
- `PAGES_DOMAIN` (default: `codeberg.page`): main domain for pages.
|
||||
- `RAW_DOMAIN` (default: `raw.codeberg.page`): domain for raw resources (must be subdomain of `PAGES_DOMAIN`).
|
||||
- `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).
|
||||
ZeroSSL might be better in the future as it doesn't have rate limits and doesn't clash with the official Codeberg certificates (which are using Let's Encrypt), but I couldn't get it to work yet.
|
||||
- `ACME_EMAIL` (default: `noreply@example.email`): Set the email sent to the ACME API server to receive, for example, renewal reminders.
|
||||
- `ACME_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.
|
||||
- `LOG_LEVEL` (default: warn): Set this to specify the level of logging.
|
||||
|
||||
## Contributing to the development
|
||||
|
||||
@ -102,7 +104,7 @@ Previous maintainers:
|
||||
|
||||
### First steps
|
||||
|
||||
The code of this repository is split in several modules.
|
||||
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.
|
||||
@ -115,10 +117,25 @@ Thank you very much.
|
||||
|
||||
Make sure you have [golang](https://go.dev) v1.21 or newer and [just](https://just.systems/man/en/) installed.
|
||||
|
||||
run `just dev`
|
||||
now this pages should work:
|
||||
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>
|
||||
|
47
cli/flags.go
47
cli/flags.go
@ -22,29 +22,31 @@ var (
|
||||
|
||||
ServerFlags = append(CertStorageFlags, []cli.Flag{
|
||||
// #############
|
||||
// ### Gitea ###
|
||||
// ### Forge ###
|
||||
// #############
|
||||
// GiteaRoot specifies the root URL of the Gitea instance, without a trailing slash.
|
||||
// ForgeRoot specifies the root URL of the Forge 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"},
|
||||
Name: "forge-root",
|
||||
Aliases: []string{"gitea-root"},
|
||||
Usage: "specifies the root URL of the Forgejo/Gitea instance, without a trailing slash.",
|
||||
EnvVars: []string{"FORGE_ROOT", "GITEA_ROOT"},
|
||||
},
|
||||
// GiteaApiToken specifies an api token for the Gitea instance
|
||||
// ForgeApiToken specifies an api token for the Forge instance
|
||||
&cli.StringFlag{
|
||||
Name: "gitea-api-token",
|
||||
Usage: "specifies an api token for the Gitea instance",
|
||||
EnvVars: []string{"GITEA_API_TOKEN"},
|
||||
Name: "forge-api-token",
|
||||
Aliases: []string{"gitea-api-token"},
|
||||
Usage: "specifies an api token for the Forgejo/Gitea instance",
|
||||
EnvVars: []string{"FORGE_API_TOKEN", "GITEA_API_TOKEN"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-lfs-support",
|
||||
Usage: "enable lfs support, require gitea >= v1.17.0 as backend",
|
||||
Usage: "enable lfs support, gitea must be version v1.17.0 or higher",
|
||||
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",
|
||||
Usage: "follow symlinks if enabled, gitea must be version v1.18.0 or higher",
|
||||
EnvVars: []string{"ENABLE_SYMLINK_SUPPORT"},
|
||||
Value: false,
|
||||
},
|
||||
@ -79,6 +81,12 @@ var (
|
||||
Usage: "specifies the domain from which raw repository content shall be served, not set disable raw content hosting",
|
||||
EnvVars: []string{"RAW_DOMAIN"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "canonical-domain-file",
|
||||
Usage: "specifies the file from which the canonical domain shall be specified in",
|
||||
EnvVars: []string{"CANONICAL_DOMAIN_FILE"},
|
||||
Value: ".domains",
|
||||
},
|
||||
|
||||
// #########################
|
||||
// ### Page Server Setup ###
|
||||
@ -139,6 +147,18 @@ var (
|
||||
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 ###
|
||||
// ############################
|
||||
@ -178,6 +198,11 @@ var (
|
||||
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",
|
||||
|
@ -10,8 +10,8 @@ rawDomain = 'raw.codeberg.page'
|
||||
allowedCorsDomains = ['fonts.codeberg.org', 'design.codeberg.org']
|
||||
blacklistedPaths = ['do/not/use']
|
||||
|
||||
[gitea]
|
||||
root = 'codeberg.org'
|
||||
[forge]
|
||||
root = 'https://codeberg.org'
|
||||
token = 'XXXXXXXX'
|
||||
lfsEnabled = true
|
||||
followSymlinks = true
|
||||
|
@ -3,24 +3,25 @@ package config
|
||||
type Config struct {
|
||||
LogLevel string `default:"warn"`
|
||||
Server ServerConfig
|
||||
Gitea GiteaConfig
|
||||
Forge ForgeConfig
|
||||
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
|
||||
Host string `default:"[::]"`
|
||||
Port uint16 `default:"443"`
|
||||
HttpPort uint16 `default:"80"`
|
||||
HttpServerEnabled bool `default:"true"`
|
||||
MainDomain string
|
||||
RawDomain string
|
||||
CanonicalDomainFile string `default:".domains"`
|
||||
PagesBranches []string
|
||||
AllowedCorsDomains []string
|
||||
BlacklistedPaths []string
|
||||
}
|
||||
|
||||
type GiteaConfig struct {
|
||||
type ForgeConfig struct {
|
||||
Root string
|
||||
Token string
|
||||
LFSEnabled bool `default:"false"`
|
||||
@ -42,5 +43,6 @@ type ACMEConfig struct {
|
||||
EAB_HMAC string
|
||||
EAB_KID string
|
||||
DNSProvider string
|
||||
NoDNS01 bool `default:"false"`
|
||||
AccountConfigFile string `default:"acme-account.json"`
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func MergeConfig(ctx *cli.Context, config *Config) {
|
||||
}
|
||||
|
||||
mergeServerConfig(ctx, &config.Server)
|
||||
mergeGiteaConfig(ctx, &config.Gitea)
|
||||
mergeForgeConfig(ctx, &config.Forge)
|
||||
mergeDatabaseConfig(ctx, &config.Database)
|
||||
mergeACMEConfig(ctx, &config.ACME)
|
||||
}
|
||||
@ -75,6 +75,9 @@ func mergeServerConfig(ctx *cli.Context, config *ServerConfig) {
|
||||
if ctx.IsSet("raw-domain") {
|
||||
config.RawDomain = ctx.String("raw-domain")
|
||||
}
|
||||
if ctx.IsSet("canonical-domain-file") {
|
||||
config.CanonicalDomainFile = ctx.String("canonical-domain-file")
|
||||
}
|
||||
if ctx.IsSet("pages-branch") {
|
||||
config.PagesBranches = ctx.StringSlice("pages-branch")
|
||||
}
|
||||
@ -89,12 +92,12 @@ func mergeServerConfig(ctx *cli.Context, config *ServerConfig) {
|
||||
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")
|
||||
func mergeForgeConfig(ctx *cli.Context, config *ForgeConfig) {
|
||||
if ctx.IsSet("forge-root") {
|
||||
config.Root = ctx.String("forge-root")
|
||||
}
|
||||
if ctx.IsSet("gitea-api-token") {
|
||||
config.Token = ctx.String("gitea-api-token")
|
||||
if ctx.IsSet("forge-api-token") {
|
||||
config.Token = ctx.String("forge-api-token")
|
||||
}
|
||||
if ctx.IsSet("enable-lfs-support") {
|
||||
config.LFSEnabled = ctx.Bool("enable-lfs-support")
|
||||
@ -141,6 +144,9 @@ func mergeACMEConfig(ctx *cli.Context, config *ACMEConfig) {
|
||||
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")
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ func TestValuesReadFromConfigFileShouldBeOverwrittenByArgs(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedConfig.LogLevel = "debug"
|
||||
expectedConfig.Gitea.Root = "not-codeberg.org"
|
||||
expectedConfig.Forge.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...)
|
||||
@ -122,7 +122,7 @@ func TestValuesReadFromConfigFileShouldBeOverwrittenByArgs(t *testing.T) {
|
||||
[]string{
|
||||
"--config-file", "assets/test_config.toml",
|
||||
"--log-level", "debug",
|
||||
"--gitea-root", "not-codeberg.org",
|
||||
"--forge-root", "not-codeberg.org",
|
||||
"--acme-accept-terms",
|
||||
"--host", "172.17.0.2",
|
||||
},
|
||||
@ -136,17 +136,18 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T
|
||||
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"},
|
||||
Host: "original",
|
||||
Port: 8080,
|
||||
HttpPort: 80,
|
||||
HttpServerEnabled: false,
|
||||
MainDomain: "original",
|
||||
RawDomain: "original",
|
||||
CanonicalDomainFile: "original",
|
||||
PagesBranches: []string{"original"},
|
||||
AllowedCorsDomains: []string{"original"},
|
||||
BlacklistedPaths: []string{"original"},
|
||||
},
|
||||
Gitea: GiteaConfig{
|
||||
Forge: ForgeConfig{
|
||||
Root: "original",
|
||||
Token: "original",
|
||||
LFSEnabled: false,
|
||||
@ -166,6 +167,7 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T
|
||||
EAB_HMAC: "original",
|
||||
EAB_KID: "original",
|
||||
DNSProvider: "original",
|
||||
NoDNS01: false,
|
||||
AccountConfigFile: "original",
|
||||
},
|
||||
}
|
||||
@ -175,17 +177,18 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T
|
||||
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...),
|
||||
Host: "changed",
|
||||
Port: 8443,
|
||||
HttpPort: 443,
|
||||
HttpServerEnabled: true,
|
||||
MainDomain: "changed",
|
||||
RawDomain: "changed",
|
||||
CanonicalDomainFile: "changed",
|
||||
PagesBranches: []string{"changed"},
|
||||
AllowedCorsDomains: []string{"changed"},
|
||||
BlacklistedPaths: append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...),
|
||||
},
|
||||
Gitea: GiteaConfig{
|
||||
Forge: ForgeConfig{
|
||||
Root: "changed",
|
||||
Token: "changed",
|
||||
LFSEnabled: true,
|
||||
@ -205,6 +208,7 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T
|
||||
EAB_HMAC: "changed",
|
||||
EAB_KID: "changed",
|
||||
DNSProvider: "changed",
|
||||
NoDNS01: true,
|
||||
AccountConfigFile: "changed",
|
||||
},
|
||||
}
|
||||
@ -218,6 +222,7 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T
|
||||
// Server
|
||||
"--pages-domain", "changed",
|
||||
"--raw-domain", "changed",
|
||||
"--canonical-domain-file", "changed",
|
||||
"--allowed-cors-domains", "changed",
|
||||
"--blacklisted-paths", "changed",
|
||||
"--pages-branch", "changed",
|
||||
@ -225,9 +230,9 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T
|
||||
"--port", "8443",
|
||||
"--http-port", "443",
|
||||
"--enable-http-server",
|
||||
// Gitea
|
||||
"--gitea-root", "changed",
|
||||
"--gitea-api-token", "changed",
|
||||
// Forge
|
||||
"--forge-root", "changed",
|
||||
"--forge-api-token", "changed",
|
||||
"--enable-lfs-support",
|
||||
"--enable-symlink-support",
|
||||
"--default-mime-type", "changed",
|
||||
@ -243,6 +248,7 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T
|
||||
"--acme-eab-hmac", "changed",
|
||||
"--acme-eab-kid", "changed",
|
||||
"--dns-provider", "changed",
|
||||
"--no-dns-01",
|
||||
"--acme-account-config", "changed",
|
||||
},
|
||||
)
|
||||
@ -270,27 +276,29 @@ func TestMergeServerConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *tes
|
||||
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"},
|
||||
Host: "original",
|
||||
Port: 8080,
|
||||
HttpPort: 80,
|
||||
HttpServerEnabled: false,
|
||||
MainDomain: "original",
|
||||
RawDomain: "original",
|
||||
CanonicalDomainFile: "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...)),
|
||||
Host: "changed",
|
||||
Port: 8443,
|
||||
HttpPort: 443,
|
||||
HttpServerEnabled: true,
|
||||
MainDomain: "changed",
|
||||
RawDomain: "changed",
|
||||
CanonicalDomainFile: "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)
|
||||
@ -300,6 +308,7 @@ func TestMergeServerConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *tes
|
||||
[]string{
|
||||
"--pages-domain", "changed",
|
||||
"--raw-domain", "changed",
|
||||
"--canonical-domain-file", "changed",
|
||||
"--allowed-cors-domains", "changed",
|
||||
"--blacklisted-paths", "changed",
|
||||
"--host", "changed",
|
||||
@ -323,6 +332,7 @@ func TestMergeServerConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgE
|
||||
{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{"--canonical-domain-file", "changed"}, callback: func(sc *ServerConfig) { sc.CanonicalDomainFile = "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"} }},
|
||||
@ -333,15 +343,16 @@ func TestMergeServerConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgE
|
||||
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"},
|
||||
Host: "original",
|
||||
Port: 8080,
|
||||
HttpPort: 80,
|
||||
HttpServerEnabled: false,
|
||||
MainDomain: "original",
|
||||
RawDomain: "original",
|
||||
CanonicalDomainFile: "original",
|
||||
PagesBranches: []string{"original"},
|
||||
AllowedCorsDomains: []string{"original"},
|
||||
BlacklistedPaths: []string{"original"},
|
||||
}
|
||||
|
||||
expectedConfig := cfg
|
||||
@ -363,11 +374,11 @@ func TestMergeServerConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgE
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeGiteaConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
func TestMergeForgeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &GiteaConfig{
|
||||
cfg := &ForgeConfig{
|
||||
Root: "original",
|
||||
Token: "original",
|
||||
LFSEnabled: false,
|
||||
@ -376,9 +387,9 @@ func TestMergeGiteaConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *test
|
||||
ForbiddenMimeTypes: []string{"original"},
|
||||
}
|
||||
|
||||
mergeGiteaConfig(ctx, cfg)
|
||||
mergeForgeConfig(ctx, cfg)
|
||||
|
||||
expectedConfig := &GiteaConfig{
|
||||
expectedConfig := &ForgeConfig{
|
||||
Root: "changed",
|
||||
Token: "changed",
|
||||
LFSEnabled: true,
|
||||
@ -392,8 +403,8 @@ func TestMergeGiteaConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *test
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--gitea-root", "changed",
|
||||
"--gitea-api-token", "changed",
|
||||
"--forge-root", "changed",
|
||||
"--forge-api-token", "changed",
|
||||
"--enable-lfs-support",
|
||||
"--enable-symlink-support",
|
||||
"--default-mime-type", "changed",
|
||||
@ -402,25 +413,25 @@ func TestMergeGiteaConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *test
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeGiteaConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
||||
func TestMergeForgeConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
||||
type testValuePair struct {
|
||||
args []string
|
||||
callback func(*GiteaConfig)
|
||||
callback func(*ForgeConfig)
|
||||
}
|
||||
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"} }},
|
||||
{args: []string{"--forge-root", "changed"}, callback: func(gc *ForgeConfig) { gc.Root = "changed" }},
|
||||
{args: []string{"--forge-api-token", "changed"}, callback: func(gc *ForgeConfig) { gc.Token = "changed" }},
|
||||
{args: []string{"--enable-lfs-support"}, callback: func(gc *ForgeConfig) { gc.LFSEnabled = true }},
|
||||
{args: []string{"--enable-symlink-support"}, callback: func(gc *ForgeConfig) { gc.FollowSymlinks = true }},
|
||||
{args: []string{"--default-mime-type", "changed"}, callback: func(gc *ForgeConfig) { gc.DefaultMimeType = "changed" }},
|
||||
{args: []string{"--forbidden-mime-types", "changed"}, callback: func(gc *ForgeConfig) { gc.ForbiddenMimeTypes = []string{"changed"} }},
|
||||
}
|
||||
|
||||
for _, pair := range testValuePairs {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := GiteaConfig{
|
||||
cfg := ForgeConfig{
|
||||
Root: "original",
|
||||
Token: "original",
|
||||
LFSEnabled: false,
|
||||
@ -432,7 +443,7 @@ func TestMergeGiteaConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgEx
|
||||
expectedConfig := cfg
|
||||
pair.callback(&expectedConfig)
|
||||
|
||||
mergeGiteaConfig(ctx, &cfg)
|
||||
mergeForgeConfig(ctx, &cfg)
|
||||
|
||||
expectedConfig.ForbiddenMimeTypes = fixArrayFromCtx(ctx, "forbidden-mime-types", expectedConfig.ForbiddenMimeTypes)
|
||||
|
||||
@ -445,6 +456,33 @@ func TestMergeGiteaConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgEx
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeForgeConfigShouldReplaceValuesGivenGiteaOptionsExist(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &ForgeConfig{
|
||||
Root: "original",
|
||||
Token: "original",
|
||||
}
|
||||
|
||||
mergeForgeConfig(ctx, cfg)
|
||||
|
||||
expectedConfig := &ForgeConfig{
|
||||
Root: "changed",
|
||||
Token: "changed",
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--gitea-root", "changed",
|
||||
"--gitea-api-token", "changed",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeDatabaseConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
@ -517,6 +555,7 @@ func TestMergeACMEConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testi
|
||||
EAB_HMAC: "original",
|
||||
EAB_KID: "original",
|
||||
DNSProvider: "original",
|
||||
NoDNS01: false,
|
||||
AccountConfigFile: "original",
|
||||
}
|
||||
|
||||
@ -530,6 +569,7 @@ func TestMergeACMEConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testi
|
||||
EAB_HMAC: "changed",
|
||||
EAB_KID: "changed",
|
||||
DNSProvider: "changed",
|
||||
NoDNS01: true,
|
||||
AccountConfigFile: "changed",
|
||||
}
|
||||
|
||||
@ -545,6 +585,7 @@ func TestMergeACMEConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testi
|
||||
"--acme-eab-hmac", "changed",
|
||||
"--acme-eab-kid", "changed",
|
||||
"--dns-provider", "changed",
|
||||
"--no-dns-01",
|
||||
"--acme-account-config", "changed",
|
||||
},
|
||||
)
|
||||
@ -563,6 +604,7 @@ func TestMergeACMEConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExi
|
||||
{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" }},
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ pagesBranches = ["pages"]
|
||||
allowedCorsDomains = []
|
||||
blacklistedPaths = []
|
||||
|
||||
[gitea]
|
||||
[forge]
|
||||
root = 'https://codeberg.org'
|
||||
token = 'ASDF1234'
|
||||
lfsEnabled = true
|
||||
|
@ -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
|
||||
|
@ -1,22 +1,21 @@
|
||||
version: "3"
|
||||
version: '3'
|
||||
services:
|
||||
haproxy:
|
||||
image: haproxy
|
||||
ports: ["443:443"]
|
||||
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
|
||||
- ./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
|
||||
- NET_ADMIN
|
||||
gitea:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./gitea-www:/srv:ro
|
||||
- ./gitea.Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./gitea-www:/srv:ro
|
||||
- ./gitea.Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
pages:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./pages-www:/srv:ro
|
||||
- ./pages.Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
|
||||
- ./pages-www:/srv:ro
|
||||
- ./pages.Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
|
73
flake.lock
Normal file
73
flake.lock
Normal file
@ -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": 1716715802,
|
||||
"narHash": "sha256-usk0vE7VlxPX8jOavrtpOqphdfqEQpf9lgedlY/r66c=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e2dd4e18cc1c7314e24154331bae07df76eb582f",
|
||||
"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
|
||||
}
|
27
flake.nix
Normal file
27
flake.nix
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
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
|
||||
golangci-lint
|
||||
gopls
|
||||
gotools
|
||||
go-tools
|
||||
sqlite-interactive
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
13
go.mod
13
go.mod
@ -5,10 +5,12 @@ go 1.21
|
||||
toolchain go1.21.4
|
||||
|
||||
require (
|
||||
code.gitea.io/sdk/gitea v0.16.1-0.20231115014337-e23e8aa3004f
|
||||
code.gitea.io/sdk/gitea v0.17.1
|
||||
github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a
|
||||
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/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
@ -18,7 +20,7 @@ require (
|
||||
github.com/rs/zerolog v1.27.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
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
|
||||
xorm.io/xorm v1.3.2
|
||||
)
|
||||
|
||||
@ -45,7 +47,6 @@ require (
|
||||
github.com/cloudflare/cloudflare-go v0.20.0 // indirect
|
||||
github.com/cpu/goacmedns v0.1.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/creasty/defaults v1.7.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.6.1 // indirect
|
||||
@ -122,11 +123,11 @@ require (
|
||||
github.com/vultr/govultr/v2 v2.7.1 // indirect
|
||||
go.opencensus.io v0.22.3 // indirect
|
||||
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/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.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // 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
|
||||
|
61
go.sum
61
go.sum
@ -22,8 +22,8 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
code.gitea.io/sdk/gitea v0.16.1-0.20231115014337-e23e8aa3004f h1:nMmwDgUIAWj9XQjzHz5unC3ZMfhhwHRk6rnuwLzdu1o=
|
||||
code.gitea.io/sdk/gitea v0.16.1-0.20231115014337-e23e8aa3004f/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg=
|
||||
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=
|
||||
@ -254,8 +254,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/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-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/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=
|
||||
@ -318,12 +318,13 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
@ -724,6 +725,7 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/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=
|
||||
@ -771,9 +773,9 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP
|
||||
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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
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=
|
||||
@ -784,8 +786,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/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
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=
|
||||
@ -806,8 +808,10 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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=
|
||||
@ -843,7 +847,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.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=
|
||||
@ -859,8 +865,11 @@ 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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=
|
||||
@ -917,12 +926,19 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/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/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/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=
|
||||
@ -931,8 +947,11 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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=
|
||||
@ -983,8 +1002,10 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs
|
||||
golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
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=
|
||||
|
@ -1,18 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<!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"
|
||||
/>
|
||||
<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 {
|
||||
@ -34,12 +28,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="10em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="var(--blue-color)"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
@ -50,18 +39,13 @@
|
||||
<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"
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
|
27
renovate.json
Normal file
27
renovate.json
Normal file
@ -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"]
|
||||
}
|
@ -13,8 +13,8 @@ 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.APIEndpoint != "https://acme.mock.directory" {
|
||||
return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig)
|
||||
if (!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)
|
||||
|
@ -56,11 +56,8 @@ func NewAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
if cfg.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")
|
||||
}
|
||||
// using mock wildcard certs
|
||||
mainDomainAcmeClient = nil
|
||||
} else {
|
||||
// use DNS-Challenge https://go-acme.github.io/lego/dns/
|
||||
provider, err := dns.NewDNSChallengeProviderByName(cfg.DNSProvider)
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
"github.com/reugn/equalizer"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
@ -31,9 +32,15 @@ func TLSConfig(mainDomainSuffix string,
|
||||
giteaClient *gitea.Client,
|
||||
acmeClient *AcmeClient,
|
||||
firstDefaultBranch string,
|
||||
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache cache.ICache,
|
||||
canonicalDomainConfig string,
|
||||
challengeCache, canonicalDomainCache cache.ICache,
|
||||
certDB database.CertDB,
|
||||
noDNS01 bool,
|
||||
rawDomain string,
|
||||
) *tls.Config {
|
||||
// every cert is at most 24h in the cache and 7 days before expiry the cert is renewed
|
||||
keyCache := expirable.NewLRU[string, *tls.Certificate](32, nil, 24*time.Hour)
|
||||
|
||||
return &tls.Config{
|
||||
// check DNS name & get certificate from Let's Encrypt
|
||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
@ -64,12 +71,27 @@ func TLSConfig(mainDomainSuffix string,
|
||||
|
||||
targetOwner := ""
|
||||
mayObtainCert := true
|
||||
|
||||
if strings.HasSuffix(domain, mainDomainSuffix) || strings.EqualFold(domain, mainDomainSuffix[1:]) {
|
||||
// deliver default certificate for the main domain (*.codeberg.page)
|
||||
domain = mainDomainSuffix
|
||||
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(domain, mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
|
||||
targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch)
|
||||
if targetOwner == "" {
|
||||
// DNS not set up, return main certificate to redirect to the docs
|
||||
domain = mainDomainSuffix
|
||||
@ -79,7 +101,7 @@ func TLSConfig(mainDomainSuffix string,
|
||||
TargetRepo: targetRepo,
|
||||
TargetBranch: targetBranch,
|
||||
}
|
||||
_, valid := targetOpt.CheckCanonicalDomain(giteaClient, domain, mainDomainSuffix, canonicalDomainCache)
|
||||
_, valid := targetOpt.CheckCanonicalDomain(giteaClient, domain, mainDomainSuffix, canonicalDomainConfig, canonicalDomainCache)
|
||||
if !valid {
|
||||
// We shouldn't obtain a certificate when we cannot check if the
|
||||
// repository has specified this domain in the `.domains` file.
|
||||
@ -90,7 +112,7 @@ func TLSConfig(mainDomainSuffix string,
|
||||
|
||||
if tlsCertificate, ok := keyCache.Get(domain); ok {
|
||||
// we can use an existing certificate object
|
||||
return tlsCertificate.(*tls.Certificate), nil
|
||||
return tlsCertificate, nil
|
||||
}
|
||||
|
||||
var tlsCertificate *tls.Certificate
|
||||
@ -115,9 +137,8 @@ func TLSConfig(mainDomainSuffix string,
|
||||
}
|
||||
}
|
||||
|
||||
if err := keyCache.Set(domain, tlsCertificate, 15*time.Minute); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyCache.Add(domain, tlsCertificate)
|
||||
|
||||
return tlsCertificate, nil
|
||||
},
|
||||
NextProtos: []string{
|
||||
@ -169,11 +190,10 @@ func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProv
|
||||
|
||||
// TODO: document & put into own function
|
||||
if !strings.EqualFold(sni, mainDomainSuffix) {
|
||||
tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0])
|
||||
tlsCertificate.Leaf, err = leaf(&tlsCertificate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing leaf tlsCert: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// renew certificates 7 days before they expire
|
||||
if tlsCertificate.Leaf.NotAfter.Before(time.Now().Add(7 * 24 * time.Hour)) {
|
||||
// TODO: use ValidTill of custom cert struct
|
||||
@ -199,9 +219,6 @@ func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProv
|
||||
|
||||
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 useDnsProvider && domains[0] != "" && domains[0][0] == '*' {
|
||||
domains = domains[1:]
|
||||
}
|
||||
|
||||
// lock to avoid simultaneous requests
|
||||
_, working := c.obtainLocks.LoadOrStore(name, struct{}{})
|
||||
@ -219,7 +236,11 @@ func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew
|
||||
defer c.obtainLocks.Delete(name)
|
||||
|
||||
if acmeClient == nil {
|
||||
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase)
|
||||
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
|
||||
@ -273,6 +294,7 @@ func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew
|
||||
}
|
||||
leaf, err := leaf(&tlsCertificate)
|
||||
if err == nil && leaf.NotAfter.After(time.Now()) {
|
||||
tlsCertificate.Leaf = leaf
|
||||
// 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 {
|
||||
@ -370,11 +392,20 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, acmeClient *Acm
|
||||
}
|
||||
}
|
||||
|
||||
// leaf returns the parsed leaf certificate, either from c.leaf or by parsing
|
||||
// leaf returns the parsed leaf certificate, either from c.Leaf or by parsing
|
||||
// the corresponding c.Certificate[0].
|
||||
// After successfully parsing the cert c.Leaf gets set to the parsed cert.
|
||||
func leaf(c *tls.Certificate) (*x509.Certificate, error) {
|
||||
if c.Leaf != nil {
|
||||
return c.Leaf, nil
|
||||
}
|
||||
return x509.ParseCertificate(c.Certificate[0])
|
||||
|
||||
leaf, err := x509.ParseCertificate(c.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tlsCert - failed to parse leaf: %w", err)
|
||||
}
|
||||
|
||||
c.Leaf = leaf
|
||||
|
||||
return leaf, err
|
||||
}
|
||||
|
@ -52,7 +52,6 @@ func (x xDB) Close() error {
|
||||
func (x xDB) Put(domain string, cert *certificate.Resource) error {
|
||||
log.Trace().Str("domain", cert.Domain).Msg("inserting cert to db")
|
||||
|
||||
domain = integrationTestReplacements(domain)
|
||||
c, err := toCert(domain, cert)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -82,7 +81,6 @@ func (x xDB) Get(domain string) (*certificate.Resource, error) {
|
||||
if domain[:1] == "." {
|
||||
domain = "*" + domain
|
||||
}
|
||||
domain = integrationTestReplacements(domain)
|
||||
|
||||
cert := new(Cert)
|
||||
log.Trace().Str("domain", domain).Msg("get cert from db")
|
||||
@ -99,7 +97,6 @@ func (x xDB) Delete(domain string) error {
|
||||
if domain[:1] == "." {
|
||||
domain = "*" + domain
|
||||
}
|
||||
domain = integrationTestReplacements(domain)
|
||||
|
||||
log.Trace().Str("domain", domain).Msg("delete cert from db")
|
||||
_, err := x.engine.ID(domain).Delete(new(Cert))
|
||||
@ -139,13 +136,3 @@ func supportedDriver(driver string) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// integrationTestReplacements is needed because integration tests use a single domain cert,
|
||||
// while production use a wildcard cert
|
||||
// TODO: find a better way to handle this
|
||||
func integrationTestReplacements(domainKey string) string {
|
||||
if domainKey == "*.localhost.mock.directory" {
|
||||
return "localhost.mock.directory"
|
||||
}
|
||||
return domainKey
|
||||
}
|
||||
|
@ -5,22 +5,26 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
)
|
||||
|
||||
// lookupCacheTimeout specifies the timeout for the DNS lookup cache.
|
||||
var lookupCacheTimeout = 15 * time.Minute
|
||||
const (
|
||||
lookupCacheValidity = 30 * time.Second
|
||||
defaultPagesRepo = "pages"
|
||||
)
|
||||
|
||||
var defaultPagesRepo = "pages"
|
||||
// TODO(#316): refactor to not use global variables
|
||||
var lookupCache *expirable.LRU[string, string] = expirable.NewLRU[string, string](4096, nil, lookupCacheValidity)
|
||||
|
||||
// GetTargetFromDNS searches for CNAME or TXT entries on the request domain ending with MainDomainSuffix.
|
||||
// If everything is fine, it returns the target data.
|
||||
func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLookupCache cache.ICache) (targetOwner, targetRepo, targetBranch string) {
|
||||
func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string) (targetOwner, targetRepo, targetBranch string) {
|
||||
// Get CNAME or TXT
|
||||
var cname string
|
||||
var err error
|
||||
if cachedName, ok := dnsLookupCache.Get(domain); ok {
|
||||
cname = cachedName.(string)
|
||||
|
||||
if entry, ok := lookupCache.Get(domain); ok {
|
||||
cname = entry
|
||||
} else {
|
||||
cname, err = net.LookupCNAME(domain)
|
||||
cname = strings.TrimSuffix(cname, ".")
|
||||
@ -38,7 +42,7 @@ func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLo
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = dnsLookupCache.Set(domain, cname, lookupCacheTimeout)
|
||||
_ = lookupCache.Add(domain, cname)
|
||||
}
|
||||
if cname == "" {
|
||||
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)
|
||||
)
|
||||
|
@ -28,6 +28,7 @@ const (
|
||||
branchTimestampCacheKeyPrefix = "branchTime"
|
||||
defaultBranchCacheKeyPrefix = "defaultBranch"
|
||||
rawContentCacheKeyPrefix = "rawContent"
|
||||
ownerExistenceKeyPrefix = "ownerExist"
|
||||
|
||||
// pages server
|
||||
PagesCacheIndicatorHeader = "X-Pages-Cache"
|
||||
@ -56,12 +57,13 @@ type Client struct {
|
||||
defaultMimeType string
|
||||
}
|
||||
|
||||
func NewClient(cfg config.GiteaConfig, respCache cache.ICache) (*Client, error) {
|
||||
rootURL, err := url.Parse(cfg.Root)
|
||||
func NewClient(cfg config.ForgeConfig, respCache cache.ICache) (*Client, error) {
|
||||
// url.Parse returns valid on almost anything...
|
||||
rootURL, err := url.ParseRequestURI(cfg.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("invalid forgejo/gitea root url: %w", err)
|
||||
}
|
||||
giteaRoot := strings.Trim(rootURL.String(), "/")
|
||||
giteaRoot := strings.TrimSuffix(rootURL.String(), "/")
|
||||
|
||||
stdClient := http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
@ -266,6 +268,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)
|
||||
|
@ -23,7 +23,7 @@ const (
|
||||
func Handler(
|
||||
cfg config.ServerConfig,
|
||||
giteaClient *gitea.Client,
|
||||
dnsLookupCache, canonicalDomainCache, redirectsCache cache.ICache,
|
||||
canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
log.Debug().Msg("\n----------------------------------------------------------")
|
||||
@ -92,6 +92,7 @@ func Handler(
|
||||
cfg.MainDomain,
|
||||
trimmedHost,
|
||||
pathElements,
|
||||
cfg.CanonicalDomainFile,
|
||||
canonicalDomainCache, redirectsCache)
|
||||
} else if strings.HasSuffix(trimmedHost, cfg.MainDomain) {
|
||||
log.Debug().Msg("subdomain request detected")
|
||||
@ -100,6 +101,7 @@ func Handler(
|
||||
cfg.PagesBranches,
|
||||
trimmedHost,
|
||||
pathElements,
|
||||
cfg.CanonicalDomainFile,
|
||||
canonicalDomainCache, redirectsCache)
|
||||
} else {
|
||||
log.Debug().Msg("custom domain request detected")
|
||||
@ -108,7 +110,8 @@ func Handler(
|
||||
trimmedHost,
|
||||
pathElements,
|
||||
cfg.PagesBranches[0],
|
||||
dnsLookupCache, canonicalDomainCache, redirectsCache)
|
||||
cfg.CanonicalDomainFile,
|
||||
canonicalDomainCache, redirectsCache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,10 +19,11 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
|
||||
trimmedHost string,
|
||||
pathElements []string,
|
||||
firstDefaultBranch string,
|
||||
dnsLookupCache, canonicalDomainCache, redirectsCache cache.ICache,
|
||||
canonicalDomainConfig string,
|
||||
canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) {
|
||||
// Serve pages from custom domains
|
||||
targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
|
||||
targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, firstDefaultBranch)
|
||||
if targetOwner == "" {
|
||||
html.ReturnErrorPage(ctx,
|
||||
"could not obtain repo owner from custom domain",
|
||||
@ -47,13 +48,13 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
|
||||
TargetBranch: targetBranch,
|
||||
TargetPath: path.Join(pathParts...),
|
||||
}, canonicalLink); works {
|
||||
canonicalDomain, valid := targetOpt.CheckCanonicalDomain(giteaClient, trimmedHost, mainDomainSuffix, canonicalDomainCache)
|
||||
canonicalDomain, valid := targetOpt.CheckCanonicalDomain(giteaClient, trimmedHost, mainDomainSuffix, canonicalDomainConfig, canonicalDomainCache)
|
||||
if !valid {
|
||||
html.ReturnErrorPage(ctx, "domain not specified in <code>.domains</code> file", http.StatusMisdirectedRequest)
|
||||
return
|
||||
} else if canonicalDomain != trimmedHost {
|
||||
// only redirect if the target is also a codeberg page!
|
||||
targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
|
||||
targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, firstDefaultBranch)
|
||||
if targetOwner != "" {
|
||||
ctx.Redirect("https://"+canonicalDomain+"/"+targetOpt.TargetPath, http.StatusTemporaryRedirect)
|
||||
return
|
||||
@ -64,7 +65,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, redirectsCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ func handleRaw(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Clie
|
||||
mainDomainSuffix string,
|
||||
trimmedHost string,
|
||||
pathElements []string,
|
||||
canonicalDomainConfig string,
|
||||
canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) {
|
||||
// Serve raw content from RawDomain
|
||||
@ -45,7 +46,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, redirectsCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
log.Debug().Msg("missing branch info")
|
||||
@ -62,7 +63,7 @@ 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, redirectsCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache)
|
||||
} else {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("raw domain could not find repo <code>%s/%s</code> or repo is empty", targetOpt.TargetOwner, targetOpt.TargetRepo),
|
||||
|
@ -21,6 +21,7 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite
|
||||
defaultPagesBranches []string,
|
||||
trimmedHost string,
|
||||
pathElements []string,
|
||||
canonicalDomainConfig string,
|
||||
canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) {
|
||||
// Serve pages from subdomains of MainDomainSuffix
|
||||
@ -53,7 +54,7 @@ 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, redirectsCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache)
|
||||
} else {
|
||||
html.ReturnErrorPage(
|
||||
ctx,
|
||||
@ -85,7 +86,7 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite
|
||||
TargetPath: path.Join(pathElements[1:]...),
|
||||
}, true); works {
|
||||
log.Trace().Msg("tryUpstream: serve default pages repo with specified branch")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache)
|
||||
} else {
|
||||
html.ReturnErrorPage(
|
||||
ctx,
|
||||
@ -110,7 +111,7 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite
|
||||
TargetPath: path.Join(pathElements[1:]...),
|
||||
}, false); works {
|
||||
log.Debug().Msg("tryBranch, now trying upstream 5")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -126,7 +127,7 @@ 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, redirectsCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -141,7 +142,7 @@ 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, redirectsCache)
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func TestHandlerPerformance(t *testing.T) {
|
||||
cfg := config.GiteaConfig{
|
||||
cfg := config.ForgeConfig{
|
||||
Root: "https://codeberg.org",
|
||||
Token: "",
|
||||
LFSEnabled: false,
|
||||
@ -29,7 +29,7 @@ func TestHandlerPerformance(t *testing.T) {
|
||||
AllowedCorsDomains: []string{"raw.codeberg.org", "fonts.codeberg.org", "design.codeberg.org"},
|
||||
PagesBranches: []string{"pages"},
|
||||
}
|
||||
testHandler := Handler(serverCfg, giteaClient, cache.NewInMemoryCache(), cache.NewInMemoryCache(), cache.NewInMemoryCache())
|
||||
testHandler := Handler(serverCfg, giteaClient, cache.NewInMemoryCache(), cache.NewInMemoryCache())
|
||||
|
||||
testCase := func(uri string, status int) {
|
||||
t.Run(uri, func(t *testing.T) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@ -17,12 +18,13 @@ import (
|
||||
func tryUpstream(ctx *context.Context, giteaClient *gitea.Client,
|
||||
mainDomainSuffix, trimmedHost string,
|
||||
options *upstream.Options,
|
||||
canonicalDomainConfig string,
|
||||
canonicalDomainCache cache.ICache,
|
||||
redirectsCache cache.ICache,
|
||||
) {
|
||||
// check if a canonical domain exists on a request on MainDomain
|
||||
if strings.HasSuffix(trimmedHost, mainDomainSuffix) && !options.ServeRaw {
|
||||
canonicalDomain, _ := options.CheckCanonicalDomain(giteaClient, "", mainDomainSuffix, canonicalDomainCache)
|
||||
canonicalDomain, _ := options.CheckCanonicalDomain(giteaClient, "", mainDomainSuffix, canonicalDomainConfig, canonicalDomainCache)
|
||||
if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix) {
|
||||
canonicalPath := ctx.Req.RequestURI
|
||||
if options.TargetRepo != defaultPagesRepo {
|
||||
@ -41,7 +43,7 @@ func tryUpstream(ctx *context.Context, giteaClient *gitea.Client,
|
||||
|
||||
// Try to request the file from the Gitea API
|
||||
if !options.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
html.ReturnErrorPage(ctx, "forge client failed", ctx.StatusCode)
|
||||
html.ReturnErrorPage(ctx, fmt.Sprintf("Forge returned %d %s", ctx.StatusCode, http.StatusText(ctx.StatusCode)), ctx.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
|
21
server/profiling.go
Normal file
21
server/profiling.go
Normal file
@ -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")
|
||||
}()
|
||||
}
|
@ -3,7 +3,6 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -43,9 +42,6 @@ func Serve(ctx *cli.Context) error {
|
||||
}
|
||||
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger().Level(logLevel)
|
||||
|
||||
foo, _ := json.Marshal(cfg)
|
||||
log.Trace().RawJSON("config", foo).Msg("starting server with config")
|
||||
|
||||
listeningSSLAddress := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
listeningHTTPAddress := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.HttpPort)
|
||||
|
||||
@ -70,18 +66,15 @@ func Serve(ctx *cli.Context) error {
|
||||
}
|
||||
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)
|
||||
giteaClient, err := gitea.NewClient(cfg.Forge, clientResponseCache)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create new gitea client: %v", err)
|
||||
}
|
||||
@ -108,8 +101,11 @@ func Serve(ctx *cli.Context) error {
|
||||
giteaClient,
|
||||
acmeClient,
|
||||
cfg.Server.PagesBranches[0],
|
||||
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache,
|
||||
cfg.Server.CanonicalDomainFile,
|
||||
challengeCache, canonicalDomainCache,
|
||||
certDB,
|
||||
cfg.ACME.NoDNS01,
|
||||
cfg.Server.RawDomain,
|
||||
))
|
||||
|
||||
interval := 12 * time.Hour
|
||||
@ -131,8 +127,12 @@ func Serve(ctx *cli.Context) error {
|
||||
}()
|
||||
}
|
||||
|
||||
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)
|
||||
sslHandler := handler.Handler(cfg.Server, giteaClient, canonicalDomainCache, redirectsCache)
|
||||
|
||||
// Start the ssl listener
|
||||
log.Info().Msgf("Start SSL server using TCP listener on %s", listener.Addr())
|
||||
|
@ -14,12 +14,12 @@ import (
|
||||
// canonicalDomainCacheTimeout specifies the timeout for the canonical domain cache.
|
||||
var canonicalDomainCacheTimeout = 15 * time.Minute
|
||||
|
||||
const canonicalDomainConfig = ".domains"
|
||||
|
||||
// CheckCanonicalDomain returns the canonical domain specified in the repo (using the `.domains` file).
|
||||
func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.ICache) (domain string, valid bool) {
|
||||
func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix, canonicalDomainConfig string, canonicalDomainCache cache.ICache) (domain string, valid bool) {
|
||||
canonicalDomainCacheKey := o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch + "/" + canonicalDomainConfig
|
||||
|
||||
// Check if this request is cached.
|
||||
if cachedValue, ok := canonicalDomainCache.Get(o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch); ok {
|
||||
if cachedValue, ok := canonicalDomainCache.Get(canonicalDomainCacheKey); ok {
|
||||
domains := cachedValue.([]string)
|
||||
for _, domain := range domains {
|
||||
if domain == actualDomain {
|
||||
@ -62,7 +62,7 @@ func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain,
|
||||
}
|
||||
|
||||
// Add result to cache.
|
||||
_ = canonicalDomainCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, domains, canonicalDomainCacheTimeout)
|
||||
_ = canonicalDomainCache.Set(canonicalDomainCacheKey, domains, canonicalDomainCacheTimeout)
|
||||
|
||||
// Return the first domain from the list and return if any of the domains
|
||||
// matched the requested domain.
|
||||
|
@ -17,6 +17,27 @@ type Redirect struct {
|
||||
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
|
||||
|
||||
@ -64,52 +85,21 @@ func (o *Options) getRedirects(giteaClient *gitea.Client, redirectsCache cache.I
|
||||
}
|
||||
|
||||
func (o *Options) matchRedirects(ctx *context.Context, giteaClient *gitea.Client, redirects []Redirect, redirectsCache cache.ICache) (final bool) {
|
||||
if len(redirects) > 0 {
|
||||
for _, redirect := range redirects {
|
||||
reqUrl := ctx.Req.RequestURI
|
||||
// remove repo and branch from request url
|
||||
reqUrl = strings.TrimPrefix(reqUrl, "/"+o.TargetRepo)
|
||||
reqUrl = strings.TrimPrefix(reqUrl, "/@"+o.TargetBranch)
|
||||
reqURL := ctx.Req.RequestURI
|
||||
// remove repo and branch from request url
|
||||
reqURL = strings.TrimPrefix(reqURL, "/"+o.TargetRepo)
|
||||
reqURL = strings.TrimPrefix(reqURL, "/@"+o.TargetBranch)
|
||||
|
||||
// check if from url matches request url
|
||||
if strings.TrimSuffix(redirect.From, "/") == strings.TrimSuffix(reqUrl, "/") {
|
||||
// do rewrite if status code is 200
|
||||
if redirect.StatusCode == 200 {
|
||||
o.TargetPath = redirect.To
|
||||
o.Upstream(ctx, giteaClient, redirectsCache)
|
||||
return true
|
||||
} else {
|
||||
ctx.Redirect(redirect.To, redirect.StatusCode)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// handle wildcard redirects
|
||||
trimmedFromUrl := strings.TrimSuffix(redirect.From, "/*")
|
||||
if strings.HasSuffix(redirect.From, "/*") && strings.HasPrefix(reqUrl, trimmedFromUrl) {
|
||||
if strings.Contains(redirect.To, ":splat") {
|
||||
splatUrl := strings.ReplaceAll(redirect.To, ":splat", strings.TrimPrefix(reqUrl, trimmedFromUrl))
|
||||
// do rewrite if status code is 200
|
||||
if redirect.StatusCode == 200 {
|
||||
o.TargetPath = splatUrl
|
||||
o.Upstream(ctx, giteaClient, redirectsCache)
|
||||
return true
|
||||
} else {
|
||||
ctx.Redirect(splatUrl, redirect.StatusCode)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// do rewrite if status code is 200
|
||||
if redirect.StatusCode == 200 {
|
||||
o.TargetPath = redirect.To
|
||||
o.Upstream(ctx, giteaClient, redirectsCache)
|
||||
return true
|
||||
} else {
|
||||
ctx.Redirect(redirect.To, redirect.StatusCode)
|
||||
return true
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
36
server/upstream/redirects_test.go
Normal file
36
server/upstream/redirects_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user