mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-16 23:47:43 +01:00
Compare commits
168 Commits
feat/agent
...
v0.26.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4a9f44f4e | ||
|
|
ae45854977 | ||
|
|
b272f3ffb7 | ||
|
|
9064a37d62 | ||
|
|
a8f4b8afee | ||
|
|
f8bc8dddb6 | ||
|
|
54ea4d8790 | ||
|
|
fbb8a1fca4 | ||
|
|
af62ac98d3 | ||
|
|
cc516d23db | ||
|
|
f92e96831c | ||
|
|
154149b06d | ||
|
|
35b8a60edb | ||
|
|
d54f0c1411 | ||
|
|
a9aabc0a20 | ||
|
|
0a139067b8 | ||
|
|
be9af03a1e | ||
|
|
898002a38e | ||
|
|
0acedb034a | ||
|
|
1244af0e38 | ||
|
|
d619562f00 | ||
|
|
6fcd570be6 | ||
|
|
8b2da08ec1 | ||
|
|
679045eb29 | ||
|
|
95ac659b1f | ||
|
|
b4eb714553 | ||
|
|
322bb70f02 | ||
|
|
fa9239f5eb | ||
|
|
91f2c4993c | ||
|
|
1a33c0079d | ||
|
|
7fc6c4ace1 | ||
|
|
c9bae6e3a0 | ||
|
|
765c64b7a1 | ||
|
|
22f03488e9 | ||
|
|
bb8b663ebc | ||
|
|
233dd72cb0 | ||
|
|
3c6e931f46 | ||
|
|
3b7a6226ad | ||
|
|
31a7827fab | ||
|
|
1579f490c0 | ||
|
|
a0d0ad0958 | ||
|
|
978dd886c0 | ||
|
|
3aba728a3a | ||
|
|
99f8bc1fb9 | ||
|
|
bd40c46928 | ||
|
|
6da7227f9b | ||
|
|
7eb2a78041 | ||
|
|
e227b9e06f | ||
|
|
5c8126c2e6 | ||
|
|
31b4fedf72 | ||
|
|
bd49f1b348 | ||
|
|
953ec80556 | ||
|
|
fc540ea419 | ||
|
|
211e4ad465 | ||
|
|
0a2df3b9e3 | ||
|
|
fb96a2a4f1 | ||
|
|
fdfb682e2a | ||
|
|
8d56c61826 | ||
|
|
d1fca7e987 | ||
|
|
95f88a6f3c | ||
|
|
c0e2cf63b5 | ||
|
|
6388d07f64 | ||
|
|
15e50322c9 | ||
|
|
3ad6e98a17 | ||
|
|
3b0484f4a5 | ||
|
|
6528fb0a8d | ||
|
|
0f13004ad6 | ||
|
|
d39660e6fa | ||
|
|
4c7d52d89d | ||
|
|
28fd502bd7 | ||
|
|
0716e80345 | ||
|
|
372132b1da | ||
|
|
06be1744ae | ||
|
|
6c6e13704e | ||
|
|
d34b62e2f5 | ||
|
|
e6bd7c2462 | ||
|
|
8b985654ef | ||
|
|
1543ffa19f | ||
|
|
730e3a2ab4 | ||
|
|
ba4af8fe77 | ||
|
|
b788e6e338 | ||
|
|
ef3aa146b5 | ||
|
|
e222e693d7 | ||
|
|
277a485afe | ||
|
|
211c466fc3 | ||
|
|
f96884c62b | ||
|
|
8b4f10f15a | ||
|
|
6c9b1fe45c | ||
|
|
73cba8b508 | ||
|
|
0633cacb2a | ||
|
|
bf5b231e52 | ||
|
|
9cda7febb4 | ||
|
|
b3d4255868 | ||
|
|
9c2051840f | ||
|
|
1a3810db3a | ||
|
|
2335ef0fb1 | ||
|
|
fc73803bc1 | ||
|
|
59953fed30 | ||
|
|
57a2ca26db | ||
|
|
09ddb925a3 | ||
|
|
55e09c02b1 | ||
|
|
9adeb3e3dd | ||
|
|
0f087edfd6 | ||
|
|
c29798a48b | ||
|
|
c202e26559 | ||
|
|
568d24d746 | ||
|
|
cdd1353102 | ||
|
|
b4646b665f | ||
|
|
c191676565 | ||
|
|
9a96f3cc53 | ||
|
|
95a72930b5 | ||
|
|
71e5a507ba | ||
|
|
8f7ef5a015 | ||
|
|
a824e4c8c2 | ||
|
|
62fb690417 | ||
|
|
9f036a61f8 | ||
|
|
cdd60d99cd | ||
|
|
e718cd4c4a | ||
|
|
8ce821adb9 | ||
|
|
92598e05a2 | ||
|
|
1c0cd1ff03 | ||
|
|
630629a3fd | ||
|
|
a1f7375e7b | ||
|
|
dba6a4fedf | ||
|
|
6b752059da | ||
|
|
262d386a97 | ||
|
|
8df7eb2fe5 | ||
|
|
b0dc0e714d | ||
|
|
01b8554c0a | ||
|
|
5e32627363 | ||
|
|
f5047f4dfa | ||
|
|
92f8590edd | ||
|
|
17f87d6ece | ||
|
|
92bf8b196f | ||
|
|
077e0bc03b | ||
|
|
1b55573cc4 | ||
|
|
243a9dc388 | ||
|
|
cfe4587ec4 | ||
|
|
f01cfd8459 | ||
|
|
b1953d86c2 | ||
|
|
46f88964bf | ||
|
|
9d20fdb5c2 | ||
|
|
3cf108569b | ||
|
|
c55157193b | ||
|
|
c5886bd1e3 | ||
|
|
8c71d880cb | ||
|
|
2d0058aebc | ||
|
|
079f5f6ef2 | ||
|
|
7ed6c53f6b | ||
|
|
9d6e3fdc87 | ||
|
|
1e567bc950 | ||
|
|
edcde00dcc | ||
|
|
7d466625d6 | ||
|
|
8399a9ece7 | ||
|
|
966f0ab9c3 | ||
|
|
aaa3c9a8d8 | ||
|
|
bc44de3196 | ||
|
|
12b784d126 | ||
|
|
71f6636cc3 | ||
|
|
cc1fe30045 | ||
|
|
4ec352f1f6 | ||
|
|
df530245bd | ||
|
|
1a022bb3f4 | ||
|
|
2e57ca7743 | ||
|
|
69d04f1b76 | ||
|
|
74f97a6621 | ||
|
|
dc1b70d2d7 | ||
|
|
6fac5d2d3e |
37
.github/workflows/docker-image.yml
vendored
37
.github/workflows/docker-image.yml
vendored
@@ -45,11 +45,37 @@ jobs:
|
||||
attestations: write
|
||||
|
||||
steps:
|
||||
- name: Checkout (for tag resolution)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Compute VERSION for build
|
||||
run: |
|
||||
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||
version="${GITHUB_REF_NAME}"
|
||||
cache_variant="release"
|
||||
elif [ "${GITHUB_REF_NAME}" = "main" ] || [ "${GITHUB_REF_NAME}" = "compat" ]; then
|
||||
git fetch --tags origin main
|
||||
version="$(git describe --tags --abbrev=0 origin/main 2>/dev/null || git describe --tags --abbrev=0 main 2>/dev/null || echo v0.0.0)"
|
||||
cache_variant="${GITHUB_REF_NAME}"
|
||||
else
|
||||
version="v$(date -u +'%Y%m%d-%H%M')"
|
||||
cache_variant="nightly"
|
||||
fi
|
||||
echo "VERSION_FOR_BUILD=$version" >> $GITHUB_ENV
|
||||
echo "CACHE_VARIANT=$cache_variant" >> $GITHUB_ENV
|
||||
if [ "${GITHUB_REF_TYPE}" = "branch" ]; then
|
||||
echo "BRANCH_FOR_BUILD=${GITHUB_REF_NAME}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BRANCH_FOR_BUILD=" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -80,14 +106,15 @@ jobs:
|
||||
file: ${{ env.DOCKERFILE }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}
|
||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
|
||||
type=gha,scope=${{ github.workflow }}-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }}
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }}
|
||||
cache-to: |
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
|
||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||
type=gha,scope=${{ github.workflow }}-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
VERSION=${{ env.VERSION_FOR_BUILD }}
|
||||
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
||||
BRANCH=${{ env.BRANCH_FOR_BUILD }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -14,30 +14,30 @@ data/
|
||||
debug/
|
||||
|
||||
logs/
|
||||
log/
|
||||
|
||||
.vscode/settings.json
|
||||
|
||||
go.work.sum
|
||||
|
||||
!cmd/**/
|
||||
!internal/**/
|
||||
|
||||
todo.md
|
||||
|
||||
.*.swp
|
||||
.aider*
|
||||
mtrace.json
|
||||
.env
|
||||
*.env
|
||||
.cursorrules
|
||||
.cursor/
|
||||
.windsurfrules
|
||||
test.Dockerfile
|
||||
|
||||
node_modules/
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
!agent.compose.yml
|
||||
!agent/pkg/**
|
||||
dev-data/
|
||||
dev-data/
|
||||
|
||||
RELEASE_NOTES.md
|
||||
CLAUDE.md
|
||||
.kilocode/**
|
||||
|
||||
!.trunk/configs
|
||||
|
||||
# minified files
|
||||
**/*-min.*
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,3 +7,6 @@
|
||||
[submodule "goutils"]
|
||||
path = goutils
|
||||
url = https://github.com/yusing/goutils.git
|
||||
[submodule "internal/go-proxmox"]
|
||||
path = internal/go-proxmox
|
||||
url = https://github.com/yusing/go-proxmox
|
||||
|
||||
@@ -47,6 +47,7 @@ linters:
|
||||
errcheck:
|
||||
exclude-functions:
|
||||
- fmt.Fprintln
|
||||
- (*gin.Context).Error # gin context error handler
|
||||
forbidigo:
|
||||
forbid:
|
||||
- pattern: ^print(ln)?$
|
||||
@@ -55,21 +56,15 @@ linters:
|
||||
statements: 120
|
||||
gocyclo:
|
||||
min-complexity: 14
|
||||
godoclint:
|
||||
ignore: internal/api/v1/.+
|
||||
godox:
|
||||
keywords:
|
||||
- FIXME
|
||||
gomoddirectives:
|
||||
replace-allow-list:
|
||||
- github.com/abbot/go-http-auth
|
||||
- github.com/gorilla/mux
|
||||
- github.com/mailgun/minheap
|
||||
- github.com/mailgun/multibuf
|
||||
- github.com/jaguilar/vt100
|
||||
- github.com/cucumber/godog
|
||||
- github.com/http-wasm/http-wasm-host-go
|
||||
govet:
|
||||
disable:
|
||||
- shadow
|
||||
- fieldalignment
|
||||
enable-all: true
|
||||
misspell:
|
||||
locale: US
|
||||
@@ -106,8 +101,7 @@ linters:
|
||||
checks:
|
||||
- all
|
||||
- -SA1019
|
||||
dot-import-whitelist:
|
||||
- github.com/yusing/godoxy/internal/utils/testing
|
||||
- -QF1008 # keep embedded field selector for clarity
|
||||
tagalign:
|
||||
align: false
|
||||
sort: true
|
||||
@@ -135,9 +129,8 @@ linters:
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- internal/api/v1/.+
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
@@ -146,6 +139,7 @@ formatters:
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- internal/api/v1/.+
|
||||
run:
|
||||
tests: false
|
||||
|
||||
2
.trunk/configs/.markdownlint.yaml
Normal file
2
.trunk/configs/.markdownlint.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
# Prettier friendly markdownlint config (all formatting rules disabled)
|
||||
extends: markdownlint/style/prettier
|
||||
7
.trunk/configs/.yamllint.yaml
Normal file
7
.trunk/configs/.yamllint.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
rules:
|
||||
quoted-strings:
|
||||
required: only-when-needed
|
||||
extra-allowed: ["{|}"]
|
||||
key-duplicates: {}
|
||||
octal-values:
|
||||
forbid-implicit-octal: true
|
||||
@@ -7,36 +7,45 @@ cli:
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
ref: v1.7.2
|
||||
ref: v1.7.4
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||
runtimes:
|
||||
enabled:
|
||||
- node@22.16.0
|
||||
- python@3.10.8
|
||||
- go@1.24.3
|
||||
- go@1.26.0
|
||||
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||
lint:
|
||||
disabled:
|
||||
- markdownlint
|
||||
- yamllint
|
||||
- bandit
|
||||
- black
|
||||
- isort
|
||||
- ruff
|
||||
enabled:
|
||||
- checkov@3.2.471
|
||||
- golangci-lint2@2.5.0
|
||||
- yamllint@1.38.0
|
||||
- markdownlint@0.47.0
|
||||
- checkov@3.2.501
|
||||
- golangci-lint2@2.9.0
|
||||
- hadolint@2.14.0
|
||||
- actionlint@1.7.7
|
||||
- actionlint@1.7.10
|
||||
- git-diff-check
|
||||
- gofmt@1.20.4
|
||||
- osv-scanner@2.2.2
|
||||
- oxipng@9.1.5
|
||||
- prettier@3.6.2
|
||||
- osv-scanner@2.3.3
|
||||
- oxipng@10.1.0
|
||||
- prettier@3.8.1
|
||||
- shellcheck@0.11.0
|
||||
- shfmt@3.6.0
|
||||
- trufflehog@3.90.8
|
||||
- trufflehog@3.93.3
|
||||
ignore:
|
||||
- linters: [ALL]
|
||||
paths:
|
||||
- internal/api/v1/docs/**
|
||||
|
||||
actions:
|
||||
disabled:
|
||||
- trunk-announce
|
||||
- trunk-check-pre-push
|
||||
- trunk-fmt-pre-commit
|
||||
enabled:
|
||||
- trunk-upgrade-available
|
||||
- trunk-check-pre-push
|
||||
- trunk-fmt-pre-commit
|
||||
|
||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
yusing@6uo.me.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
13
Dockerfile
13
Dockerfile
@@ -1,10 +1,11 @@
|
||||
# Stage 1: deps
|
||||
FROM golang:1.25.5-alpine AS deps
|
||||
FROM golang:1.26.0-alpine AS deps
|
||||
HEALTHCHECK NONE
|
||||
|
||||
# package version does not matter
|
||||
# libgcc and libstdc++ are needed for bun
|
||||
# trunk-ignore(hadolint/DL3018)
|
||||
RUN apk add --no-cache tzdata make libcap-setcap
|
||||
RUN apk add --no-cache tzdata make libcap-setcap libgcc libstdc++
|
||||
|
||||
ENV GOPATH=/root/go
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
@@ -14,8 +15,13 @@ WORKDIR /src
|
||||
COPY goutils/go.mod goutils/go.sum ./goutils/
|
||||
COPY internal/go-oidc/go.mod internal/go-oidc/go.sum ./internal/go-oidc/
|
||||
COPY internal/gopsutil/go.mod internal/gopsutil/go.sum ./internal/gopsutil/
|
||||
COPY internal/go-proxmox/go.mod internal/go-proxmox/go.sum ./internal/go-proxmox/
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# for minify-js
|
||||
COPY --from=oven/bun:1.3.9-alpine /usr/local/bin/bun /usr/local/bin/bun
|
||||
COPY --from=oven/bun:1.3.9-alpine /usr/local/bin/bunx /usr/local/bin/bunx
|
||||
|
||||
# remove godoxy stuff from go.mod first
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/root/go/pkg/mod \
|
||||
@@ -43,6 +49,9 @@ ENV VERSION=${VERSION}
|
||||
ARG MAKE_ARGS
|
||||
ENV MAKE_ARGS=${MAKE_ARGS}
|
||||
|
||||
ARG BRANCH
|
||||
ENV BRANCH=${BRANCH}
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/root/go/pkg/mod \
|
||||
make ${MAKE_ARGS} docker=1 build
|
||||
|
||||
59
Makefile
59
Makefile
@@ -1,12 +1,20 @@
|
||||
shell := /bin/sh
|
||||
export VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||
export VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null)
|
||||
export BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
||||
export GOOS = linux
|
||||
|
||||
REPO_URL ?= https://github.com/yusing/godoxy
|
||||
|
||||
WEBUI_DIR ?= ../godoxy-webui
|
||||
DOCS_DIR ?= ${WEBUI_DIR}/wiki
|
||||
|
||||
GO_TAGS = sonic
|
||||
ifneq ($(BRANCH), compat)
|
||||
GO_TAGS = sonic
|
||||
else
|
||||
GO_TAGS =
|
||||
endif
|
||||
|
||||
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
|
||||
|
||||
ifeq ($(agent), 1)
|
||||
@@ -84,7 +92,7 @@ docker-build-test:
|
||||
|
||||
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
||||
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
|
||||
gomod_paths := $(shell find . -name go.mod -type f | xargs dirname)
|
||||
gomod_paths := $(shell find . -name go.mod -type f | grep -vE '^./internal/(go-oidc|go-proxmox|gopsutil)/' | xargs dirname)
|
||||
|
||||
update-go:
|
||||
for file in ${files}; do \
|
||||
@@ -109,12 +117,27 @@ mod-tidy:
|
||||
cd ${PWD}/$$path && go mod tidy; \
|
||||
done
|
||||
|
||||
build:
|
||||
minify-js:
|
||||
@if [ "${agent}" = "1" ]; then \
|
||||
echo "minify-js: skipped for agent"; \
|
||||
elif [ "${socket-proxy}" = "1" ]; then \
|
||||
echo "minify-js: skipped for socket-proxy"; \
|
||||
else \
|
||||
for file in $$(find internal/ -name '*.js' | grep -v -- '-min\.js$$'); do \
|
||||
ext="$${file##*.}"; \
|
||||
base="$${file%.*}"; \
|
||||
min_file="$${base}-min.$$ext"; \
|
||||
echo "minifying $$file -> $$min_file"; \
|
||||
bunx --bun uglify-js $$file --compress --mangle --output $$min_file; \
|
||||
done \
|
||||
fi
|
||||
|
||||
build: minify-js
|
||||
mkdir -p $(shell dirname ${BIN_PATH})
|
||||
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
|
||||
${POST_BUILD}
|
||||
|
||||
run:
|
||||
run: minify-js
|
||||
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
|
||||
|
||||
dev:
|
||||
@@ -124,19 +147,12 @@ dev-build: build
|
||||
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
|
||||
|
||||
benchmark:
|
||||
@if [ -z "$(TARGET)" ]; then \
|
||||
docker compose -f dev.compose.yml up -d --force-recreate godoxy traefik caddy nginx; \
|
||||
else \
|
||||
docker compose -f dev.compose.yml up -d --force-recreate $(TARGET); \
|
||||
fi
|
||||
sleep 1
|
||||
@./scripts/benchmark.sh
|
||||
|
||||
dev-run: build
|
||||
cd dev-data && ${BIN_PATH}
|
||||
|
||||
mtrace:
|
||||
${BIN_PATH} debug-ls-mtrace > mtrace.json
|
||||
@TARGETS="$(TARGET)"; \
|
||||
if [ -z "$$TARGETS" ]; then TARGETS="godoxy traefik caddy nginx"; fi; \
|
||||
trap 'docker compose -f dev.compose.yml down $$TARGETS' EXIT; \
|
||||
docker compose -f dev.compose.yml up -d --force-recreate $$TARGETS; \
|
||||
sleep 1; \
|
||||
./scripts/benchmark.sh
|
||||
|
||||
rapid-crash:
|
||||
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
||||
@@ -154,7 +170,7 @@ cloc:
|
||||
scc -w -i go --not-match '_test.go$$'
|
||||
|
||||
push-github:
|
||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||
git push origin $(BRANCH)
|
||||
|
||||
gen-swagger:
|
||||
# go install github.com/swaggo/swag/cmd/swag@latest
|
||||
@@ -170,9 +186,8 @@ gen-swagger-markdown: gen-swagger
|
||||
gen-api-types: gen-swagger
|
||||
# --disable-throw-on-error
|
||||
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
||||
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
|
||||
--responses -o ${WEBUI_DIR}/src/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||
|
||||
.PHONY: update-wiki
|
||||
update-wiki:
|
||||
DOCS_DIR=${DOCS_DIR} bun --bun scripts/update-wiki/main.ts
|
||||
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts
|
||||
|
||||
63
README.md
63
README.md
@@ -33,6 +33,10 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||
- [Proxmox Integration](#proxmox-integration)
|
||||
- [Automatic Route Binding](#automatic-route-binding)
|
||||
- [WebUI Management](#webui-management)
|
||||
- [API Endpoints](#api-endpoints)
|
||||
- [Update / Uninstall system agent](#update--uninstall-system-agent)
|
||||
- [Screenshots](#screenshots)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
@@ -46,8 +50,6 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
|
||||
<https://demo.godoxy.dev>
|
||||
|
||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Simple**
|
||||
@@ -69,7 +71,11 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
- Podman
|
||||
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||
- Docker containers
|
||||
- Proxmox LXCs
|
||||
- Proxmox LXC containers
|
||||
- **Proxmox Integration**
|
||||
- **Automatic route binding**: Routes automatically bind to Proxmox nodes or LXC containers by matching hostname, IP, or alias
|
||||
- **LXC lifecycle control**: Start, stop, restart containers directly from WebUI
|
||||
- **Real-time logs**: Stream journalctl logs from nodes and LXC containers via WebSocket
|
||||
- **Traffic Management**
|
||||
- HTTP reserve proxy
|
||||
- TCP/UDP port forwarding
|
||||
@@ -82,7 +88,12 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
- App Dashboard
|
||||
- Config Editor
|
||||
- Uptime and System Metrics
|
||||
- Docker Logs Viewer
|
||||
- **Docker**
|
||||
- Container lifecycle management (start, stop, restart)
|
||||
- Real-time container logs via WebSocket
|
||||
- **Proxmox**
|
||||
- LXC container lifecycle management (start, stop, restart)
|
||||
- Real-time node and LXC journalctl logs via WebSocket
|
||||
- **Cross-Platform support**
|
||||
- Supports **linux/amd64** and **linux/arm64**
|
||||
- **Efficient and Performant**
|
||||
@@ -130,6 +141,50 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
||||
>
|
||||
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
||||
|
||||
## Proxmox Integration
|
||||
|
||||
GoDoxy can automatically discover and manage Proxmox nodes and LXC containers through configured providers.
|
||||
|
||||
### Automatic Route Binding
|
||||
|
||||
Routes are automatically linked to Proxmox resources through reverse lookup:
|
||||
|
||||
1. **Node-level routes** (VMID = 0): When hostname, IP, or alias matches a Proxmox node name or IP
|
||||
2. **Container-level routes** (VMID > 0): When hostname, IP, or alias matches an LXC container
|
||||
|
||||
This enables seamless proxy configuration without manual binding:
|
||||
|
||||
```yaml
|
||||
routes:
|
||||
pve-node-01:
|
||||
host: pve-node-01.internal
|
||||
port: 8006
|
||||
# Automatically links to Proxmox node pve-node-01
|
||||
```
|
||||
|
||||
### WebUI Management
|
||||
|
||||
From the WebUI, you can:
|
||||
|
||||
- **LXC Lifecycle Control**: Start, stop, restart containers
|
||||
- **Node Logs**: Stream real-time journalctl output from nodes
|
||||
- **LXC Logs**: Stream real-time journalctl output from containers
|
||||
|
||||
### API Endpoints
|
||||
|
||||
```http
|
||||
# Node journalctl (WebSocket)
|
||||
GET /api/v1/proxmox/journalctl/:node
|
||||
|
||||
# LXC journalctl (WebSocket)
|
||||
GET /api/v1/proxmox/journalctl/:node/:vmid
|
||||
|
||||
# LXC lifecycle control
|
||||
POST /api/v1/proxmox/lxc/:node/:vmid/start
|
||||
POST /api/v1/proxmox/lxc/:node/:vmid/stop
|
||||
POST /api/v1/proxmox/lxc/:node/:vmid/restart
|
||||
```
|
||||
|
||||
## Update / Uninstall system agent
|
||||
|
||||
Update:
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
- [安裝](#安裝)
|
||||
- [手動安裝](#手動安裝)
|
||||
- [資料夾結構](#資料夾結構)
|
||||
- [Proxmox 整合](#proxmox-整合)
|
||||
- [自動路由綁定](#自動路由綁定)
|
||||
- [WebUI 管理](#webui-管理)
|
||||
- [API 端點](#api-端點)
|
||||
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
||||
- [截圖](#截圖)
|
||||
- [閒置休眠](#閒置休眠)
|
||||
@@ -45,8 +49,6 @@
|
||||
|
||||
<https://demo.godoxy.dev>
|
||||
|
||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||
|
||||
## 主要特點
|
||||
|
||||
- **簡單易用**
|
||||
@@ -69,6 +71,10 @@
|
||||
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
||||
- Docker 容器
|
||||
- Proxmox LXC 容器
|
||||
- **Proxmox 整合**
|
||||
- **自動路由綁定**:透過比對主機名稱、IP 或別名自動將路由綁定至 Proxmox 節點或 LXC 容器
|
||||
- **LXC 生命週期控制**:可直接從 WebUI 啟動、停止、重新啟動容器
|
||||
- **即時日誌**:透過 WebSocket 串流節點和 LXC 容器的 journalctl 日誌
|
||||
- **流量管理**
|
||||
- HTTP 反向代理
|
||||
- TCP/UDP 連接埠轉送
|
||||
@@ -81,7 +87,12 @@
|
||||
- 應用程式一覽
|
||||
- 設定編輯器
|
||||
- 執行時間與系統指標
|
||||
- Docker 日誌檢視器
|
||||
- **Docker**
|
||||
- 容器生命週期管理 (啟動、停止、重新啟動)
|
||||
- 透過 WebSocket 即時串流容器日誌
|
||||
- **Proxmox**
|
||||
- LXC 容器生命週期管理 (啟動、停止、重新啟動)
|
||||
- 透過 WebSocket 即時串流節點和 LXC 容器 journalctl 日誌
|
||||
- **跨平台支援**
|
||||
- 支援 **linux/amd64** 與 **linux/arm64**
|
||||
- **高效能**
|
||||
@@ -146,6 +157,50 @@
|
||||
└── .env
|
||||
```
|
||||
|
||||
## Proxmox 整合
|
||||
|
||||
GoDoxy 可透過配置的提供者自動探索和管理 Proxmox 節點和 LXC 容器。
|
||||
|
||||
### 自動路由綁定
|
||||
|
||||
路由透過反向查詢自動連結至 Proxmox 資源:
|
||||
|
||||
1. **節點級路由** (VMID = 0):當主機名稱、IP 或別名符合 Proxmox 節點名稱或 IP 時
|
||||
2. **容器級路由** (VMID > 0):當主機名稱、IP 或別名符合 LXC 容器時
|
||||
|
||||
這可實現無需手動綁定的無縫代理配置:
|
||||
|
||||
```yaml
|
||||
routes:
|
||||
pve-node-01:
|
||||
host: pve-node-01.internal
|
||||
port: 8006
|
||||
# 自動連結至 Proxmox 節點 pve-node-01
|
||||
```
|
||||
|
||||
### WebUI 管理
|
||||
|
||||
您可以從 WebUI:
|
||||
|
||||
- **LXC 生命週期控制**:啟動、停止、重新啟動容器
|
||||
- **節點日誌**:串流來自節點的即時 journalctl 輸出
|
||||
- **LXC 日誌**:串流來自容器的即時 journalctl 輸出
|
||||
|
||||
### API 端點
|
||||
|
||||
```http
|
||||
# 節點 journalctl (WebSocket)
|
||||
GET /api/v1/proxmox/journalctl/:node
|
||||
|
||||
# LXC journalctl (WebSocket)
|
||||
GET /api/v1/proxmox/journalctl/:node/:vmid
|
||||
|
||||
# LXC 生命週期控制
|
||||
POST /api/v1/proxmox/lxc/:node/:vmid/start
|
||||
POST /api/v1/proxmox/lxc/:node/:vmid/stop
|
||||
POST /api/v1/proxmox/lxc/:node/:vmid/restart
|
||||
```
|
||||
|
||||
## 更新 / 卸載系統代理 (System Agent)
|
||||
|
||||
更新:
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
stdlog "log"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
"github.com/yusing/godoxy/agent/pkg/env"
|
||||
"github.com/yusing/godoxy/agent/pkg/server"
|
||||
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||
httpServer "github.com/yusing/goutils/server"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/task"
|
||||
"github.com/yusing/goutils/version"
|
||||
)
|
||||
|
||||
// TODO: support IPv6
|
||||
|
||||
func main() {
|
||||
writer := zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
@@ -52,30 +62,110 @@ func main() {
|
||||
Tips:
|
||||
1. To change the agent name, you can set the AGENT_NAME environment variable.
|
||||
2. To change the agent port, you can set the AGENT_PORT environment variable.
|
||||
`)
|
||||
`)
|
||||
|
||||
t := task.RootTask("agent", false)
|
||||
opts := server.Options{
|
||||
CACert: caCert,
|
||||
ServerCert: srvCert,
|
||||
Port: env.AgentPort,
|
||||
|
||||
// One TCP listener on AGENT_PORT, then multiplex by TLS ALPN:
|
||||
// - Stream ALPN: route to TCP stream tunnel handler (via http.Server.TLSNextProto)
|
||||
// - Otherwise: route to HTTPS API handler
|
||||
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: env.AgentPort})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to listen on port")
|
||||
}
|
||||
|
||||
server.StartAgentServer(t, opts)
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert.Leaf)
|
||||
|
||||
muxTLSConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*srvCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// Keep HTTP limited to HTTP/1.1 (matching current agent server behavior)
|
||||
// and add the stream tunnel ALPN for multiplexing.
|
||||
NextProtos: []string{"http/1.1", stream.StreamALPN},
|
||||
}
|
||||
if env.AgentSkipClientCertCheck {
|
||||
muxTLSConfig.ClientAuth = tls.NoClientCert
|
||||
}
|
||||
|
||||
// TLS listener feeds the HTTP server. ALPN stream connections are intercepted
|
||||
// using http.Server.TLSNextProto.
|
||||
tlsLn := tls.NewListener(tcpListener, muxTLSConfig)
|
||||
|
||||
streamSrv := stream.NewTCPServerHandler(t.Context())
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Handler: handler.NewAgentHandler(),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return t.Context()
|
||||
},
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
|
||||
// When a client negotiates StreamALPN, net/http will call this hook instead
|
||||
// of treating the connection as HTTP.
|
||||
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
||||
// ServeConn blocks until the tunnel finishes.
|
||||
streamSrv.ServeConn(conn)
|
||||
},
|
||||
},
|
||||
}
|
||||
{
|
||||
subtask := t.Subtask("agent-http", true)
|
||||
t.OnCancel("stop_http", func() {
|
||||
_ = streamSrv.Close()
|
||||
_ = httpSrv.Close()
|
||||
_ = tlsLn.Close()
|
||||
})
|
||||
go func() {
|
||||
err := httpSrv.Serve(tlsLn)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error().Err(err).Msg("agent HTTP server stopped with error")
|
||||
}
|
||||
subtask.Finish(err)
|
||||
}()
|
||||
log.Info().Int("port", env.AgentPort).Msg("HTTPS API server started (ALPN mux enabled)")
|
||||
}
|
||||
log.Info().Int("port", env.AgentPort).Msg("TCP stream handler started (via TLSNextProto)")
|
||||
|
||||
{
|
||||
udpServer := stream.NewUDPServer(t.Context(), "udp", &net.UDPAddr{Port: env.AgentPort}, caCert.Leaf, srvCert)
|
||||
subtask := t.Subtask("agent-stream-udp", true)
|
||||
t.OnCancel("stop_stream_udp", func() {
|
||||
_ = udpServer.Close()
|
||||
})
|
||||
go func() {
|
||||
err := udpServer.Start()
|
||||
subtask.Finish(err)
|
||||
}()
|
||||
log.Info().Int("port", env.AgentPort).Msg("UDP stream server started")
|
||||
}
|
||||
|
||||
if socketproxy.ListenAddr != "" {
|
||||
runtime := strutils.Title(string(env.Runtime))
|
||||
|
||||
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
||||
opts := httpServer.Options{
|
||||
Name: runtime,
|
||||
HTTPAddr: socketproxy.ListenAddr,
|
||||
Handler: socketproxy.NewHandler(),
|
||||
l, err := net.Listen("tcp", socketproxy.ListenAddr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to listen on port")
|
||||
}
|
||||
httpServer.StartServer(t, opts)
|
||||
errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger()
|
||||
srv := http.Server{
|
||||
Handler: socketproxy.NewHandler(),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return t.Context()
|
||||
},
|
||||
ErrorLog: stdlog.New(&errLog, "", 0),
|
||||
}
|
||||
go func() {
|
||||
err := srv.Serve(l)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error().Err(err).Msg("socket proxy server stopped with error")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
systeminfo.Poller.Start()
|
||||
systeminfo.Poller.Start(t)
|
||||
|
||||
task.WaitExit(3)
|
||||
}
|
||||
|
||||
71
agent/go.mod
71
agent/go.mod
@@ -1,6 +1,11 @@
|
||||
module github.com/yusing/godoxy/agent
|
||||
|
||||
go 1.25.5
|
||||
go 1.26.0
|
||||
|
||||
exclude (
|
||||
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
||||
github.com/moby/moby/client v0.2.2 // allow older daemon versions
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
||||
@@ -15,37 +20,35 @@ replace (
|
||||
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.2
|
||||
github.com/bytedance/sonic v1.15.0
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/pion/dtls/v3 v3.1.2
|
||||
github.com/pion/transport/v3 v3.1.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yusing/godoxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/godoxy v0.25.3
|
||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/server v0.0.0-20260103043911-785deb23bd64
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v29.1.3+incompatible // indirect
|
||||
github.com/docker/cli v29.2.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -54,13 +57,12 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -72,39 +74,36 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.58.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.26.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.68.0 // indirect
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
||||
github.com/yusing/ds v0.3.1 // indirect
|
||||
github.com/yusing/gointernals v0.1.16 // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260103043911-785deb23bd64 // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260103043911-785deb23bd64 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
github.com/yusing/ds v0.4.1 // indirect
|
||||
github.com/yusing/gointernals v0.2.0 // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260211095624-f5a276d5c58b // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260211095624-f5a276d5c58b // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
golang.org/x/arch v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
206
agent/go.sum
206
agent/go.sum
@@ -10,10 +10,10 @@ github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -37,8 +37,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c=
|
||||
github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
||||
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -49,14 +49,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
|
||||
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
|
||||
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -79,12 +79,11 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -100,8 +99,8 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -114,8 +113,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.3.1 h1:h64s4/zIEQ06TBo0phFKcckV441YpvUPgLfRAptYsjY=
|
||||
github.com/luthermonson/go-proxmox v0.3.1/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
|
||||
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -125,8 +124,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||
@@ -146,20 +145,28 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
|
||||
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
@@ -167,19 +174,18 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
|
||||
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
@@ -196,126 +202,62 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
||||
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg=
|
||||
github.com/yusing/gointernals v0.2.0/go.mod h1:xGzNbPGMm5Z8kG0t4JYISMscw+gMQlgghkLxlgRZv5Y=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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.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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -27,26 +27,26 @@ graph TD
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Purpose |
|
||||
| -------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [`config.go`](agent/pkg/agent/config.go) | Core configuration, initialization, and API client logic. |
|
||||
| [`new_agent.go`](agent/pkg/agent/new_agent.go) | Agent creation and certificate generation logic. |
|
||||
| [`docker_compose.go`](agent/pkg/agent/docker_compose.go) | Generator for agent Docker Compose configurations. |
|
||||
| [`bare_metal.go`](agent/pkg/agent/bare_metal.go) | Generator for bare metal installation scripts. |
|
||||
| [`env.go`](agent/pkg/agent/env.go) | Environment configuration types and constants. |
|
||||
| [`common/`](agent/pkg/agent/common) | Shared constants and utilities for agents. |
|
||||
| File | Purpose |
|
||||
| ---------------------------------------- | --------------------------------------------------------- |
|
||||
| [`config.go`](config.go) | Core configuration, initialization, and API client logic. |
|
||||
| [`new_agent.go`](new_agent.go) | Agent creation and certificate generation logic. |
|
||||
| [`docker_compose.go`](docker_compose.go) | Generator for agent Docker Compose configurations. |
|
||||
| [`bare_metal.go`](bare_metal.go) | Generator for bare metal installation scripts. |
|
||||
| [`env.go`](env.go) | Environment configuration types and constants. |
|
||||
| `common/` | Shared constants and utilities for agents. |
|
||||
|
||||
## Core Types
|
||||
|
||||
### [`AgentConfig`](agent/pkg/agent/config.go:29)
|
||||
### [`AgentConfig`](config.go:29)
|
||||
|
||||
The primary struct used by the GoDoxy server to manage a connection to an agent. It stores the agent's address, metadata, and TLS configuration.
|
||||
|
||||
### [`AgentInfo`](agent/pkg/agent/config.go:45)
|
||||
### [`AgentInfo`](config.go:45)
|
||||
|
||||
Contains basic metadata about the agent, including its version, name, and container runtime (Docker or Podman).
|
||||
|
||||
### [`PEMPair`](agent/pkg/agent/new_agent.go:53)
|
||||
### [`PEMPair`](new_agent.go:53)
|
||||
|
||||
A utility struct for handling PEM-encoded certificate and key pairs, supporting encryption, decryption, and conversion to `tls.Certificate`.
|
||||
|
||||
@@ -54,7 +54,7 @@ A utility struct for handling PEM-encoded certificate and key pairs, supporting
|
||||
|
||||
### Certificate Generation
|
||||
|
||||
The [`NewAgent`](agent/pkg/agent/new_agent.go:147) function creates a complete certificate infrastructure for an agent:
|
||||
The [`NewAgent`](new_agent.go:147) function creates a complete certificate infrastructure for an agent:
|
||||
|
||||
- **CA Certificate**: Self-signed root certificate with 1000-year validity.
|
||||
- **Server Certificate**: For the agent's HTTPS server, signed by the CA.
|
||||
@@ -65,18 +65,18 @@ All certificates use ECDSA with P-256 curve and SHA-256 signatures.
|
||||
### Certificate Security
|
||||
|
||||
- Certificates are encrypted using AES-GCM with a provided encryption key.
|
||||
- The [`PEMPair`](agent/pkg/agent/new_agent.go:53) struct provides methods for encryption, decryption, and conversion to `tls.Certificate`.
|
||||
- The [`PEMPair`](new_agent.go:53) struct provides methods for encryption, decryption, and conversion to `tls.Certificate`.
|
||||
- Base64 encoding is used for certificate storage and transmission.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Secure Communication
|
||||
|
||||
All communication between the GoDoxy server and agents is secured using mutual TLS (mTLS). The [`AgentConfig`](agent/pkg/agent/config.go:29) handles the loading of CA and client certificates to establish secure connections.
|
||||
All communication between the GoDoxy server and agents is secured using mutual TLS (mTLS). The [`AgentConfig`](config.go:29) handles the loading of CA and client certificates to establish secure connections.
|
||||
|
||||
### 2. Agent Discovery and Initialization
|
||||
|
||||
The [`Init`](agent/pkg/agent/config.go:231) and [`InitWithCerts`](agent/pkg/agent/config.go:110) methods allow the server to:
|
||||
The [`Init`](config.go:231) and [`InitWithCerts`](config.go:110) methods allow the server to:
|
||||
|
||||
- Fetch agent metadata (version, name, runtime).
|
||||
- Verify compatibility between server and agent versions.
|
||||
@@ -86,12 +86,12 @@ The [`Init`](agent/pkg/agent/config.go:231) and [`InitWithCerts`](agent/pkg/agen
|
||||
|
||||
The package provides interfaces and implementations for generating deployment artifacts:
|
||||
|
||||
- **Docker Compose**: Generates a `docker-compose.yml` for running the agent as a container via [`AgentComposeConfig.Generate()`](agent/pkg/agent/docker_compose.go:21).
|
||||
- **Bare Metal**: Generates a shell script to install and run the agent as a systemd service via [`AgentEnvConfig.Generate()`](agent/pkg/agent/bare_metal.go:27).
|
||||
- **Docker Compose**: Generates a `docker-compose.yml` for running the agent as a container via [`AgentComposeConfig.Generate()`](docker_compose.go:21).
|
||||
- **Bare Metal**: Generates a shell script to install and run the agent as a systemd service via [`AgentEnvConfig.Generate()`](bare_metal.go:27).
|
||||
|
||||
### 4. Fake Docker Host
|
||||
|
||||
The package supports a "fake" Docker host scheme (`agent://<addr>`) to identify containers managed by an agent, allowing the GoDoxy server to route requests appropriately. See [`IsDockerHostAgent`](agent/pkg/agent/config.go:90) and [`GetAgentAddrFromDockerHost`](agent/pkg/agent/config.go:94).
|
||||
The package supports a "fake" Docker host scheme (`agent://<addr>`) to identify containers managed by an agent, allowing the GoDoxy server to route requests appropriately. See [`IsDockerHostAgent`](config.go:90) and [`GetAgentAddrFromDockerHost`](config.go:94).
|
||||
|
||||
## Usage Example
|
||||
|
||||
|
||||
3
agent/pkg/agent/common/common.go
Normal file
3
agent/pkg/agent/common/common.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package common
|
||||
|
||||
const CertsDNSName = "godoxy.agent"
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -14,33 +15,54 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
agentstream "github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/version"
|
||||
)
|
||||
|
||||
type AgentConfig struct {
|
||||
Addr string `json:"addr"`
|
||||
Name string `json:"name"`
|
||||
Version version.Version `json:"version" swaggertype:"string"`
|
||||
Runtime ContainerRuntime `json:"runtime"`
|
||||
AgentInfo
|
||||
|
||||
Addr string `json:"addr"`
|
||||
IsTCPStreamSupported bool `json:"supports_tcp_stream"`
|
||||
IsUDPStreamSupported bool `json:"supports_udp_stream"`
|
||||
|
||||
// for stream
|
||||
caCert *x509.Certificate
|
||||
clientCert *tls.Certificate
|
||||
|
||||
tlsConfig tls.Config
|
||||
l zerolog.Logger
|
||||
|
||||
l zerolog.Logger
|
||||
} // @name Agent
|
||||
|
||||
type AgentInfo struct {
|
||||
Version version.Version `json:"version" swaggertype:"string"`
|
||||
Name string `json:"name"`
|
||||
Runtime ContainerRuntime `json:"runtime"`
|
||||
}
|
||||
|
||||
// Deprecated. Replaced by EndpointInfo
|
||||
const (
|
||||
EndpointVersion = "/version"
|
||||
EndpointName = "/name"
|
||||
EndpointRuntime = "/runtime"
|
||||
EndpointVersion = "/version"
|
||||
EndpointName = "/name"
|
||||
EndpointRuntime = "/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
EndpointInfo = "/info"
|
||||
EndpointProxyHTTP = "/proxy/http"
|
||||
EndpointHealth = "/health"
|
||||
EndpointLogs = "/logs"
|
||||
EndpointSystemInfo = "/system_info"
|
||||
|
||||
AgentHost = CertsDNSName
|
||||
AgentHost = common.CertsDNSName
|
||||
|
||||
APIEndpointBase = "/godoxy/agent"
|
||||
APIBaseURL = "https://" + AgentHost + APIEndpointBase
|
||||
@@ -90,6 +112,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.clientCert = &clientCert
|
||||
|
||||
// create tls config
|
||||
caCertPool := x509.NewCertPool()
|
||||
@@ -97,58 +120,105 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
||||
if !ok {
|
||||
return errors.New("invalid ca certificate")
|
||||
}
|
||||
// Keep the CA leaf for stream client dialing.
|
||||
if block, _ := pem.Decode(ca); block == nil || block.Type != "CERTIFICATE" {
|
||||
return errors.New("invalid ca certificate")
|
||||
} else if cert, err := x509.ParseCertificate(block.Bytes); err != nil {
|
||||
return err
|
||||
} else {
|
||||
cfg.caCert = cert
|
||||
}
|
||||
|
||||
cfg.tlsConfig = tls.Config{
|
||||
Certificates: []tls.Certificate{clientCert},
|
||||
RootCAs: caCertPool,
|
||||
ServerName: CertsDNSName,
|
||||
ServerName: common.CertsDNSName,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// get agent name
|
||||
name, _, err := cfg.fetchString(ctx, EndpointName)
|
||||
status, err := cfg.fetchJSON(ctx, EndpointInfo, &cfg.AgentInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Name = name
|
||||
var streamUnsupportedErrs gperr.Builder
|
||||
|
||||
if status == http.StatusOK {
|
||||
// test stream server connection
|
||||
const fakeAddress = "localhost:8080" // it won't be used, just for testing
|
||||
// test TCP stream support
|
||||
err := agentstream.TCPHealthCheck(ctx, cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||
if err != nil {
|
||||
streamUnsupportedErrs.Addf("failed to connect to stream server via TCP: %w", err)
|
||||
} else {
|
||||
cfg.IsTCPStreamSupported = true
|
||||
}
|
||||
|
||||
// test UDP stream support
|
||||
err = agentstream.UDPHealthCheck(ctx, cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||
if err != nil {
|
||||
streamUnsupportedErrs.Addf("failed to connect to stream server via UDP: %w", err)
|
||||
} else {
|
||||
cfg.IsUDPStreamSupported = true
|
||||
}
|
||||
} else {
|
||||
// old agent does not support EndpointInfo
|
||||
// fallback with old logic
|
||||
cfg.IsTCPStreamSupported = false
|
||||
cfg.IsUDPStreamSupported = false
|
||||
streamUnsupportedErrs.Adds("agent version is too old, does not support stream tunneling")
|
||||
|
||||
// get agent name
|
||||
name, _, err := cfg.fetchString(ctx, EndpointName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Name = name
|
||||
|
||||
// check agent version
|
||||
agentVersion, _, err := cfg.fetchString(ctx, EndpointVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Version = version.Parse(agentVersion)
|
||||
|
||||
// check agent runtime
|
||||
runtime, status, err := cfg.fetchString(ctx, EndpointRuntime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
switch runtime {
|
||||
case "docker":
|
||||
cfg.Runtime = ContainerRuntimeDocker
|
||||
// case "nerdctl":
|
||||
// cfg.Runtime = ContainerRuntimeNerdctl
|
||||
case "podman":
|
||||
cfg.Runtime = ContainerRuntimePodman
|
||||
default:
|
||||
return fmt.Errorf("invalid agent runtime: %s", runtime)
|
||||
}
|
||||
case http.StatusNotFound:
|
||||
// backward compatibility, old agent does not have runtime endpoint
|
||||
cfg.Runtime = ContainerRuntimeDocker
|
||||
default:
|
||||
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
cfg.l = log.With().Str("agent", cfg.Name).Logger()
|
||||
|
||||
// check agent version
|
||||
agentVersion, _, err := cfg.fetchString(ctx, EndpointVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
if err := streamUnsupportedErrs.Error(); err != nil {
|
||||
cfg.l.Warn().Err(err).Msg("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work")
|
||||
}
|
||||
|
||||
// check agent runtime
|
||||
runtime, status, err := cfg.fetchString(ctx, EndpointRuntime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
switch runtime {
|
||||
case "docker":
|
||||
cfg.Runtime = ContainerRuntimeDocker
|
||||
// case "nerdctl":
|
||||
// cfg.Runtime = ContainerRuntimeNerdctl
|
||||
case "podman":
|
||||
cfg.Runtime = ContainerRuntimePodman
|
||||
default:
|
||||
return fmt.Errorf("invalid agent runtime: %s", runtime)
|
||||
}
|
||||
case http.StatusNotFound:
|
||||
// backward compatibility, old agent does not have runtime endpoint
|
||||
cfg.Runtime = ContainerRuntimeDocker
|
||||
default:
|
||||
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtime)
|
||||
}
|
||||
|
||||
cfg.Version = version.Parse(agentVersion)
|
||||
|
||||
if serverVersion.IsNewerThanMajor(cfg.Version) {
|
||||
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.Name, serverVersion, cfg.Version)
|
||||
}
|
||||
@@ -177,6 +247,38 @@ func (cfg *AgentConfig) Init(ctx context.Context) error {
|
||||
return cfg.InitWithCerts(ctx, ca, crt, key)
|
||||
}
|
||||
|
||||
// NewTCPClient creates a new TCP client for the agent.
|
||||
//
|
||||
// It returns an error if
|
||||
// - the agent is not initialized
|
||||
// - the agent does not support TCP stream tunneling
|
||||
// - the agent stream server address is not initialized
|
||||
func (cfg *AgentConfig) NewTCPClient(targetAddress string) (net.Conn, error) {
|
||||
if cfg.caCert == nil || cfg.clientCert == nil {
|
||||
return nil, errors.New("agent is not initialized")
|
||||
}
|
||||
if !cfg.IsTCPStreamSupported {
|
||||
return nil, errors.New("agent does not support TCP stream tunneling")
|
||||
}
|
||||
return agentstream.NewTCPClient(cfg.Addr, targetAddress, cfg.caCert, cfg.clientCert)
|
||||
}
|
||||
|
||||
// NewUDPClient creates a new UDP client for the agent.
|
||||
//
|
||||
// It returns an error if
|
||||
// - the agent is not initialized
|
||||
// - the agent does not support UDP stream tunneling
|
||||
// - the agent stream server address is not initialized
|
||||
func (cfg *AgentConfig) NewUDPClient(targetAddress string) (net.Conn, error) {
|
||||
if cfg.caCert == nil || cfg.clientCert == nil {
|
||||
return nil, errors.New("agent is not initialized")
|
||||
}
|
||||
if !cfg.IsUDPStreamSupported {
|
||||
return nil, errors.New("agent does not support UDP stream tunneling")
|
||||
}
|
||||
return agentstream.NewUDPClient(cfg.Addr, targetAddress, cfg.caCert, cfg.clientCert)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Transport() *http.Transport {
|
||||
return &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
@@ -211,8 +313,18 @@ func (cfg *AgentConfig) do(ctx context.Context, method, endpoint string, body io
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeout := 5 * time.Second
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
remaining := time.Until(deadline)
|
||||
if remaining > 0 {
|
||||
timeout = remaining
|
||||
}
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Transport: cfg.Transport(),
|
||||
Timeout: timeout,
|
||||
}
|
||||
return client.Do(req)
|
||||
}
|
||||
@@ -232,3 +344,31 @@ func (cfg *AgentConfig) fetchString(ctx context.Context, endpoint string) (strin
|
||||
release(data)
|
||||
return ret, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// fetchJSON fetches a JSON response from the agent and unmarshals it into the provided struct
|
||||
//
|
||||
// It will return the status code of the response, and error if any.
|
||||
// If the status code is not http.StatusOK, out will be unchanged but error will still be nil.
|
||||
func (cfg *AgentConfig) fetchJSON(ctx context.Context, endpoint string, out any) (int, error) {
|
||||
resp, err := cfg.do(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, release, err := httputils.ReadAllBody(resp)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
defer release(data)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return resp.StatusCode, nil
|
||||
}
|
||||
|
||||
err = sonic.Unmarshal(data, out)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return resp.StatusCode, nil
|
||||
}
|
||||
|
||||
@@ -17,10 +17,8 @@ import (
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
CertsDNSName = "godoxy.agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
)
|
||||
|
||||
func toPEMPair(certDER []byte, key *ecdsa.PrivateKey) *PEMPair {
|
||||
@@ -156,7 +154,7 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||
SerialNumber: caSerialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"GoDoxy"},
|
||||
CommonName: CertsDNSName,
|
||||
CommonName: common.CertsDNSName,
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1000, 0, 0), // 1000 years
|
||||
@@ -196,9 +194,9 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||
Subject: pkix.Name{
|
||||
Organization: caTemplate.Subject.Organization,
|
||||
OrganizationalUnit: []string{"Server"},
|
||||
CommonName: CertsDNSName,
|
||||
CommonName: common.CertsDNSName,
|
||||
},
|
||||
DNSNames: []string{CertsDNSName},
|
||||
DNSNames: []string{common.CertsDNSName},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
@@ -228,9 +226,9 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||
Subject: pkix.Name{
|
||||
Organization: caTemplate.Subject.Organization,
|
||||
OrganizationalUnit: []string{"Client"},
|
||||
CommonName: CertsDNSName,
|
||||
CommonName: common.CertsDNSName,
|
||||
},
|
||||
DNSNames: []string{CertsDNSName},
|
||||
DNSNames: []string{common.CertsDNSName},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1000, 0, 0),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
)
|
||||
|
||||
func TestNewAgent(t *testing.T) {
|
||||
@@ -72,7 +73,7 @@ func TestServerClient(t *testing.T) {
|
||||
clientTLSConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*clientTLS},
|
||||
RootCAs: caPool,
|
||||
ServerName: CertsDNSName,
|
||||
ServerName: common.CertsDNSName,
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
197
agent/pkg/agent/stream/README.md
Normal file
197
agent/pkg/agent/stream/README.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Stream proxy protocol
|
||||
|
||||
This package implements a small header-based handshake that allows an authenticated client to request forwarding to a `(host, port)` destination. It supports both TCP-over-TLS and UDP-over-DTLS transports.
|
||||
|
||||
## Overview
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Client
|
||||
TC[TCPClient] -->|TLS| TSS[TCPServer]
|
||||
UC[UDPClient] -->|DTLS| USS[UDPServer]
|
||||
end
|
||||
|
||||
subgraph Stream Protocol
|
||||
H[StreamRequestHeader]
|
||||
end
|
||||
|
||||
TSS -->|Redirect| DST1[Destination TCP]
|
||||
USS -->|Forward UDP| DST2[Destination UDP]
|
||||
```
|
||||
|
||||
## Header
|
||||
|
||||
The on-wire header is a fixed-size binary blob:
|
||||
|
||||
- `Version` (8 bytes)
|
||||
- `HostLength` (1 byte)
|
||||
- `Host` (255 bytes, NUL padded)
|
||||
- `PortLength` (1 byte)
|
||||
- `Port` (5 bytes, NUL padded)
|
||||
- `Flag` (1 byte, protocol flags)
|
||||
- `Checksum` (4 bytes, big-endian CRC32)
|
||||
|
||||
Total: `headerSize = 8 + 1 + 255 + 1 + 5 + 1 + 4 = 275` bytes.
|
||||
|
||||
Checksum is `crc32.ChecksumIEEE(header[0:headerSize-4])`.
|
||||
|
||||
### Flags
|
||||
|
||||
The `Flag` field is a bitmask of protocol flags defined by `FlagType`:
|
||||
|
||||
| Flag | Value | Purpose |
|
||||
| ---------------------- | ----- | ---------------------------------------------------------------------- |
|
||||
| `FlagCloseImmediately` | `1` | Health check probe - server closes immediately after validating header |
|
||||
|
||||
See [`FlagType`](header.go:26) and [`FlagCloseImmediately`](header.go:28).
|
||||
|
||||
See [`StreamRequestHeader`](header.go:30).
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------- | ------------------------------------------------------------ |
|
||||
| [`header.go`](header.go) | Stream request header structure and validation. |
|
||||
| [`tcp_client.go`](tcp_client.go:12) | TCP client implementation with TLS transport. |
|
||||
| [`tcp_server.go`](tcp_server.go:13) | TCP server implementation for handling stream requests. |
|
||||
| [`udp_client.go`](udp_client.go:13) | UDP client implementation with DTLS transport. |
|
||||
| [`udp_server.go`](udp_server.go:17) | UDP server implementation for handling DTLS stream requests. |
|
||||
| [`common.go`](common.go:11) | Connection manager and shared constants. |
|
||||
|
||||
## Constants
|
||||
|
||||
| Constant | Value | Purpose |
|
||||
| ---------------------- | ------------------------- | ------------------------------------------------------- |
|
||||
| `StreamALPN` | `"godoxy-agent-stream/1"` | TLS ALPN protocol for stream multiplexing. |
|
||||
| `headerSize` | `275` bytes | Total size of the stream request header. |
|
||||
| `dialTimeout` | `10s` | Timeout for establishing destination connections. |
|
||||
| `readDeadline` | `10s` | Read timeout for UDP destination sockets. |
|
||||
| `FlagCloseImmediately` | `1` | Flag for health check probe - server closes immediately |
|
||||
|
||||
See [`common.go`](common.go:11).
|
||||
|
||||
## Public API
|
||||
|
||||
### Types
|
||||
|
||||
#### `StreamRequestHeader`
|
||||
|
||||
Represents the on-wire protocol header used to negotiate a stream tunnel.
|
||||
|
||||
```go
|
||||
type StreamRequestHeader struct {
|
||||
Version [8]byte // Fixed to "0.1.0" with NUL padding
|
||||
HostLength byte // Actual host name length (0-255)
|
||||
Host [255]byte // NUL-padded host name
|
||||
PortLength byte // Actual port string length (0-5)
|
||||
Port [5]byte // NUL-padded port string
|
||||
Flag FlagType // Protocol flags (e.g., FlagCloseImmediately)
|
||||
Checksum [4]byte // CRC32 checksum of header without checksum
|
||||
}
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `NewStreamRequestHeader(host, port string) (*StreamRequestHeader, error)` - Creates a header for the given host and port. Returns error if host exceeds 255 bytes or port exceeds 5 bytes.
|
||||
- `NewStreamHealthCheckHeader() *StreamRequestHeader` - Creates a header with `FlagCloseImmediately` set for health check probes.
|
||||
- `Validate() bool` - Validates the version and checksum.
|
||||
- `GetHostPort() (string, string)` - Extracts the host and port from the header.
|
||||
- `ShouldCloseImmediately() bool` - Returns true if `FlagCloseImmediately` is set.
|
||||
|
||||
### TCP Functions
|
||||
|
||||
- [`NewTCPClient()`](tcp_client.go:26) - Creates a TLS client connection and sends the stream header.
|
||||
- [`NewTCPServerHandler()`](tcp_server.go:24) - Creates a handler for ALPN-multiplexed connections (no listener).
|
||||
- [`NewTCPServerFromListener()`](tcp_server.go:36) - Wraps an existing TLS listener.
|
||||
- [`NewTCPServer()`](tcp_server.go:45) - Creates a fully-configured TCP server with TLS listener.
|
||||
|
||||
### UDP Functions
|
||||
|
||||
- [`NewUDPClient()`](udp_client.go:27) - Creates a DTLS client connection and sends the stream header.
|
||||
- [`NewUDPServer()`](udp_server.go:26) - Creates a DTLS server listening on the given UDP address.
|
||||
|
||||
## Health Check Probes
|
||||
|
||||
The protocol supports health check probes using the `FlagCloseImmediately` flag. When a client sends a header with this flag set, the server validates the header and immediately closes the connection without establishing a destination tunnel.
|
||||
|
||||
This is useful for:
|
||||
|
||||
- Connectivity testing between agent and server
|
||||
- Verifying TLS/DTLS handshake and mTLS authentication
|
||||
- Monitoring stream protocol availability
|
||||
|
||||
**Usage:**
|
||||
|
||||
```go
|
||||
header := stream.NewStreamHealthCheckHeader()
|
||||
// Send header over TLS/DTLS connection
|
||||
// Server will validate and close immediately
|
||||
```
|
||||
|
||||
Both TCP and UDP servers silently handle health check probes without logging errors.
|
||||
|
||||
See [`NewStreamHealthCheckHeader()`](header.go:66) and [`FlagCloseImmediately`](header.go:28).
|
||||
|
||||
## TCP behavior
|
||||
|
||||
1. Client establishes a TLS connection to the stream server.
|
||||
2. Client sends exactly one header as a handshake.
|
||||
3. After the handshake, both sides proxy raw TCP bytes between client and destination.
|
||||
|
||||
Server reads the header using `io.ReadFull` to avoid dropping bytes.
|
||||
|
||||
See [`NewTCPClient()`](tcp_client.go:26) and [`(*TCPServer).redirect()`](tcp_server.go:116).
|
||||
|
||||
## UDP-over-DTLS behavior
|
||||
|
||||
1. Client establishes a DTLS connection to the stream server.
|
||||
2. Client sends exactly one header as a handshake.
|
||||
3. After the handshake, both sides proxy raw UDP datagrams:
|
||||
- client -> destination: DTLS payload is written to destination `UDPConn`
|
||||
- destination -> client: destination payload is written back to the DTLS connection
|
||||
|
||||
Responses do **not** include a header.
|
||||
|
||||
The UDP server uses a bidirectional forwarding model:
|
||||
|
||||
- One goroutine forwards from client to destination
|
||||
- Another goroutine forwards from destination to client
|
||||
|
||||
The destination reader uses `readDeadline` to periodically wake up and check for context cancellation. Timeouts do not terminate the session.
|
||||
|
||||
See [`NewUDPClient()`](udp_client.go:27) and [`(*UDPServer).handleDTLSConnection()`](udp_server.go:89).
|
||||
|
||||
## Connection Management
|
||||
|
||||
Both `TCPServer` and `UDPServer` create a dedicated destination connection per incoming stream session and close it when the session ends (no destination connection reuse).
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Description |
|
||||
| --------------------- | ----------------------------------------------- |
|
||||
| `ErrInvalidHeader` | Header validation failed (version or checksum). |
|
||||
| `ErrCloseImmediately` | Health check probe - server closed immediately. |
|
||||
|
||||
Errors from connection creation are propagated to the caller.
|
||||
|
||||
See [`header.go`](header.go:23).
|
||||
|
||||
## Integration
|
||||
|
||||
This package is used by the agent to provide stream tunneling capabilities. See the parent [`agent`](../README.md) package for integration details with the GoDoxy server.
|
||||
|
||||
### Certificate Requirements
|
||||
|
||||
Both TCP and UDP servers require:
|
||||
|
||||
- CA certificate for client verification
|
||||
- Server certificate for TLS/DTLS termination
|
||||
|
||||
Both clients require:
|
||||
|
||||
- CA certificate for server verification
|
||||
- Client certificate for mTLS authentication
|
||||
|
||||
### ALPN Protocol
|
||||
|
||||
The `StreamALPN` constant (`"godoxy-agent-stream/1"`) is used to multiplex stream tunnel traffic and HTTPS API traffic on the same port. Connections negotiating this ALPN are routed to the stream handler.
|
||||
24
agent/pkg/agent/stream/common.go
Normal file
24
agent/pkg/agent/stream/common.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/yusing/goutils/synk"
|
||||
)
|
||||
|
||||
const (
|
||||
dialTimeout = 10 * time.Second
|
||||
readDeadline = 10 * time.Second
|
||||
)
|
||||
|
||||
// StreamALPN is the TLS ALPN protocol id used to multiplex the TCP stream tunnel
|
||||
// and the HTTPS API on the same TCP port.
|
||||
//
|
||||
// When a client negotiates this ALPN, the agent will route the connection to the
|
||||
// stream tunnel handler instead of the HTTP handler.
|
||||
const StreamALPN = "godoxy-agent-stream/1"
|
||||
|
||||
var dTLSCipherSuites = []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}
|
||||
|
||||
var sizedPool = synk.GetSizedBytesPool()
|
||||
117
agent/pkg/agent/stream/header.go
Normal file
117
agent/pkg/agent/stream/header.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
versionSize = 8
|
||||
hostSize = 255
|
||||
portSize = 5
|
||||
flagSize = 1
|
||||
checksumSize = 4 // crc32 checksum
|
||||
|
||||
headerSize = versionSize + 1 + hostSize + 1 + portSize + flagSize + checksumSize
|
||||
)
|
||||
|
||||
var version = [versionSize]byte{'0', '.', '1', '.', '0', 0, 0, 0}
|
||||
|
||||
var ErrInvalidHeader = errors.New("invalid header")
|
||||
var ErrCloseImmediately = errors.New("close immediately")
|
||||
|
||||
type FlagType uint8
|
||||
|
||||
const FlagCloseImmediately FlagType = 1 << iota
|
||||
|
||||
type StreamRequestHeader struct {
|
||||
Version [versionSize]byte
|
||||
|
||||
HostLength byte
|
||||
Host [hostSize]byte
|
||||
|
||||
PortLength byte
|
||||
Port [portSize]byte
|
||||
|
||||
Flag FlagType
|
||||
Checksum [checksumSize]byte
|
||||
}
|
||||
|
||||
func init() {
|
||||
if headerSize != reflect.TypeFor[StreamRequestHeader]().Size() {
|
||||
panic("headerSize does not match the size of StreamRequestHeader")
|
||||
}
|
||||
}
|
||||
|
||||
func NewStreamRequestHeader(host, port string) (*StreamRequestHeader, error) {
|
||||
if len(host) > hostSize {
|
||||
return nil, fmt.Errorf("host is too long: max %d characters, got %d", hostSize, len(host))
|
||||
}
|
||||
if len(port) > portSize {
|
||||
return nil, fmt.Errorf("port is too long: max %d characters, got %d", portSize, len(port))
|
||||
}
|
||||
header := &StreamRequestHeader{}
|
||||
copy(header.Version[:], version[:])
|
||||
header.HostLength = byte(len(host))
|
||||
copy(header.Host[:], host)
|
||||
header.PortLength = byte(len(port))
|
||||
copy(header.Port[:], port)
|
||||
header.updateChecksum()
|
||||
return header, nil
|
||||
}
|
||||
|
||||
func NewStreamHealthCheckHeader() *StreamRequestHeader {
|
||||
header := &StreamRequestHeader{}
|
||||
copy(header.Version[:], version[:])
|
||||
header.Flag |= FlagCloseImmediately
|
||||
header.updateChecksum()
|
||||
return header
|
||||
}
|
||||
|
||||
// ToHeader converts header byte array to a copy of itself as a StreamRequestHeader.
|
||||
func ToHeader(buf *[headerSize]byte) StreamRequestHeader {
|
||||
return *(*StreamRequestHeader)(unsafe.Pointer(buf))
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) GetHostPort() (string, string) {
|
||||
return string(h.Host[:h.HostLength]), string(h.Port[:h.PortLength])
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) Validate() bool {
|
||||
if h.Version != version {
|
||||
return false
|
||||
}
|
||||
if h.HostLength > hostSize {
|
||||
return false
|
||||
}
|
||||
if h.PortLength > portSize {
|
||||
return false
|
||||
}
|
||||
return h.validateChecksum()
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) ShouldCloseImmediately() bool {
|
||||
return h.Flag&FlagCloseImmediately != 0
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) updateChecksum() {
|
||||
checksum := crc32.ChecksumIEEE(h.BytesWithoutChecksum())
|
||||
binary.BigEndian.PutUint32(h.Checksum[:], checksum)
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) validateChecksum() bool {
|
||||
checksum := crc32.ChecksumIEEE(h.BytesWithoutChecksum())
|
||||
return checksum == binary.BigEndian.Uint32(h.Checksum[:])
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) BytesWithoutChecksum() []byte {
|
||||
return (*[headerSize - checksumSize]byte)(unsafe.Pointer(h))[:]
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) Bytes() []byte {
|
||||
return (*[headerSize]byte)(unsafe.Pointer(h))[:]
|
||||
}
|
||||
26
agent/pkg/agent/stream/payload_test.go
Normal file
26
agent/pkg/agent/stream/payload_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStreamRequestHeader_RoundTripAndChecksum(t *testing.T) {
|
||||
h, err := NewStreamRequestHeader("example.com", "443")
|
||||
if err != nil {
|
||||
t.Fatalf("NewStreamRequestHeader: %v", err)
|
||||
}
|
||||
if !h.Validate() {
|
||||
t.Fatalf("expected header to validate")
|
||||
}
|
||||
|
||||
var buf [headerSize]byte
|
||||
copy(buf[:], h.Bytes())
|
||||
h2 := ToHeader(&buf)
|
||||
if !h2.Validate() {
|
||||
t.Fatalf("expected round-tripped header to validate")
|
||||
}
|
||||
host, port := h2.GetHostPort()
|
||||
if host != "example.com" || port != "443" {
|
||||
t.Fatalf("unexpected host/port: %q:%q", host, port)
|
||||
}
|
||||
}
|
||||
149
agent/pkg/agent/stream/tcp_client.go
Normal file
149
agent/pkg/agent/stream/tcp_client.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
)
|
||||
|
||||
type TCPClient struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// NewTCPClient creates a new TCP client for the agent.
|
||||
//
|
||||
// It will establish a TLS connection and send a stream request header to the server.
|
||||
//
|
||||
// It returns an error if
|
||||
// - the target address is invalid
|
||||
// - the stream request header is invalid
|
||||
// - the TLS configuration is invalid
|
||||
// - the TLS connection fails
|
||||
// - the stream request header is not sent
|
||||
func NewTCPClient(serverAddr, targetAddress string, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(targetAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header, err := NewStreamRequestHeader(host, port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newTCPClientWIthHeader(context.Background(), serverAddr, header, caCert, clientCert)
|
||||
}
|
||||
|
||||
func TCPHealthCheck(ctx context.Context, serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||
header := NewStreamHealthCheckHeader()
|
||||
|
||||
conn, err := newTCPClientWIthHeader(ctx, serverAddr, header, caCert, clientCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTCPClientWIthHeader(ctx context.Context, serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
// Setup TLS configuration
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*clientCert},
|
||||
RootCAs: caCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{StreamALPN},
|
||||
ServerName: common.CertsDNSName,
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: dialTimeout,
|
||||
}
|
||||
tlsDialer := &tls.Dialer{
|
||||
NetDialer: dialer,
|
||||
Config: tlsConfig,
|
||||
}
|
||||
|
||||
// Establish TLS connection
|
||||
conn, err := tlsDialer.DialContext(ctx, "tcp", serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deadline, hasDeadline := ctx.Deadline()
|
||||
if hasDeadline {
|
||||
err := conn.SetWriteDeadline(deadline)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Send the stream header once as a handshake.
|
||||
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hasDeadline {
|
||||
// reset write deadline
|
||||
err = conn.SetWriteDeadline(time.Time{})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &TCPClient{
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *TCPClient) Read(p []byte) (n int, err error) {
|
||||
return c.conn.Read(p)
|
||||
}
|
||||
|
||||
func (c *TCPClient) Write(p []byte) (n int, err error) {
|
||||
return c.conn.Write(p)
|
||||
}
|
||||
|
||||
func (c *TCPClient) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *TCPClient) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *TCPClient) SetDeadline(t time.Time) error {
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *TCPClient) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *TCPClient) SetWriteDeadline(t time.Time) error {
|
||||
return c.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *TCPClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// ConnectionState exposes the underlying TLS connection state when the client is
|
||||
// backed by *tls.Conn.
|
||||
//
|
||||
// This is primarily used by tests and diagnostics.
|
||||
func (c *TCPClient) ConnectionState() tls.ConnectionState {
|
||||
if tc, ok := c.conn.(*tls.Conn); ok {
|
||||
return tc.ConnectionState()
|
||||
}
|
||||
return tls.ConnectionState{}
|
||||
}
|
||||
179
agent/pkg/agent/stream/tcp_server.go
Normal file
179
agent/pkg/agent/stream/tcp_server.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
ioutils "github.com/yusing/goutils/io"
|
||||
)
|
||||
|
||||
type TCPServer struct {
|
||||
ctx context.Context
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewTCPServerHandler creates a TCP stream server that can serve already-accepted
|
||||
// connections (e.g. handed off by an ALPN multiplexer).
|
||||
//
|
||||
// This variant does not require a listener. Use TCPServer.ServeConn to handle
|
||||
// each incoming stream connection.
|
||||
func NewTCPServerHandler(ctx context.Context) *TCPServer {
|
||||
s := &TCPServer{ctx: ctx}
|
||||
return s
|
||||
}
|
||||
|
||||
// NewTCPServerFromListener creates a TCP stream server from an already-prepared
|
||||
// listener.
|
||||
//
|
||||
// The listener is expected to yield connections that are already secured (e.g.
|
||||
// a TLS/mTLS listener, or pre-handshaked *tls.Conn). This is used when the agent
|
||||
// multiplexes HTTPS and stream-tunnel traffic on the same port.
|
||||
func NewTCPServerFromListener(ctx context.Context, listener net.Listener) *TCPServer {
|
||||
s := &TCPServer{
|
||||
ctx: ctx,
|
||||
listener: listener,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func NewTCPServer(ctx context.Context, listener *net.TCPListener, caCert *x509.Certificate, serverCert *tls.Certificate) *TCPServer {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*serverCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{StreamALPN},
|
||||
}
|
||||
|
||||
tcpListener := tls.NewListener(listener, tlsConfig)
|
||||
return NewTCPServerFromListener(ctx, tcpListener)
|
||||
}
|
||||
|
||||
func (s *TCPServer) Start() error {
|
||||
if s.listener == nil {
|
||||
return net.ErrClosed
|
||||
}
|
||||
context.AfterFunc(s.ctx, func() {
|
||||
_ = s.listener.Close()
|
||||
})
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) && s.ctx.Err() != nil {
|
||||
return s.ctx.Err()
|
||||
}
|
||||
return err
|
||||
}
|
||||
go s.handle(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// ServeConn serves a single stream connection.
|
||||
//
|
||||
// The provided connection is expected to be already secured (TLS/mTLS) and to
|
||||
// speak the stream protocol (i.e. the client will send the stream header first).
|
||||
//
|
||||
// This method blocks until the stream finishes.
|
||||
func (s *TCPServer) ServeConn(conn net.Conn) {
|
||||
s.handle(conn)
|
||||
}
|
||||
|
||||
func (s *TCPServer) Addr() net.Addr {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
return s.listener.Addr()
|
||||
}
|
||||
|
||||
func (s *TCPServer) Close() error {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
return s.listener.Close()
|
||||
}
|
||||
|
||||
func (s *TCPServer) logger(clientConn net.Conn) *zerolog.Logger {
|
||||
ev := log.With().Str("protocol", "tcp").
|
||||
Str("remote", clientConn.RemoteAddr().String())
|
||||
if s.listener != nil {
|
||||
ev = ev.Str("addr", s.listener.Addr().String())
|
||||
}
|
||||
l := ev.Logger()
|
||||
return &l
|
||||
}
|
||||
|
||||
func (s *TCPServer) loggerWithDst(dstConn net.Conn, clientConn net.Conn) *zerolog.Logger {
|
||||
ev := log.With().Str("protocol", "tcp").
|
||||
Str("remote", clientConn.RemoteAddr().String()).
|
||||
Str("dst", dstConn.RemoteAddr().String())
|
||||
if s.listener != nil {
|
||||
ev = ev.Str("addr", s.listener.Addr().String())
|
||||
}
|
||||
l := ev.Logger()
|
||||
return &l
|
||||
}
|
||||
|
||||
func (s *TCPServer) handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
dst, err := s.redirect(conn)
|
||||
if err != nil {
|
||||
// Health check probe: close connection
|
||||
if errors.Is(err, ErrCloseImmediately) {
|
||||
s.logger(conn).Info().Msg("Health check received")
|
||||
return
|
||||
}
|
||||
s.logger(conn).Err(err).Msg("failed to redirect connection")
|
||||
return
|
||||
}
|
||||
|
||||
defer dst.Close()
|
||||
pipe := ioutils.NewBidirectionalPipe(s.ctx, conn, dst)
|
||||
err = pipe.Start()
|
||||
if err != nil {
|
||||
s.loggerWithDst(dst, conn).Err(err).Msg("failed to start bidirectional pipe")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPServer) redirect(conn net.Conn) (net.Conn, error) {
|
||||
// Read the stream header once as a handshake.
|
||||
var headerBuf [headerSize]byte
|
||||
_ = conn.SetReadDeadline(time.Now().Add(dialTimeout))
|
||||
if _, err := io.ReadFull(conn, headerBuf[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
|
||||
header := ToHeader(&headerBuf)
|
||||
if !header.Validate() {
|
||||
return nil, ErrInvalidHeader
|
||||
}
|
||||
|
||||
// Health check: close immediately if FlagCloseImmediately is set
|
||||
if header.ShouldCloseImmediately() {
|
||||
return nil, ErrCloseImmediately
|
||||
}
|
||||
|
||||
// get destination connection
|
||||
host, port := header.GetHostPort()
|
||||
return s.createDestConnection(host, port)
|
||||
}
|
||||
|
||||
func (s *TCPServer) createDestConnection(host, port string) (net.Conn, error) {
|
||||
addr := net.JoinHostPort(host, port)
|
||||
conn, err := net.DialTimeout("tcp", addr, dialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
26
agent/pkg/agent/stream/tests/healthcheck_test.go
Normal file
26
agent/pkg/agent/stream/tests/healthcheck_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package stream_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
)
|
||||
|
||||
func TestTCPHealthCheck(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
srv := startTCPServer(t, certs)
|
||||
|
||||
err := stream.TCPHealthCheck(t.Context(), srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||
require.NoError(t, err, "health check")
|
||||
}
|
||||
|
||||
func TestUDPHealthCheck(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
err := stream.UDPHealthCheck(t.Context(), srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||
require.NoError(t, err, "health check")
|
||||
}
|
||||
94
agent/pkg/agent/stream/tests/mux_test.go
Normal file
94
agent/pkg/agent/stream/tests/mux_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package stream_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
)
|
||||
|
||||
func TestTLSALPNMux_HTTPAndStreamShareOnePort(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
baseLn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
|
||||
require.NoError(t, err, "listen tcp")
|
||||
defer baseLn.Close()
|
||||
baseAddr := baseLn.Addr().String()
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(certs.CaCert)
|
||||
|
||||
serverTLS := &tls.Config{
|
||||
Certificates: []tls.Certificate{*certs.SrvCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1", stream.StreamALPN},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
streamSrv := stream.NewTCPServerHandler(ctx)
|
||||
defer func() { _ = streamSrv.Close() }()
|
||||
|
||||
tlsLn := tls.NewListener(baseLn, serverTLS)
|
||||
defer func() { _ = tlsLn.Close() }()
|
||||
|
||||
// HTTP server
|
||||
httpSrv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}),
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
|
||||
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
||||
streamSrv.ServeConn(conn)
|
||||
},
|
||||
},
|
||||
}
|
||||
go func() { _ = httpSrv.Serve(tlsLn) }()
|
||||
defer func() { _ = httpSrv.Close() }()
|
||||
|
||||
// Stream destination
|
||||
dstAddr, closeDst := startTCPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
// HTTP client over the same port
|
||||
clientTLS := &tls.Config{
|
||||
Certificates: []tls.Certificate{*certs.ClientCert},
|
||||
RootCAs: caCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
ServerName: common.CertsDNSName,
|
||||
}
|
||||
hc, err := tls.Dial("tcp", baseAddr, clientTLS)
|
||||
require.NoError(t, err, "dial https")
|
||||
defer hc.Close()
|
||||
_ = hc.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
_, err = hc.Write([]byte("GET / HTTP/1.1\r\nHost: godoxy-agent\r\n\r\n"))
|
||||
require.NoError(t, err, "write http request")
|
||||
r := bufio.NewReader(hc)
|
||||
statusLine, err := r.ReadString('\n')
|
||||
require.NoError(t, err, "read status line")
|
||||
require.Contains(t, statusLine, "200", "expected 200")
|
||||
|
||||
// Stream client over the same port
|
||||
client := NewTCPClient(t, baseAddr, dstAddr, certs)
|
||||
defer client.Close()
|
||||
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
msg := []byte("ping over mux")
|
||||
_, err = client.Write(msg)
|
||||
require.NoError(t, err, "write stream payload")
|
||||
buf := make([]byte, len(msg))
|
||||
_, err = io.ReadFull(client, buf)
|
||||
require.NoError(t, err, "read stream payload")
|
||||
require.Equal(t, msg, buf)
|
||||
}
|
||||
201
agent/pkg/agent/stream/tests/server_flow_test.go
Normal file
201
agent/pkg/agent/stream/tests/server_flow_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package stream_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
)
|
||||
|
||||
func TestTCPServer_FullFlow(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
dstAddr, closeDst := startTCPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startTCPServer(t, certs)
|
||||
|
||||
client := NewTCPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||
defer client.Close()
|
||||
|
||||
// Ensure ALPN is negotiated as expected (required for multiplexing).
|
||||
withState, ok := client.(interface{ ConnectionState() tls.ConnectionState })
|
||||
require.True(t, ok, "tcp client should expose TLS connection state")
|
||||
require.Equal(t, stream.StreamALPN, withState.ConnectionState().NegotiatedProtocol)
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
msg := []byte("ping over tcp")
|
||||
_, err := client.Write(msg)
|
||||
require.NoError(t, err, "write to client")
|
||||
|
||||
buf := make([]byte, len(msg))
|
||||
_, err = io.ReadFull(client, buf)
|
||||
require.NoError(t, err, "read from client")
|
||||
require.Equal(t, string(msg), string(buf), "unexpected echo")
|
||||
}
|
||||
|
||||
func TestTCPServer_ConcurrentConnections(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
dstAddr, closeDst := startTCPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startTCPServer(t, certs)
|
||||
|
||||
const nClients = 25
|
||||
|
||||
errs := make(chan error, nClients)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(nClients)
|
||||
|
||||
for i := range nClients {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
client := NewTCPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||
defer client.Close()
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
msg := fmt.Appendf(nil, "ping over tcp %d", i)
|
||||
if _, err := client.Write(msg); err != nil {
|
||||
errs <- fmt.Errorf("write to client: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, len(msg))
|
||||
if _, err := io.ReadFull(client, buf); err != nil {
|
||||
errs <- fmt.Errorf("read from client: %w", err)
|
||||
return
|
||||
}
|
||||
if string(msg) != string(buf) {
|
||||
errs <- fmt.Errorf("unexpected echo: got=%q want=%q", string(buf), string(msg))
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPServer_RejectInvalidClient(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
// Generate a self-signed client cert that is NOT signed by the CA
|
||||
_, _, invalidClientPEM, err := agent.NewAgent()
|
||||
require.NoError(t, err, "generate invalid client certs")
|
||||
invalidClientCert, err := invalidClientPEM.ToTLSCert()
|
||||
require.NoError(t, err, "parse invalid client cert")
|
||||
|
||||
dstAddr, closeDst := startUDPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
|
||||
// Try to connect with a client cert from a different CA
|
||||
_, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert)
|
||||
require.Error(t, err, "expected error when connecting with client cert from different CA")
|
||||
|
||||
var handshakeErr *dtls.HandshakeError
|
||||
require.ErrorAs(t, err, &handshakeErr, "expected handshake error")
|
||||
}
|
||||
|
||||
func TestUDPServer_RejectClientWithoutCert(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
dstAddr, closeDst := startUDPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
// Try to connect without any client certificate
|
||||
// Create a TLS cert without a private key to simulate no client cert
|
||||
emptyCert := &tls.Certificate{}
|
||||
_, err := stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, emptyCert)
|
||||
require.Error(t, err, "expected error when connecting without client cert")
|
||||
|
||||
require.ErrorContains(t, err, "no certificate provided", "expected no cert error")
|
||||
}
|
||||
|
||||
func TestUDPServer_FullFlow(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
dstAddr, closeDst := startUDPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
client := NewUDPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||
defer client.Close()
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
msg := []byte("ping over udp")
|
||||
_, err := client.Write(msg)
|
||||
require.NoError(t, err, "write to client")
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
n, err := client.Read(buf)
|
||||
require.NoError(t, err, "read from client")
|
||||
require.Equal(t, string(msg), string(buf[:n]), "unexpected echo")
|
||||
}
|
||||
|
||||
func TestUDPServer_ConcurrentConnections(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
dstAddr, closeDst := startUDPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
const nClients = 25
|
||||
|
||||
errs := make(chan error, nClients)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(nClients)
|
||||
|
||||
for i := range nClients {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
client := NewUDPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||
defer client.Close()
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
msg := fmt.Appendf(nil, "ping over udp %d", i)
|
||||
if _, err := client.Write(msg); err != nil {
|
||||
errs <- fmt.Errorf("write to client: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
n, err := client.Read(buf)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("read from client: %w", err)
|
||||
return
|
||||
}
|
||||
if string(msg) != string(buf[:n]) {
|
||||
errs <- fmt.Errorf("unexpected echo: got=%q want=%q", string(buf[:n]), string(msg))
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
177
agent/pkg/agent/stream/tests/testutils_test.go
Normal file
177
agent/pkg/agent/stream/tests/testutils_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package stream_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pion/transport/v3/udp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
)
|
||||
|
||||
// CertBundle holds all certificates needed for testing.
|
||||
type CertBundle struct {
|
||||
CaCert *x509.Certificate
|
||||
SrvCert *tls.Certificate
|
||||
ClientCert *tls.Certificate
|
||||
}
|
||||
|
||||
// genTestCerts generates certificates for testing and returns them as a CertBundle.
|
||||
func genTestCerts(t *testing.T) CertBundle {
|
||||
t.Helper()
|
||||
|
||||
caPEM, srvPEM, clientPEM, err := agent.NewAgent()
|
||||
require.NoError(t, err, "generate agent certs")
|
||||
|
||||
caCert, err := caPEM.ToTLSCert()
|
||||
require.NoError(t, err, "parse CA cert")
|
||||
srvCert, err := srvPEM.ToTLSCert()
|
||||
require.NoError(t, err, "parse server cert")
|
||||
clientCert, err := clientPEM.ToTLSCert()
|
||||
require.NoError(t, err, "parse client cert")
|
||||
|
||||
return CertBundle{
|
||||
CaCert: caCert.Leaf,
|
||||
SrvCert: srvCert,
|
||||
ClientCert: clientCert,
|
||||
}
|
||||
}
|
||||
|
||||
// startTCPEcho starts a TCP echo server and returns its address and close function.
|
||||
func startTCPEcho(t *testing.T) (addr string, closeFn func()) {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err, "listen tcp")
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
_, _ = io.Copy(conn, conn)
|
||||
}(c)
|
||||
}
|
||||
}()
|
||||
|
||||
return ln.Addr().String(), func() {
|
||||
_ = ln.Close()
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
// startUDPEcho starts a UDP echo server and returns its address and close function.
|
||||
func startUDPEcho(t *testing.T) (addr string, closeFn func()) {
|
||||
t.Helper()
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err, "listen udp")
|
||||
uc := pc.(*net.UDPConn)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
buf := make([]byte, 65535)
|
||||
for {
|
||||
n, raddr, err := uc.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = uc.WriteToUDP(buf[:n], raddr)
|
||||
}
|
||||
}()
|
||||
|
||||
return uc.LocalAddr().String(), func() {
|
||||
_ = uc.Close()
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
// TestServer wraps a server with its startup goroutine for cleanup.
|
||||
type TestServer struct {
|
||||
Server interface{ Close() error }
|
||||
Addr net.Addr
|
||||
}
|
||||
|
||||
// startTCPServer starts a TCP server and returns a TestServer for cleanup.
|
||||
func startTCPServer(t *testing.T, certs CertBundle) TestServer {
|
||||
t.Helper()
|
||||
|
||||
tcpLn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
|
||||
require.NoError(t, err, "listen tcp")
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
srv := stream.NewTCPServer(ctx, tcpLn, certs.CaCert, certs.SrvCert)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- srv.Start() }()
|
||||
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
_ = srv.Close()
|
||||
err := <-errCh
|
||||
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) {
|
||||
t.Logf("tcp server exit: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return TestServer{
|
||||
Server: srv,
|
||||
Addr: srv.Addr(),
|
||||
}
|
||||
}
|
||||
|
||||
// startUDPServer starts a UDP server and returns a TestServer for cleanup.
|
||||
func startUDPServer(t *testing.T, certs CertBundle) TestServer {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
srv := stream.NewUDPServer(ctx, "udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}, certs.CaCert, certs.SrvCert)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- srv.Start() }()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
_ = srv.Close()
|
||||
err := <-errCh
|
||||
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) && !errors.Is(err, udp.ErrClosedListener) {
|
||||
t.Logf("udp server exit: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return TestServer{
|
||||
Server: srv,
|
||||
Addr: srv.Addr(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewTCPClient creates a TCP client connected to the server with test certificates.
|
||||
func NewTCPClient(t *testing.T, serverAddr, targetAddress string, certs CertBundle) net.Conn {
|
||||
t.Helper()
|
||||
client, err := stream.NewTCPClient(serverAddr, targetAddress, certs.CaCert, certs.ClientCert)
|
||||
require.NoError(t, err, "create tcp client")
|
||||
return client
|
||||
}
|
||||
|
||||
// NewUDPClient creates a UDP client connected to the server with test certificates.
|
||||
func NewUDPClient(t *testing.T, serverAddr, targetAddress string, certs CertBundle) net.Conn {
|
||||
t.Helper()
|
||||
client, err := stream.NewUDPClient(serverAddr, targetAddress, certs.CaCert, certs.ClientCert)
|
||||
require.NoError(t, err, "create udp client")
|
||||
return client
|
||||
}
|
||||
138
agent/pkg/agent/stream/udp_client.go
Normal file
138
agent/pkg/agent/stream/udp_client.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
)
|
||||
|
||||
type UDPClient struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// NewUDPClient creates a new UDP client for the agent.
|
||||
//
|
||||
// It will establish a DTLS connection and send a stream request header to the server.
|
||||
//
|
||||
// It returns an error if
|
||||
// - the target address is invalid
|
||||
// - the stream request header is invalid
|
||||
// - the DTLS configuration is invalid
|
||||
// - the DTLS connection fails
|
||||
// - the stream request header is not sent
|
||||
func NewUDPClient(serverAddr, targetAddress string, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(targetAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header, err := NewStreamRequestHeader(host, port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newUDPClientWIthHeader(context.Background(), serverAddr, header, caCert, clientCert)
|
||||
}
|
||||
|
||||
func newUDPClientWIthHeader(ctx context.Context, serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
// Setup DTLS configuration
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert)
|
||||
|
||||
dtlsConfig := &dtls.Config{
|
||||
Certificates: []tls.Certificate{*clientCert},
|
||||
RootCAs: caCertPool,
|
||||
InsecureSkipVerify: false,
|
||||
ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
|
||||
ServerName: common.CertsDNSName,
|
||||
CipherSuites: dTLSCipherSuites,
|
||||
}
|
||||
|
||||
raddr, err := net.ResolveUDPAddr("udp", serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Establish DTLS connection
|
||||
conn, err := dtls.Dial("udp", raddr, dtlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deadline, hasDeadline := ctx.Deadline()
|
||||
if hasDeadline {
|
||||
err := conn.SetWriteDeadline(deadline)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Send the stream header once as a handshake.
|
||||
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hasDeadline {
|
||||
// reset write deadline
|
||||
err = conn.SetWriteDeadline(time.Time{})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &UDPClient{
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UDPHealthCheck(ctx context.Context, serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||
header := NewStreamHealthCheckHeader()
|
||||
|
||||
conn, err := newUDPClientWIthHeader(ctx, serverAddr, header, caCert, clientCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *UDPClient) Read(p []byte) (n int, err error) {
|
||||
return c.conn.Read(p)
|
||||
}
|
||||
|
||||
func (c *UDPClient) Write(p []byte) (n int, err error) {
|
||||
return c.conn.Write(p)
|
||||
}
|
||||
|
||||
func (c *UDPClient) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *UDPClient) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *UDPClient) SetDeadline(t time.Time) error {
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *UDPClient) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *UDPClient) SetWriteDeadline(t time.Time) error {
|
||||
return c.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *UDPClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
208
agent/pkg/agent/stream/udp_server.go
Normal file
208
agent/pkg/agent/stream/udp_server.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type UDPServer struct {
|
||||
ctx context.Context
|
||||
network string
|
||||
laddr *net.UDPAddr
|
||||
listener net.Listener
|
||||
|
||||
dtlsConfig *dtls.Config
|
||||
}
|
||||
|
||||
func NewUDPServer(ctx context.Context, network string, laddr *net.UDPAddr, caCert *x509.Certificate, serverCert *tls.Certificate) *UDPServer {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert)
|
||||
|
||||
dtlsConfig := &dtls.Config{
|
||||
Certificates: []tls.Certificate{*serverCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: dtls.RequireAndVerifyClientCert,
|
||||
ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
|
||||
CipherSuites: dTLSCipherSuites,
|
||||
}
|
||||
|
||||
s := &UDPServer{
|
||||
ctx: ctx,
|
||||
network: network,
|
||||
laddr: laddr,
|
||||
dtlsConfig: dtlsConfig,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *UDPServer) Start() error {
|
||||
listener, err := dtls.Listen(s.network, s.laddr, s.dtlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.listener = listener
|
||||
|
||||
context.AfterFunc(s.ctx, func() {
|
||||
_ = s.listener.Close()
|
||||
})
|
||||
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
// Expected error when context cancelled
|
||||
if errors.Is(err, net.ErrClosed) && s.ctx.Err() != nil {
|
||||
return s.ctx.Err()
|
||||
}
|
||||
return err
|
||||
}
|
||||
go s.handleDTLSConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPServer) Addr() net.Addr {
|
||||
if s.listener != nil {
|
||||
return s.listener.Addr()
|
||||
}
|
||||
return s.laddr
|
||||
}
|
||||
|
||||
func (s *UDPServer) Close() error {
|
||||
if s.listener != nil {
|
||||
return s.listener.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UDPServer) logger(clientConn net.Conn) *zerolog.Logger {
|
||||
l := log.With().Str("protocol", "udp").
|
||||
Str("addr", s.Addr().String()).
|
||||
Str("remote", clientConn.RemoteAddr().String()).Logger()
|
||||
return &l
|
||||
}
|
||||
|
||||
func (s *UDPServer) loggerWithDst(clientConn net.Conn, dstConn *net.UDPConn) *zerolog.Logger {
|
||||
l := log.With().Str("protocol", "udp").
|
||||
Str("addr", s.Addr().String()).
|
||||
Str("remote", clientConn.RemoteAddr().String()).
|
||||
Str("dst", dstConn.RemoteAddr().String()).Logger()
|
||||
return &l
|
||||
}
|
||||
|
||||
func (s *UDPServer) handleDTLSConnection(clientConn net.Conn) {
|
||||
defer clientConn.Close()
|
||||
|
||||
// Read the stream header once as a handshake.
|
||||
var headerBuf [headerSize]byte
|
||||
_ = clientConn.SetReadDeadline(time.Now().Add(dialTimeout))
|
||||
if _, err := io.ReadFull(clientConn, headerBuf[:]); err != nil {
|
||||
s.logger(clientConn).Err(err).Msg("failed to read stream header")
|
||||
return
|
||||
}
|
||||
_ = clientConn.SetReadDeadline(time.Time{})
|
||||
|
||||
header := ToHeader(&headerBuf)
|
||||
if !header.Validate() {
|
||||
s.logger(clientConn).Error().Bytes("header", headerBuf[:]).Msg("invalid stream header received")
|
||||
return
|
||||
}
|
||||
|
||||
// Health check probe: close connection
|
||||
if header.ShouldCloseImmediately() {
|
||||
s.logger(clientConn).Info().Msg("Health check received")
|
||||
return
|
||||
}
|
||||
|
||||
host, port := header.GetHostPort()
|
||||
dstConn, err := s.createDestConnection(host, port)
|
||||
if err != nil {
|
||||
s.logger(clientConn).Err(err).Msg("failed to get or create destination connection")
|
||||
return
|
||||
}
|
||||
defer dstConn.Close()
|
||||
|
||||
go s.forwardFromDestination(dstConn, clientConn)
|
||||
|
||||
buf := sizedPool.GetSized(65535)
|
||||
defer sizedPool.Put(buf)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
n, err := clientConn.Read(buf)
|
||||
// Per net.Conn contract, Read may return (n > 0, err == io.EOF).
|
||||
// Always forward any bytes we got before acting on the error.
|
||||
if n > 0 {
|
||||
if _, werr := dstConn.Write(buf[:n]); werr != nil {
|
||||
s.logger(clientConn).Err(werr).Msgf("failed to write %d bytes to destination", n)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// Expected shutdown paths.
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
s.logger(clientConn).Err(err).Msg("failed to read from client")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPServer) createDestConnection(host, port string) (*net.UDPConn, error) {
|
||||
addr := net.JoinHostPort(host, port)
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dstConn, err := net.DialUDP("udp", nil, udpAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dstConn, nil
|
||||
}
|
||||
|
||||
func (s *UDPServer) forwardFromDestination(dstConn *net.UDPConn, clientConn net.Conn) {
|
||||
buffer := sizedPool.GetSized(65535)
|
||||
defer sizedPool.Put(buffer)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
_ = dstConn.SetReadDeadline(time.Now().Add(readDeadline))
|
||||
n, err := dstConn.Read(buffer)
|
||||
if err != nil {
|
||||
// The destination socket can be closed when the client disconnects (e.g. during
|
||||
// the stream support probe in AgentConfig.StartWithCerts). Treat that as a
|
||||
// normal exit and avoid noisy logs.
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
s.loggerWithDst(clientConn, dstConn).Err(err).Msg("failed to read from destination")
|
||||
return
|
||||
}
|
||||
if _, err := clientConn.Write(buffer[:n]); err != nil {
|
||||
s.loggerWithDst(clientConn, dstConn).Err(err).Msgf("failed to write %d bytes to client", n)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
services:
|
||||
agent:
|
||||
image: "{{.Image}}"
|
||||
container_name: godoxy-agent
|
||||
restart: always
|
||||
network_mode: host # do not change this
|
||||
environment:
|
||||
AGENT_NAME: "{{.Name}}"
|
||||
AGENT_PORT: "{{.Port}}"
|
||||
AGENT_CA_CERT: "{{.CACert}}"
|
||||
AGENT_SSL_CERT: "{{.SSLCert}}"
|
||||
# use agent as a docker socket proxy: [host]:port
|
||||
# set LISTEN_ADDR to enable (e.g. 127.0.0.1:2375)
|
||||
LISTEN_ADDR:
|
||||
POST: false
|
||||
ALLOW_RESTARTS: false
|
||||
ALLOW_START: false
|
||||
ALLOW_STOP: false
|
||||
AUTH: false
|
||||
BUILD: false
|
||||
COMMIT: false
|
||||
CONFIGS: false
|
||||
CONTAINERS: false
|
||||
DISTRIBUTION: false
|
||||
EVENTS: true
|
||||
EXEC: false
|
||||
GRPC: false
|
||||
IMAGES: false
|
||||
INFO: false
|
||||
NETWORKS: false
|
||||
NODES: false
|
||||
PING: true
|
||||
PLUGINS: false
|
||||
SECRETS: false
|
||||
SERVICES: false
|
||||
SESSION: false
|
||||
SWARM: false
|
||||
SYSTEM: false
|
||||
TASKS: false
|
||||
VERSION: true
|
||||
VOLUMES: false
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./data:/app/data
|
||||
@@ -5,7 +5,8 @@ services:
|
||||
restart: always
|
||||
{{ if eq .ContainerRuntime "podman" -}}
|
||||
ports:
|
||||
- "{{.Port}}:{{.Port}}"
|
||||
- "{{.Port}}:{{.Port}}/tcp"
|
||||
- "{{.Port}}:{{.Port}}/udp"
|
||||
{{ else -}}
|
||||
network_mode: host # do not change this
|
||||
{{ end -}}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
@@ -44,14 +44,14 @@ func NewAgentHandler() http.Handler {
|
||||
}
|
||||
|
||||
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
|
||||
mux.HandleEndpoint("GET", agent.EndpointVersion, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, version.Get())
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, env.AgentName)
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointRuntime, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, env.Runtime)
|
||||
mux.HandleFunc(agent.EndpointInfo, func(w http.ResponseWriter, r *http.Request) {
|
||||
agentInfo := agent.AgentInfo{
|
||||
Version: version.Get(),
|
||||
Name: env.AgentName,
|
||||
Runtime: env.Runtime,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
sonic.ConfigDefault.NewEncoder(w).Encode(agentInfo)
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
||||
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/env"
|
||||
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||
"github.com/yusing/goutils/server"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
CACert, ServerCert *tls.Certificate
|
||||
Port int
|
||||
}
|
||||
|
||||
func StartAgentServer(parent task.Parent, opt Options) {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(opt.CACert.Leaf)
|
||||
|
||||
// Configure TLS
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*opt.ServerCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
|
||||
if env.AgentSkipClientCertCheck {
|
||||
tlsConfig.ClientAuth = tls.NoClientCert
|
||||
}
|
||||
|
||||
agentServer := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", opt.Port),
|
||||
Handler: handler.NewAgentHandler(),
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
server.Start(parent.Subtask("agent-server", false), agentServer, server.WithLogger(&log.Logger))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.5-alpine AS builder
|
||||
FROM golang:1.26.0-alpine AS builder
|
||||
|
||||
HEALTHCHECK NONE
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module github.com/yusing/godoxy/cmd/bench_server
|
||||
|
||||
go 1.25.5
|
||||
go 1.26.0
|
||||
|
||||
@@ -181,7 +181,6 @@ func newApiHandler(debugMux *debugMux) *gin.Engine {
|
||||
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
|
||||
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
|
||||
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
|
||||
registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload)
|
||||
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
|
||||
|
||||
route := v1.Group("/route")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.5-alpine AS builder
|
||||
FROM golang:1.26.0-alpine AS builder
|
||||
|
||||
HEALTHCHECK NONE
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module github.com/yusing/godoxy/cmd/h2c_test_server
|
||||
|
||||
go 1.25.5
|
||||
go 1.26.0
|
||||
|
||||
require golang.org/x/net v0.48.0
|
||||
require golang.org/x/net v0.50.0
|
||||
|
||||
require golang.org/x/text v0.32.0 // indirect
|
||||
require golang.org/x/text v0.34.0 // indirect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
|
||||
39
cmd/main.go
39
cmd/main.go
@@ -1,24 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/api"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/config"
|
||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
"github.com/yusing/godoxy/internal/logging"
|
||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
"github.com/yusing/godoxy/internal/metrics/uptime"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
"github.com/yusing/godoxy/internal/route/rules"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/server"
|
||||
"github.com/yusing/goutils/task"
|
||||
"github.com/yusing/goutils/version"
|
||||
)
|
||||
@@ -32,6 +29,16 @@ func parallel(fns ...func()) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
done := make(chan struct{}, 1)
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(common.InitTimeout):
|
||||
log.Fatal().Msgf("timeout waiting for initialization to complete, exiting...")
|
||||
}
|
||||
}()
|
||||
|
||||
initProfiling()
|
||||
|
||||
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
||||
@@ -39,8 +46,7 @@ func main() {
|
||||
log.Trace().Msg("trace enabled")
|
||||
parallel(
|
||||
dnsproviders.InitProviders,
|
||||
homepage.InitIconListCache,
|
||||
systeminfo.Poller.Start,
|
||||
iconlist.InitCache,
|
||||
middleware.LoadComposeFiles,
|
||||
)
|
||||
|
||||
@@ -55,28 +61,23 @@ func main() {
|
||||
|
||||
err := config.Load()
|
||||
if err != nil {
|
||||
gperr.LogWarn("errors in config", err)
|
||||
if criticalErr, ok := errors.AsType[config.CriticalError](err); ok {
|
||||
log.Fatal().Err(criticalErr).Msg("critical error in config")
|
||||
}
|
||||
log.Warn().Err(err).Msg("errors in config")
|
||||
}
|
||||
|
||||
config.StartProxyServers()
|
||||
|
||||
if err := auth.Initialize(); err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||
}
|
||||
rules.InitAuthHandler(auth.AuthOrProceed)
|
||||
|
||||
// API Handler needs to start after auth is initialized.
|
||||
server.StartServer(task.RootTask("api_server", false), server.Options{
|
||||
Name: "api",
|
||||
HTTPAddr: common.APIHTTPAddr,
|
||||
Handler: api.NewHandler(),
|
||||
})
|
||||
|
||||
listenDebugServer()
|
||||
|
||||
uptime.Poller.Start()
|
||||
config.WatchChanges()
|
||||
|
||||
close(done)
|
||||
|
||||
task.WaitExit(config.Value().TimeoutShutdown)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ services:
|
||||
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /app/.next/cache # next image caching
|
||||
|
||||
- /tmp:rw
|
||||
- /app/node_modules/.cache:rw
|
||||
# for lite variant, do not change uid/gid
|
||||
# - /var/cache/nginx:uid=101,gid=101
|
||||
# - /run:uid=101,gid=101
|
||||
|
||||
111
go.mod
111
go.mod
@@ -1,9 +1,15 @@
|
||||
module github.com/yusing/godoxy
|
||||
|
||||
go 1.25.5
|
||||
go 1.26.0
|
||||
|
||||
exclude (
|
||||
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
||||
github.com/moby/moby/client v0.2.2 // allow older daemon versions
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
||||
github.com/luthermonson/go-proxmox => ./internal/go-proxmox
|
||||
github.com/shirou/gopsutil/v4 => ./internal/gopsutil
|
||||
github.com/yusing/godoxy/agent => ./agent
|
||||
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
|
||||
@@ -18,53 +24,53 @@ require (
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/gin-gonic/gin v1.11.0 // api server
|
||||
github.com/go-acme/lego/v4 v4.30.1 // acme client
|
||||
github.com/go-acme/lego/v4 v4.31.0 // acme client
|
||||
github.com/go-playground/validator/v10 v10.30.1 // validator
|
||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||
github.com/pires/go-proxyproto v0.8.1 // proxy protocol support
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // lock free map for concurrent operations
|
||||
github.com/pires/go-proxyproto v0.11.0 // proxy protocol support
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
|
||||
github.com/rs/zerolog v1.34.0 // logging
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||
golang.org/x/crypto v0.46.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.48.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.34.0 // oauth2 authentication
|
||||
golang.org/x/crypto v0.48.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.50.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.35.0 // oauth2 authentication
|
||||
golang.org/x/sync v0.19.0 // errgroup and singleflight for concurrent operations
|
||||
golang.org/x/time v0.14.0 // time utilities
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||
github.com/bytedance/sonic v1.14.2 // fast json parsing
|
||||
github.com/docker/cli v29.1.3+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||
github.com/goccy/go-yaml v1.19.1 // yaml parsing for different config files
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
|
||||
github.com/luthermonson/go-proxmox v0.3.1 // proxmox API client
|
||||
github.com/bytedance/sonic v1.15.0 // fast json parsing
|
||||
github.com/docker/cli v29.2.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // jwt authentication
|
||||
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
|
||||
github.com/moby/moby/api v1.52.0 // docker API
|
||||
github.com/moby/moby/client v0.2.1 // docker client
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
||||
github.com/quic-go/quic-go v0.58.0 // http3 support
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // system information
|
||||
github.com/quic-go/quic-go v0.59.0 // http3 support
|
||||
github.com/shirou/gopsutil/v4 v4.26.1 // system information
|
||||
github.com/spf13/afero v1.15.0 // afero for file system operations
|
||||
github.com/stretchr/testify v1.11.1 // testing framework
|
||||
github.com/valyala/fasthttp v1.68.0 // fast http for health check
|
||||
github.com/yusing/ds v0.3.1 // data structures and algorithms
|
||||
github.com/yusing/godoxy/agent v0.0.0-20260104140148-1c2515cb298d
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260104140148-1c2515cb298d
|
||||
github.com/yusing/gointernals v0.1.16
|
||||
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
||||
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
||||
github.com/yusing/godoxy/agent v0.0.0-20260211033321-22f03488e998
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260211033321-22f03488e998
|
||||
github.com/yusing/gointernals v0.2.0
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260103043911-785deb23bd64
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260103043911-785deb23bd64
|
||||
github.com/yusing/goutils/server v0.0.0-20260103043911-785deb23bd64
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260211095624-f5a276d5c58b
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260211095624-f5a276d5c58b
|
||||
github.com/yusing/goutils/server v0.0.0-20260211095624-f5a276d5c58b
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.18.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
@@ -83,7 +89,7 @@ require (
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -92,8 +98,8 @@ require (
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
@@ -103,7 +109,7 @@ require (
|
||||
github.com/magefile/mage v1.15.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@@ -118,28 +124,28 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/samber/slog-common v0.20.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/api v0.258.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/api v0.266.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/grpc v1.79.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -148,7 +154,7 @@ require (
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
@@ -160,18 +166,21 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/go-resty/resty/v2 v2.17.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/linode/linodego v1.63.0 // indirect
|
||||
github.com/linode/linodego v1.65.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.2 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
@@ -181,7 +190,7 @@ require (
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||
github.com/vultr/govultr/v3 v3.27.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/arch v0.24.0 // indirect
|
||||
)
|
||||
|
||||
198
go.sum
198
go.sum
@@ -1,12 +1,12 @@
|
||||
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
|
||||
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
|
||||
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
@@ -51,10 +51,10 @@ github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -76,8 +76,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c=
|
||||
github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
||||
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -94,14 +94,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
|
||||
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
|
||||
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -126,19 +126,19 @@ github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6x
|
||||
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -151,10 +151,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
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.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||
@@ -177,8 +177,8 @@ github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||
@@ -191,14 +191,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/linode/linodego v1.63.0 h1:MdjizfXNJDVJU6ggoJmMO5O9h4KGPGivNX0fzrAnstk=
|
||||
github.com/linode/linodego v1.63.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
|
||||
github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.3.1 h1:h64s4/zIEQ06TBo0phFKcckV441YpvUPgLfRAptYsjY=
|
||||
github.com/luthermonson/go-proxmox v0.3.1/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -210,8 +208,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
@@ -229,10 +227,10 @@ github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0
|
||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 h1:l0tH15ACQADZAzC+LZ+mo2tIX4H6uZu0ulrVmG5Tqz0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 h1:gzB4c6ztb38C/jYiqEaFC+mCGcWFHDji9e6jwymY9d4=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2/go.mod h1:l1qIPIq2uRV5WTSvkbhbl/ndbeOu7OCb3UZ+0+2ZSb8=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 h1:3oOIAQ9Fd2qTKTS/VlWmvKyBPKKhXBcCXjRZqOUypI4=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 h1:2H75475moAv1hVVYlOk815KfqeiFCiQ7ovqn3OnN6FY=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1/go.mod h1:9HGOXiiQxcsG+4amgdr4xBIMq6IchdLW/nQDyZz07IE=
|
||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -247,8 +245,14 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
|
||||
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -261,12 +265,12 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
@@ -274,14 +278,14 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
|
||||
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -294,7 +298,6 @@ github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
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.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/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
@@ -313,62 +316,62 @@ github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
||||
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
|
||||
github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
||||
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg=
|
||||
github.com/yusing/gointernals v0.2.0/go.mod h1:xGzNbPGMm5Z8kG0t4JYISMscw+gMQlgghkLxlgRZv5Y=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
||||
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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=
|
||||
@@ -378,10 +381,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
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=
|
||||
@@ -399,7 +402,6 @@ golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -410,8 +412,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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=
|
||||
@@ -430,8 +432,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -440,28 +442,28 @@ 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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
|
||||
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
||||
2
goutils
2
goutils
Submodule goutils updated: 78fda75d1e...494ab85a33
@@ -54,13 +54,13 @@ type Matchers []Matcher
|
||||
### Exported functions and methods
|
||||
|
||||
```go
|
||||
func (c *Config) Validate() gperr.Error
|
||||
func (c *Config) Validate() error
|
||||
```
|
||||
|
||||
Validates configuration and sets defaults. Must be called before `Start`.
|
||||
|
||||
```go
|
||||
func (c *Config) Start(parent task.Parent) gperr.Error
|
||||
func (c *Config) Start(parent task.Parent) error
|
||||
```
|
||||
|
||||
Initializes the ACL, starts the logger and notification goroutines.
|
||||
@@ -169,14 +169,14 @@ Configuration is loaded from `config/config.yml` under the `acl` key.
|
||||
|
||||
```yaml
|
||||
acl:
|
||||
default: "allow" # "allow" or "deny"
|
||||
allow_local: true # Allow private/loopback IPs
|
||||
default: "allow" # "allow" or "deny"
|
||||
allow_local: true # Allow private/loopback IPs
|
||||
log:
|
||||
log_allowed: false # Log allowed connections
|
||||
log_allowed: false # Log allowed connections
|
||||
notify:
|
||||
to: ["gotify"] # Notification providers
|
||||
interval: "1m" # Notification interval
|
||||
include_allowed: false # Include allowed in notifications
|
||||
to: ["gotify"] # Notification providers
|
||||
interval: "1m" # Notification interval
|
||||
include_allowed: false # Include allowed in notifications
|
||||
```
|
||||
|
||||
### Hot-reloading
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/maxmind"
|
||||
"github.com/yusing/godoxy/internal/notif"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
aclevents "github.com/yusing/goutils/events/acl"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
@@ -27,9 +27,9 @@ type Config struct {
|
||||
Log *accesslog.ACLLoggerConfig `json:"log"`
|
||||
|
||||
Notify struct {
|
||||
To []string `json:"to"` // list of notification providers
|
||||
Interval time.Duration `json:"interval"` // interval between notifications
|
||||
IncludeAllowed *bool `json:"include_allowed"` // default: false
|
||||
To []string `json:"to,omitempty"` // list of notification providers
|
||||
Interval time.Duration `json:"interval,omitempty"` // interval between notifications
|
||||
IncludeAllowed *bool `json:"include_allowed,omitzero"` // default: false
|
||||
} `json:"notify"`
|
||||
|
||||
config
|
||||
@@ -67,17 +67,16 @@ type config struct {
|
||||
type checkCache struct {
|
||||
*maxmind.IPInfo
|
||||
allow bool
|
||||
reason string
|
||||
created time.Time
|
||||
}
|
||||
|
||||
type ipLog struct {
|
||||
info *maxmind.IPInfo
|
||||
allowed bool
|
||||
reason string
|
||||
}
|
||||
|
||||
// could be nil
|
||||
var ActiveConfig atomic.Pointer[Config]
|
||||
|
||||
const cacheTTL = 1 * time.Minute
|
||||
|
||||
func (c *checkCache) Expired() bool {
|
||||
@@ -91,7 +90,7 @@ const (
|
||||
ACLDeny = "deny"
|
||||
)
|
||||
|
||||
func (c *Config) Validate() gperr.Error {
|
||||
func (c *Config) Validate() error {
|
||||
switch c.Default {
|
||||
case "", ACLAllow:
|
||||
c.defaultAllow = true
|
||||
@@ -108,7 +107,7 @@ func (c *Config) Validate() gperr.Error {
|
||||
c.allowLocal = true
|
||||
}
|
||||
|
||||
if c.Notify.Interval < 0 {
|
||||
if c.Notify.Interval <= 0 {
|
||||
c.Notify.Interval = defaultNotifyInterval
|
||||
}
|
||||
|
||||
@@ -135,7 +134,10 @@ func (c *Config) Valid() bool {
|
||||
return c != nil && c.valErr == nil
|
||||
}
|
||||
|
||||
func (c *Config) Start(parent task.Parent) gperr.Error {
|
||||
func (c *Config) Start(parent task.Parent) error {
|
||||
if c.valErr != nil {
|
||||
return c.valErr
|
||||
}
|
||||
if c.Log != nil {
|
||||
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
||||
if err != nil {
|
||||
@@ -143,9 +145,6 @@ func (c *Config) Start(parent task.Parent) gperr.Error {
|
||||
}
|
||||
c.logger = logger
|
||||
}
|
||||
if c.valErr != nil {
|
||||
return c.valErr
|
||||
}
|
||||
|
||||
if c.needLogOrNotify() {
|
||||
c.logNotifyCh = make(chan ipLog, 100)
|
||||
@@ -172,13 +171,14 @@ func (c *Config) Start(parent task.Parent) gperr.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
|
||||
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool, reason string) {
|
||||
if common.ForceResolveCountry && info.City == nil {
|
||||
maxmind.LookupCity(info)
|
||||
}
|
||||
c.ipCache.Store(info.Str, &checkCache{
|
||||
IPInfo: info,
|
||||
allow: allow,
|
||||
reason: reason,
|
||||
created: time.Now(),
|
||||
})
|
||||
}
|
||||
@@ -215,23 +215,26 @@ func (c *Config) logNotifyLoop(parent task.Parent) {
|
||||
select {
|
||||
case <-parent.Context().Done():
|
||||
return
|
||||
case log := <-c.logNotifyCh:
|
||||
case req := <-c.logNotifyCh:
|
||||
if c.logger != nil {
|
||||
if !log.allowed || c.logAllowed {
|
||||
c.logger.LogACL(log.info, !log.allowed)
|
||||
if !req.allowed || c.logAllowed {
|
||||
c.logger.LogACL(req.info, !req.allowed, req.reason)
|
||||
}
|
||||
}
|
||||
if c.needNotify() {
|
||||
if log.allowed {
|
||||
if req.allowed {
|
||||
if c.notifyAllowed {
|
||||
c.allowedCount[log.info.Str]++
|
||||
c.allowedCount[req.info.Str]++
|
||||
c.totalAllowedCount++
|
||||
}
|
||||
} else {
|
||||
c.blockedCount[log.info.Str]++
|
||||
c.blockedCount[req.info.Str]++
|
||||
c.totalBlockedCount++
|
||||
}
|
||||
}
|
||||
if !req.allowed {
|
||||
aclevents.Blocked(req.info.Str, req.reason)
|
||||
}
|
||||
case <-c.notifyTicker.C: // will never tick when notify is disabled
|
||||
total := len(c.allowedCount) + len(c.blockedCount)
|
||||
if total == 0 {
|
||||
@@ -263,9 +266,9 @@ func (c *Config) logNotifyLoop(parent task.Parent) {
|
||||
}
|
||||
|
||||
// log and notify if needed
|
||||
func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool) {
|
||||
func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool, reason string) {
|
||||
if c.logNotifyCh != nil {
|
||||
c.logNotifyCh <- ipLog{info: info, allowed: allowed}
|
||||
c.logNotifyCh <- ipLog{info: info, allowed: allowed, reason: reason}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,30 +283,36 @@ func (c *Config) IPAllowed(ip net.IP) bool {
|
||||
}
|
||||
|
||||
if c.allowLocal && ip.IsPrivate() {
|
||||
c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
|
||||
c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true, "allowed by allow_local rule")
|
||||
return true
|
||||
}
|
||||
|
||||
ipStr := ip.String()
|
||||
record, ok := c.ipCache.Load(ipStr)
|
||||
if ok && !record.Expired() {
|
||||
c.logAndNotify(record.IPInfo, record.allow)
|
||||
c.logAndNotify(record.IPInfo, record.allow, record.reason)
|
||||
return record.allow
|
||||
}
|
||||
|
||||
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
||||
if c.Allow.Match(ipAndStr) {
|
||||
c.logAndNotify(ipAndStr, true)
|
||||
c.cacheRecord(ipAndStr, true)
|
||||
return true
|
||||
}
|
||||
if c.Deny.Match(ipAndStr) {
|
||||
c.logAndNotify(ipAndStr, false)
|
||||
c.cacheRecord(ipAndStr, false)
|
||||
if index := c.Deny.MatchedIndex(ipAndStr); index != -1 {
|
||||
reason := "blocked by deny rule: " + c.Deny[index].raw
|
||||
c.logAndNotify(ipAndStr, false, reason)
|
||||
c.cacheRecord(ipAndStr, false, reason)
|
||||
return false
|
||||
}
|
||||
if index := c.Allow.MatchedIndex(ipAndStr); index != -1 {
|
||||
reason := "allowed by allow rule: " + c.Allow[index].raw
|
||||
c.logAndNotify(ipAndStr, true, reason)
|
||||
c.cacheRecord(ipAndStr, true, reason)
|
||||
return true
|
||||
}
|
||||
|
||||
c.logAndNotify(ipAndStr, c.defaultAllow)
|
||||
c.cacheRecord(ipAndStr, c.defaultAllow)
|
||||
reason := "denied by default"
|
||||
if c.defaultAllow {
|
||||
reason = "allowed by default"
|
||||
}
|
||||
c.logAndNotify(ipAndStr, c.defaultAllow, reason)
|
||||
c.cacheRecord(ipAndStr, c.defaultAllow, reason)
|
||||
return c.defaultAllow
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
@@ -12,6 +14,7 @@ type MatcherFunc func(*maxmind.IPInfo) bool
|
||||
|
||||
type Matcher struct {
|
||||
match MatcherFunc
|
||||
raw string
|
||||
}
|
||||
|
||||
type Matchers []Matcher
|
||||
@@ -36,9 +39,9 @@ var errMatcherFormat = gperr.Multiline().AddLines(
|
||||
)
|
||||
|
||||
var (
|
||||
errSyntax = gperr.New("syntax error")
|
||||
errInvalidIP = gperr.New("invalid IP")
|
||||
errInvalidCIDR = gperr.New("invalid CIDR")
|
||||
errSyntax = errors.New("syntax error")
|
||||
errInvalidIP = errors.New("invalid IP")
|
||||
errInvalidCIDR = errors.New("invalid CIDR")
|
||||
)
|
||||
|
||||
func (matcher *Matcher) Parse(s string) error {
|
||||
@@ -46,6 +49,7 @@ func (matcher *Matcher) Parse(s string) error {
|
||||
if len(parts) != 2 {
|
||||
return errSyntax
|
||||
}
|
||||
matcher.raw = s
|
||||
|
||||
switch parts[0] {
|
||||
case MatcherTypeIP:
|
||||
@@ -79,6 +83,27 @@ func (matchers Matchers) Match(ip *maxmind.IPInfo) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (matchers Matchers) MatchedIndex(ip *maxmind.IPInfo) int {
|
||||
for i, m := range matchers {
|
||||
if m.match(ip) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (matchers Matchers) MarshalText() ([]byte, error) {
|
||||
if len(matchers) == 0 {
|
||||
return []byte("[]"), nil
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for _, m := range matchers {
|
||||
buf.WriteString(m.raw)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func matchIP(ip net.IP) MatcherFunc {
|
||||
return func(ip2 *maxmind.IPInfo) bool {
|
||||
return ip.Equal(ip2.IP)
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type TCPListener struct {
|
||||
@@ -44,6 +46,7 @@ func (s *TCPListener) Accept() (net.Conn, error) {
|
||||
}
|
||||
addr, ok := c.RemoteAddr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
log.Error().Msgf("unexpected remote address type: %T, addr: %s", c.RemoteAddr(), c.RemoteAddr().String())
|
||||
// Not a TCPAddr, drop
|
||||
c.Close()
|
||||
return noConn{}, nil
|
||||
|
||||
9
internal/acl/types/acl.go
Normal file
9
internal/acl/types/acl.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package acl
|
||||
|
||||
import "net"
|
||||
|
||||
type ACL interface {
|
||||
IPAllowed(ip net.IP) bool
|
||||
WrapTCP(l net.Listener) net.Listener
|
||||
WrapUDP(l net.PacketConn) net.PacketConn
|
||||
}
|
||||
16
internal/acl/types/context.go
Normal file
16
internal/acl/types/context.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package acl
|
||||
|
||||
import "context"
|
||||
|
||||
type ContextKey struct{}
|
||||
|
||||
func SetCtx(ctx interface{ SetValue(any, any) }, acl ACL) {
|
||||
ctx.SetValue(ContextKey{}, acl)
|
||||
}
|
||||
|
||||
func FromCtx(ctx context.Context) ACL {
|
||||
if acl, ok := ctx.Value(ContextKey{}).(ACL); ok {
|
||||
return acl
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type UDPListener struct {
|
||||
@@ -33,6 +35,7 @@ func (s *UDPListener) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
}
|
||||
udpAddr, ok := addr.(*net.UDPAddr)
|
||||
if !ok {
|
||||
log.Error().Msgf("unexpected remote address type: %T, addr: %s", addr, addr.String())
|
||||
// Not a UDPAddr, drop
|
||||
continue
|
||||
}
|
||||
@@ -52,6 +55,7 @@ func (s *UDPListener) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
}
|
||||
udpAddr, ok := addr.(*net.UDPAddr)
|
||||
if !ok {
|
||||
log.Error().Msgf("unexpected remote address type: %T, addr: %s", addr, addr.String())
|
||||
// Not a UDPAddr, drop
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ func newAgent(cfg *agent.AgentConfig) *Agent {
|
||||
AgentConfig: cfg,
|
||||
httpClient: &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
fasthttpHcClient: &fasthttp.Client{
|
||||
DialTimeout: func(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
|
||||
@@ -16,29 +16,31 @@ import (
|
||||
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
|
||||
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
|
||||
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
|
||||
proxmoxApi "github.com/yusing/godoxy/internal/api/v1/proxmox"
|
||||
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
// NewHandler creates a new Gin engine for the API.
|
||||
//
|
||||
// @title GoDoxy API
|
||||
// @version 1.0
|
||||
// @description GoDoxy API
|
||||
// @termsOfService https://github.com/yusing/godoxy/blob/main/LICENSE
|
||||
|
||||
//
|
||||
// @contact.name Yusing
|
||||
// @contact.url https://github.com/yusing/godoxy/issues
|
||||
|
||||
//
|
||||
// @license.name MIT
|
||||
// @license.url https://github.com/yusing/godoxy/blob/main/LICENSE
|
||||
|
||||
//
|
||||
// @BasePath /api/v1
|
||||
|
||||
//
|
||||
// @externalDocs.description GoDoxy Docs
|
||||
// @externalDocs.url https://docs.godoxy.dev
|
||||
func NewHandler() *gin.Engine {
|
||||
func NewHandler(requireAuth bool) *gin.Engine {
|
||||
if !common.IsDebug {
|
||||
gin.SetMode("release")
|
||||
}
|
||||
@@ -51,7 +53,7 @@ func NewHandler() *gin.Engine {
|
||||
|
||||
r.GET("/api/v1/version", apiV1.Version)
|
||||
|
||||
if auth.IsEnabled() {
|
||||
if auth.IsEnabled() && requireAuth {
|
||||
v1Auth := r.Group("/api/v1/auth")
|
||||
{
|
||||
v1Auth.HEAD("/check", authApi.Check)
|
||||
@@ -64,7 +66,7 @@ func NewHandler() *gin.Engine {
|
||||
}
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
if auth.IsEnabled() {
|
||||
if auth.IsEnabled() && requireAuth {
|
||||
v1.Use(AuthMiddleware())
|
||||
}
|
||||
if common.APISkipOriginCheck {
|
||||
@@ -75,8 +77,8 @@ func NewHandler() *gin.Engine {
|
||||
v1.GET("/favicon", apiV1.FavIcon)
|
||||
v1.GET("/health", apiV1.Health)
|
||||
v1.GET("/icons", apiV1.Icons)
|
||||
v1.POST("/reload", apiV1.Reload)
|
||||
v1.GET("/stats", apiV1.Stats)
|
||||
v1.GET("/events", apiV1.Events)
|
||||
|
||||
route := v1.Group("/route")
|
||||
{
|
||||
@@ -85,6 +87,8 @@ func NewHandler() *gin.Engine {
|
||||
route.GET("/providers", routeApi.Providers)
|
||||
route.GET("/by_provider", routeApi.ByProvider)
|
||||
route.POST("/playground", routeApi.Playground)
|
||||
route.GET("/validate", routeApi.Validate) // websocket
|
||||
route.POST("/validate", routeApi.Validate)
|
||||
}
|
||||
|
||||
file := v1.Group("/file")
|
||||
@@ -140,6 +144,21 @@ func NewHandler() *gin.Engine {
|
||||
docker.POST("/start", dockerApi.Start)
|
||||
docker.POST("/stop", dockerApi.Stop)
|
||||
docker.POST("/restart", dockerApi.Restart)
|
||||
docker.GET("/stats/:id", dockerApi.Stats)
|
||||
}
|
||||
|
||||
proxmox := v1.Group("/proxmox")
|
||||
{
|
||||
proxmox.GET("/tail", proxmoxApi.Tail)
|
||||
proxmox.GET("/journalctl", proxmoxApi.Journalctl)
|
||||
proxmox.GET("/journalctl/:node", proxmoxApi.Journalctl)
|
||||
proxmox.GET("/journalctl/:node/:vmid", proxmoxApi.Journalctl)
|
||||
proxmox.GET("/journalctl/:node/:vmid/:service", proxmoxApi.Journalctl)
|
||||
proxmox.GET("/stats/:node", proxmoxApi.NodeStats)
|
||||
proxmox.GET("/stats/:node/:vmid", proxmoxApi.VMStats)
|
||||
proxmox.POST("/lxc/:node/:vmid/start", proxmoxApi.Start)
|
||||
proxmox.POST("/lxc/:node/:vmid/stop", proxmoxApi.Stop)
|
||||
proxmox.POST("/lxc/:node/:vmid/restart", proxmoxApi.Restart)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,9 +205,8 @@ func ErrorHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
if len(c.Errors) > 0 {
|
||||
logger := log.With().Str("uri", c.Request.RequestURI).Logger()
|
||||
for _, err := range c.Errors {
|
||||
gperr.LogError("Internal error", err.Err, &logger)
|
||||
log.Err(err.Err).Str("uri", c.Request.RequestURI).Msg("Internal error")
|
||||
}
|
||||
if !c.IsWebsocket() {
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
||||
|
||||
@@ -44,6 +44,7 @@ Types are defined in `goutils/apitypes`:
|
||||
| `file` | Configuration file read/write operations |
|
||||
| `auth` | Authentication and session management |
|
||||
| `agent` | Remote agent creation and management |
|
||||
| `proxmox` | Proxmox API management and monitoring |
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -77,15 +78,16 @@ API listening address is configured with `GODOXY_API_ADDR` environment variable.
|
||||
|
||||
### Internal Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
| ----------------------- | --------------------------- |
|
||||
| `internal/route/routes` | Route storage and iteration |
|
||||
| `internal/docker` | Docker client management |
|
||||
| `internal/config` | Configuration access |
|
||||
| `internal/metrics` | System metrics collection |
|
||||
| `internal/homepage` | Homepage item generation |
|
||||
| `internal/agentpool` | Remote agent management |
|
||||
| `internal/auth` | Authentication services |
|
||||
| Package | Purpose |
|
||||
| ----------------------- | ------------------------------------- |
|
||||
| `internal/route/routes` | Route storage and iteration |
|
||||
| `internal/docker` | Docker client management |
|
||||
| `internal/config` | Configuration access |
|
||||
| `internal/metrics` | System metrics collection |
|
||||
| `internal/homepage` | Homepage item generation |
|
||||
| `internal/agentpool` | Remote agent management |
|
||||
| `internal/auth` | Authentication services |
|
||||
| `internal/proxmox` | Proxmox API management and monitoring |
|
||||
|
||||
### External Dependencies
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -12,7 +14,6 @@ import (
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"github.com/yusing/godoxy/internal/route/provider"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
type VerifyNewAgentRequest struct {
|
||||
@@ -36,6 +37,9 @@ type VerifyNewAgentRequest struct {
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /agent/verify [post]
|
||||
func Verify(c *gin.Context) {
|
||||
// avoid timeout waiting for response headers
|
||||
c.Status(http.StatusContinue)
|
||||
|
||||
var request VerifyNewAgentRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
@@ -60,7 +64,7 @@ func Verify(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
nRoutesAdded, err := verifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
|
||||
nRoutesAdded, err := verifyNewAgent(c.Request.Context(), request.Host, ca, client, request.ContainerRuntime)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
@@ -80,9 +84,9 @@ func Verify(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
|
||||
}
|
||||
|
||||
var errAgentAlreadyExists = gperr.New("agent already exists")
|
||||
var errAgentAlreadyExists = errors.New("agent already exists")
|
||||
|
||||
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
||||
func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, error) {
|
||||
var agentCfg agent.AgentConfig
|
||||
agentCfg.Addr = host
|
||||
agentCfg.Runtime = containerRuntime
|
||||
@@ -99,14 +103,14 @@ func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, contain
|
||||
return 0, errAgentAlreadyExists
|
||||
}
|
||||
|
||||
err := agentCfg.InitWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
|
||||
err := agentCfg.InitWithCerts(ctx, ca.Cert, client.Cert, client.Key)
|
||||
if err != nil {
|
||||
return 0, gperr.Wrap(err, "failed to initialize agent config")
|
||||
return 0, fmt.Errorf("failed to initialize agent config: %w", err)
|
||||
}
|
||||
|
||||
provider := provider.NewAgentProvider(&agentCfg)
|
||||
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
|
||||
return 0, gperr.Errorf("provider %s already exists", provider.String())
|
||||
return 0, fmt.Errorf("provider %s already exists", provider.String())
|
||||
}
|
||||
|
||||
// agent must be added before loading routes
|
||||
@@ -118,7 +122,7 @@ func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, contain
|
||||
if err != nil {
|
||||
cfgState.DeleteProvider(provider.String())
|
||||
agentpool.Remove(&agentCfg)
|
||||
return 0, gperr.Wrap(err, "failed to load routes")
|
||||
return 0, fmt.Errorf("failed to load routes: %w", err)
|
||||
}
|
||||
|
||||
return provider.NumRoutes(), nil
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
autocertctx "github.com/yusing/godoxy/internal/autocert/types"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
@@ -21,7 +22,7 @@ import (
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /cert/info [get]
|
||||
func Info(c *gin.Context) {
|
||||
provider := autocert.ActiveProvider.Load()
|
||||
provider := autocertctx.FromCtx(c.Request.Context())
|
||||
if provider == nil {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
||||
return
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
autocertctx "github.com/yusing/godoxy/internal/autocert/types"
|
||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /cert/renew [get]
|
||||
func Renew(c *gin.Context) {
|
||||
autocert := autocert.ActiveProvider.Load()
|
||||
if autocert == nil {
|
||||
provider := autocertctx.FromCtx(c.Request.Context())
|
||||
if provider == nil {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
||||
return
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func Renew(c *gin.Context) {
|
||||
}()
|
||||
|
||||
// renewal happens in background
|
||||
ok := autocert.ForceExpiryAll()
|
||||
ok := provider.ForceExpiryAll()
|
||||
if !ok {
|
||||
log.Error().Msg("cert renewal already in progress")
|
||||
time.Sleep(1 * time.Second) // wait for the log above to be sent
|
||||
@@ -67,5 +67,5 @@ func Renew(c *gin.Context) {
|
||||
}
|
||||
log.Info().Msg("cert force renewal requested")
|
||||
|
||||
autocert.WaitRenewalDone(manager.Context())
|
||||
provider.WaitRenewalDone(manager.Context())
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
@@ -36,18 +37,18 @@ func Containers(c *gin.Context) {
|
||||
serveHTTP[Container](c, GetContainers)
|
||||
}
|
||||
|
||||
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
|
||||
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, error) {
|
||||
errs := gperr.NewBuilder("failed to get containers")
|
||||
containers := make([]Container, 0)
|
||||
for server, dockerClient := range dockerClients {
|
||||
for name, dockerClient := range dockerClients {
|
||||
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
errs.AddSubject(err, name)
|
||||
continue
|
||||
}
|
||||
for _, cont := range conts.Items {
|
||||
containers = append(containers, Container{
|
||||
Server: server,
|
||||
Server: name,
|
||||
Name: cont.Names[0],
|
||||
ID: cont.ID,
|
||||
Image: cont.Image,
|
||||
@@ -59,11 +60,10 @@ func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Containe
|
||||
return containers[i].Name < containers[j].Name
|
||||
})
|
||||
if err := errs.Error(); err != nil {
|
||||
gperr.LogError("failed to get containers", err)
|
||||
if len(containers) == 0 {
|
||||
return nil, err
|
||||
if len(containers) > 0 {
|
||||
log.Err(err).Msg("failed to get containers from some servers")
|
||||
return containers, nil
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
return containers, nil
|
||||
return containers, errs.Error()
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func Info(c *gin.Context) {
|
||||
serveHTTP[dockerInfo](c, GetDockerInfo)
|
||||
}
|
||||
|
||||
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) {
|
||||
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, error) {
|
||||
errs := gperr.NewBuilder("failed to get docker info")
|
||||
dockerInfos := make([]dockerInfo, len(dockerClients))
|
||||
|
||||
@@ -67,7 +67,7 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
|
||||
for name, dockerClient := range dockerClients {
|
||||
info, err := dockerClient.Info(ctx, client.InfoOptions{})
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
errs.AddSubject(err, name)
|
||||
continue
|
||||
}
|
||||
info.Info.Name = name
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
@@ -22,6 +23,7 @@ type LogsQueryParams struct {
|
||||
Since string `form:"from"`
|
||||
Until string `form:"to"`
|
||||
Levels string `form:"levels"`
|
||||
Limit int `form:"limit,default=100" binding:"min=1,max=1000"`
|
||||
} // @name LogsQueryParams
|
||||
|
||||
// @x-id "logs"
|
||||
@@ -34,9 +36,10 @@ type LogsQueryParams struct {
|
||||
// @Param id path string true "container id"
|
||||
// @Param stdout query bool false "show stdout"
|
||||
// @Param stderr query bool false "show stderr"
|
||||
// @Param from query string false "from timestamp"
|
||||
// @Param to query string false "to timestamp"
|
||||
// @Param from query string false "from timestamp"
|
||||
// @Param to query string false "to timestamp"
|
||||
// @Param levels query string false "levels"
|
||||
// @Param limit query int false "limit"
|
||||
// @Success 200
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
@@ -77,7 +80,7 @@ func Logs(c *gin.Context) {
|
||||
Until: queryParams.Until,
|
||||
Timestamps: true,
|
||||
Follow: true,
|
||||
Tail: "100",
|
||||
Tail: strconv.Itoa(queryParams.Limit),
|
||||
}
|
||||
if queryParams.Levels != "" {
|
||||
opts.Details = true
|
||||
|
||||
117
internal/api/v1/docker/stats.go
Normal file
117
internal/api/v1/docker/stats.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
"github.com/yusing/goutils/synk"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
type ContainerStatsResponse container.StatsResponse // @name ContainerStatsResponse
|
||||
|
||||
// @x-id "stats"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get container stats
|
||||
// @Description Get container stats by container id
|
||||
// @Tags docker,websocket
|
||||
// @Produce json
|
||||
// @Param id path string true "Container ID or route alias"
|
||||
// @Success 200 {object} ContainerStatsResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request: id is required or route is not a docker container"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/stats/{id} [get]
|
||||
func Stats(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("id is required"))
|
||||
return
|
||||
}
|
||||
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||
if !ok {
|
||||
var route types.Route
|
||||
route, ok = entrypoint.FromCtx(c.Request.Context()).GetRoute(id)
|
||||
if ok {
|
||||
cont := route.ContainerInfo()
|
||||
if cont == nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("route is not a docker container"))
|
||||
return
|
||||
}
|
||||
dockerCfg = cont.DockerCfg
|
||||
id = cont.ContainerID
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container or route not found"))
|
||||
return
|
||||
}
|
||||
|
||||
dockerClient, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, client.ContainerStatsOptions{Stream: true})
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get container stats"))
|
||||
return
|
||||
}
|
||||
defer stats.Body.Close()
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create websocket manager"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
buf := synk.GetSizedBytesPool().GetSized(4096)
|
||||
defer synk.GetSizedBytesPool().Put(buf)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
default:
|
||||
_, err = io.CopyBuffer(manager.NewWriter(websocket.TextMessage), stats.Body, buf)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, task.ErrProgramExiting) {
|
||||
return
|
||||
}
|
||||
c.Error(apitypes.InternalServerError(err, "failed to copy container stats"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, client.ContainerStatsOptions{Stream: false})
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get container stats"))
|
||||
return
|
||||
}
|
||||
defer stats.Body.Close()
|
||||
|
||||
_, err = io.Copy(c.Writer, stats.Body)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to copy container stats"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
@@ -39,7 +38,7 @@ func handleResult[V any, T ResultType[V]](c *gin.Context, errs error, result T)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
|
||||
func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, error)) {
|
||||
dockerClients := docker.Clients()
|
||||
defer closeAllClients(dockerClients)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
44
internal/api/v1/events.go
Normal file
44
internal/api/v1/events.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/events"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
// @x-id "events"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get events history
|
||||
// @Tags v1
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} events.Event
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden: unauthorized"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal Server Error: internal error"
|
||||
// @Router /events [get]
|
||||
func Events(c *gin.Context) {
|
||||
if !httpheaders.IsWebsocket(c.Request.Header) {
|
||||
c.JSON(http.StatusOK, events.Global.Get())
|
||||
return
|
||||
}
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
writer := manager.NewWriter(websocket.TextMessage)
|
||||
err = events.Global.ListenJSON(c.Request.Context(), writer)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to listen to events"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,18 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
type GetFavIconRequest struct {
|
||||
URL string `form:"url" binding:"required_without=Alias"`
|
||||
Alias string `form:"alias" binding:"required_without=URL"`
|
||||
Variant homepage.IconVariant `form:"variant" binding:"omitempty,oneof=light dark"`
|
||||
URL string `form:"url" binding:"required_without=Alias"`
|
||||
Alias string `form:"alias" binding:"required_without=URL"`
|
||||
Variant icons.Variant `form:"variant" binding:"omitempty,oneof=light dark"`
|
||||
} // @name GetFavIconRequest
|
||||
|
||||
// @x-id "favicon"
|
||||
@@ -27,7 +28,7 @@ type GetFavIconRequest struct {
|
||||
// @Produce image/svg+xml,image/x-icon,image/png,image/webp
|
||||
// @Param url query string false "URL of the route"
|
||||
// @Param alias query string false "Alias of the route"
|
||||
// @Success 200 {array} homepage.FetchResult
|
||||
// @Success 200 {array} iconfetch.Result
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Bad Request: alias is empty or route is not HTTPRoute"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden: unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Not Found: route or icon not found"
|
||||
@@ -42,18 +43,18 @@ func FavIcon(c *gin.Context) {
|
||||
|
||||
// try with url
|
||||
if request.URL != "" {
|
||||
var iconURL homepage.IconURL
|
||||
var iconURL icons.URL
|
||||
if err := iconURL.Parse(request.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
|
||||
return
|
||||
}
|
||||
icon := &iconURL
|
||||
if request.Variant != homepage.IconVariantNone {
|
||||
if request.Variant != icons.VariantNone {
|
||||
icon = icon.WithVariant(request.Variant)
|
||||
}
|
||||
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), icon)
|
||||
fetchResult, err := iconfetch.FetchFavIconFromURL(c.Request.Context(), icon)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, fetchResult.StatusCode, err)
|
||||
iconfetch.GinError(c, fetchResult.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon)
|
||||
@@ -63,40 +64,44 @@ func FavIcon(c *gin.Context) {
|
||||
// try with alias
|
||||
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias, request.Variant)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, result.StatusCode, err)
|
||||
iconfetch.GinError(c, result.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(result.StatusCode, result.ContentType(), result.Icon)
|
||||
}
|
||||
|
||||
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
||||
func GetFavIconFromAlias(ctx context.Context, alias string, variant homepage.IconVariant) (homepage.FetchResult, error) {
|
||||
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
|
||||
// try with route.Icon
|
||||
r, ok := routes.HTTP.Get(alias)
|
||||
ep := entrypoint.FromCtx(ctx)
|
||||
if ep == nil { // impossible, but just in case
|
||||
return iconfetch.FetchResultWithErrorf(http.StatusInternalServerError, "entrypoint not initialized")
|
||||
}
|
||||
r, ok := ep.HTTPRoutes().Get(alias)
|
||||
if !ok {
|
||||
return homepage.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
}
|
||||
|
||||
var (
|
||||
result homepage.FetchResult
|
||||
result iconfetch.Result
|
||||
err error
|
||||
)
|
||||
hp := r.HomepageItem()
|
||||
if hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
|
||||
} else if variant != homepage.IconVariantNone {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
|
||||
if hp.Icon.Source == icons.SourceRelative {
|
||||
result, err = iconfetch.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
|
||||
} else if variant != icons.VariantNone {
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
|
||||
if err != nil {
|
||||
// fallback to no variant
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(homepage.IconVariantNone))
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(icons.VariantNone))
|
||||
}
|
||||
} else {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
result, err = homepage.FindIcon(ctx, r, "/", variant)
|
||||
result, err = iconfetch.FindIcon(ctx, r, "/", variant)
|
||||
}
|
||||
if result.StatusCode == 0 {
|
||||
result.StatusCode = http.StatusOK
|
||||
|
||||
@@ -20,7 +20,7 @@ type ValidateFileRequest struct {
|
||||
// @Summary Validate file
|
||||
// @Description Validate file
|
||||
// @Tags file
|
||||
// @Accept text/plain
|
||||
// @Accept application/yaml
|
||||
// @Produce json
|
||||
// @Param type query FileType true "Type"
|
||||
// @Param file body string true "File content"
|
||||
@@ -29,7 +29,7 @@ type ValidateFileRequest struct {
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
||||
// @Failure 417 {object} any "Validation failed"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /file/validate [post]
|
||||
// @Router /file/validate [post]
|
||||
func Validate(c *gin.Context) {
|
||||
var request ValidateFileRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
@@ -51,7 +51,7 @@ func Validate(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, apitypes.Success("file validated"))
|
||||
}
|
||||
|
||||
func validateFile(fileType FileType, content []byte) gperr.Error {
|
||||
func validateFile(fileType FileType, content []byte) error {
|
||||
switch fileType {
|
||||
case FileTypeConfig:
|
||||
return config.Validate(content)
|
||||
|
||||
@@ -5,13 +5,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type HealthMap = map[string]types.HealthInfoWithoutDetail // @name HealthMap
|
||||
|
||||
// @x-id "health"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get routes health info
|
||||
@@ -19,16 +21,21 @@ import (
|
||||
// @Tags v1,websocket
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} routes.HealthMap "Health info by route name"
|
||||
// @Success 200 {object} HealthMap "Health info by route name"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /health [get]
|
||||
func Health(c *gin.Context) {
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
if ep == nil { // impossible, but just in case
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||
return
|
||||
}
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
|
||||
return routes.GetHealthInfoSimple(), nil
|
||||
return ep.GetHealthInfoWithoutDetail(), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, routes.GetHealthInfoSimple())
|
||||
c.JSON(http.StatusOK, ep.GetHealthInfoWithoutDetail())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "categories"
|
||||
@@ -19,17 +19,23 @@ import (
|
||||
// @Produce json
|
||||
// @Success 200 {array} string
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/categories [get]
|
||||
func Categories(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, HomepageCategories())
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
if ep == nil { // impossible, but just in case
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HomepageCategories(ep))
|
||||
}
|
||||
|
||||
func HomepageCategories() []string {
|
||||
func HomepageCategories(ep entrypoint.Entrypoint) []string {
|
||||
check := make(map[string]struct{})
|
||||
categories := make([]string, 0)
|
||||
categories = append(categories, homepage.CategoryAll)
|
||||
categories = append(categories, homepage.CategoryFavorites)
|
||||
for _, r := range routes.HTTP.Iter {
|
||||
for _, r := range ep.HTTPRoutes().Iter {
|
||||
item := r.HomepageItem()
|
||||
if item.Category == "" {
|
||||
continue
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
@@ -36,6 +36,7 @@ type HomepageItemsRequest struct {
|
||||
// @Success 200 {object} homepage.Homepage
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/items [get]
|
||||
func Items(c *gin.Context) {
|
||||
var request HomepageItemsRequest
|
||||
@@ -53,29 +54,35 @@ func Items(c *gin.Context) {
|
||||
hostname = host
|
||||
}
|
||||
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
if ep == nil {
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not found in context", nil))
|
||||
return
|
||||
}
|
||||
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
|
||||
return HomepageItems(proto, hostname, &request), nil
|
||||
return HomepageItems(ep, proto, hostname, &request), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request))
|
||||
c.JSON(http.StatusOK, HomepageItems(ep, proto, hostname, &request))
|
||||
}
|
||||
}
|
||||
|
||||
func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
|
||||
func HomepageItems(ep entrypoint.Entrypoint, proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
|
||||
switch proto {
|
||||
case "http", "https":
|
||||
default:
|
||||
proto = "http"
|
||||
}
|
||||
|
||||
hp := homepage.NewHomepageMap(routes.HTTP.Size())
|
||||
hp := homepage.NewHomepageMap(ep.HTTPRoutes().Size())
|
||||
|
||||
if strings.Count(hostname, ".") > 1 {
|
||||
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
|
||||
}
|
||||
|
||||
for _, r := range routes.HTTP.Iter {
|
||||
for _, r := range ep.HTTPRoutes().Iter {
|
||||
if request.Provider != "" && r.ProviderName() != request.Provider {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ type ListIconsRequest struct {
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit"
|
||||
// @Param keyword query string false "Keyword"
|
||||
// @Success 200 {array} homepage.IconMetaSearch
|
||||
// @Success 200 {array} iconlist.IconMetaSearch
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Router /icons [get]
|
||||
@@ -32,6 +32,6 @@ func Icons(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
icons := homepage.SearchIcons(request.Keyword, request.Limit)
|
||||
icons := iconlist.SearchIcons(request.Keyword, request.Limit)
|
||||
c.JSON(http.StatusOK, icons)
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func AllSystemInfo(c *gin.Context) {
|
||||
data, err := systeminfo.Poller.GetRespData(req.Period, query)
|
||||
if err != nil {
|
||||
numErrs.Add(1)
|
||||
return gperr.PrependSubject("Main server", err)
|
||||
return gperr.PrependSubject(err, "Main server")
|
||||
}
|
||||
select {
|
||||
case <-manager.Done():
|
||||
@@ -133,7 +133,7 @@ func AllSystemInfo(c *gin.Context) {
|
||||
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
|
||||
if err != nil {
|
||||
numErrs.Add(1)
|
||||
return gperr.PrependSubject("Agent "+a.Name, err)
|
||||
return gperr.PrependSubject(err, "Agent "+a.Name)
|
||||
}
|
||||
select {
|
||||
case <-manager.Done():
|
||||
@@ -170,7 +170,7 @@ func AllSystemInfo(c *gin.Context) {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
|
||||
return
|
||||
}
|
||||
gperr.LogWarn("failed to get some system info", err)
|
||||
log.Warn().Err(err).Msg("failed to get some system info")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
internal/api/v1/proxmox/common.go
Normal file
6
internal/api/v1/proxmox/common.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package proxmoxapi
|
||||
|
||||
type ActionRequest struct {
|
||||
Node string `uri:"node" binding:"required"`
|
||||
VMID uint64 `uri:"vmid" binding:"required"`
|
||||
} // @name ProxmoxVMActionRequest
|
||||
85
internal/api/v1/proxmox/journalctl.go
Normal file
85
internal/api/v1/proxmox/journalctl.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package proxmoxapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/proxmox"
|
||||
"github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl?node=pve&vmid=127&service=pveproxy&service=pvedaemon&limit=10
|
||||
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl/pve/127?service=pveproxy&service=pvedaemon&limit=10
|
||||
|
||||
type JournalctlRequest struct {
|
||||
Node string `form:"node" uri:"node" binding:"required"` // Node name
|
||||
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||
Services []string `form:"service" uri:"service"` // Service names
|
||||
Limit *int `form:"limit" uri:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
||||
} // @name ProxmoxJournalctlRequest
|
||||
|
||||
// @x-id "journalctl"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get journalctl output
|
||||
// @Description Get journalctl output for node or LXC container. If vmid is not provided, streams node journalctl.
|
||||
// @Tags proxmox,websocket
|
||||
// @Accept json
|
||||
// @Produce application/json
|
||||
// @Param query query JournalctlRequest true "Request"
|
||||
// @Param path path JournalctlRequest true "Request"
|
||||
// @Success 200 string plain "Journalctl output"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /proxmox/journalctl [get]
|
||||
// @Router /proxmox/journalctl/{node} [get]
|
||||
// @Router /proxmox/journalctl/{node}/{vmid} [get]
|
||||
// @Router /proxmox/journalctl/{node}/{vmid}/{service} [get]
|
||||
func Journalctl(c *gin.Context) {
|
||||
var request JournalctlRequest
|
||||
uriErr := c.ShouldBindUri(&request)
|
||||
queryErr := c.ShouldBindQuery(&request)
|
||||
if uriErr != nil && queryErr != nil { // allow both uri and query parameters to be set
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", errors.Join(uriErr, queryErr)))
|
||||
return
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(request.Node)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusContinue)
|
||||
|
||||
var reader io.ReadCloser
|
||||
var err error
|
||||
if request.VMID == nil {
|
||||
reader, err = node.NodeJournalctl(c.Request.Context(), request.Services, *request.Limit)
|
||||
} else {
|
||||
reader, err = node.LXCJournalctl(c.Request.Context(), *request.VMID, request.Services, *request.Limit)
|
||||
}
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
writer := manager.NewWriter(websocket.TextMessage)
|
||||
_, err = io.Copy(writer, reader)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to copy journalctl output"))
|
||||
return
|
||||
}
|
||||
}
|
||||
42
internal/api/v1/proxmox/restart.go
Normal file
42
internal/api/v1/proxmox/restart.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package proxmoxapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/proxmox"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "lxcRestart"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Restart LXC container
|
||||
// @Description Restart LXC container by node and vmid
|
||||
// @Tags proxmox
|
||||
// @Produce json
|
||||
// @Param path path ActionRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /proxmox/lxc/:node/:vmid/restart [post]
|
||||
func Restart(c *gin.Context) {
|
||||
var req ActionRequest
|
||||
if err := c.ShouldBindUri(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(req.Node)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := node.LXCAction(c.Request.Context(), req.VMID, proxmox.LXCReboot); err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apitypes.Success("container restarted"))
|
||||
}
|
||||
42
internal/api/v1/proxmox/start.go
Normal file
42
internal/api/v1/proxmox/start.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package proxmoxapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/proxmox"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "lxcStart"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Start LXC container
|
||||
// @Description Start LXC container by node and vmid
|
||||
// @Tags proxmox
|
||||
// @Produce json
|
||||
// @Param path path ActionRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /proxmox/lxc/:node/:vmid/start [post]
|
||||
func Start(c *gin.Context) {
|
||||
var req ActionRequest
|
||||
if err := c.ShouldBindUri(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(req.Node)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := node.LXCAction(c.Request.Context(), req.VMID, proxmox.LXCStart); err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to start container"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apitypes.Success("container started"))
|
||||
}
|
||||
136
internal/api/v1/proxmox/stats.go
Normal file
136
internal/api/v1/proxmox/stats.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package proxmoxapi
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/proxmox"
|
||||
"github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
type StatsRequest ActionRequest
|
||||
|
||||
// @x-id "nodeStats"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get proxmox node stats
|
||||
// @Description Get proxmox node stats in json
|
||||
// @Tags proxmox,websocket
|
||||
// @Produce application/json
|
||||
// @Param node path string true "Node name"
|
||||
// @Success 200 {object} proxmox.NodeStats "Stats output"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /proxmox/stats/{node} [get]
|
||||
func NodeStats(c *gin.Context) {
|
||||
nodeName := c.Param("node")
|
||||
if nodeName == "" {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("node name is required"))
|
||||
return
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(nodeName)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||
return
|
||||
}
|
||||
|
||||
isWs := httpheaders.IsWebsocket(c.Request.Header)
|
||||
|
||||
reader, err := node.NodeStats(c.Request.Context(), isWs)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get stats"))
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
if !isWs {
|
||||
var line [512]byte
|
||||
n, err := reader.Read(line[:])
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to copy stats"))
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "application/json", line[:n])
|
||||
return
|
||||
}
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
writer := manager.NewWriter(websocket.TextMessage)
|
||||
_, err = io.Copy(writer, reader)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to copy stats"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// @x-id "vmStats"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get proxmox VM stats
|
||||
// @Description Get proxmox VM stats in format of "STATUS|CPU%%|MEM USAGE/LIMIT|MEM%%|NET I/O|BLOCK I/O"
|
||||
// @Tags proxmox,websocket
|
||||
// @Produce text/plain
|
||||
// @Param path path StatsRequest true "Request"
|
||||
// @Success 200 string plain "Stats output"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /proxmox/stats/{node}/{vmid} [get]
|
||||
func VMStats(c *gin.Context) {
|
||||
var request StatsRequest
|
||||
if err := c.ShouldBindUri(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(request.Node)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||
return
|
||||
}
|
||||
|
||||
isWs := httpheaders.IsWebsocket(c.Request.Header)
|
||||
|
||||
reader, err := node.LXCStats(c.Request.Context(), request.VMID, isWs)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get stats"))
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
if !isWs {
|
||||
var line [128]byte
|
||||
n, err := reader.Read(line[:])
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to copy stats"))
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "text/plain; charset=utf-8", line[:n])
|
||||
return
|
||||
}
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
writer := manager.NewWriter(websocket.TextMessage)
|
||||
_, err = io.Copy(writer, reader)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to copy stats"))
|
||||
return
|
||||
}
|
||||
}
|
||||
42
internal/api/v1/proxmox/stop.go
Normal file
42
internal/api/v1/proxmox/stop.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package proxmoxapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/proxmox"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "lxcStop"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Stop LXC container
|
||||
// @Description Stop LXC container by node and vmid
|
||||
// @Tags proxmox
|
||||
// @Produce json
|
||||
// @Param path path ActionRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /proxmox/lxc/:node/:vmid/stop [post]
|
||||
func Stop(c *gin.Context) {
|
||||
var req ActionRequest
|
||||
if err := c.ShouldBindUri(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(req.Node)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := node.LXCAction(c.Request.Context(), req.VMID, proxmox.LXCShutdown); err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apitypes.Success("container stopped"))
|
||||
}
|
||||
77
internal/api/v1/proxmox/tail.go
Normal file
77
internal/api/v1/proxmox/tail.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package proxmoxapi
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/proxmox"
|
||||
"github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
// e.g. ws://localhost:8889/api/v1/proxmox/tail?node=pve&vmid=127&file=/var/log/immich/web.log&file=/var/log/immich/ml.log&limit=10
|
||||
|
||||
type TailRequest struct {
|
||||
Node string `form:"node" binding:"required"` // Node name
|
||||
VMID *int `form:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||
Files []string `form:"file" binding:"required,dive,filepath"` // File paths
|
||||
Limit int `form:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
||||
} // @name ProxmoxTailRequest
|
||||
|
||||
// @x-id "tail"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get tail output
|
||||
// @Description Get tail output for node or LXC container. If vmid is not provided, streams node tail.
|
||||
// @Tags proxmox,websocket
|
||||
// @Accept json
|
||||
// @Produce application/json
|
||||
// @Param query query TailRequest true "Request"
|
||||
// @Success 200 string plain "Tail output"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /proxmox/tail [get]
|
||||
func Tail(c *gin.Context) {
|
||||
var request TailRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(request.Node)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusContinue)
|
||||
|
||||
var reader io.ReadCloser
|
||||
var err error
|
||||
if request.VMID == nil {
|
||||
reader, err = node.NodeTail(c.Request.Context(), request.Files, request.Limit)
|
||||
} else {
|
||||
reader, err = node.LXCTail(c.Request.Context(), *request.VMID, request.Files, request.Limit)
|
||||
}
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
writer := manager.NewWriter(websocket.TextMessage)
|
||||
_, err = io.Copy(writer, reader)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to copy journalctl output"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/config"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "reload"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Reload config
|
||||
// @Description Reload config
|
||||
// @Tags v1
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /reload [post]
|
||||
func Reload(c *gin.Context) {
|
||||
if err := config.Reload(); err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to reload config"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, apitypes.Success("config reloaded"))
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type RoutesByProvider map[string][]route.Route
|
||||
@@ -24,5 +24,10 @@ type RoutesByProvider map[string][]route.Route
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /route/by_provider [get]
|
||||
func ByProvider(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, routes.ByProvider())
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
if ep == nil { // impossible, but just in case
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, ep.RoutesByProvider())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package routeApi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -54,16 +55,16 @@ type PlaygroundResponse struct {
|
||||
MatchedRules []string `json:"matchedRules"`
|
||||
FinalRequest FinalRequest `json:"finalRequest"`
|
||||
FinalResponse FinalResponse `json:"finalResponse"`
|
||||
ExecutionError gperr.Error `json:"executionError,omitempty"`
|
||||
ExecutionError error `json:"executionError,omitempty"` // we need the structured error, not the plain string
|
||||
UpstreamCalled bool `json:"upstreamCalled"`
|
||||
} // @name PlaygroundResponse
|
||||
|
||||
type ParsedRule struct {
|
||||
Name string `json:"name"`
|
||||
On string `json:"on"`
|
||||
Do string `json:"do"`
|
||||
ValidationError gperr.Error `json:"validationError,omitempty"`
|
||||
IsResponseRule bool `json:"isResponseRule"`
|
||||
Name string `json:"name"`
|
||||
On string `json:"on"`
|
||||
Do string `json:"do"`
|
||||
ValidationError error `json:"validationError,omitempty"` // we need the structured error, not the plain string
|
||||
IsResponseRule bool `json:"isResponseRule"`
|
||||
} // @name ParsedRule
|
||||
|
||||
type FinalRequest struct {
|
||||
@@ -138,7 +139,7 @@ func Playground(c *gin.Context) {
|
||||
// Execute rules
|
||||
matchedRules := []string{}
|
||||
upstreamCalled := false
|
||||
var executionError gperr.Error
|
||||
var executionError error
|
||||
|
||||
// Variables to capture modified request state
|
||||
var finalReqMethod, finalReqPath, finalReqHost string
|
||||
@@ -244,20 +245,22 @@ func Playground(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFunc, outErr *gperr.Error) {
|
||||
func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFunc, outErr *error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if outErr != nil {
|
||||
*outErr = gperr.Errorf("panic during rule execution: %v", r)
|
||||
*outErr = fmt.Errorf("panic during rule execution: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
h(w, r)
|
||||
}
|
||||
|
||||
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
|
||||
var parsedRules []ParsedRule
|
||||
var rulesList rules.Rules
|
||||
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
|
||||
parsedRules := make([]ParsedRule, 0, len(rawRules))
|
||||
rulesList := make(rules.Rules, 0, len(rawRules))
|
||||
|
||||
var valErrs gperr.Builder
|
||||
|
||||
// Parse each rule individually to capture per-rule errors
|
||||
for _, rawRule := range rawRules {
|
||||
@@ -284,7 +287,11 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
|
||||
|
||||
// Determine if valid
|
||||
isValid := onErr == nil && doErr == nil
|
||||
validationErr := gperr.Join(gperr.PrependSubject("on", onErr), gperr.PrependSubject("do", doErr))
|
||||
var validationErr error
|
||||
if !isValid {
|
||||
validationErr = gperr.Join(gperr.PrependSubject(onErr, "on"), gperr.PrependSubject(doErr, "do"))
|
||||
valErrs.Add(validationErr)
|
||||
}
|
||||
|
||||
parsedRules = append(parsedRules, ParsedRule{
|
||||
Name: name,
|
||||
@@ -300,7 +307,7 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
|
||||
}
|
||||
}
|
||||
|
||||
return parsedRules, rulesList, nil
|
||||
return parsedRules, rulesList, valErrs.Error()
|
||||
}
|
||||
|
||||
func createMockRequest(mock MockRequest) *http.Request {
|
||||
|
||||
@@ -79,7 +79,7 @@ func TestPlayground(t *testing.T) {
|
||||
if len(resp.MatchedRules) != 1 {
|
||||
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
|
||||
}
|
||||
if resp.FinalResponse.StatusCode != 403 {
|
||||
if resp.FinalResponse.StatusCode != http.StatusForbidden {
|
||||
t.Errorf("expected status 403, got %d", resp.FinalResponse.StatusCode)
|
||||
}
|
||||
if resp.UpstreamCalled {
|
||||
@@ -168,7 +168,7 @@ func TestPlayground(t *testing.T) {
|
||||
if len(resp.MatchedRules) != 1 {
|
||||
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
|
||||
}
|
||||
if resp.FinalResponse.StatusCode != 405 {
|
||||
if resp.FinalResponse.StatusCode != http.StatusMethodNotAllowed {
|
||||
t.Errorf("expected status 405, got %d", resp.FinalResponse.StatusCode)
|
||||
}
|
||||
},
|
||||
@@ -179,7 +179,7 @@ func TestPlayground(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create request
|
||||
body, _ := json.Marshal(tt.request)
|
||||
req := httptest.NewRequest("POST", "/api/v1/route/playground", bytes.NewReader(body))
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/route/playground", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Create response recorder
|
||||
@@ -214,7 +214,7 @@ func TestPlayground(t *testing.T) {
|
||||
func TestPlaygroundInvalidRequest(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/v1/route/playground", bytes.NewReader([]byte(`{}`)))
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/route/playground", bytes.NewReader([]byte(`{}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -4,8 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
statequery "github.com/yusing/godoxy/internal/config/query"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
@@ -33,17 +32,16 @@ func Route(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
route, ok := routes.Get(request.Which)
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
if ep == nil { // impossible, but just in case
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||
return
|
||||
}
|
||||
|
||||
route, ok := ep.GetRoute(request.Which)
|
||||
if ok {
|
||||
c.JSON(http.StatusOK, route)
|
||||
return
|
||||
}
|
||||
|
||||
// also search for excluded routes
|
||||
route = statequery.SearchRoute(request.Which)
|
||||
if route != nil {
|
||||
c.JSON(http.StatusOK, route)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusNotFound, nil)
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("route not found"))
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
@@ -32,14 +32,16 @@ func Routes(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
|
||||
provider := c.Query("provider")
|
||||
if provider == "" {
|
||||
c.JSON(http.StatusOK, slices.Collect(routes.IterAll))
|
||||
c.JSON(http.StatusOK, slices.Collect(ep.IterRoutes))
|
||||
return
|
||||
}
|
||||
|
||||
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
||||
for r := range routes.IterAll {
|
||||
rts := make([]types.Route, 0, ep.NumRoutes())
|
||||
for r := range ep.IterRoutes {
|
||||
if r.ProviderName() == provider {
|
||||
rts = append(rts, r)
|
||||
}
|
||||
@@ -48,17 +50,19 @@ func Routes(c *gin.Context) {
|
||||
}
|
||||
|
||||
func RoutesWS(c *gin.Context) {
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
|
||||
provider := c.Query("provider")
|
||||
if provider == "" {
|
||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||
return slices.Collect(routes.IterAll), nil
|
||||
return slices.Collect(ep.IterRoutes), nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
||||
for r := range routes.IterAll {
|
||||
rts := make([]types.Route, 0, ep.NumRoutes())
|
||||
for r := range ep.IterRoutes {
|
||||
if r.ProviderName() == provider {
|
||||
rts = append(rts, r)
|
||||
}
|
||||
|
||||
69
internal/api/v1/route/validate.go
Normal file
69
internal/api/v1/route/validate.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package routeApi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
type _ = route.Route
|
||||
|
||||
// @x-id "validate"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Validate route
|
||||
// @Description Validate route,
|
||||
// @Tags route,websocket
|
||||
// @Accept application/yaml
|
||||
// @Produce json
|
||||
// @Param route body route.Route true "Route"
|
||||
// @Success 200 {object} apitypes.SuccessResponse "Route validated"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Bad request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
||||
// @Failure 417 {object} any "Validation failed"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /route/validate [get]
|
||||
// @Router /route/validate [post]
|
||||
func Validate(c *gin.Context) {
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
ValidateWS(c)
|
||||
return
|
||||
}
|
||||
var request route.Route
|
||||
if err := c.ShouldBindWith(&request, serialization.GinYAMLBinding{}); err != nil {
|
||||
c.JSON(http.StatusExpectationFailed, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, apitypes.Success("route validated"))
|
||||
}
|
||||
|
||||
func ValidateWS(c *gin.Context) {
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
const writeTimeout = 5 * time.Second
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
case msg := <-manager.ReadCh():
|
||||
var request route.Route
|
||||
if err := serialization.UnmarshalValidate(msg, &request, yaml.Unmarshal); err != nil {
|
||||
manager.WriteJSON(gin.H{"error": err}, writeTimeout)
|
||||
continue
|
||||
}
|
||||
manager.WriteJSON(gin.H{"message": "route validated"}, writeTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func (auth *OIDCProvider) setSessionTokenCookie(w http.ResponseWriter, r *http.R
|
||||
|
||||
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {
|
||||
claims = &sessionClaims{}
|
||||
sessionToken, err := jwt.ParseWithClaims(sessionJWT, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
sessionToken, err := jwt.ParseWithClaims(sessionJWT, claims, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/utils"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/time/rate"
|
||||
@@ -76,8 +75,8 @@ const (
|
||||
var (
|
||||
errMissingIDToken = errors.New("missing id_token field from oauth token")
|
||||
|
||||
ErrMissingOAuthToken = gperr.New("missing oauth token")
|
||||
ErrInvalidOAuthToken = gperr.New("invalid oauth token")
|
||||
ErrMissingOAuthToken = errors.New("missing oauth token")
|
||||
ErrInvalidOAuthToken = errors.New("invalid oauth token")
|
||||
)
|
||||
|
||||
// generateState generates a random string for OIDC state.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -8,16 +9,12 @@ import (
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidUsername = gperr.New("invalid username")
|
||||
ErrInvalidPassword = gperr.New("invalid password")
|
||||
)
|
||||
var ErrInvalidUsername = errors.New("invalid username")
|
||||
|
||||
type (
|
||||
UserPassAuth struct {
|
||||
@@ -27,8 +24,9 @@ type (
|
||||
tokenTTL time.Duration
|
||||
}
|
||||
UserPassClaims struct {
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
|
||||
Username string `json:"username"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -81,7 +79,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||
return ErrMissingSessionToken
|
||||
}
|
||||
var claims UserPassClaims
|
||||
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
|
||||
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
@@ -94,9 +92,9 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||
case !token.Valid:
|
||||
return ErrInvalidSessionToken
|
||||
case claims.Username != auth.username:
|
||||
return ErrUserNotAllowed.Subject(claims.Username)
|
||||
return fmt.Errorf("%w: %s", ErrUserNotAllowed, claims.Username)
|
||||
case claims.ExpiresAt.Before(time.Now()):
|
||||
return gperr.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
|
||||
return fmt.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -139,11 +137,12 @@ func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (auth *UserPassAuth) validatePassword(user, pass string) error {
|
||||
if user != auth.username {
|
||||
return ErrInvalidUsername.Subject(user)
|
||||
}
|
||||
// always perform bcrypt comparison to avoid timing attacks
|
||||
if err := bcrypt.CompareHashAndPassword(auth.pwdHash, []byte(pass)); err != nil {
|
||||
return ErrInvalidPassword.With(err).Subject(pass)
|
||||
return err
|
||||
}
|
||||
if user != auth.username {
|
||||
return ErrInvalidUsername
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestUserPassValidateCredentials(t *testing.T) {
|
||||
err := auth.validatePassword("username", "password")
|
||||
expect.NoError(t, err)
|
||||
err = auth.validatePassword("username", "wrong-password")
|
||||
expect.ErrorIs(t, ErrInvalidPassword, err)
|
||||
expect.ErrorIs(t, bcrypt.ErrMismatchedHashAndPassword, err)
|
||||
err = auth.validatePassword("wrong-username", "password")
|
||||
expect.ErrorIs(t, ErrInvalidUsername, err)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingSessionToken = gperr.New("missing session token")
|
||||
ErrInvalidSessionToken = gperr.New("invalid session token")
|
||||
ErrUserNotAllowed = gperr.New("user not allowed")
|
||||
ErrMissingSessionToken = errors.New("missing session token")
|
||||
ErrInvalidSessionToken = errors.New("invalid session token")
|
||||
ErrUserNotAllowed = errors.New("user not allowed")
|
||||
)
|
||||
|
||||
func IsFrontend(r *http.Request) bool {
|
||||
|
||||
@@ -4,10 +4,13 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
@@ -27,7 +30,7 @@ type Config struct {
|
||||
CertPath string `json:"cert_path,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
Extra []ConfigExtra `json:"extra,omitempty"`
|
||||
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers
|
||||
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Options map[string]strutils.Redacted `json:"options,omitempty"`
|
||||
|
||||
@@ -63,13 +66,13 @@ const (
|
||||
|
||||
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
|
||||
|
||||
// Validate implements the utils.CustomValidator interface.
|
||||
func (cfg *Config) Validate() gperr.Error {
|
||||
// Validate implements the serialization.CustomValidator interface.
|
||||
func (cfg *Config) Validate() error {
|
||||
seenPaths := make(map[string]int) // path -> provider idx (0 for main, 1+ for extras)
|
||||
return cfg.validate(seenPaths)
|
||||
}
|
||||
|
||||
func (cfg *ConfigExtra) Validate() gperr.Error {
|
||||
func (cfg *ConfigExtra) Validate() error {
|
||||
return nil // done by main config's validate
|
||||
}
|
||||
|
||||
@@ -77,7 +80,7 @@ func (cfg *ConfigExtra) AsConfig() *Config {
|
||||
return (*Config)(cfg)
|
||||
}
|
||||
|
||||
func (cfg *Config) validate(seenPaths map[string]int) gperr.Error {
|
||||
func (cfg *Config) validate(seenPaths map[string]int) error {
|
||||
if cfg.Provider == "" {
|
||||
cfg.Provider = ProviderLocal
|
||||
}
|
||||
@@ -88,7 +91,7 @@ func (cfg *Config) validate(seenPaths map[string]int) gperr.Error {
|
||||
cfg.KeyPath = KeyFileDefault
|
||||
}
|
||||
if cfg.ACMEKeyPath == "" {
|
||||
cfg.ACMEKeyPath = ACMEKeyFileDefault
|
||||
cfg.ACMEKeyPath = acmeKeyPath(cfg.CADirURL)
|
||||
}
|
||||
|
||||
b := gperr.NewBuilder("certificate error")
|
||||
@@ -154,7 +157,7 @@ func (cfg *Config) validate(seenPaths map[string]int) gperr.Error {
|
||||
cfg.Extra[i].AsConfig().idx = i + 1
|
||||
err := cfg.Extra[i].AsConfig().validate(seenPaths)
|
||||
if err != nil {
|
||||
b.Add(err.Subjectf("extra[%d]", i))
|
||||
b.AddSubjectf(err, "extra[%d]", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,10 +179,10 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
|
||||
log.Info().Err(err).Msg("failed to load ACME private key, generating a now one")
|
||||
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, gperr.New("generate ACME private key").With(err)
|
||||
return nil, nil, fmt.Errorf("generate ACME private key: %w", err)
|
||||
}
|
||||
if err = cfg.SaveACMEKey(privKey); err != nil {
|
||||
return nil, nil, gperr.New("save ACME private key").With(err)
|
||||
return nil, nil, fmt.Errorf("save ACME private key: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +206,7 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
|
||||
if len(cfg.CACerts) > 0 {
|
||||
certPool, err := lego.CreateCertPool(cfg.CACerts, true)
|
||||
if err != nil {
|
||||
return nil, nil, gperr.New("failed to create cert pool").With(err)
|
||||
return nil, nil, fmt.Errorf("failed to create cert pool: %w", err)
|
||||
}
|
||||
legoCfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = certPool
|
||||
}
|
||||
@@ -272,3 +275,16 @@ func (cfg *Config) SaveACMEKey(key *ecdsa.PrivateKey) error {
|
||||
}
|
||||
return os.WriteFile(cfg.ACMEKeyPath, data, 0o600)
|
||||
}
|
||||
|
||||
// acmeKeyPath returns the path to the ACME key file based on the CA directory URL.
|
||||
// Different CA directory URLs will use different key files to avoid key conflicts.
|
||||
func acmeKeyPath(caDirURL string) string {
|
||||
// Use a hash of the CA directory URL to create a unique key filename
|
||||
// Default to "acme" if no custom CA is configured (Let's Encrypt default)
|
||||
filename := "acme"
|
||||
if caDirURL != "" {
|
||||
hash := sha256.Sum256([]byte(caDirURL))
|
||||
filename = "acme_" + hex.EncodeToString(hash[:])[:16]
|
||||
}
|
||||
return filepath.Join(certBasePath, filename+".key")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||
@@ -25,9 +26,9 @@ func TestEABConfigRequired(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
yaml := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
||||
yamlCfg := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
||||
cfg := autocert.Config{}
|
||||
err := serialization.UnmarshalValidateYAML(yaml, &cfg)
|
||||
err := serialization.UnmarshalValidate(yamlCfg, &cfg, yaml.Unmarshal)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, test.wantErr)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package autocert
|
||||
|
||||
const (
|
||||
certBasePath = "certs/"
|
||||
CertFileDefault = certBasePath + "cert.crt"
|
||||
KeyFileDefault = certBasePath + "priv.key"
|
||||
ACMEKeyFileDefault = certBasePath + "acme.key"
|
||||
certBasePath = "certs/"
|
||||
CertFileDefault = certBasePath + "cert.crt"
|
||||
KeyFileDefault = certBasePath + "priv.key"
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
autocert "github.com/yusing/godoxy/internal/autocert/types"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/notif"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -56,15 +57,6 @@ type (
|
||||
|
||||
CertExpiries map[string]time.Time
|
||||
|
||||
CertInfo struct {
|
||||
Subject string `json:"subject"`
|
||||
Issuer string `json:"issuer"`
|
||||
NotBefore int64 `json:"not_before"`
|
||||
NotAfter int64 `json:"not_after"`
|
||||
DNSNames []string `json:"dns_names"`
|
||||
EmailAddresses []string `json:"email_addresses"`
|
||||
} // @name CertInfo
|
||||
|
||||
RenewMode uint8
|
||||
)
|
||||
|
||||
@@ -82,9 +74,6 @@ const (
|
||||
renewModeIfNeeded
|
||||
)
|
||||
|
||||
// could be nil
|
||||
var ActiveProvider atomic.Pointer[Provider]
|
||||
|
||||
func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, error) {
|
||||
p := &Provider{
|
||||
cfg: cfg,
|
||||
@@ -119,14 +108,14 @@ func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||
return p.tlsCert, nil
|
||||
}
|
||||
|
||||
func (p *Provider) GetCertInfos() ([]CertInfo, error) {
|
||||
func (p *Provider) GetCertInfos() ([]autocert.CertInfo, error) {
|
||||
allProviders := p.allProviders()
|
||||
certInfos := make([]CertInfo, 0, len(allProviders))
|
||||
certInfos := make([]autocert.CertInfo, 0, len(allProviders))
|
||||
for _, provider := range allProviders {
|
||||
if provider.tlsCert == nil {
|
||||
continue
|
||||
}
|
||||
certInfos = append(certInfos, CertInfo{
|
||||
certInfos = append(certInfos, autocert.CertInfo{
|
||||
Subject: provider.tlsCert.Leaf.Subject.CommonName,
|
||||
Issuer: provider.tlsCert.Leaf.Issuer.CommonName,
|
||||
NotBefore: provider.tlsCert.Leaf.NotBefore.Unix(),
|
||||
@@ -150,7 +139,7 @@ func (p *Provider) GetName() string {
|
||||
}
|
||||
|
||||
func (p *Provider) fmtError(err error) error {
|
||||
return gperr.PrependSubject(fmt.Sprintf("provider: %s", p.GetName()), err)
|
||||
return gperr.PrependSubject(err, "provider: "+p.GetName())
|
||||
}
|
||||
|
||||
func (p *Provider) GetCertPath() string {
|
||||
@@ -216,19 +205,20 @@ func (p *Provider) ObtainCertIfNotExistsAll() error {
|
||||
for _, provider := range p.allProviders() {
|
||||
errs.Go(func() error {
|
||||
if err := provider.obtainCertIfNotExists(); err != nil {
|
||||
return fmt.Errorf("failed to obtain cert for %s: %w", provider.GetName(), err)
|
||||
return gperr.PrependSubject(err, provider.GetName())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err := errs.Wait().Error()
|
||||
p.rebuildSNIMatcher()
|
||||
return errs.Wait().Error()
|
||||
return err
|
||||
}
|
||||
|
||||
// obtainCertIfNotExists obtains a new certificate for this provider if it does not exist.
|
||||
func (p *Provider) obtainCertIfNotExists() error {
|
||||
err := p.LoadCert()
|
||||
err := p.loadCert()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -256,12 +246,15 @@ func (p *Provider) ObtainCertAll() error {
|
||||
for _, provider := range p.allProviders() {
|
||||
errs.Go(func() error {
|
||||
if err := provider.obtainCertIfNotExists(); err != nil {
|
||||
return fmt.Errorf("failed to obtain cert for %s: %w", provider.GetName(), err)
|
||||
return gperr.PrependSubject(err, provider.GetName())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return errs.Wait().Error()
|
||||
|
||||
err := errs.Wait().Error()
|
||||
p.rebuildSNIMatcher()
|
||||
return err
|
||||
}
|
||||
|
||||
// ObtainCert renews existing certificate or obtains a new certificate for this provider.
|
||||
@@ -346,29 +339,32 @@ func (p *Provider) ObtainCert() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) LoadCert() error {
|
||||
func (p *Provider) LoadCertAll() error {
|
||||
var errs gperr.Builder
|
||||
for _, provider := range p.allProviders() {
|
||||
if err := provider.loadCert(); err != nil {
|
||||
errs.Add(provider.fmtError(err))
|
||||
}
|
||||
}
|
||||
p.rebuildSNIMatcher()
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
func (p *Provider) loadCert() error {
|
||||
cert, err := tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath)
|
||||
if err != nil {
|
||||
errs.Addf("load SSL certificate: %w", p.fmtError(err))
|
||||
return err
|
||||
}
|
||||
|
||||
expiries, err := getCertExpiries(&cert)
|
||||
if err != nil {
|
||||
errs.Addf("parse SSL certificate: %w", p.fmtError(err))
|
||||
return err
|
||||
}
|
||||
|
||||
p.tlsCert = &cert
|
||||
p.certExpiries = expiries
|
||||
|
||||
for _, ep := range p.extraProviders {
|
||||
if err := ep.LoadCert(); err != nil {
|
||||
errs.Add(err)
|
||||
}
|
||||
}
|
||||
|
||||
p.rebuildSNIMatcher()
|
||||
return errs.Error()
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintCertExpiriesAll prints the certificate expiries for this provider and all extra providers.
|
||||
@@ -468,10 +464,10 @@ func (p *Provider) scheduleRenewal(parent task.Parent) {
|
||||
|
||||
renewed, err := p.renew(renewMode)
|
||||
if err != nil {
|
||||
gperr.LogWarn("autocert: cert renew failed", p.fmtError(err))
|
||||
log.Warn().Err(p.fmtError(err)).Msg("autocert: cert renew failed")
|
||||
notif.Notify(¬if.LogMessage{
|
||||
Level: zerolog.ErrorLevel,
|
||||
Title: fmt.Sprintf("SSL certificate renewal failed for %s", p.GetName()),
|
||||
Title: "SSL certificate renewal failed for " + p.GetName(),
|
||||
Body: notif.MessageBody(err.Error()),
|
||||
})
|
||||
return
|
||||
@@ -481,13 +477,13 @@ func (p *Provider) scheduleRenewal(parent task.Parent) {
|
||||
|
||||
notif.Notify(¬if.LogMessage{
|
||||
Level: zerolog.InfoLevel,
|
||||
Title: fmt.Sprintf("SSL certificate renewed for %s", p.GetName()),
|
||||
Title: "SSL certificate renewed for " + p.GetName(),
|
||||
Body: notif.ListBody(p.cfg.Domains),
|
||||
})
|
||||
|
||||
// Reset on success
|
||||
if err := p.ClearLastFailure(); err != nil {
|
||||
gperr.LogWarn("autocert: failed to clear last failure", p.fmtError(err))
|
||||
log.Warn().Err(p.fmtError(err)).Msg("autocert: failed to clear last failure")
|
||||
}
|
||||
timer.Reset(time.Until(p.ShouldRenewOn()))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
@@ -41,7 +42,7 @@ func TestMultipleCertificatesLifecycle(t *testing.T) {
|
||||
cfg.HTTPClient = acmeServer.httpClient()
|
||||
|
||||
/* unmarshal yaml config with multiple certs */
|
||||
err := error(serialization.UnmarshalValidateYAML(yamlConfig, &cfg))
|
||||
err := error(serialization.UnmarshalValidate(yamlConfig, &cfg, yaml.Unmarshal))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"main.example.com"}, cfg.Domains)
|
||||
require.Len(t, cfg.Extra, 2)
|
||||
|
||||
@@ -81,7 +81,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "a.internal.example.com"})
|
||||
@@ -113,7 +113,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.example.com"})
|
||||
@@ -145,7 +145,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "unknown.domain.com"})
|
||||
@@ -171,7 +171,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(nil)
|
||||
@@ -197,7 +197,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: ""})
|
||||
@@ -229,7 +229,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "FOO.EXAMPLE.COM"})
|
||||
@@ -261,7 +261,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: " foo.example.com. "})
|
||||
@@ -293,7 +293,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.a.example.com"})
|
||||
@@ -319,7 +319,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "bar.example.com"})
|
||||
@@ -355,7 +355,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert1, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.test.com"})
|
||||
@@ -392,7 +392,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
err = p.LoadCertAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert1, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.example.com"})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user