fork from d7z-project/caddy-gitea-pages
This commit is contained in:
parent
50a258ea59
commit
9d86fd33c6
86 changed files with 2452 additions and 4500 deletions
9
.ecrc
9
.ecrc
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"Exclude": [
|
||||
".git",
|
||||
"go.mod", "go.sum",
|
||||
"vendor",
|
||||
"LICENSE",
|
||||
"_test.go"
|
||||
]
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
indent_size = 1
|
11
.env-dev
11
.env-dev
|
@ -1,11 +0,0 @@
|
|||
ACME_API=https://acme.mock.directory
|
||||
ACME_ACCEPT_TERMS=true
|
||||
PAGES_DOMAIN=localhost.mock.directory
|
||||
RAW_DOMAIN=raw.localhost.mock.directory
|
||||
PAGES_BRANCHES=pages,master,main
|
||||
GITEA_ROOT=https://codeberg.org
|
||||
PORT=4430
|
||||
HTTP_PORT=8880
|
||||
ENABLE_HTTP_SERVER=true
|
||||
LOG_LEVEL=trace
|
||||
ACME_ACCOUNT_CONFIG=integration/acme-account.json
|
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -1,10 +1,4 @@
|
|||
.idea/
|
||||
.cache/
|
||||
/caddy
|
||||
/.idea
|
||||
*.iml
|
||||
key-database.pogreb/
|
||||
acme-account.json
|
||||
build/
|
||||
vendor/
|
||||
pages
|
||||
certs.sqlite
|
||||
.bash_history
|
||||
/Caddyfile.local
|
|
@ -1,34 +0,0 @@
|
|||
linters-settings:
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- experimental
|
||||
- opinionated
|
||||
- performance
|
||||
- style
|
||||
disabled-checks:
|
||||
- importShadow
|
||||
- ifElseChain
|
||||
- hugeParam
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- unconvert
|
||||
- gocritic
|
||||
- gofumpt
|
||||
- bidichk
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- whitespace
|
||||
|
||||
run:
|
||||
timeout: 5m
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf"
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
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
|
47
Caddyfile
Normal file
47
Caddyfile
Normal file
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
order gitea before file_server
|
||||
auto_https disable_redirects
|
||||
}
|
||||
|
||||
(default) {
|
||||
# Gitea 服务器地址
|
||||
server https://gitea.com
|
||||
# Gitea Token,需要 organization:read、repository:read、user:read 权限
|
||||
token please-replace-it
|
||||
# 默认域名,类似于 Github 的 github.io
|
||||
domain example.com
|
||||
# CNAME 配置保存地址
|
||||
# shared: 在 caddy 实例中共享 alias,一般不建议使用
|
||||
alias path/to/file shared
|
||||
# 配置缓存 (缓存刷新时间, 文件缓存时间 , 最大单文件缓存大小)
|
||||
cache 30s 24h 1MB
|
||||
# 默认 返回 Header,可以配置同源策略或更多内容
|
||||
headers {
|
||||
Access-Control-Allow-Origin *
|
||||
}
|
||||
errors {
|
||||
# 默认 404 页面,可填写路径或者 URL
|
||||
404 path/to/file
|
||||
|
||||
# 默认 40x 页面,可填写路径或者 URL
|
||||
40x path/to/file
|
||||
|
||||
# 默认 50x 页面,可填写路径或者 URL
|
||||
50x path/to/file
|
||||
|
||||
# 默认 500 页面,可填写路径或者 URL
|
||||
500 path/to/file
|
||||
}
|
||||
# 开启重定向 scheme port
|
||||
redirect https 302
|
||||
}
|
||||
|
||||
http:// {
|
||||
import default
|
||||
}
|
||||
|
||||
https:// {
|
||||
## 填入 https 配置
|
||||
# tls
|
||||
import default
|
||||
}
|
45
Dockerfile
45
Dockerfile
|
@ -1,36 +1,9 @@
|
|||
# Set the default Go version as a build argument
|
||||
ARG XGO="go-1.21.x"
|
||||
|
||||
# Use xgo (a Go cross-compiler tool) as build image
|
||||
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:${XGO} as build
|
||||
|
||||
# Set the working directory and copy the source code
|
||||
WORKDIR /go/src/codeberg.org/codeberg/pages
|
||||
COPY . /go/src/codeberg.org/codeberg/pages
|
||||
|
||||
# Set the target architecture (can be set using --build-arg), buildx set it automatically
|
||||
ARG TARGETOS TARGETARCH
|
||||
|
||||
# Build the binary using xgo
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg \
|
||||
GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=1 \
|
||||
xgo -x -v --targets=${TARGETOS}/${TARGETARCH} -tags='sqlite sqlite_unlock_notify netgo' -ldflags='-s -w -extldflags "-static" -linkmode external' -out pages .
|
||||
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 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"]
|
||||
FROM docker.io/library/caddy:2.8-builder-alpine as builder
|
||||
RUN mkdir -p /usr/local/src
|
||||
COPY go.mod go.sum caddyfile.go /usr/local/src/
|
||||
COPY pages /usr/local/src/pages
|
||||
WORKDIR /usr/local/src
|
||||
RUN ls && xcaddy build \
|
||||
--with github.com/d7z-project/caddy-gitea-pages=./
|
||||
FROM docker.io/library/caddy:2.8
|
||||
COPY --from=builder /usr/local/src/caddy /usr/bin/caddy
|
51
FEATURES.md
51
FEATURES.md
|
@ -1,51 +0,0 @@
|
|||
# Features
|
||||
|
||||
## Custom domains
|
||||
|
||||
Custom domains can be used by creating a `.domains` file with the domain name, e.g.:
|
||||
|
||||
```text
|
||||
codeberg.page
|
||||
```
|
||||
|
||||
You also have to set some DNS records, see the [Codeberg Documentation](https://docs.codeberg.org/codeberg-pages/using-custom-domain/).
|
||||
|
||||
## Redirects
|
||||
|
||||
Redirects can be created with a `_redirects` file with the following format:
|
||||
|
||||
```text
|
||||
# Comment
|
||||
from to [status]
|
||||
```
|
||||
|
||||
- Lines starting with `#` are ignored
|
||||
- `from` - the path to redirect from (Note: repository and branch names are removed from request URLs)
|
||||
- `to` - the path or URL to redirect to
|
||||
- `status` - status code to use when redirecting (default 301)
|
||||
|
||||
### Status codes
|
||||
|
||||
- `200` - returns content from specified path (no external URLs) without changing the URL (rewrite)
|
||||
- `301` - Moved Permanently (Permanent redirect)
|
||||
- `302` - Found (Temporary redirect)
|
||||
|
||||
### Examples
|
||||
|
||||
#### SPA (single-page application) rewrite
|
||||
|
||||
Redirects all paths to `/index.html` for single-page apps.
|
||||
|
||||
```text
|
||||
/* /index.html 200
|
||||
```
|
||||
|
||||
#### Splats
|
||||
|
||||
Redirects every path under `/articles` to `/posts` while keeping the path.
|
||||
|
||||
```text
|
||||
/articles/* /posts/:splat 302
|
||||
```
|
||||
|
||||
Example: `/articles/2022/10/12/post-1/` -> `/posts/2022/10/12/post-1/`
|
51
Justfile
51
Justfile
|
@ -1,51 +0,0 @@
|
|||
CGO_FLAGS := '-extldflags "-static" -linkmode external'
|
||||
TAGS := 'sqlite sqlite_unlock_notify netgo'
|
||||
|
||||
dev *FLAGS:
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
set -a # automatically export all variables
|
||||
source .env-dev
|
||||
set +a
|
||||
go run -tags '{{TAGS}}' . {{FLAGS}}
|
||||
|
||||
build:
|
||||
CGO_ENABLED=1 go build -tags '{{TAGS}}' -ldflags '-s -w {{CGO_FLAGS}}' -v -o build/codeberg-pages-server ./
|
||||
|
||||
build-tag VERSION:
|
||||
CGO_ENABLED=1 go build -tags '{{TAGS}}' -ldflags '-s -w -X "codeberg.org/codeberg/pages/server/version.Version={{VERSION}}" {{CGO_FLAGS}}' -v -o build/codeberg-pages-server ./
|
||||
|
||||
lint: tool-golangci tool-gofumpt
|
||||
golangci-lint run --timeout 5m --build-tags integration
|
||||
# TODO: run editorconfig-checker
|
||||
|
||||
fmt: tool-gofumpt
|
||||
gofumpt -w --extra .
|
||||
|
||||
clean:
|
||||
go clean ./...
|
||||
|
||||
tool-golangci:
|
||||
@hash golangci-lint> /dev/null 2>&1; if [ $? -ne 0 ]; then \
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest; \
|
||||
fi
|
||||
|
||||
tool-gofumpt:
|
||||
@hash gofumpt> /dev/null 2>&1; if [ $? -ne 0 ]; then \
|
||||
go install mvdan.cc/gofumpt@latest; \
|
||||
fi
|
||||
|
||||
test:
|
||||
go test -race -cover -tags '{{TAGS}}' codeberg.org/codeberg/pages/config/ codeberg.org/codeberg/pages/html/ codeberg.org/codeberg/pages/server/...
|
||||
|
||||
test-run TEST:
|
||||
go test -race -tags '{{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/config/ codeberg.org/codeberg/pages/html/ codeberg.org/codeberg/pages/server/...
|
||||
|
||||
integration:
|
||||
go test -race -tags 'integration {{TAGS}}' codeberg.org/codeberg/pages/integration/...
|
||||
|
||||
integration-run TEST:
|
||||
go test -race -tags 'integration {{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/...
|
||||
|
||||
docker:
|
||||
docker run --rm -it --user $(id -u) -v $(pwd):/work --workdir /work -e HOME=/work codeberg.org/6543/docker-images/golang_just
|
506
LICENSE
506
LICENSE
|
@ -1,305 +1,201 @@
|
|||
European Union Public Licence v. 1.2
|
||||
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the 'EUPL') applies to the Work (as defined
|
||||
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||
other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
|
||||
The Work is provided under the terms of this Licence when the Licensor (as
|
||||
defined below) has placed the following notice immediately following the copyright
|
||||
notice for the Work:
|
||||
|
||||
|
||||
|
||||
Licensed under the EUPL
|
||||
|
||||
|
||||
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1. Definitions
|
||||
|
||||
In this Licence, the following terms have the following meaning:
|
||||
|
||||
— 'The Licence': this Licence.
|
||||
|
||||
— 'The Original Work': the work or software distributed or communicated by
|
||||
the Licensor under this Licence, available as Source Code and also as Executable
|
||||
Code as the case may be.
|
||||
|
||||
— 'Derivative Works': the works or software that could be created by the Licensee,
|
||||
based upon the Original Work or modifications thereof. This Licence does not
|
||||
define the extent of modification or dependence on the Original Work required
|
||||
in order to classify a work as a Derivative Work; this extent is determined
|
||||
by copyright law applicable in the country mentioned in Article 15.
|
||||
|
||||
— 'The Work': the Original Work or its Derivative Works.
|
||||
|
||||
— 'The Source Code': the human-readable form of the Work which is the most
|
||||
convenient for people to study and modify.
|
||||
|
||||
— 'The Executable Code': any code which has generally been compiled and which
|
||||
is meant to be interpreted by a computer as a program.
|
||||
|
||||
— 'The Licensor': the natural or legal person that distributes or communicates
|
||||
the Work under the Licence.
|
||||
|
||||
— 'Contributor(s)': any natural or legal person who modifies the Work under
|
||||
the Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||
|
||||
— 'The Licensee' or 'You': any natural or legal person who makes any usage
|
||||
of the Work under the terms of the Licence.
|
||||
|
||||
— 'Distribution' or 'Communication': any act of selling, giving, lending,
|
||||
renting, distributing, communicating, transmitting, or otherwise making available,
|
||||
online or offline, copies of the Work or providing access to its essential
|
||||
functionalities at the disposal of any other natural or legal person.
|
||||
|
||||
2. Scope of the rights granted by the Licence
|
||||
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable
|
||||
licence to do the following, for the duration of copyright vested in the Original
|
||||
Work:
|
||||
|
||||
— use the Work in any circumstance and for all usage,
|
||||
|
||||
— reproduce the Work,
|
||||
|
||||
— modify the Work, and make Derivative Works based upon the Work,
|
||||
|
||||
— communicate to the public, including the right to make available or display
|
||||
the Work or copies thereof to the public and perform publicly, as the case
|
||||
may be, the Work,
|
||||
|
||||
— distribute the Work or copies thereof,
|
||||
|
||||
— lend and rent the Work or copies thereof,
|
||||
|
||||
— sublicense rights in the Work or copies thereof.
|
||||
|
||||
Those rights can be exercised on any media, supports and formats, whether
|
||||
now known or later invented, as far as the applicable law permits so.
|
||||
|
||||
In the countries where moral rights apply, the Licensor waives his right to
|
||||
exercise his moral right to the extent allowed by law in order to make effective
|
||||
the licence of the economic rights here above listed.
|
||||
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights
|
||||
to any patents held by the Licensor, to the extent necessary to make use of
|
||||
the rights granted on the Work under this Licence.
|
||||
|
||||
3. Communication of the Source Code
|
||||
|
||||
The Licensor may provide the Work either in its Source Code form, or as Executable
|
||||
Code. If the Work is provided as Executable Code, the Licensor provides in
|
||||
addition a machine-readable copy of the Source Code of the Work along with
|
||||
each copy of the Work that the Licensor distributes or indicates, in a notice
|
||||
following the copyright notice attached to the Work, a repository where the
|
||||
Source Code is easily and freely accessible for as long as the Licensor continues
|
||||
to distribute or communicate the Work.
|
||||
|
||||
4. Limitations on copyright
|
||||
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits
|
||||
from any exception or limitation to the exclusive rights of the rights owners
|
||||
in the Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5. Obligations of the Licensee
|
||||
|
||||
The grant of the rights mentioned above is subject to some restrictions and
|
||||
obligations imposed on the Licensee. Those obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||
trademarks notices and all notices that refer to the Licence and to the disclaimer
|
||||
of warranties. The Licensee must include a copy of such notices and a copy
|
||||
of the Licence with every copy of the Work he/she distributes or communicates.
|
||||
The Licensee must cause any Derivative Work to carry prominent notices stating
|
||||
that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||
Original Works or Derivative Works, this Distribution or Communication will
|
||||
be done under the terms of this Licence or of a later version of this Licence
|
||||
unless the Original Work is expressly distributed only under this version
|
||||
of the Licence — for example by communicating 'EUPL v. 1.2 only'. The Licensee
|
||||
(becoming Licensor) cannot offer or impose any additional terms or conditions
|
||||
on the Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
||||
Works or copies thereof based upon both the Work and another work licensed
|
||||
under a Compatible Licence, this Distribution or Communication can be done
|
||||
under the terms of this Compatible Licence. For the sake of this clause, 'Compatible
|
||||
Licence' refers to the licences listed in the appendix attached to this Licence.
|
||||
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible
|
||||
Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the
|
||||
Work, the Licensee will provide a machine-readable copy of the Source Code
|
||||
or indicate a repository where this Source will be easily and freely available
|
||||
for as long as the Licensee continues to distribute or communicate the Work.
|
||||
|
||||
Legal Protection: This Licence does not grant permission to use the trade
|
||||
names, trademarks, service marks, or names of the Licensor, except as required
|
||||
for reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6. Chain of Authorship
|
||||
|
||||
The original Licensor warrants that the copyright in the Original Work granted
|
||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||
to the Work are owned by him/her or licensed to him/her and that he/she has
|
||||
the power and authority to grant the Licence.
|
||||
|
||||
Each time You accept the Licence, the original Licensor and subsequent Contributors
|
||||
grant You a licence to their contributions to the Work, under the terms of
|
||||
this Licence.
|
||||
|
||||
7. Disclaimer of Warranty
|
||||
|
||||
The Work is a work in progress, which is continuously improved by numerous
|
||||
Contributors. It is not a finished work and may therefore contain defects
|
||||
or 'bugs' inherent to this type of development.
|
||||
|
||||
For the above reason, the Work is provided under the Licence on an 'as is'
|
||||
basis and without warranties of any kind concerning the Work, including without
|
||||
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||
or errors, accuracy, non-infringement of intellectual property rights other
|
||||
than copyright as stated in Article 6 of this Licence.
|
||||
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||
for the grant of any rights to the Work.
|
||||
|
||||
8. Disclaimer of Liability
|
||||
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||
material or moral, damages of any kind, arising out of the Licence or of the
|
||||
use of the Work, including without limitation, damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, loss of data or any commercial
|
||||
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||
However, the Licensor will be liable under statutory product liability laws
|
||||
as far such laws apply to the Work.
|
||||
|
||||
9. Additional agreements
|
||||
|
||||
While distributing the Work, You may choose to conclude an additional agreement,
|
||||
defining obligations or services consistent with this Licence. However, if
|
||||
accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against such Contributor
|
||||
by the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10. Acceptance of the Licence
|
||||
|
||||
The provisions of this Licence can be accepted by clicking on an icon 'I agree'
|
||||
placed under the bottom of a window displaying the text of this Licence or
|
||||
by affirming consent in any other similar way, in accordance with the rules
|
||||
of applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||
acceptance of this Licence and all of its terms and conditions.
|
||||
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and conditions
|
||||
by exercising any rights granted to You by Article 2 of this Licence, such
|
||||
as the use of the Work, the creation by You of a Derivative Work or the Distribution
|
||||
or Communication by You of the Work or copies thereof.
|
||||
|
||||
11. Information to the public
|
||||
|
||||
In case of any Distribution or Communication of the Work by means of electronic
|
||||
communication by You (for example, by offering to download the Work from a
|
||||
remote location) the distribution channel or media (for example, a website)
|
||||
must at least provide to the public the information requested by the applicable
|
||||
law regarding the Licensor, the Licence and the way it may be accessible,
|
||||
concluded, stored and reproduced by the Licensee.
|
||||
|
||||
12. Termination of the Licence
|
||||
|
||||
The Licence and the rights granted hereunder will terminate automatically
|
||||
upon any breach by the Licensee of the terms of the Licence.
|
||||
|
||||
Such a termination will not terminate the licences of any person who has received
|
||||
the Work from the Licensee under the Licence, provided such persons remain
|
||||
in full compliance with the Licence.
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
Without prejudice of Article 9 above, the Licence represents the complete
|
||||
agreement between the Parties as to the Work.
|
||||
|
||||
If any provision of the Licence is invalid or unenforceable under applicable
|
||||
law, this will not affect the validity or enforceability of the Licence as
|
||||
a whole. Such provision will be construed or reformed so as necessary to make
|
||||
it valid and enforceable.
|
||||
|
||||
The European Commission may publish other linguistic versions or new versions
|
||||
of this Licence or updated versions of the Appendix, so far this is required
|
||||
and reasonable, without reducing the scope of the rights granted by the Licence.
|
||||
New versions of the Licence will be published with a unique version number.
|
||||
|
||||
All linguistic versions of this Licence, approved by the European Commission,
|
||||
have identical value. Parties can take advantage of the linguistic version
|
||||
of their choice.
|
||||
|
||||
14. Jurisdiction
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
— any litigation resulting from the interpretation of this License, arising
|
||||
between the European Union institutions, bodies, offices or agencies, as a
|
||||
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
||||
of Justice of the European Union, as laid down in article 272 of the Treaty
|
||||
on the Functioning of the European Union,
|
||||
|
||||
— any litigation arising between other parties and resulting from the interpretation
|
||||
of this License, will be subject to the exclusive jurisdiction of the competent
|
||||
court where the Licensor resides or conducts its primary business.
|
||||
|
||||
15. Applicable Law
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
— this Licence shall be governed by the law of the European Union Member State
|
||||
where the Licensor has his seat, resides or has his registered office,
|
||||
|
||||
— this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||
residence or registered office inside a European Union Member State.
|
||||
|
||||
Appendix
|
||||
|
||||
'Compatible Licences' according to Article 5 EUPL are:
|
||||
|
||||
— GNU General Public License (GPL) v. 2, v. 3
|
||||
|
||||
— GNU Affero General Public License (AGPL) v. 3
|
||||
|
||||
— Open Software License (OSL) v. 2.1, v. 3.0
|
||||
|
||||
— Eclipse Public License (EPL) v. 1.0
|
||||
|
||||
— CeCILL v. 2.0, v. 2.1
|
||||
|
||||
— Mozilla Public Licence (MPL) v. 2
|
||||
|
||||
— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||
|
||||
— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||
works other than software
|
||||
|
||||
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
|
||||
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity
|
||||
(LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the
|
||||
above licences without producing a new version of the EUPL, as long as they
|
||||
provide the rights granted in Article 2 of this Licence and protect the covered
|
||||
Source Code from exclusive appropriation.
|
||||
|
||||
All other changes or additions to this Appendix require the production of
|
||||
a new EUPL version.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
10
Makefile
Normal file
10
Makefile
Normal file
|
@ -0,0 +1,10 @@
|
|||
VERSION := 0.0.2
|
||||
|
||||
dev:
|
||||
@xcaddy run v2.8.4 -c Caddyfile.local
|
||||
|
||||
fmt:
|
||||
@go fmt
|
||||
|
||||
image:
|
||||
@podman build -t ghcr.io/d7z-project/caddy-gitea-pages:$(VERSION) -f Dockerfile .
|
208
README.md
208
README.md
|
@ -1,141 +1,83 @@
|
|||
# Codeberg Pages
|
||||
# Gitea Pages
|
||||
|
||||
[](https://opensource.org/license/eupl-1-2/)
|
||||
[](https://ci.codeberg.org/Codeberg/pages-server)
|
||||
<a href="https://matrix.to/#/#gitea-pages-server:matrix.org" title="Join the Matrix room at https://matrix.to/#/#gitea-pages-server:matrix.org">
|
||||
<img src="https://img.shields.io/matrix/gitea-pages-server:matrix.org?label=matrix">
|
||||
</a>
|
||||
[English (Google TR)](./README_en.md) | 中文
|
||||
|
||||
Gitea lacks the ability to host static pages from Git.
|
||||
The Codeberg Pages Server addresses this lack by implementing a standalone service
|
||||
that connects to Gitea via API.
|
||||
It is suitable to be deployed by other Gitea instances, too, to offer static pages hosting to their users.
|
||||
> 参照 Github Pages 实现的 Gitea Pages
|
||||
|
||||
**End user documentation** can mainly be found at the [Wiki](https://codeberg.org/Codeberg/pages-server/wiki/Overview)
|
||||
and the [Codeberg Documentation](https://docs.codeberg.org/codeberg-pages/).
|
||||
## 安装说明
|
||||
|
||||
<a href="https://codeberg.org/Codeberg/pages-server"> <img src="https://codeberg.org/Codeberg/GetItOnCodeberg/raw/branch/main/get-it-on-blue-on-white.svg" alt="Get It On Codeberg" width="250"/> </a>
|
||||
|
||||
## Quickstart
|
||||
|
||||
This is the new Codeberg Pages server, a solution for serving static pages from Gitea repositories.
|
||||
Mapping custom domains is not static anymore, but can be done with DNS:
|
||||
|
||||
1. add a `.domains` text file to your repository, containing the allowed domains, separated by new lines. The
|
||||
first line will be the canonical domain/URL; all other occurrences will be redirected to it.
|
||||
|
||||
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.`
|
||||
|
||||
Certificates are generated, updated and cleaned up automatically via Let's Encrypt through a TLS challenge.
|
||||
|
||||
## Chat for admins & devs
|
||||
|
||||
[matrix: #gitea-pages-server:matrix.org](https://matrix.to/#/#gitea-pages-server:matrix.org)
|
||||
|
||||
## Deployment
|
||||
|
||||
**Warning: Some Caveats Apply**
|
||||
|
||||
> 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.
|
||||
> If you consider using Pages in practice, please consider contacting us first,
|
||||
> we'll then try to share some basic steps and document the current usage for admins
|
||||
> (might be changing in the current state).
|
||||
|
||||
Deploying the software itself is very easy. You can grab a current release binary or build yourself,
|
||||
configure the environment as described below, and you are done.
|
||||
|
||||
The hard part is about adding **custom domain support** if you intend to use it.
|
||||
SSL certificates (request + renewal) is automatically handled by the Pages Server,
|
||||
but if you want to run it on a shared IP address (and not a standalone),
|
||||
you'll need to configure your reverse proxy not to terminate the TLS connections,
|
||||
but forward the requests on the IP level to the Pages Server.
|
||||
|
||||
You can check out a proof of concept in the `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.directory> 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`).
|
||||
|
||||
## Contributing to the development
|
||||
|
||||
The Codeberg team is very open to your contribution.
|
||||
Since we are working nicely in a team, it might be hard at times to get started
|
||||
(still check out the issues, we always aim to have some things to get you started).
|
||||
|
||||
If you have any questions, want to work on a feature or could imagine collaborating with us for some time,
|
||||
feel free to ping us in an issue or in a general [Matrix chat room](#chat-for-admins--devs).
|
||||
|
||||
You can also contact the maintainer(s) of this project:
|
||||
|
||||
- [crapStone](https://codeberg.org/crapStone) [(Matrix)](https://matrix.to/#/@crapstone:obermui.de)
|
||||
|
||||
Previous maintainers:
|
||||
|
||||
- [momar](https://codeberg.org/momar) [(Matrix)](https://matrix.to/#/@moritz:wuks.space)
|
||||
- [6543](https://codeberg.org/6543) [(Matrix)](https://matrix.to/#/@marddl:obermui.de)
|
||||
|
||||
### First steps
|
||||
|
||||
The code of this repository is split in several modules.
|
||||
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.
|
||||
The heart of the software lives in the `server` folder and is split in several modules.
|
||||
|
||||
Again: Feel free to get in touch with us for any questions that might arise.
|
||||
Thank you very much.
|
||||
|
||||
### Test Server
|
||||
|
||||
Make sure you have [golang](https://go.dev) v1.21 or newer and [just](https://just.systems/man/en/) installed.
|
||||
|
||||
run `just dev`
|
||||
now these pages should work:
|
||||
|
||||
- <https://cb_pages_tests.localhost.mock.directory:4430/images/827679288a.jpg>
|
||||
- <https://momar.localhost.mock.directory:4430/ci-testing/>
|
||||
- <https://momar.localhost.mock.directory:4430/pag/@master/>
|
||||
- <https://mock-pages.codeberg-test.org:4430/README.md>
|
||||
|
||||
### Profiling
|
||||
|
||||
> This section is just a collection of commands for quick reference. If you want to learn more about profiling read [this](https://go.dev/doc/diagnostics) article or google `golang profiling`.
|
||||
|
||||
First enable profiling by supplying the cli arg `--enable-profiling` or using the environment variable `EENABLE_PROFILING`.
|
||||
|
||||
Get cpu and mem stats:
|
||||
此处需要用到 `xcaddy` 工具,使用如下命令生成 Caddy 执行文件,
|
||||
如果 `xcaddy` 不存在,需先前往 [caddyserver/xcaddy](https://github.com/caddyserver/xcaddy/releases) 安装 `xcaddy`,
|
||||
同时安装好 Golang 1.22
|
||||
|
||||
```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'
|
||||
xcaddy build v2.8.4 --with github.com/d7z-project/caddy-gitea-pages
|
||||
# 列出当前模块
|
||||
./caddy list-modules | grep gitea
|
||||
```
|
||||
|
||||
More endpoints are documented here: <https://pkg.go.dev/net/http/pprof>
|
||||
当前项目也提供 `linux/amd64` 和 `linux/arm64` 的镜像:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/d7z-project/caddy-gitea-pages:nightly
|
||||
```
|
||||
|
||||
具体配置说明参考 `docker.io/library/caddy` 镜像。
|
||||
|
||||
## 配置说明
|
||||
|
||||
安装后 Caddy 后, 在 `Caddyfile` 写入如下配置:
|
||||
|
||||
```conf
|
||||
{
|
||||
order gitea before file_server
|
||||
}
|
||||
|
||||
:80
|
||||
gitea {
|
||||
# Gitea 服务器地址
|
||||
server https://gitea.com
|
||||
# Gitea Token
|
||||
token please-replace-it
|
||||
# 默认域名,类似于 Github 的 github.io
|
||||
domain example.com
|
||||
}
|
||||
```
|
||||
|
||||
其中,token 需要如下权限:
|
||||
|
||||
- `organization:read`
|
||||
- `repository:read`
|
||||
- `user:read`
|
||||
|
||||
更详细的配置可查看 [Caddyfile](./Caddyfile)
|
||||
|
||||
## 使用说明
|
||||
|
||||
仓库 `https://gitea.com/owner/repo.git` 对应示例配置中的 `owner.example.com/repo`
|
||||
|
||||
如需访问 `CNAME` 配置的域名,则需要先访问仓库对应的 `<owner>.example.com/<repo>` 域名, 此操作只需完成一次。
|
||||
|
||||
**注意**: 需要仓库存在 `gh-pages` 分支和分支内存在 `index.html` 文件才可访问,如果配置后仍无法访问可重启 Caddy 来清理缓存。
|
||||
|
||||
### 文件回退策略
|
||||
|
||||
- URL 末尾为 `/` 时将自动追加 `index.html`
|
||||
- 未找到文件时,如果存在 `404.html` 将使用此文件,响应 404 状态码
|
||||
- 如果仓库带有 `routes-history` 和 `routes-hash` 标签时,默认回退使用 `index.html`, 同时返回 200 状态码
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] 支持 CNAME 自定义路径 (仅适用于 HTTP 模式,不处理 acme 相关的内容)
|
||||
- [x] 支持内容缓存
|
||||
- [ ] 优化并发模型和处理竞争问题
|
||||
- [ ] 支持 Http Range 断点续传
|
||||
- [ ] 支持 oauth2 登录访问私有页面
|
||||
|
||||
## 致谢
|
||||
|
||||
此项目参考了 [42wim/caddy-gitea](https://github.com/42wim/caddy-gitea)
|
||||
|
||||
## LICENSE
|
||||
|
||||
此项目使用 [Apache-2.0](./LICENSE)
|
||||
|
|
67
README_en.md
Normal file
67
README_en.md
Normal file
|
@ -0,0 +1,67 @@
|
|||
# Gitea Pages Caddy Plugin
|
||||
|
||||
English (Google TR) | [中文](./README.md)
|
||||
|
||||
> Gitea Pages implemented with reference to Github Pages.
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
`xcaddy` utility is required to generate the Caddy executable with the following command
|
||||
|
||||
```bash
|
||||
xcaddy build --with github.com/d7z-project/caddy-gitea-pages
|
||||
# List the current modules
|
||||
. /caddy list-modules | grep gitea
|
||||
```
|
||||
|
||||
We also provides `linux/amd64` and `linux/arm64` images:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/d7z-project/caddy-gitea-pages:nightly
|
||||
```
|
||||
|
||||
## Configuration Notes
|
||||
|
||||
After installing Caddy, write the following configuration in `Caddyfile`.
|
||||
|
||||
```conf
|
||||
{
|
||||
order gitea before file_server
|
||||
}
|
||||
|
||||
:80
|
||||
gitea {
|
||||
# Gitea server address
|
||||
server https://gitea.com
|
||||
# Gitea Token
|
||||
token please-replace-it
|
||||
# Default domain, similar to Github's github.io
|
||||
domain example.com
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The token requires the following permissions:
|
||||
|
||||
- `organization:read`
|
||||
- `repository:read`
|
||||
- `user:read`
|
||||
|
||||
More detailed configuration can be found in [Caddyfile](./Caddyfile)
|
||||
|
||||
|
||||
## Usage Notes
|
||||
|
||||
The repository `https://gitea.com/owner/repo.git` corresponds to `owner.example.com/repo` in the example configuration.
|
||||
|
||||
To access the `CNAME` configured domain, you need to access the `<owner>.example.com/<repo>` domain of the repository, which needs to be done only once.
|
||||
|
||||
**Note**: You need to have `gh-pages` branch and `index.html` file in the branch to access the repository, if you still can't access it, you can restart Caddy to clear the cache.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
This project was inspired by [42wim/caddy-gitea](https://github.com/42wim/caddy-gitea).
|
||||
|
||||
## LICENSE
|
||||
|
||||
uses [Apache-2.0](./LICENSE)
|
246
caddyfile.go
Normal file
246
caddyfile.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alecthomas/units"
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/d7z-project/caddy-gitea-pages/pages"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
middle := &Middleware{
|
||||
Config: &pages.MiddlewareConfig{},
|
||||
}
|
||||
caddy.RegisterModule(middle)
|
||||
httpcaddyfile.RegisterHandlerDirective("gitea", parseCaddyfile(middle))
|
||||
}
|
||||
|
||||
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
for n := d.Nesting(); d.NextBlock(n); {
|
||||
switch d.Val() {
|
||||
case "server":
|
||||
d.Args(&m.Config.Server)
|
||||
case "token":
|
||||
d.Args(&m.Config.Token)
|
||||
case "cache":
|
||||
remainingArgs := d.RemainingArgs()
|
||||
if len(remainingArgs) != 3 {
|
||||
return d.Errf("expected 3 argument for 'cache'; got %v", remainingArgs)
|
||||
}
|
||||
var err error
|
||||
m.Config.CacheTimeout, err = time.ParseDuration(remainingArgs[0])
|
||||
if err != nil {
|
||||
return d.Errf("invalid duration: %v", err)
|
||||
}
|
||||
m.Config.CacheRefresh, err = time.ParseDuration(remainingArgs[1])
|
||||
if err != nil {
|
||||
return d.Errf("invalid duration: %v", err)
|
||||
}
|
||||
size, err := units.ParseBase2Bytes(remainingArgs[2])
|
||||
if err != nil {
|
||||
return d.Errf("invalid CacheSize: %v", err)
|
||||
}
|
||||
m.Config.CacheMaxSize = int(size)
|
||||
case "domain":
|
||||
d.Args(&m.Config.Domain)
|
||||
case "alias":
|
||||
remainingArgs := d.RemainingArgs()
|
||||
if len(remainingArgs) == 0 {
|
||||
return d.Errf("expected 2 argument for 'alias'; got %v", remainingArgs)
|
||||
}
|
||||
if len(remainingArgs) == 1 {
|
||||
m.Config.Alias = remainingArgs[0]
|
||||
}
|
||||
if len(remainingArgs) == 2 && remainingArgs[1] == "shared" {
|
||||
m.Config.Alias = remainingArgs[0]
|
||||
m.Config.SharedAlias = true
|
||||
}
|
||||
case "headers":
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
args := []string{d.Val()}
|
||||
args = append(args, d.RemainingArgs()...)
|
||||
if len(args) != 2 {
|
||||
return d.Errf("expected 2 arguments, got %d", len(args))
|
||||
}
|
||||
if m.Config.CustomHeaders == nil {
|
||||
m.Config.CustomHeaders = make(map[string]string)
|
||||
}
|
||||
m.Config.CustomHeaders[args[0]] = args[1]
|
||||
}
|
||||
case "errors":
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
args := []string{d.Val()}
|
||||
args = append(args, d.RemainingArgs()...)
|
||||
if len(args) != 2 {
|
||||
return d.Errf("expected 2 arguments, got %d", len(args))
|
||||
}
|
||||
body, err := parseBody(args[1])
|
||||
if err != nil {
|
||||
return d.Errf("failed to parse %s: %v", args[0], err)
|
||||
}
|
||||
if m.Config.ErrorPages == nil {
|
||||
m.Config.ErrorPages = make(map[string]string)
|
||||
}
|
||||
m.Config.ErrorPages[strings.ToLower(args[0])] = body
|
||||
}
|
||||
case "redirect":
|
||||
remainingArgs := d.RemainingArgs()
|
||||
if len(remainingArgs) != 2 {
|
||||
return d.Errf("expected 2 arguments, got %d", len(remainingArgs))
|
||||
}
|
||||
code, err := strconv.Atoi(remainingArgs[1])
|
||||
if err != nil {
|
||||
return d.WrapErr(err)
|
||||
}
|
||||
m.Config.AutoRedirect = &pages.AutoRedirect{
|
||||
Enabled: true,
|
||||
Scheme: remainingArgs[0],
|
||||
Code: code,
|
||||
}
|
||||
default:
|
||||
return d.Errf("unrecognized subdirective '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBody(path string) (string, error) {
|
||||
fileData, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
return string(fileData), nil
|
||||
} else if strings.HasPrefix(path, "http://") ||
|
||||
strings.HasPrefix(path, "https://") {
|
||||
resp, err := http.Get(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errors.New(resp.Status)
|
||||
}
|
||||
all, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(all), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func parseCaddyfile(middleware *Middleware) func(httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
return func(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
err := middleware.UnmarshalCaddyfile(h.Dispenser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if middleware.Config.ErrorPages == nil {
|
||||
middleware.Config.ErrorPages = make(map[string]string)
|
||||
}
|
||||
if middleware.Config.CustomHeaders == nil {
|
||||
middleware.Config.CustomHeaders = make(map[string]string)
|
||||
}
|
||||
if middleware.Config.AutoRedirect == nil {
|
||||
middleware.Config.AutoRedirect = &pages.AutoRedirect{
|
||||
Enabled: false,
|
||||
}
|
||||
}
|
||||
if middleware.Config.CacheRefresh <= 0 {
|
||||
middleware.Config.CacheRefresh = 1 * time.Minute
|
||||
}
|
||||
if middleware.Config.CacheTimeout <= 0 {
|
||||
middleware.Config.CacheTimeout = 3 * time.Minute
|
||||
}
|
||||
if middleware.Config.CacheMaxSize <= 0 {
|
||||
middleware.Config.CacheMaxSize = 3 * 1024 * 1024
|
||||
}
|
||||
return middleware, nil
|
||||
}
|
||||
}
|
||||
|
||||
type Middleware struct {
|
||||
Config *pages.MiddlewareConfig `json:"config"`
|
||||
Logger *zap.Logger `json:"-"`
|
||||
Client *pages.PageClient `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Middleware) ServeHTTP(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
handler caddyhttp.Handler,
|
||||
) error {
|
||||
|
||||
type stackTracer interface {
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
err := m.Client.Route(writer, request)
|
||||
if errors.Is(err, pages.ErrorNotMatches) {
|
||||
return handler.ServeHTTP(writer, request)
|
||||
} else {
|
||||
if err != nil {
|
||||
if err, ok := err.(stackTracer); ok {
|
||||
for _, f := range err.StackTrace() {
|
||||
fmt.Printf("%+s:%d\n", f, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Middleware) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.handlers.gitea",
|
||||
New: func() caddy.Module {
|
||||
return new(Middleware)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Middleware) Validate() error {
|
||||
return m.Client.Validate()
|
||||
}
|
||||
|
||||
func (m *Middleware) Cleanup() error {
|
||||
m.Logger.Info("cleaning up gitea middleware.")
|
||||
return m.Client.Close()
|
||||
}
|
||||
|
||||
func (m *Middleware) Provision(ctx caddy.Context) error {
|
||||
var err error
|
||||
m.Logger = ctx.Logger() // g.Logger is a *zap.Logger
|
||||
m.Client, err = pages.NewPageClient(
|
||||
m.Config,
|
||||
m.Logger,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ caddy.Provisioner = (*Middleware)(nil)
|
||||
_ caddy.CleanerUpper = (*Middleware)(nil)
|
||||
_ caddy.Validator = (*Middleware)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
|
||||
_ caddyfile.Unmarshaler = (*Middleware)(nil)
|
||||
)
|
129
cli/flags.go
129
cli/flags.go
|
@ -1,129 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
ServerFlags = []cli.Flag{
|
||||
// #############
|
||||
// ### Forge ###
|
||||
// #############
|
||||
// ForgeRoot specifies the root URL of the Forge instance, without a trailing slash.
|
||||
&cli.StringFlag{
|
||||
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"},
|
||||
},
|
||||
// ForgeApiToken specifies an api token for the Forge instance
|
||||
&cli.StringFlag{
|
||||
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, 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, gitea must be version v1.18.0 or higher",
|
||||
EnvVars: []string{"ENABLE_SYMLINK_SUPPORT"},
|
||||
Value: false,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "default-mime-type",
|
||||
Usage: "specifies the default mime type for files that don't have a specific mime type.",
|
||||
EnvVars: []string{"DEFAULT_MIME_TYPE"},
|
||||
Value: "application/octet-stream",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "forbidden-mime-types",
|
||||
Usage: "specifies the forbidden mime types. Use this flag multiple times for multiple mime types.",
|
||||
EnvVars: []string{"FORBIDDEN_MIME_TYPES"},
|
||||
},
|
||||
|
||||
// ###########################
|
||||
// ### Page Server Domains ###
|
||||
// ###########################
|
||||
// MainDomainSuffix specifies the main domain (starting with a dot) for which subdomains shall be served as static
|
||||
// pages, or used for comparison in CNAME lookups. Static pages can be accessed through
|
||||
// http://{owner}.{MainDomain}[/{repo}], with repo defaulting to "pages".
|
||||
&cli.StringFlag{
|
||||
Name: "pages-domain",
|
||||
Usage: "specifies the main domain (starting with a dot) for which subdomains shall be served as static pages",
|
||||
EnvVars: []string{"PAGES_DOMAIN"},
|
||||
},
|
||||
// RawDomain specifies the domain from which raw repository content shall be served in the following format:
|
||||
// http://{RawDomain}/{owner}/{repo}[/{branch|tag|commit}/{version}]/{filepath...}
|
||||
// (set to []byte(nil) to disable raw content hosting)
|
||||
&cli.StringFlag{
|
||||
Name: "raw-domain",
|
||||
Usage: "specifies the domain from which raw repository content shall be served, not set disable raw content hosting",
|
||||
EnvVars: []string{"RAW_DOMAIN"},
|
||||
},
|
||||
|
||||
// #########################
|
||||
// ### Page Server Setup ###
|
||||
// #########################
|
||||
&cli.StringFlag{
|
||||
Name: "host",
|
||||
Usage: "specifies host of listening address",
|
||||
EnvVars: []string{"HOST"},
|
||||
Value: "[::]",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "port",
|
||||
Usage: "specifies the http port to listen to ssl requests",
|
||||
EnvVars: []string{"PORT", "HTTP_PORT"},
|
||||
Value: 8080,
|
||||
},
|
||||
// Default branches to fetch assets from
|
||||
&cli.StringSliceFlag{
|
||||
Name: "pages-branch",
|
||||
Usage: "define a branch to fetch assets from. Use this flag multiple times for multiple branches.",
|
||||
EnvVars: []string{"PAGES_BRANCHES"},
|
||||
Value: cli.NewStringSlice("pages"),
|
||||
},
|
||||
|
||||
&cli.StringSliceFlag{
|
||||
Name: "allowed-cors-domains",
|
||||
Usage: "specify allowed CORS domains. Use this flag multiple times for multiple domains.",
|
||||
EnvVars: []string{"ALLOWED_CORS_DOMAINS"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "blacklisted-paths",
|
||||
Usage: "return an error on these url paths.Use this flag multiple times for multiple paths.",
|
||||
EnvVars: []string{"BLACKLISTED_PATHS"},
|
||||
},
|
||||
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Value: "warn",
|
||||
Usage: "specify at which log level should be logged. Possible options: info, warn, error, fatal",
|
||||
EnvVars: []string{"LOG_LEVEL"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config-file",
|
||||
Usage: "specify the location of the config file",
|
||||
Aliases: []string{"config"},
|
||||
EnvVars: []string{"CONFIG_FILE"},
|
||||
},
|
||||
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-profiling",
|
||||
Usage: "enables the go http profiling endpoints",
|
||||
EnvVars: []string{"ENABLE_PROFILING"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "profiling-address",
|
||||
Usage: "specify ip address and port the profiling server should listen on",
|
||||
EnvVars: []string{"PROFILING_ADDRESS"},
|
||||
Value: "localhost:9999",
|
||||
},
|
||||
}
|
||||
)
|
17
cli/setup.go
17
cli/setup.go
|
@ -1,17 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"pages-server/server/version"
|
||||
)
|
||||
|
||||
func CreatePagesApp() *cli.App {
|
||||
app := cli.NewApp()
|
||||
app.Name = "pages-server"
|
||||
app.Version = version.Version
|
||||
app.Usage = "pages server"
|
||||
app.Flags = ServerFlags
|
||||
|
||||
return app
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
logLevel = 'trace'
|
||||
|
||||
[server]
|
||||
host = '127.0.0.1'
|
||||
port = 443
|
||||
mainDomain = 'codeberg.page'
|
||||
rawDomain = 'raw.codeberg.page'
|
||||
allowedCorsDomains = ['fonts.codeberg.org', 'design.codeberg.org']
|
||||
blacklistedPaths = ['do/not/use']
|
||||
|
||||
[forge]
|
||||
root = 'https://codeberg.org'
|
||||
token = 'XXXXXXXX'
|
||||
lfsEnabled = true
|
||||
followSymlinks = true
|
||||
defaultMimeType = "application/wasm"
|
||||
forbiddenMimeTypes = ["text/html"]
|
|
@ -1,26 +0,0 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
LogLevel string `default:"warn"`
|
||||
Server ServerConfig
|
||||
Forge ForgeConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `default:"127.0.0.1"`
|
||||
Port uint16 `default:"8080"`
|
||||
MainDomain string
|
||||
RawDomain string
|
||||
PagesBranches []string
|
||||
AllowedCorsDomains []string
|
||||
BlacklistedPaths []string
|
||||
}
|
||||
|
||||
type ForgeConfig struct {
|
||||
Root string
|
||||
Token string
|
||||
LFSEnabled bool `default:"false"`
|
||||
FollowSymlinks bool `default:"false"`
|
||||
DefaultMimeType string `default:"application/octet-stream"`
|
||||
ForbiddenMimeTypes []string
|
||||
}
|
103
config/setup.go
103
config/setup.go
|
@ -1,103 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var ALWAYS_BLACKLISTED_PATHS = []string{
|
||||
"/.well-known/acme-challenge/",
|
||||
}
|
||||
|
||||
func NewDefaultConfig() Config {
|
||||
config := Config{}
|
||||
if err := defaults.Set(&config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// defaults does not support setting arrays from strings
|
||||
config.Server.PagesBranches = []string{"main", "master", "pages"}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func ReadConfig(ctx *cli.Context) (*Config, error) {
|
||||
config := NewDefaultConfig()
|
||||
// if config is not given as argument return empty config
|
||||
if !ctx.IsSet("config-file") {
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
configFile := path.Clean(ctx.String("config-file"))
|
||||
|
||||
log.Debug().Str("config-file", configFile).Msg("reading config file")
|
||||
content, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = toml.Unmarshal(content, &config)
|
||||
return &config, err
|
||||
}
|
||||
|
||||
func MergeConfig(ctx *cli.Context, config *Config) {
|
||||
if ctx.IsSet("log-level") {
|
||||
config.LogLevel = ctx.String("log-level")
|
||||
}
|
||||
|
||||
mergeServerConfig(ctx, &config.Server)
|
||||
mergeForgeConfig(ctx, &config.Forge)
|
||||
}
|
||||
|
||||
func mergeServerConfig(ctx *cli.Context, config *ServerConfig) {
|
||||
if ctx.IsSet("host") {
|
||||
config.Host = ctx.String("host")
|
||||
}
|
||||
if ctx.IsSet("port") {
|
||||
config.Port = uint16(ctx.Uint("port"))
|
||||
}
|
||||
if ctx.IsSet("pages-domain") {
|
||||
config.MainDomain = ctx.String("pages-domain")
|
||||
}
|
||||
if ctx.IsSet("raw-domain") {
|
||||
config.RawDomain = ctx.String("raw-domain")
|
||||
}
|
||||
if ctx.IsSet("pages-branch") {
|
||||
config.PagesBranches = ctx.StringSlice("pages-branch")
|
||||
}
|
||||
if ctx.IsSet("allowed-cors-domains") {
|
||||
config.AllowedCorsDomains = ctx.StringSlice("allowed-cors-domains")
|
||||
}
|
||||
if ctx.IsSet("blacklisted-paths") {
|
||||
config.BlacklistedPaths = ctx.StringSlice("blacklisted-paths")
|
||||
}
|
||||
|
||||
// add the paths that should always be blacklisted
|
||||
config.BlacklistedPaths = append(config.BlacklistedPaths, ALWAYS_BLACKLISTED_PATHS...)
|
||||
}
|
||||
|
||||
func mergeForgeConfig(ctx *cli.Context, config *ForgeConfig) {
|
||||
if ctx.IsSet("forge-root") {
|
||||
config.Root = ctx.String("forge-root")
|
||||
}
|
||||
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")
|
||||
}
|
||||
if ctx.IsSet("enable-symlink-support") {
|
||||
config.FollowSymlinks = ctx.Bool("enable-symlink-support")
|
||||
}
|
||||
if ctx.IsSet("default-mime-type") {
|
||||
config.DefaultMimeType = ctx.String("default-mime-type")
|
||||
}
|
||||
if ctx.IsSet("forbidden-mime-types") {
|
||||
config.ForbiddenMimeTypes = ctx.StringSlice("forbidden-mime-types")
|
||||
}
|
||||
}
|
|
@ -1,413 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
cmd "pages-server/cli"
|
||||
)
|
||||
|
||||
func runApp(t *testing.T, fn func(*cli.Context) error, args []string) {
|
||||
app := cmd.CreatePagesApp()
|
||||
app.Action = fn
|
||||
|
||||
appCtx, appCancel := context.WithCancel(context.Background())
|
||||
defer appCancel()
|
||||
|
||||
// os.Args always contains the binary name
|
||||
args = append([]string{"testing"}, args...)
|
||||
|
||||
err := app.RunContext(appCtx, args)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// fixArrayFromCtx fixes the number of "changed" strings in a string slice according to the number of values in the context.
|
||||
// This is a workaround because the cli library has a bug where the number of values in the context gets bigger the more tests are run.
|
||||
func fixArrayFromCtx(ctx *cli.Context, key string, expected []string) []string {
|
||||
if ctx.IsSet(key) {
|
||||
ctxSlice := ctx.StringSlice(key)
|
||||
|
||||
if len(ctxSlice) > 1 {
|
||||
for i := 1; i < len(ctxSlice); i++ {
|
||||
expected = append([]string{"changed"}, expected...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expected
|
||||
}
|
||||
|
||||
func readTestConfig() (*Config, error) {
|
||||
content, err := os.ReadFile("assets/test_config.toml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expectedConfig := NewDefaultConfig()
|
||||
err = toml.Unmarshal(content, &expectedConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &expectedConfig, nil
|
||||
}
|
||||
|
||||
func TestReadConfigShouldReturnEmptyConfigWhenConfigArgEmpty(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg, err := ReadConfig(ctx)
|
||||
expected := NewDefaultConfig()
|
||||
assert.Equal(t, &expected, cfg)
|
||||
|
||||
return err
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
}
|
||||
|
||||
func TestReadConfigShouldReturnConfigFromFileWhenConfigArgPresent(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg, err := ReadConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedConfig, err := readTestConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{"--config-file", "assets/test_config.toml"},
|
||||
)
|
||||
}
|
||||
|
||||
func TestValuesReadFromConfigFileShouldBeOverwrittenByArgs(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg, err := ReadConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
MergeConfig(ctx, cfg)
|
||||
|
||||
expectedConfig, err := readTestConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedConfig.LogLevel = "debug"
|
||||
expectedConfig.Forge.Root = "not-codeberg.org"
|
||||
expectedConfig.Server.Host = "172.17.0.2"
|
||||
expectedConfig.Server.BlacklistedPaths = append(expectedConfig.Server.BlacklistedPaths, ALWAYS_BLACKLISTED_PATHS...)
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--config-file", "assets/test_config.toml",
|
||||
"--log-level", "debug",
|
||||
"--forge-root", "not-codeberg.org",
|
||||
"--host", "172.17.0.2",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &Config{
|
||||
LogLevel: "original",
|
||||
Server: ServerConfig{
|
||||
Host: "original",
|
||||
Port: 8080,
|
||||
|
||||
MainDomain: "original",
|
||||
RawDomain: "original",
|
||||
PagesBranches: []string{"original"},
|
||||
AllowedCorsDomains: []string{"original"},
|
||||
BlacklistedPaths: []string{"original"},
|
||||
},
|
||||
Forge: ForgeConfig{
|
||||
Root: "original",
|
||||
Token: "original",
|
||||
LFSEnabled: false,
|
||||
FollowSymlinks: false,
|
||||
DefaultMimeType: "original",
|
||||
ForbiddenMimeTypes: []string{"original"},
|
||||
},
|
||||
}
|
||||
|
||||
MergeConfig(ctx, cfg)
|
||||
|
||||
expectedConfig := &Config{
|
||||
LogLevel: "changed",
|
||||
Server: ServerConfig{
|
||||
Host: "changed",
|
||||
Port: 8443,
|
||||
MainDomain: "changed",
|
||||
RawDomain: "changed",
|
||||
PagesBranches: []string{"changed"},
|
||||
AllowedCorsDomains: []string{"changed"},
|
||||
BlacklistedPaths: append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...),
|
||||
},
|
||||
Forge: ForgeConfig{
|
||||
Root: "changed",
|
||||
Token: "changed",
|
||||
LFSEnabled: true,
|
||||
FollowSymlinks: true,
|
||||
DefaultMimeType: "changed",
|
||||
ForbiddenMimeTypes: []string{"changed"},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--log-level", "changed",
|
||||
// Server
|
||||
"--pages-domain", "changed",
|
||||
"--raw-domain", "changed",
|
||||
"--allowed-cors-domains", "changed",
|
||||
"--blacklisted-paths", "changed",
|
||||
"--pages-branch", "changed",
|
||||
"--host", "changed",
|
||||
"--port", "8443",
|
||||
// Forge
|
||||
"--forge-root", "changed",
|
||||
"--forge-api-token", "changed",
|
||||
"--enable-lfs-support",
|
||||
"--enable-symlink-support",
|
||||
"--default-mime-type", "changed",
|
||||
"--forbidden-mime-types", "changed",
|
||||
// Database
|
||||
"--db-type", "changed",
|
||||
"--db-conn", "changed",
|
||||
// ACME
|
||||
"--acme-email", "changed",
|
||||
"--acme-api-endpoint", "changed",
|
||||
"--acme-accept-terms",
|
||||
"--acme-use-rate-limits",
|
||||
"--acme-eab-hmac", "changed",
|
||||
"--acme-eab-kid", "changed",
|
||||
"--dns-provider", "changed",
|
||||
"--no-dns-01",
|
||||
"--acme-account-config", "changed",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeServerConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
for range []uint8{0, 1} {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &ServerConfig{
|
||||
Host: "original",
|
||||
Port: 8080,
|
||||
MainDomain: "original",
|
||||
RawDomain: "original",
|
||||
AllowedCorsDomains: []string{"original"},
|
||||
BlacklistedPaths: []string{"original"},
|
||||
}
|
||||
|
||||
mergeServerConfig(ctx, cfg)
|
||||
|
||||
expectedConfig := &ServerConfig{
|
||||
Host: "changed",
|
||||
Port: 8080,
|
||||
MainDomain: "changed",
|
||||
RawDomain: "changed",
|
||||
AllowedCorsDomains: fixArrayFromCtx(ctx, "allowed-cors-domains", []string{"changed"}),
|
||||
BlacklistedPaths: fixArrayFromCtx(ctx, "blacklisted-paths", append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...)),
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--pages-domain", "changed",
|
||||
"--raw-domain", "changed",
|
||||
"--allowed-cors-domains", "changed",
|
||||
"--blacklisted-paths", "changed",
|
||||
"--host", "changed",
|
||||
"--port", "8443",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeServerConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
||||
type testValuePair struct {
|
||||
args []string
|
||||
callback func(*ServerConfig)
|
||||
}
|
||||
testValuePairs := []testValuePair{
|
||||
{args: []string{"--host", "changed"}, callback: func(sc *ServerConfig) { sc.Host = "changed" }},
|
||||
{args: []string{"--port", "8080"}, callback: func(sc *ServerConfig) { sc.Port = 8080 }},
|
||||
{args: []string{"--pages-domain", "changed"}, callback: func(sc *ServerConfig) { sc.MainDomain = "changed" }},
|
||||
{args: []string{"--raw-domain", "changed"}, callback: func(sc *ServerConfig) { sc.RawDomain = "changed" }},
|
||||
{args: []string{"--pages-branch", "changed"}, callback: func(sc *ServerConfig) { sc.PagesBranches = []string{"changed"} }},
|
||||
{args: []string{"--allowed-cors-domains", "changed"}, callback: func(sc *ServerConfig) { sc.AllowedCorsDomains = []string{"changed"} }},
|
||||
{args: []string{"--blacklisted-paths", "changed"}, callback: func(sc *ServerConfig) { sc.BlacklistedPaths = []string{"changed"} }},
|
||||
}
|
||||
|
||||
for _, pair := range testValuePairs {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := ServerConfig{
|
||||
Host: "original",
|
||||
Port: 8080,
|
||||
MainDomain: "original",
|
||||
RawDomain: "original",
|
||||
PagesBranches: []string{"original"},
|
||||
AllowedCorsDomains: []string{"original"},
|
||||
BlacklistedPaths: []string{"original"},
|
||||
}
|
||||
|
||||
expectedConfig := cfg
|
||||
pair.callback(&expectedConfig)
|
||||
expectedConfig.BlacklistedPaths = append(expectedConfig.BlacklistedPaths, ALWAYS_BLACKLISTED_PATHS...)
|
||||
|
||||
expectedConfig.PagesBranches = fixArrayFromCtx(ctx, "pages-branch", expectedConfig.PagesBranches)
|
||||
expectedConfig.AllowedCorsDomains = fixArrayFromCtx(ctx, "allowed-cors-domains", expectedConfig.AllowedCorsDomains)
|
||||
expectedConfig.BlacklistedPaths = fixArrayFromCtx(ctx, "blacklisted-paths", expectedConfig.BlacklistedPaths)
|
||||
|
||||
mergeServerConfig(ctx, &cfg)
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
pair.args,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeForgeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
||||
runApp(
|
||||
t,
|
||||
func(ctx *cli.Context) error {
|
||||
cfg := &ForgeConfig{
|
||||
Root: "original",
|
||||
Token: "original",
|
||||
LFSEnabled: false,
|
||||
FollowSymlinks: false,
|
||||
DefaultMimeType: "original",
|
||||
ForbiddenMimeTypes: []string{"original"},
|
||||
}
|
||||
|
||||
mergeForgeConfig(ctx, cfg)
|
||||
|
||||
expectedConfig := &ForgeConfig{
|
||||
Root: "changed",
|
||||
Token: "changed",
|
||||
LFSEnabled: true,
|
||||
FollowSymlinks: true,
|
||||
DefaultMimeType: "changed",
|
||||
ForbiddenMimeTypes: fixArrayFromCtx(ctx, "forbidden-mime-types", []string{"changed"}),
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
[]string{
|
||||
"--forge-root", "changed",
|
||||
"--forge-api-token", "changed",
|
||||
"--enable-lfs-support",
|
||||
"--enable-symlink-support",
|
||||
"--default-mime-type", "changed",
|
||||
"--forbidden-mime-types", "changed",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMergeForgeConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
||||
type testValuePair struct {
|
||||
args []string
|
||||
callback func(*ForgeConfig)
|
||||
}
|
||||
testValuePairs := []testValuePair{
|
||||
{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 := ForgeConfig{
|
||||
Root: "original",
|
||||
Token: "original",
|
||||
LFSEnabled: false,
|
||||
FollowSymlinks: false,
|
||||
DefaultMimeType: "original",
|
||||
ForbiddenMimeTypes: []string{"original"},
|
||||
}
|
||||
|
||||
expectedConfig := cfg
|
||||
pair.callback(&expectedConfig)
|
||||
|
||||
mergeForgeConfig(ctx, &cfg)
|
||||
|
||||
expectedConfig.ForbiddenMimeTypes = fixArrayFromCtx(ctx, "forbidden-mime-types", expectedConfig.ForbiddenMimeTypes)
|
||||
|
||||
assert.Equal(t, expectedConfig, cfg)
|
||||
|
||||
return nil
|
||||
},
|
||||
pair.args,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
)
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
logLevel = 'debug'
|
||||
|
||||
[server]
|
||||
host = '127.0.0.1'
|
||||
port = 8080
|
||||
mainDomain = 'codeberg.page'
|
||||
rawDomain = 'raw.codeberg.page'
|
||||
pagesBranches = ["pages"]
|
||||
allowedCorsDomains = []
|
||||
blacklistedPaths = []
|
||||
|
||||
[forge]
|
||||
root = 'https://codeberg.org'
|
||||
token = 'ASDF1234'
|
||||
lfsEnabled = true
|
||||
followSymlinks = true
|
1
examples/haproxy-sni/.gitignore
vendored
1
examples/haproxy-sni/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
*.dump
|
|
@ -1,25 +0,0 @@
|
|||
# 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).
|
||||
|
||||
## 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).
|
||||
4. The `https_termination_frontend` listens on said unix socket, terminates the HTTPS connections and then chooses a backend based on the Host header.
|
||||
|
||||
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
|
||||
docker-compose down
|
||||
|
||||
# For manual testing: all HTTPS URLs connect to localhost:443 & certificates are not verified.
|
||||
./test.sh [curl-options...] <url>
|
||||
```
|
||||
|
||||

|
|
@ -1,8 +0,0 @@
|
|||
-----BEGIN DH PARAMETERS-----
|
||||
MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
|
||||
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
|
||||
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
|
||||
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
|
||||
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
|
||||
ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
|
||||
-----END DH PARAMETERS-----
|
|
@ -1,21 +0,0 @@
|
|||
version: '3'
|
||||
services:
|
||||
haproxy:
|
||||
image: haproxy
|
||||
ports: ['443:443']
|
||||
volumes:
|
||||
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
||||
- ./dhparam.pem:/etc/ssl/dhparam.pem:ro
|
||||
- ./haproxy-certificates:/etc/ssl/private/haproxy:ro
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
gitea:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./gitea-www:/srv:ro
|
||||
- ./gitea.Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
pages:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./pages-www:/srv:ro
|
||||
- ./pages.Caddyfile:/etc/caddy/Caddyfile:ro
|
|
@ -1 +0,0 @@
|
|||
Hello to Gitea!
|
|
@ -1,3 +0,0 @@
|
|||
http://codeberg.org
|
||||
|
||||
file_server
|
|
@ -1,26 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIEUDCCArigAwIBAgIRAMq3iwF963VGkzXFpbrpAtkwDQYJKoZIhvcNAQELBQAw
|
||||
gYkxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEvMC0GA1UECwwmbW9t
|
||||
YXJAbW9yaXR6LWxhcHRvcCAoTW9yaXR6IE1hcnF1YXJkdCkxNjA0BgNVBAMMLW1r
|
||||
Y2VydCBtb21hckBtb3JpdHotbGFwdG9wIChNb3JpdHogTWFycXVhcmR0KTAeFw0y
|
||||
MTA2MDYwOTQ4NDFaFw0yMzA5MDYwOTQ4NDFaMFoxJzAlBgNVBAoTHm1rY2VydCBk
|
||||
ZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEvMC0GA1UECwwmbW9tYXJAbW9yaXR6LWxh
|
||||
cHRvcCAoTW9yaXR6IE1hcnF1YXJkdCkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||
ggEKAoIBAQCrSPSPM6grNZMG4ZKFCVxuXu+qkHdzSR96QUxi00VkIrkGPmyMN7q7
|
||||
rUQJto9C9guJio3n7y3Bvr5kBjICjyWQd7GfkVuYgiYiG/O2hy1u1dIMCAB/Zhx1
|
||||
F1mvRfn/Q4eZk2GSOUM+kC0xaNsn2827VGLOGFywUhRmu7J9QSQ3x1Pi5BME7eNC
|
||||
AKup0CbrMrZSzKAEuYujLY0UYRxUrguMnV60wxJDCYE14YDxn9t0g7wQmzyndupk
|
||||
AMLNJZX5L83RA6vUEuTVYBFcyB0Fu3oBLQ31y5QOZ7WF/QiO5cPicQJI/oyXlHq4
|
||||
97BWS/H28kj1H5ZM8+5yhCYDtgj7dERpAgMBAAGjYTBfMA4GA1UdDwEB/wQEAwIF
|
||||
oDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSOSXQZqt2gjbTOkE9Q
|
||||
ddI8SYPqrDAXBgNVHREEEDAOggxjb2RlYmVyZy5vcmcwDQYJKoZIhvcNAQELBQAD
|
||||
ggGBAJ/57DGqfuOa3aS/nLeAzl8komvyHuoOZi9yDK2Jqr+COxP58zSu8xwhiZfc
|
||||
TJvIyB9QR7imGiQ7fEKby40q8uxGGx13oY7gQy7PG8hHk2dkfDZuSQacnpPRC3W0
|
||||
0dL2CQIog6rw6jJHjxneitkX9FUmOnHIKy7LHya0Sthg36Z0Qw5JA3SCy6OQNepR
|
||||
R2XzwTZ0KFk6gAuKCto8ENUlU5lV9PM4X3U0cBOIc5LJAPM+cxEDUocFtFqKJPbe
|
||||
YYlSeB200YhYOdi+x34n9xnQjFu/jVlWF+Y0tMBB1WWq6rZbnuylwWLYQZAo10Co
|
||||
D3oWsYRlD/ZL7X20ztIy8vRXz33ugnxxf88Q7csWDYb4S325svLfI2EjciIxYmBo
|
||||
dSJxXRQkadjIoI7gNvzeWBkYSJpQUbaD4nT2xRS8vfuv42/DrIehb8SbTivHmcB3
|
||||
OibpWIvDtS1B8thIlzl0edb+8pb6mof7pOBxoZdcBsSAk2/48s+jfRHfD9XcuKnv
|
||||
hGCdSQ==
|
||||
-----END CERTIFICATE-----
|
|
@ -1,28 +0,0 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrSPSPM6grNZMG
|
||||
4ZKFCVxuXu+qkHdzSR96QUxi00VkIrkGPmyMN7q7rUQJto9C9guJio3n7y3Bvr5k
|
||||
BjICjyWQd7GfkVuYgiYiG/O2hy1u1dIMCAB/Zhx1F1mvRfn/Q4eZk2GSOUM+kC0x
|
||||
aNsn2827VGLOGFywUhRmu7J9QSQ3x1Pi5BME7eNCAKup0CbrMrZSzKAEuYujLY0U
|
||||
YRxUrguMnV60wxJDCYE14YDxn9t0g7wQmzyndupkAMLNJZX5L83RA6vUEuTVYBFc
|
||||
yB0Fu3oBLQ31y5QOZ7WF/QiO5cPicQJI/oyXlHq497BWS/H28kj1H5ZM8+5yhCYD
|
||||
tgj7dERpAgMBAAECggEAAeW+/88cr83aIRtimiKuaXKXyRXsnNRUivAqPnYEsMVJ
|
||||
s24BmdQMN4QF2u2wzJcZLZ7hT45wvVK1nToMV8bqLZ2F1DSyBRB8B6iznHQG5tFr
|
||||
kEKObtrcuddWYQCvckp3OBZP4GTN/+Vs+r0koF5o+whGR+4xKKrgGvs9UPHlytBf
|
||||
0DMzAzWzGPp6qBPw2sUx/fa9r5TqFW+p4SEOZJUqL2/zEZ6KBWbKw5T1e1y2kMEc
|
||||
cquUQ4avqK/N1nwRNKUnTvW827v0k7HQ2cFdrjIATNlICslOWJQicG5GUOuSBkTC
|
||||
0FFkSTtHP4qm0BqShjv6NDmzX+3WCVkGOKFOI+zuWQKBgQDBq8yEcvfMJY98KNlR
|
||||
eKKdJAMJvKdoD65Yv6EG7ZzpeEWHaTGhu71RPgHYkHn8h1T/9WniroSk19+zb4lP
|
||||
mMsBwxpg5HejWPzIiiJRkRCRA7aZZfvaXfIWryB4kI1tlGHBNN/+SYpG1zdNumtp
|
||||
Xyb/sQWMMWRZdRgclF8V+NvduwKBgQDiaM59gBROleREduFZE1a0oXtt+CrwrPlz
|
||||
hclrkYl1FbTA4TdL4JNbj5jCXCR8YakFhxWEmhwq+Dgl1NQY/YjHyG3w2imaeASX
|
||||
QUsEvAIvNrv1mIELiYCLmUElyX4WL3UhqveOFcZUvR1Z4TTwruPQmXf6BJEBLbWI
|
||||
f7odmG6yKwKBgQCzpuLjZiZY9+qe2OGmQopNzE8JJDgCPrGS38fGvnnU1N1iXAFP
|
||||
LvDRwPxDYNnXl84QVR2wygR/SUTYlTlBXdHKw6nfgW89Vlm+yOxGz5MXgeNLbp/u
|
||||
k0DzK+aqECUxJfh8GclCgANF7XP+pVPn/f0WKKalwld86DLCqBuALUX+6wKBgCUh
|
||||
gxvZ8Xqh4nnH9VUicsnU4eU7Ge+2roJfopTdnWlyUd6AEQ2EmyYc+rSFYAZ2Db42
|
||||
VTUWASCa7LpnmREwI0qAeGdToBcRL8+OibsRClqr409331IBDu/WBnUoAmGpDtCi
|
||||
tU68C3bCPRoMcR430GzZfm+maBGFaYwlRmSsJxtZAoGADSA3uAZBuWNDPNKUas2k
|
||||
Z2dXFEPNpViMjQzJ+Ko7lbOBpUUUQfZF2VMSK4lcnhhbmhcMrYzWWmh6uaw78aHY
|
||||
e3M//BfcVMdxHw7EemGOViNNq3uDIwzvYteoe6fAOA7MaV+WjJaf+smceR4o38fk
|
||||
U9RTkKpRJIcvEW5bvTI9h4o=
|
||||
-----END PRIVATE KEY-----
|
|
@ -1,99 +0,0 @@
|
|||
#####################################
|
||||
## Global Configuration & Defaults ##
|
||||
#####################################
|
||||
|
||||
global
|
||||
log stderr format iso local7
|
||||
|
||||
# generated 2021-06-05, Mozilla Guideline v5.6, HAProxy 2.1, OpenSSL 1.1.1d, intermediate configuration
|
||||
# https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.1d&guideline=5.6
|
||||
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
|
||||
|
||||
ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
|
||||
|
||||
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
|
||||
ssl-dh-param-file /etc/ssl/dhparam.pem
|
||||
|
||||
defaults
|
||||
log global
|
||||
timeout connect 30000
|
||||
timeout check 300000
|
||||
timeout client 300000
|
||||
timeout server 300000
|
||||
|
||||
############################################################################
|
||||
## Frontends: HTTP; HTTPS → HTTPS SNI-based; HTTPS → HTTP(S) header-based ##
|
||||
############################################################################
|
||||
|
||||
frontend http_redirect_frontend
|
||||
# HTTP backend to redirect everything to HTTPS
|
||||
bind :::80 v4v6
|
||||
mode http
|
||||
http-request redirect scheme https
|
||||
|
||||
frontend https_sni_frontend
|
||||
# TCP backend to forward to HTTPS backends based on SNI
|
||||
bind :::443 v4v6
|
||||
mode tcp
|
||||
|
||||
# Wait up to 5s for a SNI header & only accept TLS connections
|
||||
tcp-request inspect-delay 5s
|
||||
tcp-request content capture req.ssl_sni len 255
|
||||
log-format "%ci:%cp -> %[capture.req.hdr(0)] @ %f (%fi:%fp) -> %b (%bi:%bp)"
|
||||
tcp-request content accept if { req.ssl_hello_type 1 }
|
||||
|
||||
###################################################
|
||||
## Rules: forward to HTTPS(S) header-based rules ##
|
||||
###################################################
|
||||
acl use_http_backend req.ssl_sni -i "codeberg.org"
|
||||
acl use_http_backend req.ssl_sni -i "join.codeberg.org"
|
||||
# TODO: use this if no SNI exists
|
||||
use_backend https_termination_backend if use_http_backend
|
||||
|
||||
############################
|
||||
## Rules: HTTPS SNI-based ##
|
||||
############################
|
||||
# use_backend xyz_backend if { req.ssl_sni -i "xyz" }
|
||||
default_backend pages_backend
|
||||
|
||||
frontend https_termination_frontend
|
||||
# Terminate TLS for HTTP backends
|
||||
bind /tmp/haproxy-tls-termination.sock accept-proxy ssl strict-sni alpn h2,http/1.1 crt /etc/ssl/private/haproxy/
|
||||
mode http
|
||||
|
||||
# HSTS (63072000 seconds)
|
||||
http-response set-header Strict-Transport-Security max-age=63072000
|
||||
|
||||
http-request capture req.hdr(Host) len 255
|
||||
log-format "%ci:%cp -> %[capture.req.hdr(0)] @ %f (%fi:%fp) -> %b (%bi:%bp)"
|
||||
|
||||
##################################
|
||||
## Rules: HTTPS(S) header-based ##
|
||||
##################################
|
||||
use_backend gitea_backend if { hdr(host) -i codeberg.org }
|
||||
|
||||
backend https_termination_backend
|
||||
# Redirect to the terminating HTTPS frontend for all HTTP backends
|
||||
server https_termination_server /tmp/haproxy-tls-termination.sock send-proxy-v2-ssl-cn
|
||||
mode tcp
|
||||
|
||||
###############################
|
||||
## Backends: HTTPS SNI-based ##
|
||||
###############################
|
||||
|
||||
backend pages_backend
|
||||
# Pages server is a HTTP backend that uses its own certificates for custom domains
|
||||
server pages_server pages:443
|
||||
mode tcp
|
||||
|
||||
####################################
|
||||
## Backends: HTTP(S) header-based ##
|
||||
####################################
|
||||
|
||||
backend gitea_backend
|
||||
server gitea_server gitea:80
|
||||
mode http
|
|
@ -1 +0,0 @@
|
|||
Hello to Pages!
|
|
@ -1,4 +0,0 @@
|
|||
https://example-page.org
|
||||
|
||||
tls internal
|
||||
file_server
|
|
@ -1,22 +0,0 @@
|
|||
#!/bin/sh
|
||||
if [ $# -gt 0 ]; then
|
||||
exec curl -k --resolve '*:443:127.0.0.1' "$@"
|
||||
fi
|
||||
|
||||
fail() {
|
||||
echo "[FAIL] $@"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "Connecting to Gitea..."
|
||||
res=$(curl https://codeberg.org -sk --resolve '*:443:127.0.0.1' --trace-ascii gitea.dump | tee /dev/stderr)
|
||||
echo "$res" | grep -Fx 'Hello to Gitea!' >/dev/null || fail "Gitea didn't answer"
|
||||
grep '^== Info: issuer: O=mkcert development CA;' gitea.dump || { grep grep '^== Info: issuer:' gitea.dump; fail "Gitea didn't use the correct certificate!"; }
|
||||
|
||||
echo "Connecting to Pages..."
|
||||
res=$(curl https://example-page.org -sk --resolve '*:443:127.0.0.1' --trace-ascii pages.dump | tee /dev/stderr)
|
||||
echo "$res" | grep -Fx 'Hello to Pages!' >/dev/null || fail "Pages didn't answer"
|
||||
grep '^== Info: issuer: CN=Caddy Local Authority\b' pages.dump || { grep '^== Info: issuer:' pages.dump; fail "Pages didn't use the correct certificate!"; }
|
||||
|
||||
echo "All tests succeeded"
|
||||
rm *.dump
|
73
flake.lock
generated
73
flake.lock
generated
|
@ -1,73 +0,0 @@
|
|||
{
|
||||
"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
27
flake.nix
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
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
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
144
go.mod
144
go.mod
|
@ -1,40 +1,126 @@
|
|||
module pages-server
|
||||
module github.com/d7z-project/caddy-gitea-pages
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.4
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
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/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/pelletier/go-toml/v2 v2.1.0
|
||||
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-20240719175910-8a7402abbf56
|
||||
code.gitea.io/sdk/gitea v0.18.0
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/caddyserver/caddy/v2 v2.8.4
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
go.uber.org/zap v1.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/caddyserver/certmagic v0.21.3 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/go-kit/kit v0.13.0 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/golang/glog v1.2.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/cel-go v0.20.1 // indirect
|
||||
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.14.3 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/pgtype v1.14.3 // indirect
|
||||
github.com/jackc/pgx/v4 v4.18.3 // indirect
|
||||
github.com/klauspost/compress v1.17.8 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/libdns/libdns v0.2.2 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez/v2 v2.0.1 // indirect
|
||||
github.com/miekg/dns v1.1.59 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.19.0 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.53.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/quic-go v0.44.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
github.com/slackhq/nebula v1.9.1 // indirect
|
||||
github.com/smallstep/certificates v0.26.1 // indirect
|
||||
github.com/smallstep/nosql v0.6.1 // indirect
|
||||
github.com/smallstep/pkcs7 v0.0.0-20240411202544-a82ada2bab6b // indirect
|
||||
github.com/smallstep/scep v0.0.0-20240214080410-892e41795b99 // indirect
|
||||
github.com/smallstep/truststore v0.13.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/cobra v1.8.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect
|
||||
github.com/urfave/cli v1.22.15 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
go.step.sm/cli-utils v0.9.0 // indirect
|
||||
go.step.sm/crypto v0.46.0 // indirect
|
||||
go.step.sm/linkedca v0.20.1 // indirect
|
||||
go.uber.org/automaxprocs v1.5.3 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap/exp v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240529182030-349231f7e4e4 // indirect
|
||||
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/term v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
)
|
||||
|
|
607
go.sum
607
go.sum
|
@ -1,120 +1,621 @@
|
|||
code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8=
|
||||
code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM=
|
||||
cloud.google.com/go v0.113.0 h1:g3C70mn3lWfckKBiCVsAshabrDg01pQ0pnX1MNtnMkA=
|
||||
cloud.google.com/go v0.113.0/go.mod h1:glEqlogERKYeePz6ZdkcLJ28Q2I6aERgDDErBg9GzO8=
|
||||
cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
|
||||
cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
|
||||
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
|
||||
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
|
||||
cloud.google.com/go/kms v1.17.1 h1:5k0wXqkxL+YcXd4viQzTqCgzzVKKxzgrK+rCZJytEQs=
|
||||
cloud.google.com/go/kms v1.17.1/go.mod h1:DCMnCF/apA6fZk5Cj4XsD979OyHAqFasPuA5Sd0kGlQ=
|
||||
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
|
||||
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
|
||||
code.gitea.io/sdk/gitea v0.18.0 h1:+zZrwVmujIrgobt6wVBWCqITz6bn1aBjnCUHmpZrerI=
|
||||
code.gitea.io/sdk/gitea v0.18.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a h1:Cf4CrDeyrIcuIiJZEZJAH5dapqQ6J3OmP/vHPbDjaFA=
|
||||
github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a/go.mod h1:ig6eVXkYn/9dz0Vm8UdLf+E0u1bE6kBSn3n2hqk6jas=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA=
|
||||
github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs=
|
||||
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-sdk-go-v2 v1.27.0 h1:7bZWKoXhzI+mMR/HjdMx8ZCC5+6fY0lS5tr0bbgiLlo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.27.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.16 h1:knpCuH7laFVGYTNd99Ns5t+8PuRjDn4HnnZK48csipM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.16/go.mod h1:vutqgRhDUktwSge3hrC3nkuirzkJ4E/mLj5GvI0BQas=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.16 h1:7d2QxY83uYl0l58ceyiSpxg9bSbStqBC6BeEeHEchwo=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.16/go.mod h1:Ae6li/6Yc6eMzysRL2BXlPYvnrLLBg3D11/AmOjw50k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3 h1:dQLK4TjtnlRGb0czOht2CevZ5l6RSyRWAnKeGd7VAFE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3/go.mod h1:TL79f2P6+8Q7dTsILpiVST+AL9lkF6PPGI167Ny0Cjw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 h1:lf/8VTF2cM+N4SLzaYJERKEWAXq8MOMpZfU6wEPWsPk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7/go.mod h1:4SjkU7QiqK2M9oozyMzfZ/23LmUY+h3oFqhdeP5OMiI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7 h1:4OYVp0705xu8yjdyoWix0r9wPIRXnIzzOoUpQVHIJ/g=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7/go.mod h1:vd7ESTEvI76T2Na050gODNmNU7+OyKrIKroYTu4ABiI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9 h1:Wx0rlZoEJR7JwlSZcHnEa7CNjrSIyVxMFWGAaXy4fJY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9/go.mod h1:aVMHdE0aHO3v+f/iw01fmXV/5DbfQ3Bi9nN7nd9bE9Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1 h1:FARrQLRQXpCFYylIUVF1dRij6YbPCmtwudq9NBk4kFc=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1/go.mod h1:8lETO9lelSG2B6KMXFh2OwPPqGV6WQM3RqLAEjP1xaU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.9 h1:aD7AGQhvPuAxlSUfo0CWU7s6FpkbyykMhGYMvlqTjVs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.9/go.mod h1:c1qtZUWtygI6ZdvKppzCSXsDOq5I4luJPZ0Ud3juFCA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.3 h1:Pav5q3cA260Zqez42T9UhIlsd9QeypszRPwC9LdSSsQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.3/go.mod h1:9lmoVDVLz/yUZwLaQ676TK02fhCu4+PgRSmMaKR1ozk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.10 h1:69tpbPED7jKPyzMcrwSvhWcJ9bPnZsZs18NT40JwM0g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.10/go.mod h1:0Aqn1MnEuitqfsCNyKsdKLhDUOr4txD/g19EfiUqgws=
|
||||
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/caddyserver/caddy/v2 v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=
|
||||
github.com/caddyserver/caddy/v2 v2.8.4/go.mod h1:vmDAHp3d05JIvuhc24LmnxVlsZmWnUwbP5WMjzcMPWw=
|
||||
github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0=
|
||||
github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI=
|
||||
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
|
||||
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
|
||||
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/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/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/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
|
||||
github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
|
||||
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
|
||||
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
||||
github.com/google/go-cmp v0.5.9/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-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
||||
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
|
||||
github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY=
|
||||
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
||||
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g=
|
||||
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
|
||||
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
|
||||
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgtype v1.14.3 h1:h6W9cPuHsRWQFTWUZMAKMgG5jSwQI0Zurzdvlx3Plus=
|
||||
github.com/jackc/pgtype v1.14.3/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
||||
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
|
||||
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k=
|
||||
github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U=
|
||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
|
||||
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
|
||||
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
|
||||
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
|
||||
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0=
|
||||
github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
|
||||
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slackhq/nebula v1.9.1 h1:E90JNw+QNz9RjbTKho9UGIQicx/Ppvx25MFfm6+Mqek=
|
||||
github.com/slackhq/nebula v1.9.1/go.mod h1:PMJer5rZe0H/O+kUiKOL9AJ/pL9+ryzNXtSN7ABfjfM=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
||||
github.com/smallstep/certificates v0.26.1 h1:FIUliEBcExSfJJDhRFA/s8aZgMIFuorexnRSKQd884o=
|
||||
github.com/smallstep/certificates v0.26.1/go.mod h1:OQMrW39IrGKDViKSHrKcgSQArMZ8c7EcjhYKK7mYqis=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
||||
github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y=
|
||||
github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y=
|
||||
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y=
|
||||
github.com/smallstep/pkcs7 v0.0.0-20240411202544-a82ada2bab6b h1:WwKnv9cYAjKHQ7IXQ/b88kfJjofMcjFjSV8ZXzpcMCk=
|
||||
github.com/smallstep/pkcs7 v0.0.0-20240411202544-a82ada2bab6b/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y=
|
||||
github.com/smallstep/scep v0.0.0-20240214080410-892e41795b99 h1:e85HuLX5/MW15yJ7yWb/PMNFW1Kx1N+DeQtpQnlMUbw=
|
||||
github.com/smallstep/scep v0.0.0-20240214080410-892e41795b99/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU=
|
||||
github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
|
||||
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl1MRtyPXh5hqoxB5I7snOtTHgrn6HU=
|
||||
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM=
|
||||
github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
|
||||
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
|
||||
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
|
||||
go.step.sm/crypto v0.46.0 h1:cuVZMpDbmEsUX+atC24+VineQr4gO+zO46MxbIVai4Y=
|
||||
go.step.sm/crypto v0.46.0/go.mod h1:hcr0oTS2vGRTGSZxoVYxE+RRcsd4xP3rpqt3wPwYGqc=
|
||||
go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU=
|
||||
go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs=
|
||||
go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-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-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
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-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240529182030-349231f7e4e4 h1:4+O65d2kC/+OwkhzSLfWeDqJyhHHsi2LHp/m3fkWQ0I=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240529182030-349231f7e4e4/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
||||
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck=
|
||||
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
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.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
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/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
||||
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.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/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
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/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
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/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
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.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.181.0 h1:rPdjwnWgiPPOJx3IcSAQ2III5aX5tCer6wMpa/xmZi4=
|
||||
google.golang.org/api v0.181.0/go.mod h1:MnQ+M0CFsfUwA5beZ+g/vCBCPXvtmZwRz2qzZk8ih1k=
|
||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
|
||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
|
|
54
html/html.go
54
html/html.go
|
@ -1,54 +0,0 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"text/template" // do not use html/template here, we sanitize the message before passing it to the template
|
||||
|
||||
"pages-server/server/context"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
//go:embed templates/error.html
|
||||
var errorPage string
|
||||
|
||||
var (
|
||||
errorTemplate = template.Must(template.New("error").Parse(errorPage))
|
||||
sanitizer = createBlueMondayPolicy()
|
||||
)
|
||||
|
||||
type TemplateContext struct {
|
||||
StatusCode int
|
||||
StatusText string
|
||||
Message string
|
||||
}
|
||||
|
||||
// ReturnErrorPage sets the response status code and writes the error page to the response body.
|
||||
// The error page contains a sanitized version of the message and the statusCode both in text and numeric form.
|
||||
//
|
||||
// Currently, only the following html tags are supported: <code>
|
||||
func ReturnErrorPage(ctx *context.Context, msg string, statusCode int) {
|
||||
ctx.RespWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
ctx.RespWriter.WriteHeader(statusCode)
|
||||
|
||||
templateContext := TemplateContext{
|
||||
StatusCode: statusCode,
|
||||
StatusText: http.StatusText(statusCode),
|
||||
Message: sanitizer.Sanitize(msg),
|
||||
}
|
||||
|
||||
err := errorTemplate.Execute(ctx.RespWriter, templateContext)
|
||||
if err != nil {
|
||||
log.Err(err).Str("message", msg).Int("status", statusCode).Msg("could not write response")
|
||||
}
|
||||
}
|
||||
|
||||
func createBlueMondayPolicy() *bluemonday.Policy {
|
||||
p := bluemonday.NewPolicy()
|
||||
|
||||
p.AllowElements("code")
|
||||
|
||||
return p
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSanitizerSimpleString(t *testing.T) {
|
||||
str := "simple text message without any html elements"
|
||||
|
||||
assert.Equal(t, str, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithCodeTag(t *testing.T) {
|
||||
str := "simple text message with <code>html</code> tag"
|
||||
|
||||
assert.Equal(t, str, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithCodeTagWithAttribute(t *testing.T) {
|
||||
str := "simple text message with <code id=\"code\">html</code> tag"
|
||||
expected := "simple text message with <code>html</code> tag"
|
||||
|
||||
assert.Equal(t, expected, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithATag(t *testing.T) {
|
||||
str := "simple text message with <a>a link to another page</a>"
|
||||
expected := "simple text message with a link to another page"
|
||||
|
||||
assert.Equal(t, expected, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithATagAndHref(t *testing.T) {
|
||||
str := "simple text message with <a href=\"http://evil.site\">a link to another page</a>"
|
||||
expected := "simple text message with a link to another page"
|
||||
|
||||
assert.Equal(t, expected, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithImgTag(t *testing.T) {
|
||||
str := "simple text message with a <img alt=\"not found\" src=\"http://evil.site\">"
|
||||
expected := "simple text message with a "
|
||||
|
||||
assert.Equal(t, expected, sanitizer.Sanitize(str))
|
||||
}
|
||||
|
||||
func TestSanitizerStringWithImgTagAndOnerrorAttribute(t *testing.T) {
|
||||
str := "simple text message with a <img alt=\"not found\" src=\"http://evil.site\" onerror=\"alert(secret)\">"
|
||||
expected := "simple text message with a "
|
||||
|
||||
assert.Equal(t, expected, sanitizer.Sanitize(str))
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<!doctype html>
|
||||
<html class="codeberg-design">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{{.StatusText}}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://design.codeberg.org/design-kit/codeberg.css" />
|
||||
<link rel="stylesheet" href="https://fonts.codeberg.org/dist/inter/Inter%20Web/inter.css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
code {
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
background-color: silver;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="10em" viewBox="0 0 24 24" fill="var(--blue-color)">
|
||||
<path
|
||||
d="M 9 2 C 5.1458514 2 2 5.1458514 2 9 C 2 12.854149 5.1458514 16 9 16 C 10.747998 16 12.345009 15.348024 13.574219 14.28125 L 14 14.707031 L 14 16 L 19.585938 21.585938 C 20.137937 22.137937 21.033938 22.137938 21.585938 21.585938 C 22.137938 21.033938 22.137938 20.137938 21.585938 19.585938 L 16 14 L 14.707031 14 L 14.28125 13.574219 C 15.348024 12.345009 16 10.747998 16 9 C 16 5.1458514 12.854149 2 9 2 z M 9 4 C 11.773268 4 14 6.2267316 14 9 C 14 11.773268 11.773268 14 9 14 C 6.2267316 14 4 11.773268 4 9 C 4 6.2267316 6.2267316 4 9 4 z"
|
||||
/>
|
||||
</svg>
|
||||
<h1 class="mb-0 text-primary">{{.StatusText}} ({{.StatusCode}})!</h1>
|
||||
<h5 class="text-center" style="max-width: 25em">
|
||||
<p>Sorry, but this page couldn't be served.</p>
|
||||
<p><b>"{{.Message}}"</b></p>
|
||||
<p>
|
||||
We hope this isn't a problem on our end ;) - Make sure to check the
|
||||
<a href="https://docs.codeberg.org/codeberg-pages/troubleshooting/" target="_blank"
|
||||
>troubleshooting section in the Docs</a
|
||||
>!
|
||||
</p>
|
||||
</h5>
|
||||
<small class="text-muted">
|
||||
<img src="https://design.codeberg.org/logo-kit/icon.svg" class="align-top" />
|
||||
Static pages made easy -
|
||||
<a href="https://codeberg.page">Codeberg Pages</a>
|
||||
</small>
|
||||
</body>
|
||||
</html>
|
|
@ -1,282 +0,0 @@
|
|||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetRedirect(t *testing.T) {
|
||||
log.Println("=== TestGetRedirect ===")
|
||||
// test custom domain redirect
|
||||
resp, err := getTestHTTPSClient().Get("https://calciumdibromid.localhost.mock.directory:4430")
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.EqualValues(t, http.StatusTemporaryRedirect, resp.StatusCode) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, "https://www.cabr2.de/", resp.Header.Get("Location"))
|
||||
assert.EqualValues(t, `<a href="https://www.cabr2.de/">Temporary Redirect</a>.`, strings.TrimSpace(string(getBytes(resp.Body))))
|
||||
}
|
||||
|
||||
func TestGetContent(t *testing.T) {
|
||||
log.Println("=== TestGetContent ===")
|
||||
// test get image
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/images/827679288a.jpg")
|
||||
assert.NoError(t, err)
|
||||
if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, "image/jpeg", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "124635", resp.Header.Get("Content-Length"))
|
||||
assert.EqualValues(t, 124635, getSize(resp.Body))
|
||||
assert.Len(t, resp.Header.Get("ETag"), 42)
|
||||
|
||||
// specify branch
|
||||
resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pag/@master/")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.True(t, getSize(resp.Body) > 1000)
|
||||
assert.Len(t, resp.Header.Get("ETag"), 44)
|
||||
|
||||
// access branch name contains '/'
|
||||
resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/blumia/@docs~main/")
|
||||
assert.NoError(t, err)
|
||||
if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.True(t, getSize(resp.Body) > 100)
|
||||
assert.Len(t, resp.Header.Get("ETag"), 44)
|
||||
|
||||
// TODO: test get of non cacheable content (content size > fileCacheSizeLimit)
|
||||
}
|
||||
|
||||
func TestCustomDomain(t *testing.T) {
|
||||
log.Println("=== TestCustomDomain ===")
|
||||
resp, err := getTestHTTPSClient().Get("https://mock-pages.codeberg-test.org:4430/README.md")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/markdown; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "106", resp.Header.Get("Content-Length"))
|
||||
assert.EqualValues(t, 106, getSize(resp.Body))
|
||||
}
|
||||
|
||||
func TestCustomDomainRedirects(t *testing.T) {
|
||||
log.Println("=== TestCustomDomainRedirects ===")
|
||||
// test redirect from default pages domain to custom domain
|
||||
resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/test_pages-server_custom-mock-domain/@main/README.md")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
// TODO: custom port is not evaluated (witch does hurt tests & dev env only)
|
||||
// assert.EqualValues(t, "https://mock-pages.codeberg-test.org:4430/@main/README.md", resp.Header.Get("Location"))
|
||||
assert.EqualValues(t, "https://mock-pages.codeberg-test.org/@main/README.md", resp.Header.Get("Location"))
|
||||
assert.EqualValues(t, `https:/codeberg.org/6543/test_pages-server_custom-mock-domain/src/branch/main/README.md; rel="canonical"; rel="canonical"`, resp.Header.Get("Link"))
|
||||
|
||||
// test redirect from an custom domain to the primary custom domain (www.example.com -> example.com)
|
||||
// regression test to https://codeberg.org/Codeberg/pages-server/issues/153
|
||||
resp, err = getTestHTTPSClient().Get("https://mock-pages-redirect.codeberg-test.org:4430/README.md")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
// TODO: custom port is not evaluated (witch does hurt tests & dev env only)
|
||||
// assert.EqualValues(t, "https://mock-pages.codeberg-test.org:4430/README.md", resp.Header.Get("Location"))
|
||||
assert.EqualValues(t, "https://mock-pages.codeberg-test.org/README.md", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestRawCustomDomain(t *testing.T) {
|
||||
log.Println("=== TestRawCustomDomain ===")
|
||||
// test raw domain response for custom domain branch
|
||||
resp, err := getTestHTTPSClient().Get("https://raw.localhost.mock.directory:4430/cb_pages_tests/raw-test/example") // need cb_pages_tests fork
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "76", resp.Header.Get("Content-Length"))
|
||||
assert.EqualValues(t, 76, getSize(resp.Body))
|
||||
}
|
||||
|
||||
func TestRawIndex(t *testing.T) {
|
||||
log.Println("=== TestRawIndex ===")
|
||||
// test raw domain response for index.html
|
||||
resp, err := getTestHTTPSClient().Get("https://raw.localhost.mock.directory:4430/cb_pages_tests/raw-test/@branch-test/index.html") // need cb_pages_tests fork
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "597", resp.Header.Get("Content-Length"))
|
||||
assert.EqualValues(t, 597, getSize(resp.Body))
|
||||
}
|
||||
|
||||
func TestGetNotFound(t *testing.T) {
|
||||
log.Println("=== TestGetNotFound ===")
|
||||
// test custom not found pages
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pages-404-demo/blah")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusNotFound, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "37", resp.Header.Get("Content-Length"))
|
||||
assert.EqualValues(t, 37, getSize(resp.Body))
|
||||
}
|
||||
|
||||
func TestRedirect(t *testing.T) {
|
||||
log.Println("=== TestRedirect ===")
|
||||
// test redirects
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/some_redirects/redirect")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusMovedPermanently, resp.StatusCode)
|
||||
assert.EqualValues(t, "https://example.com/", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestSPARedirect(t *testing.T) {
|
||||
log.Println("=== TestSPARedirect ===")
|
||||
// test SPA redirects
|
||||
url := "https://cb_pages_tests.localhost.mock.directory:4430/some_redirects/app/aqdjw"
|
||||
resp, err := getTestHTTPSClient().Get(url)
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, url, resp.Request.URL.String())
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "258", resp.Header.Get("Content-Length"))
|
||||
assert.EqualValues(t, 258, getSize(resp.Body))
|
||||
}
|
||||
|
||||
func TestSplatRedirect(t *testing.T) {
|
||||
log.Println("=== TestSplatRedirect ===")
|
||||
// test splat redirects
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/some_redirects/articles/qfopefe")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusMovedPermanently, resp.StatusCode)
|
||||
assert.EqualValues(t, "/posts/qfopefe", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestFollowSymlink(t *testing.T) {
|
||||
log.Printf("=== TestFollowSymlink ===\n")
|
||||
|
||||
// file symlink
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/link")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, "application/octet-stream", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "4", resp.Header.Get("Content-Length"))
|
||||
body := getBytes(resp.Body)
|
||||
assert.EqualValues(t, 4, len(body))
|
||||
assert.EqualValues(t, "abc\n", string(body))
|
||||
|
||||
// relative file links (../index.html file in this case)
|
||||
resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/dir_aim/some/")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "an index\n", string(getBytes(resp.Body)))
|
||||
}
|
||||
|
||||
func TestLFSSupport(t *testing.T) {
|
||||
log.Printf("=== TestLFSSupport ===\n")
|
||||
|
||||
resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
|
||||
body := strings.TrimSpace(string(getBytes(resp.Body)))
|
||||
assert.EqualValues(t, 12, len(body))
|
||||
assert.EqualValues(t, "actual value", body)
|
||||
}
|
||||
|
||||
func TestGetOptions(t *testing.T) {
|
||||
log.Println("=== TestGetOptions ===")
|
||||
req, _ := http.NewRequest(http.MethodOptions, "https://mock-pages.codeberg-test.org:4430/README.md", http.NoBody)
|
||||
resp, err := getTestHTTPSClient().Do(req)
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusNoContent, resp.StatusCode)
|
||||
assert.EqualValues(t, "GET, HEAD, OPTIONS", resp.Header.Get("Allow"))
|
||||
}
|
||||
|
||||
func TestHttpRedirect(t *testing.T) {
|
||||
log.Println("=== TestHttpRedirect ===")
|
||||
resp, err := getTestHTTPSClient().Get("http://mock-pages.codeberg-test.org:8880/README.md")
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, resp) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.EqualValues(t, http.StatusMovedPermanently, resp.StatusCode)
|
||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
assert.EqualValues(t, "https://mock-pages.codeberg-test.org:4430/README.md", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func getTestHTTPSClient() *http.Client {
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
return &http.Client{
|
||||
Jar: cookieJar,
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getBytes(stream io.Reader) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
_, _ = buf.ReadFrom(stream)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func getSize(stream io.Reader) int {
|
||||
buf := new(bytes.Buffer)
|
||||
_, _ = buf.ReadFrom(stream)
|
||||
return buf.Len()
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
cmd "codeberg.org/codeberg/pages/cli"
|
||||
"codeberg.org/codeberg/pages/server"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log.Println("=== TestMain: START Server ===")
|
||||
serverCtx, serverCancel := context.WithCancel(context.Background())
|
||||
if err := startServer(serverCtx); err != nil {
|
||||
log.Fatalf("could not start server: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
serverCancel()
|
||||
log.Println("=== TestMain: Server STOPPED ===")
|
||||
}()
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func startServer(ctx context.Context) error {
|
||||
args := []string{"integration"}
|
||||
setEnvIfNotSet("ACME_API", "https://acme.mock.directory")
|
||||
setEnvIfNotSet("PAGES_DOMAIN", "localhost.mock.directory")
|
||||
setEnvIfNotSet("RAW_DOMAIN", "raw.localhost.mock.directory")
|
||||
setEnvIfNotSet("PAGES_BRANCHES", "pages,main,master")
|
||||
setEnvIfNotSet("PORT", "4430")
|
||||
setEnvIfNotSet("HTTP_PORT", "8880")
|
||||
setEnvIfNotSet("ENABLE_HTTP_SERVER", "true")
|
||||
setEnvIfNotSet("DB_TYPE", "sqlite3")
|
||||
setEnvIfNotSet("GITEA_ROOT", "https://codeberg.org")
|
||||
setEnvIfNotSet("LOG_LEVEL", "trace")
|
||||
setEnvIfNotSet("ENABLE_LFS_SUPPORT", "true")
|
||||
setEnvIfNotSet("ENABLE_SYMLINK_SUPPORT", "true")
|
||||
setEnvIfNotSet("ACME_ACCOUNT_CONFIG", "integration/acme-account.json")
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "pages-server"
|
||||
app.Action = server.Serve
|
||||
app.Flags = cmd.ServerFlags
|
||||
|
||||
go func() {
|
||||
if err := app.RunContext(ctx, args); err != nil {
|
||||
log.Fatalf("run server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setEnvIfNotSet(key, value string) {
|
||||
if _, set := os.LookupEnv(key); !set {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
}
|
21
main.go
21
main.go
|
@ -1,21 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/cli"
|
||||
"pages-server/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.CreatePagesApp()
|
||||
app.Action = server.Serve
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Error().Err(err).Msg("A fatal error occurred")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
15
pages/40x.gohtml
Normal file
15
pages/40x.gohtml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>404 Not Found</title>
|
||||
</head>
|
||||
<Body>
|
||||
<div style="text-align: center;"><h1>404 Not Found</h1></div>
|
||||
<hr>
|
||||
<div style="text-align: center;">Gitea Pages</div>
|
||||
</Body>
|
||||
</html>
|
15
pages/50x.gohtml
Normal file
15
pages/50x.gohtml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>500 Internal Error</title>
|
||||
</head>
|
||||
<Body>
|
||||
<div style="text-align: center;"><h1>500 Internal Error</h1></div>
|
||||
<hr>
|
||||
<div style="text-align: center;">Gitea Pages</div>
|
||||
</Body>
|
||||
</html>
|
19
pages/buf.go
Normal file
19
pages/buf.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
type ByteBuf struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
func NewByteBuf(byte []byte) *ByteBuf {
|
||||
return &ByteBuf{
|
||||
bytes.NewBuffer(byte),
|
||||
}
|
||||
}
|
||||
|
||||
func (b ByteBuf) Close() error {
|
||||
return nil
|
||||
}
|
370
pages/cache_domain.go
Normal file
370
pages/cache_domain.go
Normal file
|
@ -0,0 +1,370 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DomainCache struct {
|
||||
ttl time.Duration
|
||||
*cache.Cache
|
||||
mutexes sync.Map
|
||||
}
|
||||
|
||||
func (c *DomainCache) Close() error {
|
||||
if c.Cache != nil {
|
||||
c.Cache.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DomainConfig struct {
|
||||
FetchTime int64 //上次刷新时间
|
||||
|
||||
PageDomain PageDomain
|
||||
Exists bool // 当前项目是否为 Pages
|
||||
FileCache *cache.Cache // 文件缓存
|
||||
|
||||
CNAME []string // 重定向地址
|
||||
SHA string // 缓存 SHA
|
||||
DATE time.Time // 文件提交时间
|
||||
BasePath string // 根目录
|
||||
Topics map[string]bool // 存储库标记
|
||||
|
||||
Index string //默认页面
|
||||
NotFound string //不存在页面
|
||||
}
|
||||
|
||||
func (receiver *DomainConfig) Close() error {
|
||||
receiver.FileCache.Flush()
|
||||
return nil
|
||||
}
|
||||
func (receiver *DomainConfig) IsRoutePage() bool {
|
||||
return receiver.Topics["routes-history"] || receiver.Topics["routes-hash"]
|
||||
}
|
||||
|
||||
func NewDomainCache(ttl time.Duration, refreshTtl time.Duration) DomainCache {
|
||||
c := cache.New(refreshTtl, 2*refreshTtl)
|
||||
c.OnEvicted(func(_ string, i interface{}) {
|
||||
config := i.(*DomainConfig)
|
||||
if config != nil {
|
||||
err := config.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return DomainCache{
|
||||
ttl: ttl,
|
||||
Cache: c,
|
||||
mutexes: sync.Map{},
|
||||
}
|
||||
}
|
||||
|
||||
func fetch(client *GiteaConfig, domain *PageDomain, result *DomainConfig) error {
|
||||
branches, resp, err := client.Client.ListRepoBranches(domain.Owner, domain.Repo,
|
||||
gitea.ListRepoBranchesOptions{
|
||||
ListOptions: gitea.ListOptions{
|
||||
PageSize: 999,
|
||||
},
|
||||
})
|
||||
// 缓存 404 内容
|
||||
if resp != nil && resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
||||
result.Exists = false
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
topics, resp, err := client.Client.ListRepoTopics(domain.Owner, domain.Repo, gitea.ListRepoTopicsOptions{
|
||||
ListOptions: gitea.ListOptions{
|
||||
PageSize: 999,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
branchIndex := slices.IndexFunc(branches, func(x *gitea.Branch) bool { return x.Name == domain.Branch })
|
||||
if branchIndex == -1 {
|
||||
return errors.Wrap(ErrorNotFound, "branch not found")
|
||||
}
|
||||
currentSHA := branches[branchIndex].Commit.ID
|
||||
commitTime := branches[branchIndex].Commit.Timestamp
|
||||
result.Topics = make(map[string]bool)
|
||||
for _, topic := range topics {
|
||||
result.Topics[strings.ToLower(topic)] = true
|
||||
}
|
||||
if result.SHA == currentSHA {
|
||||
// 历史缓存一致,跳过
|
||||
result.FetchTime = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
// 清理历史缓存
|
||||
if result.SHA != currentSHA {
|
||||
if result.FileCache != nil {
|
||||
result.FileCache.Flush()
|
||||
}
|
||||
result.SHA = currentSHA
|
||||
result.DATE = commitTime
|
||||
}
|
||||
//查询是否为仓库
|
||||
result.Exists, err = client.FileExists(domain, result.BasePath+"/index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !result.Exists {
|
||||
return nil
|
||||
}
|
||||
result.Index = "index.html"
|
||||
//############# 处理 404
|
||||
if result.IsRoutePage() {
|
||||
result.NotFound = "/index.html"
|
||||
} else {
|
||||
notFound, err := client.FileExists(domain, result.BasePath+"/404.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if notFound {
|
||||
result.NotFound = "/404.html"
|
||||
}
|
||||
}
|
||||
// ############ 拉取 CNAME
|
||||
cname, err := client.ReadStringRepoFile(domain, "/CNAME")
|
||||
if err != nil && !errors.Is(err, ErrorNotFound) {
|
||||
// ignore not fond error
|
||||
return err
|
||||
} else if cname != "" {
|
||||
// 清理重定向
|
||||
result.CNAME = make([]string, 0)
|
||||
scanner := bufio.NewScanner(strings.NewReader(cname))
|
||||
for scanner.Scan() {
|
||||
alias := scanner.Text()
|
||||
alias = strings.TrimSpace(alias)
|
||||
alias = strings.TrimPrefix(strings.TrimPrefix(alias, "https://"), "http://")
|
||||
alias = strings.Split(alias, "/")[0]
|
||||
if len(strings.TrimSpace(alias)) > 0 {
|
||||
result.CNAME = append(result.CNAME, alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
result.FetchTime = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (receiver *DomainConfig) tag(path string) string {
|
||||
return fmt.Sprintf("%x", sha1.Sum([]byte(
|
||||
fmt.Sprintf("%s|%s|%s", receiver.SHA, receiver.PageDomain.Key(), path))))
|
||||
}
|
||||
|
||||
func (receiver *DomainConfig) withNotFoundPage(
|
||||
client *GiteaConfig,
|
||||
response *FakeResponse,
|
||||
) error {
|
||||
if receiver.NotFound == "" {
|
||||
// 没有默认页面
|
||||
return ErrorNotFound
|
||||
}
|
||||
notFound, _ := receiver.FileCache.Get(receiver.NotFound)
|
||||
if notFound == nil {
|
||||
// 不存在 notfound
|
||||
domain := &receiver.PageDomain
|
||||
fileContext, err := client.OpenFileContext(domain, receiver.BasePath+receiver.NotFound)
|
||||
if errors.Is(err, ErrorNotFound) {
|
||||
//缓存 not found 不存在
|
||||
receiver.FileCache.Set(receiver.NotFound, make([]byte, 0), cache.DefaultExpiration)
|
||||
return err
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
length, _ := strconv.Atoi(fileContext.Header.Get("Content-Length"))
|
||||
if length > client.CacheMaxSize {
|
||||
client.Logger.Debug("default page too large.")
|
||||
response.Body = fileContext.Body
|
||||
response.CacheModeIgnore()
|
||||
} else {
|
||||
// 保存缓存
|
||||
client.Logger.Debug("create default error page.")
|
||||
defer fileContext.Body.Close()
|
||||
defBuf, _ := io.ReadAll(fileContext.Body)
|
||||
receiver.FileCache.Set(receiver.NotFound, defBuf, cache.DefaultExpiration)
|
||||
response.Body = NewByteBuf(defBuf)
|
||||
response.CacheModeMiss()
|
||||
}
|
||||
response.ContentTypeExt(receiver.NotFound)
|
||||
response.Length(length)
|
||||
} else {
|
||||
notFound := notFound.([]byte)
|
||||
if len(notFound) == 0 {
|
||||
// 不存在 NotFound
|
||||
return ErrorNotFound
|
||||
}
|
||||
response.Length(len(notFound))
|
||||
response.Body = NewByteBuf(notFound)
|
||||
response.CacheModeHit()
|
||||
}
|
||||
client.Logger.Debug("use cache error page.")
|
||||
response.ContentTypeExt(receiver.NotFound)
|
||||
if receiver.IsRoutePage() {
|
||||
response.StatusCode = http.StatusOK
|
||||
} else {
|
||||
response.StatusCode = http.StatusNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// todo: 读写加锁
|
||||
func (receiver *DomainConfig) getCachedData(
|
||||
client *GiteaConfig,
|
||||
path string,
|
||||
) (*FakeResponse, error) {
|
||||
result := NewFakeResponse()
|
||||
if strings.HasSuffix(path, "/") {
|
||||
path = path + "index.html"
|
||||
}
|
||||
for k, v := range client.CustomHeaders {
|
||||
result.SetHeader(k, v)
|
||||
}
|
||||
result.ETag(receiver.tag(path))
|
||||
result.ContentTypeExt(path)
|
||||
cacheBuf, _ := receiver.FileCache.Get(path)
|
||||
// 使用缓存内容
|
||||
if cacheBuf != nil {
|
||||
cacheBuf := cacheBuf.([]byte)
|
||||
if len(cacheBuf) == 0 {
|
||||
//使用 NotFound 内容
|
||||
client.Logger.Debug("location not found ,", zap.Any("path", path))
|
||||
return result, receiver.withNotFoundPage(client, result)
|
||||
} else {
|
||||
// 使用缓存
|
||||
client.Logger.Debug("location use cache ,", zap.Any("path", path))
|
||||
result.Body = ByteBuf{
|
||||
bytes.NewBuffer(cacheBuf),
|
||||
}
|
||||
result.Length(len(cacheBuf))
|
||||
result.CacheModeHit()
|
||||
return result, nil
|
||||
}
|
||||
} else {
|
||||
// 添加缓存
|
||||
client.Logger.Debug("location add cache ,", zap.Any("path", path))
|
||||
domain := *(&receiver.PageDomain)
|
||||
domain.Branch = receiver.SHA
|
||||
fileContext, err := client.OpenFileContext(&domain, receiver.BasePath+path)
|
||||
if err != nil && !errors.Is(err, ErrorNotFound) {
|
||||
return nil, err
|
||||
} else if errors.Is(err, ErrorNotFound) {
|
||||
client.Logger.Debug("location not found and src not found,", zap.Any("path", path))
|
||||
// 不存在且源不存在
|
||||
receiver.FileCache.Set(path, make([]byte, 0), cache.DefaultExpiration)
|
||||
return result, receiver.withNotFoundPage(client, result)
|
||||
} else {
|
||||
// 源存在,执行缓存
|
||||
client.Logger.Debug("location found and set cache,", zap.Any("path", path))
|
||||
length, _ := strconv.Atoi(fileContext.Header.Get("Content-Length"))
|
||||
if length > client.CacheMaxSize {
|
||||
client.Logger.Debug("location too large , skip cache.", zap.Any("path", path))
|
||||
// 超过大小,回源
|
||||
result.Body = fileContext.Body
|
||||
result.Length(length)
|
||||
result.CacheModeIgnore()
|
||||
return result, nil
|
||||
} else {
|
||||
client.Logger.Debug("location saved,", zap.Any("path", path))
|
||||
// 未超过大小,缓存
|
||||
body, _ := io.ReadAll(fileContext.Body)
|
||||
receiver.FileCache.Set(path, body, cache.DefaultExpiration)
|
||||
result.Body = NewByteBuf(body)
|
||||
result.Length(len(body))
|
||||
result.CacheModeMiss()
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver *DomainConfig) Copy(
|
||||
client *GiteaConfig,
|
||||
path string,
|
||||
writer http.ResponseWriter,
|
||||
_ *http.Request,
|
||||
) (bool, error) {
|
||||
fakeResp, err := receiver.getCachedData(client, path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for k, v := range fakeResp.Header {
|
||||
for _, s := range v {
|
||||
writer.Header().Add(k, s)
|
||||
}
|
||||
}
|
||||
writer.Header().Add("Pages-Server-Hash", receiver.SHA)
|
||||
writer.Header().Add("Last-Modified", receiver.DATE.UTC().Format(http.TimeFormat))
|
||||
writer.WriteHeader(fakeResp.StatusCode)
|
||||
defer fakeResp.Body.Close()
|
||||
_, err = io.Copy(writer, fakeResp.Body)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// FetchRepo 拉取 Repo 信息
|
||||
func (c *DomainCache) FetchRepo(client *GiteaConfig, domain *PageDomain) (*DomainConfig, bool, error) {
|
||||
nextTime := time.Now().UnixMilli() - c.ttl.Milliseconds()
|
||||
cacheKey := domain.Key()
|
||||
result, _ := c.Get(cacheKey)
|
||||
if result != nil {
|
||||
result := result.(*DomainConfig)
|
||||
if nextTime > result.FetchTime {
|
||||
// 刷新旧的缓存
|
||||
result = nil
|
||||
} else {
|
||||
return result, true, nil
|
||||
|
||||
}
|
||||
}
|
||||
if result == nil {
|
||||
lock := c.Lock(domain)
|
||||
defer lock()
|
||||
if result, find := c.Get(cacheKey); find {
|
||||
return result.(*DomainConfig), true, nil
|
||||
}
|
||||
result = &DomainConfig{
|
||||
PageDomain: *domain,
|
||||
FileCache: cache.New(c.ttl, c.ttl*2),
|
||||
}
|
||||
if err := fetch(client, domain, result.(*DomainConfig)); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
err := c.Add(cacheKey, result, cache.DefaultExpiration)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return result.(*DomainConfig), false, nil
|
||||
} else {
|
||||
return result.(*DomainConfig), true, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *DomainCache) Lock(any *PageDomain) func() {
|
||||
return c.LockAny(any.Key())
|
||||
}
|
||||
|
||||
func (c *DomainCache) LockAny(any string) func() {
|
||||
value, _ := c.mutexes.LoadOrStore(any, &sync.Mutex{})
|
||||
mtx := value.(*sync.Mutex)
|
||||
mtx.Lock()
|
||||
|
||||
return func() { mtx.Unlock() }
|
||||
}
|
111
pages/cache_owner.go
Normal file
111
pages/cache_owner.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OwnerCache struct {
|
||||
ttl time.Duration
|
||||
*cache.Cache
|
||||
mutexes sync.Map
|
||||
}
|
||||
|
||||
func NewOwnerCache(ttl time.Duration, cacheTtl time.Duration) OwnerCache {
|
||||
return OwnerCache{
|
||||
ttl: ttl,
|
||||
mutexes: sync.Map{},
|
||||
Cache: cache.New(cacheTtl, cacheTtl*2),
|
||||
}
|
||||
}
|
||||
|
||||
type OwnerConfig struct {
|
||||
FetchTime int64 `json:"fetch_time,omitempty"`
|
||||
Repos map[string]bool `json:"repos,omitempty"`
|
||||
LowerRepos map[string]bool `json:"lower_repos,omitempty"`
|
||||
}
|
||||
|
||||
func NewOwnerConfig() *OwnerConfig {
|
||||
return &OwnerConfig{
|
||||
Repos: make(map[string]bool),
|
||||
LowerRepos: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// 直接查询 Owner 信息
|
||||
func getOwner(giteaConfig *GiteaConfig, owner string) (*OwnerConfig, error) {
|
||||
result := NewOwnerConfig()
|
||||
repos, resp, err := giteaConfig.Client.ListOrgRepos(owner, gitea.ListOrgReposOptions{
|
||||
ListOptions: gitea.ListOptions{
|
||||
PageSize: 999,
|
||||
},
|
||||
})
|
||||
if err != nil && resp.StatusCode == http.StatusNotFound {
|
||||
// 调用用户接口查询
|
||||
repos, resp, err = giteaConfig.Client.ListUserRepos(owner, gitea.ListReposOptions{
|
||||
ListOptions: gitea.ListOptions{
|
||||
PageSize: 999,
|
||||
},
|
||||
})
|
||||
if err != nil && resp.StatusCode == http.StatusNotFound {
|
||||
return nil, errors.Wrap(ErrorNotFound, err.Error())
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, repo := range repos {
|
||||
result.Repos[repo.Name] = true
|
||||
result.LowerRepos[strings.ToLower(repo.Name)] = true
|
||||
}
|
||||
result.FetchTime = time.Now().UnixMilli()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *OwnerCache) GetOwnerConfig(giteaConfig *GiteaConfig, owner string) (*OwnerConfig, error) {
|
||||
raw, _ := c.Get(owner)
|
||||
// 每固定时间刷新一次
|
||||
nextTime := time.Now().UnixMilli() - c.ttl.Milliseconds()
|
||||
var result *OwnerConfig
|
||||
if raw != nil {
|
||||
result = raw.(*OwnerConfig)
|
||||
if nextTime > result.FetchTime {
|
||||
//移除旧数据
|
||||
c.Delete(owner)
|
||||
raw = nil
|
||||
}
|
||||
}
|
||||
if raw == nil {
|
||||
lock := c.Lock(owner)
|
||||
defer lock()
|
||||
if raw, find := c.Get(owner); find {
|
||||
return raw.(*OwnerConfig), nil
|
||||
}
|
||||
//不存在缓存
|
||||
var err error
|
||||
result, err = getOwner(giteaConfig, owner)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "owner config not found")
|
||||
}
|
||||
c.Set(owner, result, cache.DefaultExpiration)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *OwnerCache) Lock(any string) func() {
|
||||
value, _ := c.mutexes.LoadOrStore(any, &sync.Mutex{})
|
||||
mtx := value.(*sync.Mutex)
|
||||
mtx.Lock()
|
||||
|
||||
return func() { mtx.Unlock() }
|
||||
}
|
||||
|
||||
func (c *OwnerConfig) Exists(repo string) bool {
|
||||
return c.LowerRepos[strings.ToLower(repo)]
|
||||
}
|
88
pages/client.go
Normal file
88
pages/client.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"go.uber.org/zap"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AutoRedirect struct {
|
||||
Enabled bool
|
||||
Scheme string
|
||||
Code int
|
||||
}
|
||||
|
||||
type PageClient struct {
|
||||
BaseDomain string
|
||||
GiteaConfig *GiteaConfig
|
||||
DomainAlias *CustomDomains
|
||||
ErrorPages *ErrorPages
|
||||
AutoRedirect *AutoRedirect
|
||||
OwnerCache *OwnerCache
|
||||
DomainCache *DomainCache
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func (p *PageClient) Close() error {
|
||||
if p.OwnerCache != nil {
|
||||
p.OwnerCache.Cache.Flush()
|
||||
}
|
||||
if p.DomainCache != nil {
|
||||
_ = p.DomainCache.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPageClient(
|
||||
config *MiddlewareConfig,
|
||||
logger *zap.Logger,
|
||||
) (*PageClient, error) {
|
||||
options := make([]gitea.ClientOption, 0)
|
||||
if config.Token != "" {
|
||||
options = append(options, gitea.SetToken(config.Token))
|
||||
}
|
||||
options = append(options, gitea.SetGiteaVersion(""))
|
||||
client, err := gitea.NewClient(config.Server, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alias, err := NewCustomDomains(config.Alias, config.SharedAlias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pages, err := NewErrorPages(config.ErrorPages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ownerCache := NewOwnerCache(config.CacheRefresh, config.CacheTimeout)
|
||||
giteaConfig := &GiteaConfig{
|
||||
Server: config.Server,
|
||||
Token: config.Token,
|
||||
Client: client,
|
||||
Logger: logger,
|
||||
CacheMaxSize: config.CacheMaxSize,
|
||||
CustomHeaders: config.CustomHeaders,
|
||||
}
|
||||
domainCache := NewDomainCache(config.CacheRefresh, config.CacheTimeout)
|
||||
logger.Info("gitea cache ttl " + strconv.FormatInt(config.CacheTimeout.Milliseconds(), 10) + " ms .")
|
||||
return &PageClient{
|
||||
GiteaConfig: giteaConfig,
|
||||
BaseDomain: "." + strings.Trim(config.Domain, "."),
|
||||
DomainAlias: alias,
|
||||
ErrorPages: pages,
|
||||
logger: logger,
|
||||
AutoRedirect: config.AutoRedirect,
|
||||
DomainCache: &domainCache,
|
||||
OwnerCache: &ownerCache,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PageClient) Validate() error {
|
||||
ver, _, err := p.GiteaConfig.Client.ServerVersion()
|
||||
p.logger.Info("Gitea Version ", zap.String("version", ver))
|
||||
if err != nil {
|
||||
p.logger.Warn("Failed to get Gitea version", zap.Error(err))
|
||||
}
|
||||
return nil
|
||||
}
|
98
pages/cname.go
Normal file
98
pages/cname.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var shared = cmap.New[PageDomain]()
|
||||
|
||||
type CustomDomains struct {
|
||||
/// 映射关系
|
||||
Alias *cmap.ConcurrentMap[string, PageDomain] `json:"alias,omitempty"`
|
||||
/// 反向链接
|
||||
Reverse *cmap.ConcurrentMap[string, string] `json:"reverse,omitempty"`
|
||||
/// 写锁
|
||||
Mutex sync.Mutex `json:"-"`
|
||||
/// 文件落盘
|
||||
Local string `json:"-"`
|
||||
// 是否全局共享
|
||||
Share bool `json:"-"`
|
||||
}
|
||||
|
||||
func (d *CustomDomains) Get(host string) (PageDomain, bool) {
|
||||
get, b := d.Alias.Get(strings.ToLower(host))
|
||||
if !b && d.Share {
|
||||
return shared.Get(strings.ToLower(host))
|
||||
}
|
||||
return get, b
|
||||
}
|
||||
|
||||
func (d *CustomDomains) add(domain *PageDomain, aliases ...string) {
|
||||
d.Mutex.Lock()
|
||||
defer d.Mutex.Unlock()
|
||||
for _, alias := range aliases {
|
||||
key := strings.ToLower(domain.Key())
|
||||
alias = strings.ToLower(alias)
|
||||
old, b := d.Reverse.Get(key)
|
||||
if b {
|
||||
// 移除旧的映射关系
|
||||
if d.Share {
|
||||
shared.Remove(old)
|
||||
}
|
||||
d.Alias.Remove(old)
|
||||
d.Reverse.Remove(key)
|
||||
}
|
||||
if d.Share {
|
||||
shared.Set(alias, *domain)
|
||||
}
|
||||
d.Alias.Set(alias, *domain)
|
||||
d.Reverse.Set(key, alias)
|
||||
}
|
||||
if d.Local != "" {
|
||||
marshal, err := json.Marshal(d)
|
||||
err = os.WriteFile(d.Local, marshal, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewCustomDomains(local string, share bool) (*CustomDomains, error) {
|
||||
if share {
|
||||
fmt.Printf("Global Alias Enabled.\n")
|
||||
}
|
||||
stat, err := os.Stat(local)
|
||||
alias := cmap.New[PageDomain]()
|
||||
reverse := cmap.New[string]()
|
||||
result := &CustomDomains{
|
||||
Alias: &alias,
|
||||
Reverse: &reverse,
|
||||
Mutex: sync.Mutex{},
|
||||
Local: local,
|
||||
Share: share,
|
||||
}
|
||||
fmt.Printf("Discover alias file :%s.\n", local)
|
||||
if local != "" && err == nil && !stat.IsDir() {
|
||||
bytes, err := os.ReadFile(local)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(bytes, result)
|
||||
fmt.Printf("Found %d Alias records.\n", result.Alias.Count())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if share {
|
||||
for k, v := range result.Alias.Items() {
|
||||
shared.Set(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
10
pages/error.go
Normal file
10
pages/error.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package pages
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
var (
|
||||
// ErrorNotMatches 确认这不是 Gitea Pages 相关的域名
|
||||
ErrorNotMatches = errors.New("not matching")
|
||||
ErrorNotFound = errors.New("not found")
|
||||
ErrorInternal = errors.New("internal error")
|
||||
)
|
94
pages/failback.go
Normal file
94
pages/failback.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/pkg/errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type ErrorMetadata struct {
|
||||
StatusCode int
|
||||
Request *http.Request
|
||||
Error string
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed 40x.gohtml 50x.gohtml
|
||||
embedPages embed.FS
|
||||
)
|
||||
|
||||
type ErrorPages struct {
|
||||
errorPages map[string]*template.Template
|
||||
}
|
||||
|
||||
func newTemplate(key, text string) (*template.Template, error) {
|
||||
return template.New(key).Funcs(sprig.TxtFuncMap()).Parse(text)
|
||||
}
|
||||
func NewErrorPages(pagesTmpl map[string]string) (*ErrorPages, error) {
|
||||
pages := make(map[string]*template.Template)
|
||||
for key, value := range pagesTmpl {
|
||||
tmpl, err := newTemplate(key, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pages[key] = tmpl
|
||||
}
|
||||
|
||||
if pages["40x"] == nil {
|
||||
data, err := embedPages.ReadFile("40x.gohtml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pages["40x"], err = newTemplate("40x", string(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if pages["50x"] == nil {
|
||||
data, err := embedPages.ReadFile("50x.gohtml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pages["50x"], err = newTemplate("50x", string(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &ErrorPages{
|
||||
errorPages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *ErrorPages) flushError(err error, request *http.Request, writer http.ResponseWriter) error {
|
||||
var code = http.StatusInternalServerError
|
||||
if errors.Is(err, ErrorNotMatches) {
|
||||
// 跳过不匹配
|
||||
return err
|
||||
} else if errors.Is(err, ErrorNotFound) {
|
||||
code = http.StatusNotFound
|
||||
} else {
|
||||
code = http.StatusInternalServerError
|
||||
}
|
||||
var metadata = &ErrorMetadata{
|
||||
StatusCode: code,
|
||||
Request: request,
|
||||
Error: err.Error(),
|
||||
}
|
||||
codeStr := strconv.Itoa(code)
|
||||
writer.Header().Add("Content-Type", "text/html;charset=utf-8")
|
||||
writer.WriteHeader(code)
|
||||
if result := p.errorPages[codeStr]; result != nil {
|
||||
return result.Execute(writer, metadata)
|
||||
}
|
||||
switch {
|
||||
case code >= 400 && code < 500:
|
||||
return p.errorPages["40x"].Execute(writer, metadata)
|
||||
case code >= 500:
|
||||
return p.errorPages["50x"].Execute(writer, metadata)
|
||||
default:
|
||||
return p.errorPages["50x"].Execute(writer, metadata)
|
||||
}
|
||||
}
|
53
pages/fake_resp.go
Normal file
53
pages/fake_resp.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type FakeResponse struct {
|
||||
*http.Response
|
||||
}
|
||||
|
||||
func (r *FakeResponse) Length(len int) {
|
||||
r.ContentLength = int64(len)
|
||||
r.Header.Set("Content-Length", strconv.Itoa(len))
|
||||
}
|
||||
|
||||
func (r *FakeResponse) CacheModeIgnore() {
|
||||
r.CacheMode("SKIP")
|
||||
}
|
||||
func (r *FakeResponse) CacheModeMiss() {
|
||||
r.CacheMode("MISS")
|
||||
}
|
||||
func (r *FakeResponse) CacheModeHit() {
|
||||
r.CacheMode("HIT")
|
||||
}
|
||||
func (r *FakeResponse) CacheMode(mode string) {
|
||||
r.SetHeader("Pages-Server-Cache", mode)
|
||||
}
|
||||
func (r *FakeResponse) ContentTypeExt(path string) {
|
||||
r.ContentType(mime.TypeByExtension(filepath.Ext(path)))
|
||||
}
|
||||
func (r *FakeResponse) ContentType(types string) {
|
||||
r.Header.Set("Content-Type", types)
|
||||
}
|
||||
func (r *FakeResponse) ETag(tag string) {
|
||||
r.Header.Set("ETag", fmt.Sprintf("\"%s\"", tag))
|
||||
}
|
||||
|
||||
func NewFakeResponse() *FakeResponse {
|
||||
return &FakeResponse{
|
||||
&http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FakeResponse) SetHeader(key string, value string) {
|
||||
r.Header.Set(key, value)
|
||||
}
|
85
pages/gitea.go
Normal file
85
pages/gitea.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type GiteaConfig struct {
|
||||
Server string `json:"server"`
|
||||
Token string `json:"token"`
|
||||
Client *gitea.Client `json:"-"`
|
||||
Logger *zap.Logger `json:"-"`
|
||||
CustomHeaders map[string]string `json:"custom_headers"`
|
||||
CacheMaxSize int `json:"max_cache_size"`
|
||||
}
|
||||
|
||||
func (c *GiteaConfig) FileExists(domain *PageDomain, path string) (bool, error) {
|
||||
context, err := c.OpenFileContext(domain, path)
|
||||
if context != nil {
|
||||
defer context.Body.Close()
|
||||
}
|
||||
if errors.Is(err, ErrorNotFound) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *GiteaConfig) ReadStringRepoFile(domain *PageDomain, path string) (string, error) {
|
||||
data, err := c.ReadRepoFile(domain, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *GiteaConfig) ReadRepoFile(domain *PageDomain, path string) ([]byte, error) {
|
||||
context, err := c.OpenFileContext(domain, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer context.Body.Close()
|
||||
all, err := io.ReadAll(context.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (c *GiteaConfig) OpenFileContext(domain *PageDomain, path string) (*http.Response, error) {
|
||||
var (
|
||||
giteaURL string
|
||||
err error
|
||||
)
|
||||
giteaURL, err = url.JoinPath(c.Server+"/api/v1/repos/", domain.Owner, domain.Repo, "media", path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
giteaURL += "?ref=" + url.QueryEscape(domain.Branch)
|
||||
req, err := http.NewRequest(http.MethodGet, giteaURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Authorization", "token "+c.Token)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "")
|
||||
}
|
||||
switch resp.StatusCode {
|
||||
case http.StatusForbidden:
|
||||
return nil, errors.Wrap(ErrorNotFound, "domain file not forbidden")
|
||||
case http.StatusNotFound:
|
||||
return nil, errors.Wrap(ErrorNotFound, fmt.Sprintf("domain file not found: %s", path))
|
||||
case http.StatusOK:
|
||||
default:
|
||||
return nil, errors.Wrap(ErrorInternal, fmt.Sprintf("unexpected status code '%d'", resp.StatusCode))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
17
pages/middle.go
Normal file
17
pages/middle.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package pages
|
||||
|
||||
import "time"
|
||||
|
||||
type MiddlewareConfig struct {
|
||||
Server string `json:"server"`
|
||||
Token string `json:"token"`
|
||||
Domain string `json:"domain"`
|
||||
Alias string `json:"alias"`
|
||||
CacheRefresh time.Duration `json:"cache_refresh"`
|
||||
CacheTimeout time.Duration `json:"cache_timeout"`
|
||||
ErrorPages map[string]string `json:"errors"`
|
||||
CustomHeaders map[string]string `json:"custom_headers"`
|
||||
AutoRedirect *AutoRedirect `json:"redirect"`
|
||||
SharedAlias bool `json:"shared_alias"`
|
||||
CacheMaxSize int `json:"cache_max_size"`
|
||||
}
|
21
pages/page.go
Normal file
21
pages/page.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package pages
|
||||
|
||||
import "fmt"
|
||||
|
||||
type PageDomain struct {
|
||||
Owner string `json:"owner"`
|
||||
Repo string `json:"repo"`
|
||||
Branch string `json:"branch"`
|
||||
}
|
||||
|
||||
func NewPageDomain(owner string, repo string, branch string) *PageDomain {
|
||||
return &PageDomain{
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PageDomain) Key() string {
|
||||
return fmt.Sprintf("%s|%s|%s", p.Owner, p.Repo, p.Branch)
|
||||
}
|
70
pages/parser.go
Normal file
70
pages/parser.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (p *PageClient) parseDomain(request *http.Request) (*PageDomain, string, error) {
|
||||
if strings.Contains(request.Host, "]") {
|
||||
//跳过 ipv6 address 直接访问, 因为仅支持域名的方式
|
||||
return nil, "", ErrorNotMatches
|
||||
}
|
||||
host := strings.Split(request.Host, ":")[0]
|
||||
filePath := request.URL.Path
|
||||
pathTrim := strings.Split(strings.Trim(filePath, "/"), "/")
|
||||
repo := pathTrim[0]
|
||||
// 处理 scheme://domain/path 的情况
|
||||
if !strings.HasPrefix(filePath, fmt.Sprintf("/%s/", repo)) {
|
||||
repo = ""
|
||||
}
|
||||
if strings.HasSuffix(host, p.BaseDomain) {
|
||||
child := strings.Split(strings.TrimSuffix(host, p.BaseDomain), ".")
|
||||
result := NewPageDomain(
|
||||
child[len(child)-1],
|
||||
repo,
|
||||
"gh-pages",
|
||||
)
|
||||
// 处于使用默认 Domain 下
|
||||
config, err := p.OwnerCache.GetOwnerConfig(p.GiteaConfig, result.Owner)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
ownerRepoName := result.Owner + p.BaseDomain
|
||||
if result.Repo == "" && config.Exists(ownerRepoName) {
|
||||
// 推导为默认仓库
|
||||
result.Repo = ownerRepoName
|
||||
return result, filePath, nil
|
||||
} else if result.Repo == "" || !config.Exists(result.Repo) {
|
||||
if config.Exists(ownerRepoName) {
|
||||
result.Repo = ownerRepoName
|
||||
return result, filePath, nil
|
||||
}
|
||||
// 未指定 repo 或者 repo 不存在,跳过
|
||||
return nil, "", errors.Wrap(ErrorNotFound, result.Repo+" not found")
|
||||
}
|
||||
// 存在子目录且仓库存在
|
||||
pathTrim = pathTrim[1:]
|
||||
path := ""
|
||||
if strings.HasSuffix(filePath, "/") {
|
||||
path = "/" + strings.Join(pathTrim, "/") + "/"
|
||||
} else {
|
||||
path = "/" + strings.Join(pathTrim, "/")
|
||||
}
|
||||
path = strings.ReplaceAll(path, "//", "/")
|
||||
path = strings.ReplaceAll(path, "//", "/")
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
return result, path, nil
|
||||
} else {
|
||||
get, exists := p.DomainAlias.Get(host)
|
||||
if exists {
|
||||
return &get, filePath, nil
|
||||
} else {
|
||||
return nil, "", errors.Wrap(ErrorNotFound, "")
|
||||
}
|
||||
}
|
||||
}
|
59
pages/route.go
Normal file
59
pages/route.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (p *PageClient) Route(writer http.ResponseWriter, request *http.Request) error {
|
||||
defer func() error {
|
||||
//放在匿名函数里,err捕获到错误信息,并且输出
|
||||
err := recover()
|
||||
if err != nil {
|
||||
p.logger.Error("recovered from panic", zap.Any("err", err))
|
||||
var buf [4096]byte
|
||||
n := runtime.Stack(buf[:], false)
|
||||
println(string(buf[:n]))
|
||||
return p.ErrorPages.flushError(errors.New(fmt.Sprintf("%v", err)), request, writer)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
err := p.RouteExists(writer, request)
|
||||
if err != nil {
|
||||
p.logger.Debug("route exists error", zap.String("host", request.Host),
|
||||
zap.String("path", request.RequestURI), zap.Error(err))
|
||||
return p.ErrorPages.flushError(err, request, writer)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *PageClient) RouteExists(writer http.ResponseWriter, request *http.Request) error {
|
||||
domain, filePath, err := p.parseDomain(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config, cache, err := p.DomainCache.FetchRepo(p.GiteaConfig, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !config.Exists {
|
||||
return ErrorNotFound
|
||||
}
|
||||
if !cache && len(config.CNAME) > 0 {
|
||||
p.logger.Info("Add CNAME link.", zap.Any("CNAME", config.CNAME))
|
||||
p.DomainAlias.add(domain, config.CNAME...)
|
||||
}
|
||||
// 跳过 30x 重定向
|
||||
if p.AutoRedirect.Enabled &&
|
||||
len(config.CNAME) > 0 &&
|
||||
strings.HasPrefix(request.Host, domain.Owner+p.BaseDomain) {
|
||||
http.Redirect(writer, request, p.AutoRedirect.Scheme+"://"+config.CNAME[0], p.AutoRedirect.Code)
|
||||
return nil
|
||||
}
|
||||
_, err = config.Copy(p.GiteaConfig, filePath, writer, request)
|
||||
return err
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"$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"]
|
||||
}
|
10
server/cache/interface.go
vendored
10
server/cache/interface.go
vendored
|
@ -1,10 +0,0 @@
|
|||
package cache
|
||||
|
||||
import "time"
|
||||
|
||||
// ICache is an interface that defines how the pages server interacts with the cache.
|
||||
type ICache interface {
|
||||
Set(key string, value interface{}, ttl time.Duration) error
|
||||
Get(key string) (interface{}, bool)
|
||||
Remove(key string)
|
||||
}
|
7
server/cache/memory.go
vendored
7
server/cache/memory.go
vendored
|
@ -1,7 +0,0 @@
|
|||
package cache
|
||||
|
||||
import "github.com/OrlovEvgeny/go-mcache"
|
||||
|
||||
func NewInMemoryCache() ICache {
|
||||
return mcache.New()
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
stdContext "context"
|
||||
"net/http"
|
||||
|
||||
"pages-server/server/utils"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
RespWriter http.ResponseWriter
|
||||
Req *http.Request
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func New(w http.ResponseWriter, r *http.Request) *Context {
|
||||
return &Context{
|
||||
RespWriter: w,
|
||||
Req: r,
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) Context() stdContext.Context {
|
||||
if c.Req != nil {
|
||||
return c.Req.Context()
|
||||
}
|
||||
return stdContext.Background()
|
||||
}
|
||||
|
||||
func (c *Context) Response() *http.Response {
|
||||
if c.Req != nil && c.Req.Response != nil {
|
||||
return c.Req.Response
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Context) String(raw string, status ...int) {
|
||||
code := http.StatusOK
|
||||
if len(status) != 0 {
|
||||
code = status[0]
|
||||
}
|
||||
c.RespWriter.WriteHeader(code)
|
||||
_, _ = c.RespWriter.Write([]byte(raw))
|
||||
}
|
||||
|
||||
func (c *Context) Redirect(uri string, statusCode int) {
|
||||
http.Redirect(c.RespWriter, c.Req, uri, statusCode)
|
||||
}
|
||||
|
||||
// Path returns the cleaned requested path.
|
||||
func (c *Context) Path() string {
|
||||
return utils.CleanPath(c.Req.URL.Path)
|
||||
}
|
||||
|
||||
func (c *Context) Host() string {
|
||||
return c.Req.URL.Host
|
||||
}
|
||||
|
||||
func (c *Context) TrimHostPort() string {
|
||||
return utils.TrimHostPort(c.Req.Host)
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
)
|
||||
|
||||
const (
|
||||
lookupCacheValidity = 30 * time.Second
|
||||
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) (targetOwner, targetRepo, targetBranch string) {
|
||||
// Get CNAME or TXT
|
||||
var cname string
|
||||
var err error
|
||||
|
||||
if entry, ok := lookupCache.Get(domain); ok {
|
||||
cname = entry
|
||||
} else {
|
||||
cname, err = net.LookupCNAME(domain)
|
||||
cname = strings.TrimSuffix(cname, ".")
|
||||
if err != nil || !strings.HasSuffix(cname, mainDomainSuffix) {
|
||||
cname = ""
|
||||
// TODO: check if the A record matches!
|
||||
names, err := net.LookupTXT(domain)
|
||||
if err == nil {
|
||||
for _, name := range names {
|
||||
name = strings.TrimSuffix(strings.TrimSpace(name), ".")
|
||||
if strings.HasSuffix(name, mainDomainSuffix) {
|
||||
cname = name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = lookupCache.Add(domain, cname)
|
||||
}
|
||||
if cname == "" {
|
||||
return
|
||||
}
|
||||
cnameParts := strings.Split(strings.TrimSuffix(cname, mainDomainSuffix), ".")
|
||||
targetOwner = cnameParts[len(cnameParts)-1]
|
||||
if len(cnameParts) > 1 {
|
||||
targetRepo = cnameParts[len(cnameParts)-2]
|
||||
}
|
||||
if len(cnameParts) > 2 {
|
||||
targetBranch = cnameParts[len(cnameParts)-3]
|
||||
}
|
||||
if targetRepo == "" {
|
||||
targetRepo = defaultPagesRepo
|
||||
}
|
||||
if targetBranch == "" && targetRepo != defaultPagesRepo {
|
||||
targetBranch = firstDefaultBranch
|
||||
}
|
||||
// if targetBranch is still empty, the caller must find the default branch
|
||||
return
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
package gitea
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/server/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultBranchCacheTimeout specifies the timeout for the default branch cache. It can be quite long.
|
||||
defaultBranchCacheTimeout = 15 * time.Minute
|
||||
|
||||
// branchExistenceCacheTimeout specifies the timeout for the branch timestamp & existence cache. It should be shorter
|
||||
// than fileCacheTimeout, as that gets invalidated if the branch timestamp has changed. That way, repo changes will be
|
||||
// picked up faster, while still allowing the content to be cached longer if nothing changes.
|
||||
branchExistenceCacheTimeout = 5 * time.Minute
|
||||
|
||||
// fileCacheTimeout specifies the timeout for the file content cache - you might want to make this quite long, depending
|
||||
// on your available memory.
|
||||
// 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)
|
||||
)
|
||||
|
||||
type FileResponse struct {
|
||||
Exists bool
|
||||
IsSymlink bool
|
||||
ETag string
|
||||
MimeType string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (f FileResponse) IsEmpty() bool {
|
||||
return len(f.Body) == 0
|
||||
}
|
||||
|
||||
func (f FileResponse) createHttpResponse(cacheKey string) (header http.Header, statusCode int) {
|
||||
header = make(http.Header)
|
||||
|
||||
if f.Exists {
|
||||
statusCode = http.StatusOK
|
||||
} else {
|
||||
statusCode = http.StatusNotFound
|
||||
}
|
||||
|
||||
if f.IsSymlink {
|
||||
header.Set(giteaObjectTypeHeader, objTypeSymlink)
|
||||
}
|
||||
header.Set(ETagHeader, f.ETag)
|
||||
header.Set(ContentTypeHeader, f.MimeType)
|
||||
header.Set(ContentLengthHeader, fmt.Sprintf("%d", len(f.Body)))
|
||||
header.Set(PagesCacheIndicatorHeader, "true")
|
||||
|
||||
log.Trace().Msgf("fileCache for %q used", cacheKey)
|
||||
return header, statusCode
|
||||
}
|
||||
|
||||
type BranchTimestamp struct {
|
||||
Branch string
|
||||
Timestamp time.Time
|
||||
notFound bool
|
||||
}
|
||||
|
||||
type writeCacheReader struct {
|
||||
originalReader io.ReadCloser
|
||||
buffer *bytes.Buffer
|
||||
fileResponse *FileResponse
|
||||
cacheKey string
|
||||
cache cache.ICache
|
||||
hasError bool
|
||||
}
|
||||
|
||||
func (t *writeCacheReader) Read(p []byte) (n int, err error) {
|
||||
log.Trace().Msgf("[cache] read %q", t.cacheKey)
|
||||
n, err = t.originalReader.Read(p)
|
||||
if err != nil && err != io.EOF {
|
||||
log.Trace().Err(err).Msgf("[cache] original reader for %q has returned an error", t.cacheKey)
|
||||
t.hasError = true
|
||||
} else if n > 0 {
|
||||
_, _ = t.buffer.Write(p[:n])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *writeCacheReader) Close() error {
|
||||
doWrite := !t.hasError
|
||||
fc := *t.fileResponse
|
||||
fc.Body = t.buffer.Bytes()
|
||||
if fc.IsEmpty() {
|
||||
log.Trace().Msg("[cache] file response is empty")
|
||||
doWrite = false
|
||||
}
|
||||
if doWrite {
|
||||
err := t.cache.Set(t.cacheKey, fc, fileCacheTimeout)
|
||||
if err != nil {
|
||||
log.Trace().Err(err).Msgf("[cache] writer for %q has returned an error", t.cacheKey)
|
||||
}
|
||||
}
|
||||
log.Trace().Msgf("cacheReader for %q saved=%t closed", t.cacheKey, doWrite)
|
||||
return t.originalReader.Close()
|
||||
}
|
||||
|
||||
func (f FileResponse) CreateCacheReader(r io.ReadCloser, cache cache.ICache, cacheKey string) io.ReadCloser {
|
||||
if r == nil || cache == nil || cacheKey == "" {
|
||||
log.Error().Msg("could not create CacheReader")
|
||||
return nil
|
||||
}
|
||||
|
||||
return &writeCacheReader{
|
||||
originalReader: r,
|
||||
buffer: bytes.NewBuffer(make([]byte, 0)),
|
||||
fileResponse: &f,
|
||||
cache: cache,
|
||||
cacheKey: cacheKey,
|
||||
}
|
||||
}
|
|
@ -1,330 +0,0 @@
|
|||
package gitea
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/config"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/version"
|
||||
)
|
||||
|
||||
var ErrorNotFound = errors.New("not found")
|
||||
|
||||
const (
|
||||
// cache key prefixes
|
||||
branchTimestampCacheKeyPrefix = "branchTime"
|
||||
defaultBranchCacheKeyPrefix = "defaultBranch"
|
||||
rawContentCacheKeyPrefix = "rawContent"
|
||||
ownerExistenceKeyPrefix = "ownerExist"
|
||||
|
||||
// pages server
|
||||
PagesCacheIndicatorHeader = "X-Pages-Cache"
|
||||
symlinkReadLimit = 10000
|
||||
|
||||
// gitea
|
||||
giteaObjectTypeHeader = "X-Gitea-Object-Type"
|
||||
objTypeSymlink = "symlink"
|
||||
|
||||
// std
|
||||
ETagHeader = "ETag"
|
||||
ContentTypeHeader = "Content-Type"
|
||||
ContentLengthHeader = "Content-Length"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
sdkClient *gitea.Client
|
||||
responseCache cache.ICache
|
||||
|
||||
giteaRoot string
|
||||
|
||||
followSymlinks bool
|
||||
supportLFS bool
|
||||
|
||||
forbiddenMimeTypes map[string]bool
|
||||
defaultMimeType string
|
||||
}
|
||||
|
||||
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, fmt.Errorf("invalid forgejo/gitea root url: %w", err)
|
||||
}
|
||||
giteaRoot := strings.TrimSuffix(rootURL.String(), "/")
|
||||
|
||||
stdClient := http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
forbiddenMimeTypes := make(map[string]bool, len(cfg.ForbiddenMimeTypes))
|
||||
for _, mimeType := range cfg.ForbiddenMimeTypes {
|
||||
forbiddenMimeTypes[mimeType] = true
|
||||
}
|
||||
|
||||
defaultMimeType := cfg.DefaultMimeType
|
||||
if defaultMimeType == "" {
|
||||
defaultMimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
sdk, err := gitea.NewClient(
|
||||
giteaRoot,
|
||||
gitea.SetHTTPClient(&stdClient),
|
||||
gitea.SetToken(cfg.Token),
|
||||
gitea.SetUserAgent("pages-server/"+version.Version),
|
||||
)
|
||||
|
||||
return &Client{
|
||||
sdkClient: sdk,
|
||||
responseCache: respCache,
|
||||
|
||||
giteaRoot: giteaRoot,
|
||||
|
||||
followSymlinks: cfg.FollowSymlinks,
|
||||
supportLFS: cfg.LFSEnabled,
|
||||
|
||||
forbiddenMimeTypes: forbiddenMimeTypes,
|
||||
defaultMimeType: defaultMimeType,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (client *Client) ContentWebLink(targetOwner, targetRepo, branch, resource string) string {
|
||||
return path.Join(client.giteaRoot, targetOwner, targetRepo, "src/branch", branch, resource)
|
||||
}
|
||||
|
||||
func (client *Client) GiteaRawContent(targetOwner, targetRepo, ref, resource string) ([]byte, error) {
|
||||
reader, _, _, err := client.ServeRawContent(targetOwner, targetRepo, ref, resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
return io.ReadAll(reader)
|
||||
}
|
||||
|
||||
func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource string) (io.ReadCloser, http.Header, int, error) {
|
||||
cacheKey := fmt.Sprintf("%s/%s/%s|%s|%s", rawContentCacheKeyPrefix, targetOwner, targetRepo, ref, resource)
|
||||
log := log.With().Str("cache_key", cacheKey).Logger()
|
||||
log.Trace().Msg("try file in cache")
|
||||
// handle if cache entry exist
|
||||
if cache, ok := client.responseCache.Get(cacheKey); ok {
|
||||
cache := cache.(FileResponse)
|
||||
cachedHeader, cachedStatusCode := cache.createHttpResponse(cacheKey)
|
||||
// TODO: check against some timestamp mismatch?!?
|
||||
if cache.Exists {
|
||||
log.Debug().Msg("[cache] exists")
|
||||
if cache.IsSymlink {
|
||||
linkDest := string(cache.Body)
|
||||
log.Debug().Msgf("[cache] follow symlink from %q to %q", resource, linkDest)
|
||||
return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest)
|
||||
} else if !cache.IsEmpty() {
|
||||
log.Debug().Msgf("[cache] return %d bytes", len(cache.Body))
|
||||
return io.NopCloser(bytes.NewReader(cache.Body)), cachedHeader, cachedStatusCode, nil
|
||||
} else if cache.IsEmpty() {
|
||||
log.Debug().Msg("[cache] is empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("file not in cache")
|
||||
// not in cache, open reader via gitea api
|
||||
reader, resp, err := client.sdkClient.GetFileReader(targetOwner, targetRepo, ref, resource, client.supportLFS)
|
||||
if resp != nil {
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
// first handle symlinks
|
||||
{
|
||||
objType := resp.Header.Get(giteaObjectTypeHeader)
|
||||
log.Trace().Msgf("server raw content object %q", objType)
|
||||
if client.followSymlinks && objType == objTypeSymlink {
|
||||
defer reader.Close()
|
||||
// read limited chars for symlink
|
||||
linkDestBytes, err := io.ReadAll(io.LimitReader(reader, symlinkReadLimit))
|
||||
if err != nil {
|
||||
return nil, nil, http.StatusInternalServerError, err
|
||||
}
|
||||
linkDest := strings.TrimSpace(string(linkDestBytes))
|
||||
|
||||
// handle relative links
|
||||
// we first remove the link from the path, and make a relative join (resolve parent paths like "/../" too)
|
||||
linkDest = path.Join(path.Dir(resource), linkDest)
|
||||
|
||||
// we store symlink not content to reduce duplicates in cache
|
||||
fileResponse := FileResponse{
|
||||
Exists: true,
|
||||
IsSymlink: true,
|
||||
Body: []byte(linkDest),
|
||||
ETag: resp.Header.Get(ETagHeader),
|
||||
}
|
||||
log.Trace().Msgf("file response has %d bytes", len(fileResponse.Body))
|
||||
if err := client.responseCache.Set(cacheKey, fileResponse, fileCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
|
||||
log.Debug().Msgf("follow symlink from %q to %q", resource, linkDest)
|
||||
return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest)
|
||||
}
|
||||
}
|
||||
|
||||
// now we are sure it's content so set the MIME type
|
||||
mimeType := client.getMimeTypeByExtension(resource)
|
||||
resp.Response.Header.Set(ContentTypeHeader, mimeType)
|
||||
|
||||
if !shouldRespBeSavedToCache(resp.Response) {
|
||||
return reader, resp.Response.Header, resp.StatusCode, err
|
||||
}
|
||||
|
||||
// now we write to cache and respond at the same time
|
||||
fileResp := FileResponse{
|
||||
Exists: true,
|
||||
ETag: resp.Header.Get(ETagHeader),
|
||||
MimeType: mimeType,
|
||||
}
|
||||
return fileResp.CreateCacheReader(reader, client.responseCache, cacheKey), resp.Response.Header, resp.StatusCode, nil
|
||||
|
||||
case http.StatusNotFound:
|
||||
if err := client.responseCache.Set(cacheKey, FileResponse{
|
||||
Exists: false,
|
||||
ETag: resp.Header.Get(ETagHeader),
|
||||
}, fileCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
|
||||
return nil, resp.Response.Header, http.StatusNotFound, ErrorNotFound
|
||||
default:
|
||||
return nil, resp.Response.Header, resp.StatusCode, fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
return nil, nil, http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchName string) (*BranchTimestamp, error) {
|
||||
cacheKey := fmt.Sprintf("%s/%s/%s/%s", branchTimestampCacheKeyPrefix, repoOwner, repoName, branchName)
|
||||
|
||||
if stamp, ok := client.responseCache.Get(cacheKey); ok && stamp != nil {
|
||||
branchTimeStamp := stamp.(*BranchTimestamp)
|
||||
if branchTimeStamp.notFound {
|
||||
log.Trace().Msgf("[cache] use branch %q not found", branchName)
|
||||
return &BranchTimestamp{}, ErrorNotFound
|
||||
}
|
||||
log.Trace().Msgf("[cache] use branch %q exist", branchName)
|
||||
return branchTimeStamp, nil
|
||||
}
|
||||
|
||||
branch, resp, err := client.sdkClient.GetRepoBranch(repoOwner, repoName, branchName)
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
||||
log.Trace().Msgf("[cache] set cache branch %q not found", branchName)
|
||||
if err := client.responseCache.Set(cacheKey, &BranchTimestamp{Branch: branchName, notFound: true}, branchExistenceCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
return &BranchTimestamp{}, ErrorNotFound
|
||||
}
|
||||
return &BranchTimestamp{}, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return &BranchTimestamp{}, fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
|
||||
}
|
||||
|
||||
stamp := &BranchTimestamp{
|
||||
Branch: branch.Name,
|
||||
Timestamp: branch.Commit.Timestamp,
|
||||
}
|
||||
|
||||
log.Trace().Msgf("set cache branch [%s] exist", branchName)
|
||||
if err := client.responseCache.Set(cacheKey, stamp, branchExistenceCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
return stamp, nil
|
||||
}
|
||||
|
||||
func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (string, error) {
|
||||
cacheKey := fmt.Sprintf("%s/%s/%s", defaultBranchCacheKeyPrefix, repoOwner, repoName)
|
||||
|
||||
if branch, ok := client.responseCache.Get(cacheKey); ok && branch != nil {
|
||||
return branch.(string), nil
|
||||
}
|
||||
|
||||
repo, resp, err := client.sdkClient.GetRepo(repoOwner, repoName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
|
||||
}
|
||||
|
||||
branch := repo.DefaultBranch
|
||||
if err := client.responseCache.Set(cacheKey, branch, defaultBranchCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
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)
|
||||
if client.forbiddenMimeTypes[mimeTypeSplit[0]] || mimeType == "" {
|
||||
mimeType = client.defaultMimeType
|
||||
}
|
||||
log.Trace().Msgf("probe mime of %q is %q", resource, mimeType)
|
||||
return mimeType
|
||||
}
|
||||
|
||||
func shouldRespBeSavedToCache(resp *http.Response) bool {
|
||||
if resp == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
contentLengthRaw := resp.Header.Get(ContentLengthHeader)
|
||||
if contentLengthRaw == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
contentLength, err := strconv.ParseInt(contentLengthRaw, 10, 64)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("could not parse content length")
|
||||
}
|
||||
|
||||
// if content to big or could not be determined we not cache it
|
||||
return contentLength > 0 && contentLength < fileCacheSizeLimit
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/config"
|
||||
"pages-server/html"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
const (
|
||||
headerAccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
||||
headerAccessControlAllowMethods = "Access-Control-Allow-Methods"
|
||||
defaultPagesRepo = "pages"
|
||||
)
|
||||
|
||||
// Handler handles a single HTTP request to the web server.
|
||||
func Handler(
|
||||
cfg config.ServerConfig,
|
||||
giteaClient *gitea.Client,
|
||||
canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
log.Debug().Msg("\n----------------------------------------------------------")
|
||||
log := log.With().Strs("Handler", []string{req.Host, req.RequestURI}).Logger()
|
||||
ctx := context.New(w, req)
|
||||
|
||||
ctx.RespWriter.Header().Set("Server", "pages-server")
|
||||
|
||||
// Force new default from specification (since November 2020) - see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#strict-origin-when-cross-origin
|
||||
ctx.RespWriter.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
|
||||
// Enable browser caching for up to 10 minutes
|
||||
ctx.RespWriter.Header().Set("Cache-Control", "public, max-age=600")
|
||||
|
||||
trimmedHost := ctx.TrimHostPort()
|
||||
|
||||
// Add HSTS for RawDomain and MainDomain
|
||||
if hsts := getHSTSHeader(trimmedHost, cfg.MainDomain, cfg.RawDomain); hsts != "" {
|
||||
ctx.RespWriter.Header().Set("Strict-Transport-Security", hsts)
|
||||
}
|
||||
|
||||
// Handle all http methods
|
||||
ctx.RespWriter.Header().Set("Allow", http.MethodGet+", "+http.MethodHead+", "+http.MethodOptions)
|
||||
switch ctx.Req.Method {
|
||||
case http.MethodOptions:
|
||||
// return Allow header
|
||||
ctx.RespWriter.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
case http.MethodGet,
|
||||
http.MethodHead:
|
||||
// end switch case and handle allowed requests
|
||||
break
|
||||
default:
|
||||
// Block all methods not required for static pages
|
||||
ctx.String("Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Block blacklisted paths (like ACME challenges)
|
||||
for _, blacklistedPath := range cfg.BlacklistedPaths {
|
||||
if strings.HasPrefix(ctx.Path(), blacklistedPath) {
|
||||
html.ReturnErrorPage(ctx, "requested path is blacklisted", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Allow CORS for specified domains
|
||||
allowCors := false
|
||||
for _, allowedCorsDomain := range cfg.AllowedCorsDomains {
|
||||
if strings.EqualFold(trimmedHost, allowedCorsDomain) {
|
||||
allowCors = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if allowCors {
|
||||
ctx.RespWriter.Header().Set(headerAccessControlAllowOrigin, "*")
|
||||
ctx.RespWriter.Header().Set(headerAccessControlAllowMethods, http.MethodGet+", "+http.MethodHead)
|
||||
}
|
||||
|
||||
// Prepare request information to Gitea
|
||||
pathElements := strings.Split(strings.Trim(ctx.Path(), "/"), "/")
|
||||
|
||||
if cfg.RawDomain != "" && strings.EqualFold(trimmedHost, cfg.RawDomain) {
|
||||
log.Debug().Msg("raw domain request detected")
|
||||
handleRaw(log, ctx, giteaClient,
|
||||
cfg.MainDomain,
|
||||
trimmedHost,
|
||||
pathElements,
|
||||
canonicalDomainCache, redirectsCache)
|
||||
} else if strings.HasSuffix(trimmedHost, cfg.MainDomain) {
|
||||
log.Debug().Msg("subdomain request detected")
|
||||
handleSubDomain(log, ctx, giteaClient,
|
||||
cfg.MainDomain,
|
||||
cfg.PagesBranches,
|
||||
trimmedHost,
|
||||
pathElements,
|
||||
canonicalDomainCache, redirectsCache)
|
||||
} else {
|
||||
log.Debug().Msg("custom domain request detected")
|
||||
handleCustomDomain(log, ctx, giteaClient,
|
||||
cfg.MainDomain,
|
||||
trimmedHost,
|
||||
pathElements,
|
||||
cfg.PagesBranches[0],
|
||||
canonicalDomainCache, redirectsCache)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"pages-server/html"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/dns"
|
||||
"pages-server/server/gitea"
|
||||
"pages-server/server/upstream"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client,
|
||||
mainDomainSuffix string,
|
||||
trimmedHost string,
|
||||
pathElements []string,
|
||||
firstDefaultBranch string,
|
||||
canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) {
|
||||
// Serve pages from custom domains
|
||||
targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, firstDefaultBranch)
|
||||
if targetOwner == "" {
|
||||
html.ReturnErrorPage(ctx,
|
||||
"could not obtain repo owner from custom domain",
|
||||
http.StatusFailedDependency)
|
||||
return
|
||||
}
|
||||
|
||||
pathParts := pathElements
|
||||
canonicalLink := false
|
||||
if strings.HasPrefix(pathElements[0], "@") {
|
||||
targetBranch = pathElements[0][1:]
|
||||
pathParts = pathElements[1:]
|
||||
canonicalLink = true
|
||||
}
|
||||
|
||||
// Try to use the given repo on the given branch or the default branch
|
||||
log.Debug().Msg("custom domain preparations, now trying with details from DNS")
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
TryIndexPages: true,
|
||||
TargetOwner: targetOwner,
|
||||
TargetRepo: targetRepo,
|
||||
TargetBranch: targetBranch,
|
||||
TargetPath: path.Join(pathParts...),
|
||||
}, canonicalLink); works {
|
||||
canonicalDomain, valid := targetOpt.CheckCanonicalDomain(giteaClient, trimmedHost, mainDomainSuffix, 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)
|
||||
if targetOwner != "" {
|
||||
ctx.Redirect("http://"+canonicalDomain+"/"+targetOpt.TargetPath, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
html.ReturnErrorPage(ctx, "target is no codeberg page", http.StatusFailedDependency)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msg("tryBranch, now trying upstream 7")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
|
||||
html.ReturnErrorPage(ctx, "could not find target for custom domain", http.StatusFailedDependency)
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"pages-server/html"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
"pages-server/server/upstream"
|
||||
)
|
||||
|
||||
func handleRaw(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client,
|
||||
mainDomainSuffix string,
|
||||
trimmedHost string,
|
||||
pathElements []string,
|
||||
canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) {
|
||||
// Serve raw content from RawDomain
|
||||
log.Debug().Msg("raw domain")
|
||||
|
||||
if len(pathElements) < 2 {
|
||||
html.ReturnErrorPage(
|
||||
ctx,
|
||||
"a url in the form of <code>http://{domain}/{owner}/{repo}[/@{branch}]/{path}</code> is required",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// raw.codeberg.org/example/myrepo/@main/index.html
|
||||
if len(pathElements) > 2 && strings.HasPrefix(pathElements[2], "@") {
|
||||
log.Debug().Msg("raw domain preparations, now trying with specified branch")
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
ServeRaw: true,
|
||||
TargetOwner: pathElements[0],
|
||||
TargetRepo: pathElements[1],
|
||||
TargetBranch: pathElements[2][1:],
|
||||
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)
|
||||
return
|
||||
}
|
||||
log.Debug().Msg("missing branch info")
|
||||
html.ReturnErrorPage(ctx, "missing branch info", http.StatusFailedDependency)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msg("raw domain preparations, now trying with default branch")
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
TryIndexPages: false,
|
||||
ServeRaw: true,
|
||||
TargetOwner: pathElements[0],
|
||||
TargetRepo: pathElements[1],
|
||||
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)
|
||||
} else {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("raw domain could not find repo <code>%s/%s</code> or repo is empty", targetOpt.TargetOwner, targetOpt.TargetRepo),
|
||||
http.StatusNotFound)
|
||||
}
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"pages-server/html"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
"pages-server/server/upstream"
|
||||
)
|
||||
|
||||
func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client,
|
||||
mainDomainSuffix string,
|
||||
defaultPagesBranches []string,
|
||||
trimmedHost string,
|
||||
pathElements []string,
|
||||
canonicalDomainCache, redirectsCache cache.ICache,
|
||||
) {
|
||||
// Serve pages from subdomains of MainDomainSuffix
|
||||
log.Debug().Msg("main domain suffix")
|
||||
|
||||
targetOwner := strings.TrimSuffix(trimmedHost, mainDomainSuffix)
|
||||
targetRepo := pathElements[0]
|
||||
|
||||
if targetOwner == "www" {
|
||||
// www.codeberg.page redirects to codeberg.page // TODO: rm hardcoded - use cname?
|
||||
ctx.Redirect("http://"+mainDomainSuffix[1:]+ctx.Path(), http.StatusPermanentRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the first directory is a repo with the second directory as a branch
|
||||
// example.codeberg.page/myrepo/@main/index.html
|
||||
if len(pathElements) > 1 && strings.HasPrefix(pathElements[1], "@") {
|
||||
if targetRepo == defaultPagesRepo {
|
||||
// example.codeberg.org/pages/@... redirects to example.codeberg.org/@...
|
||||
ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msg("main domain preparations, now trying with specified repo & branch")
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
TryIndexPages: true,
|
||||
TargetOwner: targetOwner,
|
||||
TargetRepo: pathElements[0],
|
||||
TargetBranch: pathElements[1][1:],
|
||||
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)
|
||||
} else {
|
||||
html.ReturnErrorPage(
|
||||
ctx,
|
||||
formatSetBranchNotFoundMessage(pathElements[1][1:], targetOwner, pathElements[0]),
|
||||
http.StatusFailedDependency,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the first directory is a branch for the defaultPagesRepo
|
||||
// example.codeberg.page/@main/index.html
|
||||
if strings.HasPrefix(pathElements[0], "@") {
|
||||
targetBranch := pathElements[0][1:]
|
||||
|
||||
// if the default pages branch can be determined exactly, it does not need to be set
|
||||
if len(defaultPagesBranches) == 1 && slices.Contains(defaultPagesBranches, targetBranch) {
|
||||
// example.codeberg.org/@pages/... redirects to example.codeberg.org/...
|
||||
ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msg("main domain preparations, now trying with specified branch")
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
TryIndexPages: true,
|
||||
TargetOwner: targetOwner,
|
||||
TargetRepo: defaultPagesRepo,
|
||||
TargetBranch: targetBranch,
|
||||
TargetPath: path.Join(pathElements[1:]...),
|
||||
}, true); works {
|
||||
log.Trace().Msg("tryUpstream: serve default pages repo with specified branch")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
} else {
|
||||
html.ReturnErrorPage(
|
||||
ctx,
|
||||
formatSetBranchNotFoundMessage(targetBranch, targetOwner, defaultPagesRepo),
|
||||
http.StatusFailedDependency,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, defaultPagesBranch := range defaultPagesBranches {
|
||||
// Check if the first directory is a repo with a default pages branch
|
||||
// example.codeberg.page/myrepo/index.html
|
||||
// example.codeberg.page/{PAGES_BRANCHE}/... is not allowed here.
|
||||
log.Debug().Msg("main domain preparations, now trying with specified repo")
|
||||
if pathElements[0] != defaultPagesBranch {
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
TryIndexPages: true,
|
||||
TargetOwner: targetOwner,
|
||||
TargetRepo: pathElements[0],
|
||||
TargetBranch: defaultPagesBranch,
|
||||
TargetPath: path.Join(pathElements[1:]...),
|
||||
}, false); works {
|
||||
log.Debug().Msg("tryBranch, now trying upstream 5")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use the defaultPagesRepo on an default pages branch
|
||||
// example.codeberg.page/index.html
|
||||
log.Debug().Msg("main domain preparations, now trying with default repo")
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
TryIndexPages: true,
|
||||
TargetOwner: targetOwner,
|
||||
TargetRepo: defaultPagesRepo,
|
||||
TargetBranch: defaultPagesBranch,
|
||||
TargetPath: path.Join(pathElements...),
|
||||
}, false); works {
|
||||
log.Debug().Msg("tryBranch, now trying upstream 6")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use the defaultPagesRepo on its default branch
|
||||
// example.codeberg.page/index.html
|
||||
log.Debug().Msg("main domain preparations, now trying with default repo/branch")
|
||||
if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{
|
||||
TryIndexPages: true,
|
||||
TargetOwner: targetOwner,
|
||||
TargetRepo: defaultPagesRepo,
|
||||
TargetPath: path.Join(pathElements...),
|
||||
}, false); works {
|
||||
log.Debug().Msg("tryBranch, now trying upstream 6")
|
||||
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache, redirectsCache)
|
||||
return
|
||||
}
|
||||
|
||||
// Couldn't find a valid repo/branch
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("could not find a valid repository or branch for repository: <code>%s</code>", targetRepo),
|
||||
http.StatusNotFound)
|
||||
}
|
||||
|
||||
func formatSetBranchNotFoundMessage(branch, owner, repo string) string {
|
||||
return fmt.Sprintf("explicitly set branch <code>%q</code> does not exist at <code>%s/%s</code>", branch, owner, repo)
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"pages-server/config"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/gitea"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func TestHandlerPerformance(t *testing.T) {
|
||||
cfg := config.ForgeConfig{
|
||||
Root: "https://codeberg.org",
|
||||
Token: "",
|
||||
LFSEnabled: false,
|
||||
FollowSymlinks: false,
|
||||
}
|
||||
giteaClient, _ := gitea.NewClient(cfg, cache.NewInMemoryCache())
|
||||
serverCfg := config.ServerConfig{
|
||||
MainDomain: "codeberg.page",
|
||||
RawDomain: "raw.codeberg.page",
|
||||
BlacklistedPaths: []string{
|
||||
"/.well-known/acme-challenge/",
|
||||
},
|
||||
AllowedCorsDomains: []string{"raw.codeberg.org", "fonts.codeberg.org", "design.codeberg.org"},
|
||||
PagesBranches: []string{"pages"},
|
||||
}
|
||||
testHandler := Handler(serverCfg, giteaClient, cache.NewInMemoryCache(), cache.NewInMemoryCache())
|
||||
|
||||
testCase := func(uri string, status int) {
|
||||
t.Run(uri, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", uri, http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
log.Printf("Start: %v\n", time.Now())
|
||||
start := time.Now()
|
||||
testHandler(w, req)
|
||||
end := time.Now()
|
||||
log.Printf("Done: %v\n", time.Now())
|
||||
|
||||
resp := w.Result()
|
||||
|
||||
if resp.StatusCode != status {
|
||||
t.Errorf("request failed with status code %d", resp.StatusCode)
|
||||
} else {
|
||||
t.Logf("request took %d milliseconds", end.Sub(start).Milliseconds())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
testCase("https://mondstern.codeberg.page/", 404) // TODO: expect 200
|
||||
testCase("https://codeberg.page/", 404) // TODO: expect 200
|
||||
testCase("https://example.momar.xyz/", 424)
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// getHSTSHeader returns a HSTS header with includeSubdomains & preload for MainDomainSuffix and RawDomain, or an empty
|
||||
// string for custom domains.
|
||||
func getHSTSHeader(host, mainDomainSuffix, rawDomain string) string {
|
||||
if strings.HasSuffix(host, mainDomainSuffix) || strings.EqualFold(host, rawDomain) {
|
||||
return "max-age=63072000; includeSubdomains; preload"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"pages-server/html"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
"pages-server/server/upstream"
|
||||
)
|
||||
|
||||
// tryUpstream forwards the target request to the Gitea API, and shows an error page on failure.
|
||||
func tryUpstream(ctx *context.Context, giteaClient *gitea.Client,
|
||||
mainDomainSuffix, trimmedHost string,
|
||||
options *upstream.Options,
|
||||
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)
|
||||
if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix) {
|
||||
canonicalPath := ctx.Req.RequestURI
|
||||
if options.TargetRepo != defaultPagesRepo {
|
||||
path := strings.SplitN(canonicalPath, "/", 3)
|
||||
if len(path) >= 3 {
|
||||
canonicalPath = "/" + path[2]
|
||||
}
|
||||
}
|
||||
ctx.Redirect("http://"+canonicalDomain+canonicalPath, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add host for debugging.
|
||||
options.Host = trimmedHost
|
||||
|
||||
// Try to request the file from the Gitea API
|
||||
if !options.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
html.ReturnErrorPage(ctx, fmt.Sprintf("Forge returned %d %s", ctx.StatusCode, http.StatusText(ctx.StatusCode)), ctx.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// tryBranch checks if a branch exists and populates the target variables. If canonicalLink is non-empty,
|
||||
// it will also disallow search indexing and add a Link header to the canonical URL.
|
||||
func tryBranch(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client,
|
||||
targetOptions *upstream.Options, canonicalLink bool,
|
||||
) (*upstream.Options, bool) {
|
||||
if targetOptions.TargetOwner == "" || targetOptions.TargetRepo == "" {
|
||||
log.Debug().Msg("tryBranch: owner or repo is empty")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Replace "~" to "/" so we can access branch that contains slash character
|
||||
// Branch name cannot contain "~" so doing this is okay
|
||||
targetOptions.TargetBranch = strings.ReplaceAll(targetOptions.TargetBranch, "~", "/")
|
||||
|
||||
// Check if the branch exists, otherwise treat it as a file path
|
||||
branchExist, _ := targetOptions.GetBranchTimestamp(giteaClient)
|
||||
if !branchExist {
|
||||
log.Debug().Msg("tryBranch: branch doesn't exist")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if canonicalLink {
|
||||
// Hide from search machines & add canonical link
|
||||
ctx.RespWriter.Header().Set("X-Robots-Tag", "noarchive, noindex")
|
||||
ctx.RespWriter.Header().Set("Link", targetOptions.ContentWebLink(giteaClient)+"; rel=\"canonical\"")
|
||||
}
|
||||
|
||||
log.Debug().Msg("tryBranch: true")
|
||||
return targetOptions, true
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func StartProfilingServer(listeningAddress string) {
|
||||
server := &http.Server{
|
||||
Addr: listeningAddress,
|
||||
Handler: http.DefaultServeMux,
|
||||
}
|
||||
|
||||
log.Info().Msgf("Starting debug server on %s", listeningAddress)
|
||||
|
||||
go func() {
|
||||
log.Fatal().Err(server.ListenAndServe()).Msg("Failed to start debug server")
|
||||
}()
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"pages-server/config"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/gitea"
|
||||
"pages-server/server/handler"
|
||||
)
|
||||
|
||||
// Serve sets up and starts the web server.
|
||||
func Serve(ctx *cli.Context) error {
|
||||
// initialize logger with Trace, overridden later with actual level
|
||||
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger().Level(zerolog.TraceLevel)
|
||||
|
||||
cfg, err := config.ReadConfig(ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("could not read config")
|
||||
}
|
||||
|
||||
config.MergeConfig(ctx, cfg)
|
||||
|
||||
// Initialize the logger.
|
||||
logLevel, err := zerolog.ParseLevel(cfg.LogLevel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger().Level(logLevel)
|
||||
|
||||
listeningHTTPAddress := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
|
||||
if cfg.Server.RawDomain != "" {
|
||||
cfg.Server.AllowedCorsDomains = append(cfg.Server.AllowedCorsDomains, cfg.Server.RawDomain)
|
||||
}
|
||||
|
||||
// Make sure MainDomain has a leading dot
|
||||
if !strings.HasPrefix(cfg.Server.MainDomain, ".") {
|
||||
// TODO make this better
|
||||
cfg.Server.MainDomain = "." + cfg.Server.MainDomain
|
||||
}
|
||||
|
||||
if len(cfg.Server.PagesBranches) == 0 {
|
||||
return fmt.Errorf("no default branches set (PAGES_BRANCHES)")
|
||||
}
|
||||
|
||||
// canonicalDomainCache stores canonical domains
|
||||
canonicalDomainCache := 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.Forge, clientResponseCache)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create new gitea client: %v", err)
|
||||
}
|
||||
|
||||
// Create listener
|
||||
log.Info().Msgf("Create TCP listener on %s", listeningHTTPAddress)
|
||||
listener, err := net.Listen("tcp", listeningHTTPAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't create listener: %v", err)
|
||||
}
|
||||
|
||||
// // Create listener for http and start listening
|
||||
// go func() {
|
||||
// log.Info().Msgf("Start HTTP server listening on %s", listeningHTTPAddress)
|
||||
// err := http.ListenAndServe(listeningHTTPAddress, nil)
|
||||
// if err != nil {
|
||||
// log.Error().Err(err).Msg("Couldn't start HTTP server")
|
||||
// }
|
||||
// }()
|
||||
|
||||
if ctx.IsSet("enable-profiling") {
|
||||
StartProfilingServer(ctx.String("profiling-address"))
|
||||
}
|
||||
|
||||
// Create handler based on settings
|
||||
httpHandler := handler.Handler(cfg.Server, giteaClient, canonicalDomainCache, redirectsCache)
|
||||
|
||||
// Start the listener
|
||||
log.Info().Msgf("Start server using TCP listener on %s", listener.Addr())
|
||||
|
||||
return http.Serve(listener, httpHandler)
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
// Check if this request is cached.
|
||||
if cachedValue, ok := canonicalDomainCache.Get(o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch); ok {
|
||||
domains := cachedValue.([]string)
|
||||
for _, domain := range domains {
|
||||
if domain == actualDomain {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return domains[0], valid
|
||||
}
|
||||
|
||||
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig)
|
||||
if err != nil && !errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Error().Err(err).Msgf("could not read %s of %s/%s", canonicalDomainConfig, o.TargetOwner, o.TargetRepo)
|
||||
}
|
||||
|
||||
var domains []string
|
||||
for _, domain := range strings.Split(string(body), "\n") {
|
||||
domain = strings.ToLower(domain)
|
||||
domain = strings.TrimSpace(domain)
|
||||
domain = strings.TrimPrefix(domain, "http://")
|
||||
domain = strings.TrimPrefix(domain, "http://")
|
||||
if domain != "" && !strings.HasPrefix(domain, "#") && !strings.ContainsAny(domain, "\t /") && strings.ContainsRune(domain, '.') {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
if domain == actualDomain {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add [owner].[pages-domain] as valid domain.
|
||||
domains = append(domains, o.TargetOwner+mainDomainSuffix)
|
||||
if domains[len(domains)-1] == actualDomain {
|
||||
valid = true
|
||||
}
|
||||
|
||||
// If the target repository isn't called pages, add `/[repository]` to the
|
||||
// previous valid domain.
|
||||
if o.TargetRepo != "" && o.TargetRepo != "pages" {
|
||||
domains[len(domains)-1] += "/" + o.TargetRepo
|
||||
}
|
||||
|
||||
// Add result to cache.
|
||||
_ = canonicalDomainCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, domains, canonicalDomainCacheTimeout)
|
||||
|
||||
// Return the first domain from the list and return if any of the domains
|
||||
// matched the requested domain.
|
||||
return domains[0], valid
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
// setHeader set values to response header
|
||||
func (o *Options) setHeader(ctx *context.Context, header http.Header) {
|
||||
if eTag := header.Get(gitea.ETagHeader); eTag != "" {
|
||||
ctx.RespWriter.Header().Set(gitea.ETagHeader, eTag)
|
||||
}
|
||||
if cacheIndicator := header.Get(gitea.PagesCacheIndicatorHeader); cacheIndicator != "" {
|
||||
ctx.RespWriter.Header().Set(gitea.PagesCacheIndicatorHeader, cacheIndicator)
|
||||
}
|
||||
if length := header.Get(gitea.ContentLengthHeader); length != "" {
|
||||
ctx.RespWriter.Header().Set(gitea.ContentLengthHeader, length)
|
||||
}
|
||||
if mime := header.Get(gitea.ContentTypeHeader); mime == "" || o.ServeRaw {
|
||||
ctx.RespWriter.Header().Set(gitea.ContentTypeHeader, rawMime)
|
||||
} else {
|
||||
ctx.RespWriter.Header().Set(gitea.ContentTypeHeader, mime)
|
||||
}
|
||||
ctx.RespWriter.Header().Set(headerLastModified, o.BranchTimestamp.In(time.UTC).Format(http.TimeFormat))
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
// GetBranchTimestamp finds the default branch (if branch is "") and save branch and it's last modification time to Options
|
||||
func (o *Options) GetBranchTimestamp(giteaClient *gitea.Client) (bool, error) {
|
||||
log := log.With().Strs("BranchInfo", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch}).Logger()
|
||||
|
||||
if o.TargetBranch == "" {
|
||||
// Get default branch
|
||||
defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(o.TargetOwner, o.TargetRepo)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Couldn't fetch default branch from repository")
|
||||
return false, err
|
||||
}
|
||||
log.Debug().Msgf("Successfully fetched default branch %q from Gitea", defaultBranch)
|
||||
o.TargetBranch = defaultBranch
|
||||
}
|
||||
|
||||
timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(o.TargetOwner, o.TargetRepo, o.TargetBranch)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Error().Err(err).Msg("Could not get latest commit timestamp from branch")
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
if timestamp == nil || timestamp.Branch == "" {
|
||||
return false, fmt.Errorf("empty response")
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Successfully fetched latest commit timestamp from branch: %#v", timestamp)
|
||||
o.BranchTimestamp = timestamp.Timestamp
|
||||
o.TargetBranch = timestamp.Branch
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (o *Options) ContentWebLink(giteaClient *gitea.Client) string {
|
||||
return giteaClient.ContentWebLink(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath) + "; rel=\"canonical\""
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Redirect struct {
|
||||
From string
|
||||
To string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
// rewriteURL returns the destination URL and true if r matches reqURL.
|
||||
func (r *Redirect) rewriteURL(reqURL string) (dstURL string, ok bool) {
|
||||
// check if from url matches request url
|
||||
if strings.TrimSuffix(r.From, "/") == strings.TrimSuffix(reqURL, "/") {
|
||||
return r.To, true
|
||||
}
|
||||
// handle wildcard redirects
|
||||
if strings.HasSuffix(r.From, "/*") {
|
||||
trimmedFromURL := strings.TrimSuffix(r.From, "/*")
|
||||
if reqURL == trimmedFromURL || strings.HasPrefix(reqURL, trimmedFromURL+"/") {
|
||||
if strings.Contains(r.To, ":splat") {
|
||||
matched := strings.TrimPrefix(reqURL, trimmedFromURL)
|
||||
matched = strings.TrimPrefix(matched, "/")
|
||||
return strings.ReplaceAll(r.To, ":splat", matched), true
|
||||
}
|
||||
return r.To, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// redirectsCacheTimeout specifies the timeout for the redirects cache.
|
||||
var redirectsCacheTimeout = 10 * time.Minute
|
||||
|
||||
const redirectsConfig = "_redirects"
|
||||
|
||||
// getRedirects returns redirects specified in the _redirects file.
|
||||
func (o *Options) getRedirects(giteaClient *gitea.Client, redirectsCache cache.ICache) []Redirect {
|
||||
var redirects []Redirect
|
||||
cacheKey := o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch
|
||||
|
||||
// Check for cached redirects
|
||||
if cachedValue, ok := redirectsCache.Get(cacheKey); ok {
|
||||
redirects = cachedValue.([]Redirect)
|
||||
} else {
|
||||
// Get _redirects file and parse
|
||||
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, redirectsConfig)
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(string(body), "\n") {
|
||||
redirectArr := strings.Fields(line)
|
||||
|
||||
// Ignore comments and invalid lines
|
||||
if strings.HasPrefix(line, "#") || len(redirectArr) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get redirect status code
|
||||
statusCode := 301
|
||||
if len(redirectArr) == 3 {
|
||||
statusCode, err = strconv.Atoi(redirectArr[2])
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msgf("could not read %s of %s/%s", redirectsConfig, o.TargetOwner, o.TargetRepo)
|
||||
}
|
||||
}
|
||||
|
||||
redirects = append(redirects, Redirect{
|
||||
From: redirectArr[0],
|
||||
To: redirectArr[1],
|
||||
StatusCode: statusCode,
|
||||
})
|
||||
}
|
||||
}
|
||||
_ = redirectsCache.Set(cacheKey, redirects, redirectsCacheTimeout)
|
||||
}
|
||||
return redirects
|
||||
}
|
||||
|
||||
func (o *Options) matchRedirects(ctx *context.Context, giteaClient *gitea.Client, redirects []Redirect, redirectsCache cache.ICache) (final bool) {
|
||||
reqURL := ctx.Req.RequestURI
|
||||
// remove repo and branch from request url
|
||||
reqURL = strings.TrimPrefix(reqURL, "/"+o.TargetRepo)
|
||||
reqURL = strings.TrimPrefix(reqURL, "/@"+o.TargetBranch)
|
||||
|
||||
for _, redirect := range redirects {
|
||||
if dstURL, ok := redirect.rewriteURL(reqURL); ok {
|
||||
// do rewrite if status code is 200
|
||||
if redirect.StatusCode == 200 {
|
||||
o.TargetPath = dstURL
|
||||
o.Upstream(ctx, giteaClient, redirectsCache)
|
||||
} else {
|
||||
ctx.Redirect(dstURL, redirect.StatusCode)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,225 +0,0 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/html"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
const (
|
||||
headerLastModified = "Last-Modified"
|
||||
headerIfModifiedSince = "If-Modified-Since"
|
||||
|
||||
rawMime = "text/plain; charset=utf-8"
|
||||
)
|
||||
|
||||
var upstreamIndexPages = []string{
|
||||
"index.html",
|
||||
}
|
||||
|
||||
var upstreamNotFoundPages = []string{
|
||||
"404.html",
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
TargetOwner string
|
||||
TargetRepo string
|
||||
TargetBranch string
|
||||
TargetPath string
|
||||
|
||||
Host string
|
||||
|
||||
TryIndexPages bool
|
||||
BranchTimestamp time.Time
|
||||
|
||||
appendTrailingSlash bool
|
||||
redirectIfExists string
|
||||
|
||||
ServeRaw bool
|
||||
}
|
||||
|
||||
func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client, redirectsCache cache.ICache) bool {
|
||||
log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger()
|
||||
|
||||
log.Debug().Msg("Start")
|
||||
|
||||
if o.TargetOwner == "" || o.TargetRepo == "" {
|
||||
html.ReturnErrorPage(ctx, "forge client: either repo owner or name info is missing", http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
|
||||
if o.BranchTimestamp.IsZero() {
|
||||
branchExist, err := o.GetBranchTimestamp(giteaClient)
|
||||
if err != nil && errors.Is(err, gitea.ErrorNotFound) || !branchExist {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("branch <code>%q</code> for <code>%s/%s</code> not found", o.TargetBranch, o.TargetOwner, o.TargetRepo),
|
||||
http.StatusNotFound)
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("could not get timestamp of branch <code>%q</code>: '%v'", o.TargetBranch, err),
|
||||
http.StatusFailedDependency)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Response() != nil {
|
||||
ifModifiedSince, err := time.Parse(time.RFC1123, ctx.Response().Header.Get(headerIfModifiedSince))
|
||||
if err == nil && ifModifiedSince.After(o.BranchTimestamp) {
|
||||
ctx.RespWriter.WriteHeader(http.StatusNotModified)
|
||||
log.Trace().Msg("check response against last modified: valid")
|
||||
return true
|
||||
}
|
||||
log.Trace().Msg("check response against last modified: outdated")
|
||||
}
|
||||
|
||||
reader, header, statusCode, err := giteaClient.ServeRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath)
|
||||
if err != nil {
|
||||
handleGiteaError(ctx, log, err, statusCode)
|
||||
return false
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if reader != nil {
|
||||
reader.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if errors.Is(err, gitea.ErrorNotFound) {
|
||||
handleNotFound(ctx, log, giteaClient, redirectsCache, o)
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil || reader == nil || statusCode != http.StatusOK {
|
||||
handleUnexpectedError(ctx, log, err, statusCode)
|
||||
return false
|
||||
}
|
||||
|
||||
handleRedirects(ctx, log, o, redirectsCache)
|
||||
setHeaders(ctx, header)
|
||||
writeResponse(ctx, reader)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func handleGiteaError(ctx *context.Context, log zerolog.Logger, err error, statusCode int) {
|
||||
var msg string
|
||||
if err != nil {
|
||||
msg = "forge client: returned unexpected error"
|
||||
log.Error().Err(err).Msg(msg)
|
||||
msg = fmt.Sprintf("%s: '%v'", msg, err)
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
msg = fmt.Sprintf("forge client: couldn't fetch contents: <code>%d - %s</code>", statusCode, http.StatusText(statusCode))
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
|
||||
html.ReturnErrorPage(ctx, msg, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func handleNotFound(ctx *context.Context, log zerolog.Logger, giteaClient *gitea.Client, redirectsCache cache.ICache, o *Options) {
|
||||
log.Debug().Msg("Handling not found error")
|
||||
redirects := o.getRedirects(giteaClient, redirectsCache)
|
||||
if o.matchRedirects(ctx, giteaClient, redirects, redirectsCache) {
|
||||
log.Trace().Msg("redirect")
|
||||
return
|
||||
}
|
||||
|
||||
if o.TryIndexPages {
|
||||
log.Trace().Msg("try index page")
|
||||
optionsForIndexPages := *o
|
||||
optionsForIndexPages.TryIndexPages = false
|
||||
optionsForIndexPages.appendTrailingSlash = true
|
||||
for _, indexPage := range upstreamIndexPages {
|
||||
optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("try html file with path name")
|
||||
optionsForIndexPages.appendTrailingSlash = false
|
||||
optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Path(), "/") + ".html"
|
||||
optionsForIndexPages.TargetPath = o.TargetPath + ".html"
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace().Msg("not found")
|
||||
ctx.StatusCode = http.StatusNotFound
|
||||
|
||||
if o.TryIndexPages {
|
||||
log.Trace().Msg("try not found page")
|
||||
optionsForNotFoundPages := *o
|
||||
optionsForNotFoundPages.TryIndexPages = false
|
||||
optionsForNotFoundPages.appendTrailingSlash = false
|
||||
for _, notFoundPage := range upstreamNotFoundPages {
|
||||
optionsForNotFoundPages.TargetPath = "/" + notFoundPage
|
||||
if optionsForNotFoundPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("not found page missing")
|
||||
}
|
||||
}
|
||||
|
||||
func handleUnexpectedError(ctx *context.Context, log zerolog.Logger, err error, statusCode int) {
|
||||
var msg string
|
||||
if err != nil {
|
||||
msg = "forge client: returned unexpected error"
|
||||
log.Error().Err(err).Msg(msg)
|
||||
msg = fmt.Sprintf("%s: '%v'", msg, err)
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
msg = fmt.Sprintf("forge client: couldn't fetch contents: <code>%d - %s</code>", statusCode, http.StatusText(statusCode))
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
|
||||
html.ReturnErrorPage(ctx, msg, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func handleRedirects(ctx *context.Context, log zerolog.Logger, o *Options, redirectsCache cache.ICache) {
|
||||
if o.appendTrailingSlash && !strings.HasSuffix(ctx.Path(), "/") {
|
||||
log.Trace().Msg("append trailing slash and redirect")
|
||||
ctx.Redirect(ctx.Path()+"/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(ctx.Path(), "/index.html") && !o.ServeRaw {
|
||||
log.Trace().Msg("remove index.html from path and redirect")
|
||||
ctx.Redirect(strings.TrimSuffix(ctx.Path(), "index.html"), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
if o.redirectIfExists != "" {
|
||||
ctx.Redirect(o.redirectIfExists, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func setHeaders(ctx *context.Context, header http.Header) {
|
||||
ctx.RespWriter.Header().Set("ETag", header.Get("ETag"))
|
||||
ctx.RespWriter.Header().Set("Content-Type", header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
func writeResponse(ctx *context.Context, reader io.Reader) {
|
||||
ctx.RespWriter.WriteHeader(ctx.StatusCode)
|
||||
if reader != nil {
|
||||
_, err := io.Copy(ctx.RespWriter, reader)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Couldn't write body for %q", ctx.Path())
|
||||
html.ReturnErrorPage(ctx, "", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/html"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
const (
|
||||
headerLastModified = "Last-Modified"
|
||||
headerIfModifiedSince = "If-Modified-Since"
|
||||
|
||||
rawMime = "text/plain; charset=utf-8"
|
||||
)
|
||||
|
||||
// upstreamIndexPages lists pages that may be considered as index pages for directories.
|
||||
var upstreamIndexPages = []string{
|
||||
"index.html",
|
||||
}
|
||||
|
||||
// upstreamNotFoundPages lists pages that may be considered as custom 404 Not Found pages.
|
||||
var upstreamNotFoundPages = []string{
|
||||
"404.html",
|
||||
}
|
||||
|
||||
// Options provides various options for the upstream request.
|
||||
type Options struct {
|
||||
TargetOwner string
|
||||
TargetRepo string
|
||||
TargetBranch string
|
||||
TargetPath string
|
||||
|
||||
// Used for debugging purposes.
|
||||
Host string
|
||||
|
||||
TryIndexPages bool
|
||||
BranchTimestamp time.Time
|
||||
// internal
|
||||
appendTrailingSlash bool
|
||||
redirectIfExists string
|
||||
|
||||
ServeRaw bool
|
||||
}
|
||||
|
||||
// Upstream requests a file from the Gitea API at GiteaRoot and writes it to the request context.
|
||||
func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client, redirectsCache cache.ICache) bool {
|
||||
log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger()
|
||||
|
||||
log.Debug().Msg("Start")
|
||||
|
||||
if o.TargetOwner == "" || o.TargetRepo == "" {
|
||||
html.ReturnErrorPage(ctx, "forge client: either repo owner or name info is missing", http.StatusBadRequest)
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the branch exists and when it was modified
|
||||
if o.BranchTimestamp.IsZero() {
|
||||
branchExist, err := o.GetBranchTimestamp(giteaClient)
|
||||
// handle 404
|
||||
if err != nil && errors.Is(err, gitea.ErrorNotFound) || !branchExist {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("branch <code>%q</code> for <code>%s/%s</code> not found", o.TargetBranch, o.TargetOwner, o.TargetRepo),
|
||||
http.StatusNotFound)
|
||||
return true
|
||||
}
|
||||
|
||||
// handle unexpected errors
|
||||
if err != nil {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("could not get timestamp of branch <code>%q</code>: '%v'", o.TargetBranch, err),
|
||||
http.StatusFailedDependency)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the browser has a cached version
|
||||
if ctx.Response() != nil {
|
||||
if ifModifiedSince, err := time.Parse(time.RFC1123, ctx.Response().Header.Get(headerIfModifiedSince)); err == nil {
|
||||
if ifModifiedSince.After(o.BranchTimestamp) {
|
||||
ctx.RespWriter.WriteHeader(http.StatusNotModified)
|
||||
log.Trace().Msg("check response against last modified: valid")
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("check response against last modified: outdated")
|
||||
}
|
||||
|
||||
log.Debug().Msg("Preparing")
|
||||
|
||||
reader, header, statusCode, err := giteaClient.ServeRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath)
|
||||
if reader != nil {
|
||||
defer reader.Close()
|
||||
}
|
||||
|
||||
log.Debug().Msg("Aquisting")
|
||||
|
||||
// Handle not found error
|
||||
if err != nil && errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Debug().Msg("Handling not found error")
|
||||
// Get and match redirects
|
||||
redirects := o.getRedirects(giteaClient, redirectsCache)
|
||||
if o.matchRedirects(ctx, giteaClient, redirects, redirectsCache) {
|
||||
log.Trace().Msg("redirect")
|
||||
return true
|
||||
}
|
||||
|
||||
if o.TryIndexPages {
|
||||
log.Trace().Msg("try index page")
|
||||
// copy the o struct & try if an index page exists
|
||||
optionsForIndexPages := *o
|
||||
optionsForIndexPages.TryIndexPages = false
|
||||
optionsForIndexPages.appendTrailingSlash = true
|
||||
for _, indexPage := range upstreamIndexPages {
|
||||
optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("try html file with path name")
|
||||
// compatibility fix for GitHub Pages (/example → /example.html)
|
||||
optionsForIndexPages.appendTrailingSlash = false
|
||||
optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Path(), "/") + ".html"
|
||||
optionsForIndexPages.TargetPath = o.TargetPath + ".html"
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace().Msg("not found")
|
||||
|
||||
ctx.StatusCode = http.StatusNotFound
|
||||
if o.TryIndexPages {
|
||||
log.Trace().Msg("try not found page")
|
||||
// copy the o struct & try if a not found page exists
|
||||
optionsForNotFoundPages := *o
|
||||
optionsForNotFoundPages.TryIndexPages = false
|
||||
optionsForNotFoundPages.appendTrailingSlash = false
|
||||
for _, notFoundPage := range upstreamNotFoundPages {
|
||||
optionsForNotFoundPages.TargetPath = "/" + notFoundPage
|
||||
if optionsForNotFoundPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("not found page missing")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// handle unexpected client errors
|
||||
if err != nil || reader == nil || statusCode != http.StatusOK {
|
||||
log.Debug().Msg("Handling error")
|
||||
var msg string
|
||||
|
||||
if err != nil {
|
||||
msg = "forge client: returned unexpected error"
|
||||
log.Error().Err(err).Msg(msg)
|
||||
msg = fmt.Sprintf("%s: '%v'", msg, err)
|
||||
}
|
||||
if reader == nil {
|
||||
msg = "forge client: returned no reader"
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
msg = fmt.Sprintf("forge client: couldn't fetch contents: <code>%d - %s</code>", statusCode, http.StatusText(statusCode))
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
|
||||
html.ReturnErrorPage(ctx, msg, http.StatusInternalServerError)
|
||||
return true
|
||||
}
|
||||
|
||||
// Append trailing slash if missing (for index files), and redirect to fix filenames in general
|
||||
// o.appendTrailingSlash is only true when looking for index pages
|
||||
if o.appendTrailingSlash && !strings.HasSuffix(ctx.Path(), "/") {
|
||||
log.Trace().Msg("append trailing slash and redirect")
|
||||
ctx.Redirect(ctx.Path()+"/", http.StatusTemporaryRedirect)
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(ctx.Path(), "/index.html") && !o.ServeRaw {
|
||||
log.Trace().Msg("remove index.html from path and redirect")
|
||||
ctx.Redirect(strings.TrimSuffix(ctx.Path(), "index.html"), http.StatusTemporaryRedirect)
|
||||
return true
|
||||
}
|
||||
if o.redirectIfExists != "" {
|
||||
ctx.Redirect(o.redirectIfExists, http.StatusTemporaryRedirect)
|
||||
return true
|
||||
}
|
||||
|
||||
// Set ETag & MIME
|
||||
o.setHeader(ctx, header)
|
||||
|
||||
log.Debug().Msg("Prepare response")
|
||||
|
||||
ctx.RespWriter.WriteHeader(ctx.StatusCode)
|
||||
|
||||
// Write the response body to the original request
|
||||
if reader != nil {
|
||||
_, err := io.Copy(ctx.RespWriter, reader)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Couldn't write body for %q", o.TargetPath)
|
||||
html.ReturnErrorPage(ctx, "", http.StatusInternalServerError)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msg("Sending response")
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TrimHostPort(host string) string {
|
||||
i := strings.IndexByte(host, ':')
|
||||
if i >= 0 {
|
||||
return host[:i]
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func CleanPath(uriPath string) string {
|
||||
unescapedPath, _ := url.PathUnescape(uriPath)
|
||||
cleanedPath := path.Join("/", unescapedPath)
|
||||
|
||||
// If the path refers to a directory, add a trailing slash.
|
||||
if !strings.HasSuffix(cleanedPath, "/") && (strings.HasSuffix(unescapedPath, "/") || strings.HasSuffix(unescapedPath, "/.") || strings.HasSuffix(unescapedPath, "/..")) {
|
||||
cleanedPath += "/"
|
||||
}
|
||||
|
||||
return cleanedPath
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTrimHostPort(t *testing.T) {
|
||||
assert.EqualValues(t, "aa", TrimHostPort("aa"))
|
||||
assert.EqualValues(t, "", TrimHostPort(":"))
|
||||
assert.EqualValues(t, "example.com", TrimHostPort("example.com:80"))
|
||||
}
|
||||
|
||||
// TestCleanPath is mostly copied from fasthttp, to keep the behaviour we had before migrating away from it.
|
||||
// Source (MIT licensed): https://github.com/valyala/fasthttp/blob/v1.48.0/uri_test.go#L154
|
||||
// Copyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors
|
||||
func TestCleanPath(t *testing.T) {
|
||||
// double slash
|
||||
testURIPathNormalize(t, "/aa//bb", "/aa/bb")
|
||||
|
||||
// triple slash
|
||||
testURIPathNormalize(t, "/x///y/", "/x/y/")
|
||||
|
||||
// multi slashes
|
||||
testURIPathNormalize(t, "/abc//de///fg////", "/abc/de/fg/")
|
||||
|
||||
// encoded slashes
|
||||
testURIPathNormalize(t, "/xxxx%2fyyy%2f%2F%2F", "/xxxx/yyy/")
|
||||
|
||||
// dotdot
|
||||
testURIPathNormalize(t, "/aaa/..", "/")
|
||||
|
||||
// dotdot with trailing slash
|
||||
testURIPathNormalize(t, "/xxx/yyy/../", "/xxx/")
|
||||
|
||||
// multi dotdots
|
||||
testURIPathNormalize(t, "/aaa/bbb/ccc/../../ddd", "/aaa/ddd")
|
||||
|
||||
// dotdots separated by other data
|
||||
testURIPathNormalize(t, "/a/b/../c/d/../e/..", "/a/c/")
|
||||
|
||||
// too many dotdots
|
||||
testURIPathNormalize(t, "/aaa/../../../../xxx", "/xxx")
|
||||
testURIPathNormalize(t, "/../../../../../..", "/")
|
||||
testURIPathNormalize(t, "/../../../../../../", "/")
|
||||
|
||||
// encoded dotdots
|
||||
testURIPathNormalize(t, "/aaa%2Fbbb%2F%2E.%2Fxxx", "/aaa/xxx")
|
||||
|
||||
// double slash with dotdots
|
||||
testURIPathNormalize(t, "/aaa////..//b", "/b")
|
||||
|
||||
// fake dotdot
|
||||
testURIPathNormalize(t, "/aaa/..bbb/ccc/..", "/aaa/..bbb/")
|
||||
|
||||
// single dot
|
||||
testURIPathNormalize(t, "/a/./b/././c/./d.html", "/a/b/c/d.html")
|
||||
testURIPathNormalize(t, "./foo/", "/foo/")
|
||||
testURIPathNormalize(t, "./../.././../../aaa/bbb/../../../././../", "/")
|
||||
testURIPathNormalize(t, "./a/./.././../b/./foo.html", "/b/foo.html")
|
||||
}
|
||||
|
||||
func testURIPathNormalize(t *testing.T, requestURI, expectedPath string) {
|
||||
cleanedPath := CleanPath(requestURI)
|
||||
if cleanedPath != expectedPath {
|
||||
t.Fatalf("Unexpected path %q. Expected %q. requestURI=%q", cleanedPath, expectedPath, requestURI)
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package version
|
||||
|
||||
var Version string = "dev"
|
Loading…
Add table
Add a link
Reference in a new issue