mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-27 11:31:06 +01:00
Compare commits
443 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59238adb5b | ||
|
|
5f48f141ca | ||
|
|
a0adc51269 | ||
|
|
c002055892 | ||
|
|
d5406fb039 | ||
|
|
1bd8b5a696 | ||
|
|
79327e98bd | ||
|
|
206f69d249 | ||
|
|
3f6b09d05e | ||
|
|
af68eb4b18 | ||
|
|
9927267149 | ||
|
|
af8cddc1b2 | ||
|
|
c74da5cba9 | ||
|
|
c23cf8ef06 | ||
|
|
733716ba2b | ||
|
|
0716d3dc0d | ||
|
|
b64944cfc3 | ||
|
|
5b068469ef | ||
|
|
6576b7640a | ||
|
|
d4e552754e | ||
|
|
9ca2983a52 | ||
|
|
ed2ca236b0 | ||
|
|
0eba045104 | ||
|
|
77f2779114 | ||
|
|
743eb03b27 | ||
|
|
d2d686b4d1 | ||
|
|
169358659a | ||
|
|
0850ea3918 | ||
|
|
dd84d57f10 | ||
|
|
0aae9f07d1 | ||
|
|
ac1d8f3487 | ||
|
|
6e8f5fb58d | ||
|
|
3001417a37 | ||
|
|
730757e2c3 | ||
|
|
be53b961b6 | ||
|
|
f6a82a3b7c | ||
|
|
4e5ded13fb | ||
|
|
2305eca90b | ||
|
|
4580543693 | ||
|
|
bf54b51036 | ||
|
|
8ba937ec4a | ||
|
|
0f78158c64 | ||
|
|
3a7d1f8b18 | ||
|
|
64ffe44a2d | ||
|
|
dea37a437b | ||
|
|
ee973f7997 | ||
|
|
8756baf7fc | ||
|
|
a12bdeaf55 | ||
|
|
f7676b2dbd | ||
|
|
add7884a36 | ||
|
|
115fba4ff4 | ||
|
|
bb757b2432 | ||
|
|
c2d8cca3b4 | ||
|
|
20695c52e8 | ||
|
|
7baf0b6fe5 | ||
|
|
863f16862b | ||
|
|
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 | ||
|
|
4275cdae38 | ||
|
|
45c821fa98 | ||
|
|
d4b7ae808f | ||
|
|
7687dca456 | ||
|
|
45d6e3bab7 | ||
|
|
41eb8c2ffa | ||
|
|
2e3ebefc4e | ||
|
|
5aa7dc09e5 | ||
|
|
c7d4703622 | ||
|
|
7e99f3465f | ||
|
|
e9d7edef12 | ||
|
|
13441286d1 | ||
|
|
86f35878fb | ||
|
|
7556a06716 | ||
|
|
7385761bdf | ||
|
|
581503e160 | ||
|
|
243e7e9e95 | ||
|
|
8b5cb947c8 | ||
|
|
9ea9e62ee8 | ||
|
|
1ebba20216 | ||
|
|
7bfb57ea30 | ||
|
|
25ceb512b4 | ||
|
|
9205af3a4f | ||
|
|
08f4d9e95f | ||
|
|
a44b9e352c | ||
|
|
424398442b | ||
|
|
724617a2b3 | ||
|
|
61c8ac04e8 | ||
|
|
cc27942c4d | ||
|
|
1c2515cb29 | ||
|
|
45720db754 | ||
|
|
1b9cfa6540 | ||
|
|
f1d906ac11 | ||
|
|
2835fd5fb0 | ||
|
|
11d0c61b9c | ||
|
|
c00854a124 | ||
|
|
117dbb62f4 | ||
|
|
2c28bc116c | ||
|
|
1d90bec9ed | ||
|
|
b2df749cd1 | ||
|
|
1916f73e78 | ||
|
|
99ab9beb4a | ||
|
|
5de064aa47 | ||
|
|
880e11c414 | ||
|
|
0dfce823bf | ||
|
|
c2583fc756 | ||
|
|
cf6246d58a | ||
|
|
fb040afe90 | ||
|
|
dc8abe943d | ||
|
|
587b83cf14 | ||
|
|
a4658caf02 | ||
|
|
ef9ee0e169 | ||
|
|
7eadec9752 | ||
|
|
dd35a4159f | ||
|
|
f28667e23e | ||
|
|
8009da9e4d | ||
|
|
590743f1ef | ||
|
|
1f4c30a48e | ||
|
|
bae7387a5d | ||
|
|
67fc48383d | ||
|
|
1406881071 | ||
|
|
7976befda4 | ||
|
|
8139311074 | ||
|
|
2690bf548d | ||
|
|
d3358ebd89 | ||
|
|
fd74bfedf0 | ||
|
|
a47170da39 | ||
|
|
89a4ca767d | ||
|
|
3dbbde164b | ||
|
|
e75eede332 | ||
|
|
e4658a8f09 | ||
|
|
e25ccdbd24 | ||
|
|
5087800fd7 | ||
|
|
d7f33b7390 | ||
|
|
1978329314 | ||
|
|
dba8441e8a | ||
|
|
44fc678496 | ||
|
|
0b410311da | ||
|
|
dc39f0cb6e | ||
|
|
e232b9d122 | ||
|
|
41f8d3cfc0 | ||
|
|
5ab0392cd3 | ||
|
|
09702266a9 | ||
|
|
14f3ed95ea | ||
|
|
eb3aa21e37 | ||
|
|
a6e86ea420 | ||
|
|
dd96e09a7a | ||
|
|
4d08efbd4f | ||
|
|
f67480d085 | ||
|
|
736985b79d | ||
|
|
1fb1ee0279 | ||
|
|
4b2a6023bb | ||
|
|
5852053ef9 | ||
|
|
c687795cd8 | ||
|
|
93af695e95 | ||
|
|
58325e60b4 | ||
|
|
b134b92704 | ||
|
|
376ac61279 | ||
|
|
dca701e044 | ||
|
|
4bb3af3671 | ||
|
|
95efc127cf | ||
|
|
6e55c4624b | ||
|
|
e9374364dd | ||
|
|
216679eb8d | ||
|
|
505a3d3972 | ||
|
|
27512b4d04 | ||
|
|
88d7255c7a | ||
|
|
ea67095967 | ||
|
|
86a46d191d | ||
|
|
b7250b29e0 | ||
|
|
e44ecc0ccc | ||
|
|
6f9f995100 | ||
|
|
496aec6bb6 | ||
|
|
4afed02fc2 | ||
|
|
f7eb4b132a | ||
|
|
ff934a4bb2 | ||
|
|
db0cbc6577 | ||
|
|
de3f92246f | ||
|
|
c143593284 | ||
|
|
31bf889d4a | ||
|
|
baa7e72ad6 | ||
|
|
f43e07fe60 | ||
|
|
d319ee99ad | ||
|
|
ab58559afc | ||
|
|
a6bdbb5603 | ||
|
|
a0c589c546 | ||
|
|
76b8252755 | ||
|
|
d547872a41 | ||
|
|
8d4618cedf | ||
|
|
2ba758939b | ||
|
|
fdd37b777a | ||
|
|
bc19a54976 | ||
|
|
12d999809f | ||
|
|
6771293336 | ||
|
|
d240c9dfee | ||
|
|
c7eda38933 | ||
|
|
09caa888ad | ||
|
|
e41a487371 | ||
|
|
7c08a8da2e | ||
|
|
82df824490 | ||
|
|
2f341001c1 | ||
|
|
25ee8041da | ||
|
|
8687a57b6c | ||
|
|
3f4ed31e46 | ||
|
|
9930f3fa2e | ||
|
|
2157545e17 | ||
|
|
f721395ff0 | ||
|
|
0dc7c59af1 | ||
|
|
e3fe126a5c | ||
|
|
aa2575696d | ||
|
|
c1f9c2c957 | ||
|
|
c098fef615 | ||
|
|
9cdc985fb0 | ||
|
|
2034738422 | ||
|
|
55a42b81de | ||
|
|
48627753d6 | ||
|
|
09b514393d | ||
|
|
3b2ae5dbd6 | ||
|
|
fac3d67a51 | ||
|
|
cb642d7b32 | ||
|
|
9285977495 | ||
|
|
e00cd8a35b | ||
|
|
8ac459c038 | ||
|
|
1bcaf0dab5 | ||
|
|
a291a49a0e | ||
|
|
28fdf3d2f4 | ||
|
|
84b17baf46 | ||
|
|
06ddb178f8 | ||
|
|
61fa7d2665 | ||
|
|
615521ee1c | ||
|
|
bbe308e821 | ||
|
|
c156173757 | ||
|
|
b1aae1cacf | ||
|
|
f46552b477 | ||
|
|
efe1350ffd | ||
|
|
219eedf3c5 | ||
|
|
f6dcc8f118 | ||
|
|
4d6541c851 | ||
|
|
c9db350cbc | ||
|
|
56374d595a | ||
|
|
d81521f293 | ||
|
|
e9ac3cd1a9 | ||
|
|
d33ff2192a | ||
|
|
910ef639a4 | ||
|
|
3cbd70f73a | ||
|
|
83d70d3bb2 | ||
|
|
bbb1b8497f | ||
|
|
d57d76dc65 | ||
|
|
ef893974ea | ||
|
|
b90f2409ab | ||
|
|
36e9b0d416 | ||
|
|
306cb7a20e | ||
|
|
e3915210aa | ||
|
|
e8fb202ea9 | ||
|
|
082b2f5da2 | ||
|
|
e670acb4b8 | ||
|
|
77e486f4fe | ||
|
|
3ccaba3163 | ||
|
|
705923960c | ||
|
|
ca737c8979 | ||
|
|
b6b5d4dbd7 | ||
|
|
b2919fbaf6 | ||
|
|
722c40d103 | ||
|
|
860d9c71b6 | ||
|
|
e354d901c4 | ||
|
|
921a8fb935 | ||
|
|
975354cdc1 | ||
|
|
7d38bfd2d2 | ||
|
|
5506cafa26 | ||
|
|
9fd5bff81a | ||
|
|
38041ca5b8 | ||
|
|
61be88c1d3 | ||
|
|
cb4dcb962e | ||
|
|
1797a222cd | ||
|
|
098fb7e62d | ||
|
|
d4dfec8293 | ||
|
|
f29b69ff3b | ||
|
|
5e00e1c437 | ||
|
|
39c8cc2820 |
@@ -56,6 +56,10 @@ GODOXY_HTTP3_ENABLED=true
|
|||||||
# API listening address
|
# API listening address
|
||||||
GODOXY_API_ADDR=127.0.0.1:8888
|
GODOXY_API_ADDR=127.0.0.1:8888
|
||||||
|
|
||||||
|
# Local API listening address (unauthenticated, optional)
|
||||||
|
# Useful for local development, debugging or automation
|
||||||
|
GODOXY_LOCAL_API_ADDR=
|
||||||
|
|
||||||
# Metrics
|
# Metrics
|
||||||
GODOXY_METRICS_DISABLE_CPU=false
|
GODOXY_METRICS_DISABLE_CPU=false
|
||||||
GODOXY_METRICS_DISABLE_MEMORY=false
|
GODOXY_METRICS_DISABLE_MEMORY=false
|
||||||
|
|||||||
66
.github/workflows/cli-binary.yml
vendored
Executable file
66
.github/workflows/cli-binary.yml
vendored
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
name: GoDoxy CLI Binary
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "cmd/cli/**"
|
||||||
|
- "internal/api/v1/docs/swagger.json"
|
||||||
|
- "Makefile"
|
||||||
|
- ".github/workflows/cli-binary.yml"
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "cmd/cli/**"
|
||||||
|
- "internal/api/v1/docs/swagger.json"
|
||||||
|
- "Makefile"
|
||||||
|
- ".github/workflows/cli-binary.yml"
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- runner: ubuntu-latest
|
||||||
|
platform: linux/amd64
|
||||||
|
binary_name: godoxy-cli-linux-amd64
|
||||||
|
- runner: ubuntu-24.04-arm
|
||||||
|
platform: linux/arm64
|
||||||
|
binary_name: godoxy-cli-linux-arm64
|
||||||
|
name: Build ${{ matrix.platform }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: "recursive"
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Verify dependencies
|
||||||
|
run: go mod verify
|
||||||
|
|
||||||
|
- name: Build CLI
|
||||||
|
run: |
|
||||||
|
make cli=1 NAME=${{ matrix.binary_name }} build
|
||||||
|
|
||||||
|
- name: Check binary
|
||||||
|
run: |
|
||||||
|
file bin/${{ matrix.binary_name }}
|
||||||
|
|
||||||
|
- name: Upload
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.binary_name }}
|
||||||
|
path: bin/${{ matrix.binary_name }}
|
||||||
|
|
||||||
|
- name: Upload to release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
files: bin/${{ matrix.binary_name }}
|
||||||
1
.github/workflows/docker-image-prod.yml
vendored
1
.github/workflows/docker-image-prod.yml
vendored
@@ -10,7 +10,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/docker-image.yml
|
uses: ./.github/workflows/docker-image.yml
|
||||||
with:
|
with:
|
||||||
image_name: ${{ github.repository_owner }}/godoxy
|
image_name: ${{ github.repository_owner }}/godoxy
|
||||||
old_image_name: ${{ github.repository_owner }}/go-proxy
|
|
||||||
tag: latest
|
tag: latest
|
||||||
target: main
|
target: main
|
||||||
build-prod-agent:
|
build-prod-agent:
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- "socket-proxy/**"
|
- "socket-proxy/**"
|
||||||
|
- "socket-proxy.Dockerfile"
|
||||||
|
- ".github/workflows/docker-image-socket-proxy.yml"
|
||||||
tags-ignore:
|
tags-ignore:
|
||||||
- '**'
|
- "**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
uses: ./.github/workflows/docker-image.yml
|
uses: ./.github/workflows/docker-image.yml
|
||||||
|
|||||||
51
.github/workflows/docker-image.yml
vendored
51
.github/workflows/docker-image.yml
vendored
@@ -9,9 +9,6 @@ on:
|
|||||||
image_name:
|
image_name:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
old_image_name:
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
target:
|
target:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
@@ -48,11 +45,37 @@ jobs:
|
|||||||
attestations: write
|
attestations: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout (for tag resolution)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
platform=${{ matrix.platform }}
|
platform=${{ matrix.platform }}
|
||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
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
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -83,14 +106,15 @@ jobs:
|
|||||||
file: ${{ env.DOCKERFILE }}
|
file: ${{ env.DOCKERFILE }}
|
||||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||||
cache-from: |
|
cache-from: |
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}
|
type=gha,scope=${{ github.workflow }}-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }}
|
||||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
|
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }}
|
||||||
cache-to: |
|
cache-to: |
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
|
type=gha,scope=${{ github.workflow }}-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
|
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ github.ref_name }}
|
VERSION=${{ env.VERSION_FOR_BUILD }}
|
||||||
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
||||||
|
BRANCH=${{ env.BRANCH_FOR_BUILD }}
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
uses: actions/attest-build-provenance@v1
|
uses: actions/attest-build-provenance@v1
|
||||||
@@ -156,17 +180,6 @@ jobs:
|
|||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
$(printf '${{ env.REGISTRY }}/${{ inputs.image_name }}@sha256:%s ' *)
|
$(printf '${{ env.REGISTRY }}/${{ inputs.image_name }}@sha256:%s ' *)
|
||||||
|
|
||||||
- name: Old image name
|
|
||||||
if: inputs.old_image_name != ''
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create -t ${{ env.REGISTRY }}/${{ inputs.old_image_name }}:${{ steps.meta.outputs.version }}\
|
|
||||||
${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ steps.meta.outputs.version }}
|
|
||||||
|
|
||||||
- name: Inspect image
|
- name: Inspect image
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ steps.meta.outputs.version }}
|
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ steps.meta.outputs.version }}
|
||||||
|
|
||||||
- name: Inspect image (old)
|
|
||||||
if: inputs.old_image_name != ''
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.old_image_name }}:${{ steps.meta.outputs.version }}
|
|
||||||
|
|||||||
28
.github/workflows/merge-main-into-compat.yml
vendored
Normal file
28
.github/workflows/merge-main-into-compat.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: Refresh Compat from Main Patch
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/merge-main-into-compat.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
refresh-compat:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Configure git user
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
- name: Refresh compat with single patch commit
|
||||||
|
run: |
|
||||||
|
./scripts/refresh-compat.sh
|
||||||
|
- name: Push compat
|
||||||
|
run: |
|
||||||
|
git push origin compat --force
|
||||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -14,29 +14,33 @@ data/
|
|||||||
debug/
|
debug/
|
||||||
|
|
||||||
logs/
|
logs/
|
||||||
log/
|
|
||||||
|
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
|
||||||
go.work.sum
|
|
||||||
|
|
||||||
!cmd/**/
|
!cmd/**/
|
||||||
!internal/**/
|
!internal/**/
|
||||||
|
|
||||||
todo.md
|
todo.md
|
||||||
|
|
||||||
.*.swp
|
.*.swp
|
||||||
.aider*
|
|
||||||
mtrace.json
|
mtrace.json
|
||||||
.env
|
.env
|
||||||
*.env
|
*.env
|
||||||
.cursorrules
|
|
||||||
.cursor/
|
.cursor/
|
||||||
.windsurfrules
|
|
||||||
test.Dockerfile
|
test.Dockerfile
|
||||||
|
|
||||||
node_modules/
|
|
||||||
tsconfig.tsbuildinfo
|
|
||||||
|
|
||||||
!agent.compose.yml
|
!agent.compose.yml
|
||||||
!agent/pkg/**
|
!agent/pkg/**
|
||||||
|
dev-data/
|
||||||
|
|
||||||
|
RELEASE_NOTES.md
|
||||||
|
CLAUDE.md
|
||||||
|
.kilocode/**
|
||||||
|
|
||||||
|
!.trunk/configs
|
||||||
|
|
||||||
|
# minified files
|
||||||
|
**/*-min.*
|
||||||
|
|
||||||
|
# generated CLI commands
|
||||||
|
cmd/cli/generated_commands.go
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,3 +7,6 @@
|
|||||||
[submodule "goutils"]
|
[submodule "goutils"]
|
||||||
path = goutils
|
path = goutils
|
||||||
url = https://github.com/yusing/goutils.git
|
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:
|
errcheck:
|
||||||
exclude-functions:
|
exclude-functions:
|
||||||
- fmt.Fprintln
|
- fmt.Fprintln
|
||||||
|
- (*gin.Context).Error # gin context error handler
|
||||||
forbidigo:
|
forbidigo:
|
||||||
forbid:
|
forbid:
|
||||||
- pattern: ^print(ln)?$
|
- pattern: ^print(ln)?$
|
||||||
@@ -55,21 +56,15 @@ linters:
|
|||||||
statements: 120
|
statements: 120
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 14
|
min-complexity: 14
|
||||||
|
godoclint:
|
||||||
|
ignore: internal/api/v1/.+
|
||||||
godox:
|
godox:
|
||||||
keywords:
|
keywords:
|
||||||
- FIXME
|
- 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:
|
govet:
|
||||||
disable:
|
disable:
|
||||||
- shadow
|
- shadow
|
||||||
|
- fieldalignment
|
||||||
enable-all: true
|
enable-all: true
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
@@ -106,8 +101,7 @@ linters:
|
|||||||
checks:
|
checks:
|
||||||
- all
|
- all
|
||||||
- -SA1019
|
- -SA1019
|
||||||
dot-import-whitelist:
|
- -QF1008 # keep embedded field selector for clarity
|
||||||
- github.com/yusing/godoxy/internal/utils/testing
|
|
||||||
tagalign:
|
tagalign:
|
||||||
align: false
|
align: false
|
||||||
sort: true
|
sort: true
|
||||||
@@ -135,9 +129,8 @@ linters:
|
|||||||
- legacy
|
- legacy
|
||||||
- std-error-handling
|
- std-error-handling
|
||||||
paths:
|
paths:
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
- examples$
|
||||||
|
- internal/api/v1/.+
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
- gofmt
|
- gofmt
|
||||||
@@ -146,6 +139,7 @@ formatters:
|
|||||||
exclusions:
|
exclusions:
|
||||||
generated: lax
|
generated: lax
|
||||||
paths:
|
paths:
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
- 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:
|
plugins:
|
||||||
sources:
|
sources:
|
||||||
- id: trunk
|
- id: trunk
|
||||||
ref: v1.7.2
|
ref: v1.7.4
|
||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||||
runtimes:
|
runtimes:
|
||||||
enabled:
|
enabled:
|
||||||
- node@22.16.0
|
- node@22.16.0
|
||||||
- python@3.10.8
|
- 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)
|
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||||
lint:
|
lint:
|
||||||
disabled:
|
disabled:
|
||||||
- markdownlint
|
- bandit
|
||||||
- yamllint
|
- black
|
||||||
|
- isort
|
||||||
|
- ruff
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.471
|
- yamllint@1.38.0
|
||||||
- golangci-lint2@2.5.0
|
- markdownlint@0.47.0
|
||||||
|
- checkov@3.2.501
|
||||||
|
- golangci-lint2@2.9.0
|
||||||
- hadolint@2.14.0
|
- hadolint@2.14.0
|
||||||
- actionlint@1.7.7
|
- actionlint@1.7.10
|
||||||
- git-diff-check
|
- git-diff-check
|
||||||
- gofmt@1.20.4
|
- gofmt@1.20.4
|
||||||
- osv-scanner@2.2.2
|
- osv-scanner@2.3.3
|
||||||
- oxipng@9.1.5
|
- oxipng@10.1.0
|
||||||
- prettier@3.6.2
|
- prettier@3.8.1
|
||||||
- shellcheck@0.11.0
|
- shellcheck@0.11.0
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
- trufflehog@3.90.8
|
- trufflehog@3.93.3
|
||||||
|
ignore:
|
||||||
|
- linters: [ALL]
|
||||||
|
paths:
|
||||||
|
- internal/api/v1/docs/**
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
disabled:
|
disabled:
|
||||||
- trunk-announce
|
- trunk-announce
|
||||||
- trunk-check-pre-push
|
|
||||||
- trunk-fmt-pre-commit
|
|
||||||
enabled:
|
enabled:
|
||||||
- trunk-upgrade-available
|
- trunk-upgrade-available
|
||||||
|
- trunk-check-pre-push
|
||||||
|
- trunk-fmt-pre-commit
|
||||||
|
|||||||
6
.vscode/settings.example.json
vendored
6
.vscode/settings.example.json
vendored
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"yaml.schemas": {
|
"yaml.schemas": {
|
||||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/config.schema.json": [
|
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/config.schema.json": [
|
||||||
"config.example.yml",
|
"config.example.yml",
|
||||||
"config.yml"
|
"config.yml"
|
||||||
],
|
],
|
||||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/routes.schema.json": [
|
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/routes.schema.json": [
|
||||||
"providers.example.yml"
|
"providers.example.yml"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
AGENTS.md
Normal file
32
AGENTS.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
- Build: You should not run build command.
|
||||||
|
- Test: `go test -ldflags="-checklinkname=0" ...`
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Update package level `README.md` if exists after making significant changes.
|
||||||
|
|
||||||
|
## Go Guidelines
|
||||||
|
|
||||||
|
1. Use builtin `min` and `max` functions instead of creating custom ones
|
||||||
|
2. Prefer `for i := range 10` over `for i := 0; i < 10; i++`
|
||||||
|
3. Beware of variable shadowing when making edits
|
||||||
|
4. Use `internal/task/task.go` for lifetime management:
|
||||||
|
- `task.RootTask()` for background operations
|
||||||
|
- `parent.Subtask()` for nested tasks
|
||||||
|
- `OnFinished()` and `OnCancel()` callbacks for cleanup
|
||||||
|
5. Use `gperr "goutils/errs"` to build pretty nested errors:
|
||||||
|
- `gperr.Multiline()` for multiple operation attempts
|
||||||
|
- `gperr.NewBuilder()` to collect errors
|
||||||
|
- `gperr.NewGroup() + group.Go()` to collect errors of multiple concurrent operations
|
||||||
|
- `gperr.PrependSubject()` to prepend subject to errors
|
||||||
|
6. Use `github.com/puzpuzpuz/xsync/v4` for lock-free thread safe maps
|
||||||
|
7. Use `goutils/synk` to retrieve and put byte buffer
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Run scoped tests instead of `./...`
|
||||||
|
- Use `testify`, no manual assertions.
|
||||||
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.
|
||||||
17
Dockerfile
17
Dockerfile
@@ -1,10 +1,11 @@
|
|||||||
# Stage 1: deps
|
# Stage 1: deps
|
||||||
FROM golang:1.25.3-alpine AS deps
|
FROM golang:1.26.0-alpine AS deps
|
||||||
HEALTHCHECK NONE
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
# package version does not matter
|
# package version does not matter
|
||||||
|
# libgcc and libstdc++ are needed for bun
|
||||||
# trunk-ignore(hadolint/DL3018)
|
# 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 GOPATH=/root/go
|
||||||
ENV GOCACHE=/root/.cache/go-build
|
ENV GOCACHE=/root/.cache/go-build
|
||||||
@@ -14,12 +15,19 @@ WORKDIR /src
|
|||||||
COPY goutils/go.mod goutils/go.sum ./goutils/
|
COPY goutils/go.mod goutils/go.sum ./goutils/
|
||||||
COPY internal/go-oidc/go.mod internal/go-oidc/go.sum ./internal/go-oidc/
|
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/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 ./
|
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
|
# remove godoxy stuff from go.mod first
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
--mount=type=cache,target=/root/go/pkg/mod \
|
--mount=type=cache,target=/root/go/pkg/mod \
|
||||||
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && go mod download -x
|
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && \
|
||||||
|
sed -i '/^module github\.com\/yusing\/goutils/!{/github\.com\/yusing\/goutils/d}' go.mod && \
|
||||||
|
go mod download -x
|
||||||
|
|
||||||
# Stage 2: builder
|
# Stage 2: builder
|
||||||
FROM deps AS builder
|
FROM deps AS builder
|
||||||
@@ -41,6 +49,9 @@ ENV VERSION=${VERSION}
|
|||||||
ARG MAKE_ARGS
|
ARG MAKE_ARGS
|
||||||
ENV MAKE_ARGS=${MAKE_ARGS}
|
ENV MAKE_ARGS=${MAKE_ARGS}
|
||||||
|
|
||||||
|
ARG BRANCH
|
||||||
|
ENV BRANCH=${BRANCH}
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
--mount=type=cache,target=/root/go/pkg/mod \
|
--mount=type=cache,target=/root/go/pkg/mod \
|
||||||
make ${MAKE_ARGS} docker=1 build
|
make ${MAKE_ARGS} docker=1 build
|
||||||
|
|||||||
11
Jenkinsfile
vendored
Normal file
11
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
node {
|
||||||
|
stage('SCM') {
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
stage('SonarQube Analysis') {
|
||||||
|
def scannerHome = tool 'SonarScanner';
|
||||||
|
withSonarQubeEnv() {
|
||||||
|
sh "${scannerHome}/bin/sonar-scanner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
Makefile
93
Makefile
@@ -1,23 +1,38 @@
|
|||||||
shell := /bin/sh
|
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 BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
||||||
export GOOS = linux
|
export GOOS = linux
|
||||||
|
|
||||||
WEBUI_DIR ?= ../godoxy-webui
|
REPO_URL ?= https://github.com/yusing/godoxy
|
||||||
|
|
||||||
|
WEBUI_DIR ?= $(shell pwd)/../godoxy-webui
|
||||||
DOCS_DIR ?= ${WEBUI_DIR}/wiki
|
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
|
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
|
||||||
|
|
||||||
|
PACKAGE ?= ./cmd
|
||||||
|
|
||||||
ifeq ($(agent), 1)
|
ifeq ($(agent), 1)
|
||||||
NAME = godoxy-agent
|
NAME = godoxy-agent
|
||||||
PWD = ${shell pwd}/agent
|
PWD = ${shell pwd}/agent
|
||||||
else ifeq ($(socket-proxy), 1)
|
else ifeq ($(socket-proxy), 1)
|
||||||
NAME = godoxy-socket-proxy
|
NAME = godoxy-socket-proxy
|
||||||
PWD = ${shell pwd}/socket-proxy
|
PWD = ${shell pwd}/socket-proxy
|
||||||
|
else ifeq ($(cli), 1)
|
||||||
|
NAME = godoxy-cli
|
||||||
|
PWD = ${shell pwd}/cmd/cli
|
||||||
|
PACKAGE = .
|
||||||
else
|
else
|
||||||
NAME = godoxy
|
NAME = godoxy
|
||||||
PWD = ${shell pwd}
|
PWD = ${shell pwd}
|
||||||
|
godoxy = 1
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(trace), 1)
|
ifeq ($(trace), 1)
|
||||||
@@ -35,7 +50,7 @@ else ifeq ($(debug), 1)
|
|||||||
CGO_ENABLED = 1
|
CGO_ENABLED = 1
|
||||||
GODOXY_DEBUG = 1
|
GODOXY_DEBUG = 1
|
||||||
GO_TAGS += debug
|
GO_TAGS += debug
|
||||||
BUILD_FLAGS += -asan # FIXME: -gcflags=all='-N -l'
|
# FIXME: BUILD_FLAGS += -asan -gcflags=all='-N -l'
|
||||||
else ifeq ($(pprof), 1)
|
else ifeq ($(pprof), 1)
|
||||||
CGO_ENABLED = 0
|
CGO_ENABLED = 0
|
||||||
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
||||||
@@ -67,7 +82,11 @@ endif
|
|||||||
|
|
||||||
|
|
||||||
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
|
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
|
||||||
POST_BUILD = $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
|
POST_BUILD = echo;
|
||||||
|
|
||||||
|
ifeq ($(godoxy), 1)
|
||||||
|
POST_BUILD += $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
|
||||||
|
endif
|
||||||
ifeq ($(docker), 1)
|
ifeq ($(docker), 1)
|
||||||
POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run;
|
POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run;
|
||||||
endif
|
endif
|
||||||
@@ -75,15 +94,16 @@ endif
|
|||||||
.PHONY: debug
|
.PHONY: debug
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -v -race ./internal/...
|
CGO_ENABLED=1 go test -v -race ${BUILD_FLAGS} ./internal/...
|
||||||
|
|
||||||
docker-build-test:
|
docker-build-test:
|
||||||
docker build -t godoxy .
|
docker build -t godoxy .
|
||||||
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
|
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
|
||||||
|
docker build --build-arg=MAKE_ARGS=socket-proxy=1 -t godoxy-socket-proxy .
|
||||||
|
|
||||||
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
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)
|
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:
|
update-go:
|
||||||
for file in ${files}; do \
|
for file in ${files}; do \
|
||||||
@@ -108,13 +128,33 @@ mod-tidy:
|
|||||||
cd ${PWD}/$$path && go mod tidy; \
|
cd ${PWD}/$$path && go mod tidy; \
|
||||||
done
|
done
|
||||||
|
|
||||||
|
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:
|
build:
|
||||||
|
@if [ "${godoxy}" = "1" ]; then \
|
||||||
|
make minify-js; \
|
||||||
|
elif [ "${cli}" = "1" ]; then \
|
||||||
|
make gen-cli; \
|
||||||
|
fi
|
||||||
mkdir -p $(shell dirname ${BIN_PATH})
|
mkdir -p $(shell dirname ${BIN_PATH})
|
||||||
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
|
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ${PACKAGE}
|
||||||
${POST_BUILD}
|
${POST_BUILD}
|
||||||
|
|
||||||
run:
|
run: minify-js
|
||||||
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
|
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${PACKAGE}
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
docker compose -f dev.compose.yml $(args)
|
docker compose -f dev.compose.yml $(args)
|
||||||
@@ -122,11 +162,13 @@ dev:
|
|||||||
dev-build: build
|
dev-build: build
|
||||||
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
|
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
|
||||||
|
|
||||||
dev-run: build
|
benchmark:
|
||||||
cd dev-data && ${BIN_PATH}
|
@TARGETS="$(TARGET)"; \
|
||||||
|
if [ -z "$$TARGETS" ]; then TARGETS="godoxy traefik caddy nginx"; fi; \
|
||||||
mtrace:
|
trap 'docker compose -f dev.compose.yml down $$TARGETS' EXIT; \
|
||||||
${BIN_PATH} debug-ls-mtrace > mtrace.json
|
docker compose -f dev.compose.yml up -d --force-recreate $$TARGETS; \
|
||||||
|
sleep 1; \
|
||||||
|
./scripts/benchmark.sh
|
||||||
|
|
||||||
rapid-crash:
|
rapid-crash:
|
||||||
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
||||||
@@ -141,23 +183,28 @@ ci-test:
|
|||||||
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
||||||
|
|
||||||
cloc:
|
cloc:
|
||||||
cloc --include-lang=Go --not-match-f '_test.go$$' .
|
scc -w -i go --not-match '_test.go$$'
|
||||||
|
|
||||||
push-github:
|
push-github:
|
||||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
git push origin $(BRANCH)
|
||||||
|
|
||||||
gen-swagger:
|
gen-swagger:
|
||||||
|
# go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
swag init --parseDependency --parseInternal --parseFuncBody -g handler.go -d internal/api -o internal/api/v1/docs
|
swag init --parseDependency --parseInternal --parseFuncBody -g handler.go -d internal/api -o internal/api/v1/docs
|
||||||
python3 scripts/fix-swagger-json.py
|
python3 scripts/fix-swagger-json.py
|
||||||
# we don't need this
|
# we don't need this
|
||||||
rm internal/api/v1/docs/docs.go
|
rm internal/api/v1/docs/docs.go
|
||||||
|
cp internal/api/v1/docs/swagger.json ${DOCS_DIR}/public/api.json
|
||||||
gen-swagger-markdown: gen-swagger
|
|
||||||
# brew tap go-swagger/go-swagger && brew install go-swagger
|
|
||||||
swagger generate markdown -f internal/api/v1/docs/swagger.yaml --skip-validation --output ${DOCS_DIR}/src/API.md
|
|
||||||
|
|
||||||
gen-api-types: gen-swagger
|
gen-api-types: gen-swagger
|
||||||
# --disable-throw-on-error
|
# --disable-throw-on-error
|
||||||
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
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
|
--responses -o ${WEBUI_DIR}/src/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||||
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
|
|
||||||
|
.PHONY: gen-cli build-cli update-wiki
|
||||||
|
|
||||||
|
gen-cli:
|
||||||
|
cd cmd/cli && go run ./gen
|
||||||
|
|
||||||
|
update-wiki:
|
||||||
|
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts
|
||||||
|
|||||||
47
README.md
47
README.md
@@ -33,6 +33,9 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
|||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Setup](#setup)
|
- [Setup](#setup)
|
||||||
- [How does GoDoxy work](#how-does-godoxy-work)
|
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||||
|
- [Proxmox Integration](#proxmox-integration)
|
||||||
|
- [Automatic Route Binding](#automatic-route-binding)
|
||||||
|
- [WebUI Management](#webui-management)
|
||||||
- [Update / Uninstall system agent](#update--uninstall-system-agent)
|
- [Update / Uninstall system agent](#update--uninstall-system-agent)
|
||||||
- [Screenshots](#screenshots)
|
- [Screenshots](#screenshots)
|
||||||
- [idlesleeper](#idlesleeper)
|
- [idlesleeper](#idlesleeper)
|
||||||
@@ -46,8 +49,6 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
|||||||
|
|
||||||
<https://demo.godoxy.dev>
|
<https://demo.godoxy.dev>
|
||||||
|
|
||||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- **Simple**
|
- **Simple**
|
||||||
@@ -69,7 +70,11 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
|||||||
- Podman
|
- Podman
|
||||||
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||||
- Docker containers
|
- 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**
|
- **Traffic Management**
|
||||||
- HTTP reserve proxy
|
- HTTP reserve proxy
|
||||||
- TCP/UDP port forwarding
|
- TCP/UDP port forwarding
|
||||||
@@ -82,7 +87,12 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
|||||||
- App Dashboard
|
- App Dashboard
|
||||||
- Config Editor
|
- Config Editor
|
||||||
- Uptime and System Metrics
|
- 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**
|
- **Cross-Platform support**
|
||||||
- Supports **linux/amd64** and **linux/arm64**
|
- Supports **linux/amd64** and **linux/arm64**
|
||||||
- **Efficient and Performant**
|
- **Efficient and Performant**
|
||||||
@@ -130,6 +140,35 @@ 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`.
|
> 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 or log files output from nodes
|
||||||
|
- **LXC Logs**: Stream real-time journalctl or log files output from containers
|
||||||
|
|
||||||
## Update / Uninstall system agent
|
## Update / Uninstall system agent
|
||||||
|
|
||||||
Update:
|
Update:
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
- [安裝](#安裝)
|
- [安裝](#安裝)
|
||||||
- [手動安裝](#手動安裝)
|
- [手動安裝](#手動安裝)
|
||||||
- [資料夾結構](#資料夾結構)
|
- [資料夾結構](#資料夾結構)
|
||||||
|
- [Proxmox 整合](#proxmox-整合)
|
||||||
|
- [自動路由綁定](#自動路由綁定)
|
||||||
|
- [WebUI 管理](#webui-管理)
|
||||||
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
||||||
- [截圖](#截圖)
|
- [截圖](#截圖)
|
||||||
- [閒置休眠](#閒置休眠)
|
- [閒置休眠](#閒置休眠)
|
||||||
@@ -45,8 +48,6 @@
|
|||||||
|
|
||||||
<https://demo.godoxy.dev>
|
<https://demo.godoxy.dev>
|
||||||
|
|
||||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
|
||||||
|
|
||||||
## 主要特點
|
## 主要特點
|
||||||
|
|
||||||
- **簡單易用**
|
- **簡單易用**
|
||||||
@@ -69,6 +70,10 @@
|
|||||||
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
||||||
- Docker 容器
|
- Docker 容器
|
||||||
- Proxmox LXC 容器
|
- Proxmox LXC 容器
|
||||||
|
- **Proxmox 整合**
|
||||||
|
- **自動路由綁定**:透過比對主機名稱、IP 或別名自動將路由綁定至 Proxmox 節點或 LXC 容器
|
||||||
|
- **LXC 生命週期控制**:可直接從 WebUI 啟動、停止、重新啟動容器
|
||||||
|
- **即時日誌**:透過 WebSocket 串流節點和 LXC 容器的 journalctl 日誌
|
||||||
- **流量管理**
|
- **流量管理**
|
||||||
- HTTP 反向代理
|
- HTTP 反向代理
|
||||||
- TCP/UDP 連接埠轉送
|
- TCP/UDP 連接埠轉送
|
||||||
@@ -81,7 +86,12 @@
|
|||||||
- 應用程式一覽
|
- 應用程式一覽
|
||||||
- 設定編輯器
|
- 設定編輯器
|
||||||
- 執行時間與系統指標
|
- 執行時間與系統指標
|
||||||
- Docker 日誌檢視器
|
- **Docker**
|
||||||
|
- 容器生命週期管理 (啟動、停止、重新啟動)
|
||||||
|
- 透過 WebSocket 即時串流容器日誌
|
||||||
|
- **Proxmox**
|
||||||
|
- LXC 容器生命週期管理 (啟動、停止、重新啟動)
|
||||||
|
- 透過 WebSocket 即時串流節點和 LXC 容器 journalctl 日誌
|
||||||
- **跨平台支援**
|
- **跨平台支援**
|
||||||
- 支援 **linux/amd64** 與 **linux/arm64**
|
- 支援 **linux/amd64** 與 **linux/arm64**
|
||||||
- **高效能**
|
- **高效能**
|
||||||
@@ -146,6 +156,35 @@
|
|||||||
└── .env
|
└── .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 或日誌檔案輸出
|
||||||
|
|
||||||
## 更新 / 卸載系統代理 (System Agent)
|
## 更新 / 卸載系統代理 (System Agent)
|
||||||
|
|
||||||
更新:
|
更新:
|
||||||
|
|||||||
52
agent/cmd/README.md
Normal file
52
agent/cmd/README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# agent/cmd
|
||||||
|
|
||||||
|
The main entry point for the GoDoxy Agent, a secure monitoring and proxy agent that runs alongside Docker containers.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package contains the `main.go` entry point for the GoDoxy Agent. The agent is a TLS-enabled server that provides:
|
||||||
|
|
||||||
|
- Secure Docker socket proxying with client certificate authentication
|
||||||
|
- HTTP proxy capabilities for container traffic
|
||||||
|
- System metrics collection and monitoring
|
||||||
|
- Health check endpoints
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[main] --> B[Logger Init]
|
||||||
|
A --> C[Load CA Certificate]
|
||||||
|
A --> D[Load Server Certificate]
|
||||||
|
A --> E[Log Version Info]
|
||||||
|
A --> F[Start Agent Server]
|
||||||
|
A --> G[Start Socket Proxy]
|
||||||
|
A --> H[Start System Info Poller]
|
||||||
|
A --> I[Wait Exit]
|
||||||
|
|
||||||
|
F --> F1[TLS with mTLS]
|
||||||
|
F --> F2[Agent Handler]
|
||||||
|
G --> G1[Docker Socket Proxy]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main Function Flow
|
||||||
|
|
||||||
|
1. **Logger Setup**: Configures zerolog with console output
|
||||||
|
1. **Certificate Loading**: Loads CA and server certificates for TLS/mTLS
|
||||||
|
1. **Version Logging**: Logs agent version and configuration
|
||||||
|
1. **Agent Server**: Starts the main HTTPS server with agent handlers
|
||||||
|
1. **Socket Proxy**: Starts Docker socket proxy if configured
|
||||||
|
1. **System Monitoring**: Starts system info polling
|
||||||
|
1. **Graceful Shutdown**: Waits for exit signal (3 second timeout)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
See `agent/pkg/env/README.md` for configuration options.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `agent/pkg/agent` - Core agent types and constants
|
||||||
|
- `agent/pkg/env` - Environment configuration
|
||||||
|
- `agent/pkg/server` - Server implementation
|
||||||
|
- `socketproxy/pkg` - Docker socket proxy
|
||||||
|
- `internal/metrics/systeminfo` - System metrics
|
||||||
@@ -1,21 +1,31 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
stdlog "log"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"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/env"
|
||||||
"github.com/yusing/godoxy/agent/pkg/server"
|
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||||
httpServer "github.com/yusing/goutils/server"
|
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
"github.com/yusing/goutils/version"
|
"github.com/yusing/goutils/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: support IPv6
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
writer := zerolog.ConsoleWriter{
|
writer := zerolog.ConsoleWriter{
|
||||||
Out: os.Stderr,
|
Out: os.Stderr,
|
||||||
@@ -52,30 +62,110 @@ func main() {
|
|||||||
Tips:
|
Tips:
|
||||||
1. To change the agent name, you can set the AGENT_NAME environment variable.
|
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.
|
2. To change the agent port, you can set the AGENT_PORT environment variable.
|
||||||
`)
|
`)
|
||||||
|
|
||||||
t := task.RootTask("agent", false)
|
t := task.RootTask("agent", false)
|
||||||
opts := server.Options{
|
|
||||||
CACert: caCert,
|
// One TCP listener on AGENT_PORT, then multiplex by TLS ALPN:
|
||||||
ServerCert: srvCert,
|
// - Stream ALPN: route to TCP stream tunnel handler (via http.Server.TLSNextProto)
|
||||||
Port: env.AgentPort,
|
// - 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 != "" {
|
if socketproxy.ListenAddr != "" {
|
||||||
runtime := strutils.Title(string(env.Runtime))
|
runtime := strutils.Title(string(env.Runtime))
|
||||||
|
|
||||||
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
||||||
opts := httpServer.Options{
|
l, err := net.Listen("tcp", socketproxy.ListenAddr)
|
||||||
Name: runtime,
|
if err != nil {
|
||||||
HTTPAddr: socketproxy.ListenAddr,
|
log.Fatal().Err(err).Msg("failed to listen on port")
|
||||||
Handler: socketproxy.NewHandler(),
|
|
||||||
}
|
}
|
||||||
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)
|
task.WaitExit(3)
|
||||||
}
|
}
|
||||||
|
|||||||
114
agent/go.mod
114
agent/go.mod
@@ -1,115 +1,109 @@
|
|||||||
module github.com/yusing/godoxy/agent
|
module github.com/yusing/godoxy/agent
|
||||||
|
|
||||||
go 1.25.3
|
go 1.26.0
|
||||||
|
|
||||||
replace github.com/yusing/godoxy => ..
|
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/yusing/godoxy/socketproxy => ../socket-proxy
|
replace (
|
||||||
|
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
||||||
replace github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
github.com/yusing/godoxy => ../
|
||||||
|
github.com/yusing/godoxy/socketproxy => ../socket-proxy
|
||||||
replace github.com/yusing/goutils => ../goutils
|
github.com/yusing/goutils => ../goutils
|
||||||
|
github.com/yusing/goutils/http/reverseproxy => ../goutils/http/reverseproxy
|
||||||
|
github.com/yusing/goutils/http/websocket => ../goutils/http/websocket
|
||||||
|
github.com/yusing/goutils/server => ../goutils/server
|
||||||
|
)
|
||||||
|
|
||||||
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.14.1
|
github.com/bytedance/sonic v1.15.0
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.2.0
|
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/rs/zerolog v1.34.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/valyala/fasthttp v1.68.0
|
github.com/yusing/godoxy v0.26.0
|
||||||
github.com/yusing/godoxy v0.19.2
|
|
||||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||||
github.com/yusing/goutils v0.7.0
|
github.com/yusing/goutils v0.7.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
|
||||||
github.com/andybalholm/brotli v1.2.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/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.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
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/cli v28.5.1+incompatible // indirect
|
github.com/docker/cli v29.2.1+incompatible // indirect
|
||||||
github.com/docker/docker v28.5.1+incompatible // indirect
|
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.9.0 // indirect
|
github.com/ebitengine/purego v0.10.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/gotify/server/v2 v2.7.3 // indirect
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.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-20260216142805-b3301c5f2a88 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
github.com/moby/moby/api v1.52.0 // indirect
|
||||||
|
github.com/moby/moby/client v0.2.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // 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/pkg/errors v0.9.1 // indirect
|
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/samber/lo v1.52.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/samber/slog-common v0.19.0 // indirect
|
github.com/shirou/gopsutil/v4 v4.26.1 // indirect
|
||||||
github.com/samber/slog-zerolog/v2 v2.8.0 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/shirou/gopsutil/v4 v4.25.9 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
github.com/yusing/ds v0.3.1 // indirect
|
github.com/yusing/ds v0.4.1 // indirect
|
||||||
github.com/yusing/gointernals v0.1.16 // indirect
|
github.com/yusing/gointernals v0.2.0 // indirect
|
||||||
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260223150038-3be815cb6e3b // indirect
|
||||||
|
github.com/yusing/goutils/http/websocket v0.0.0-20260223150038-3be815cb6e3b // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
golang.org/x/arch v0.24.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/crypto v0.43.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
golang.org/x/net v0.46.0 // indirect
|
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
|
||||||
golang.org/x/text v0.30.0 // indirect
|
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
301
agent/go.sum
301
agent/go.sum
@@ -1,9 +1,7 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
@@ -12,22 +10,22 @@ github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
|||||||
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
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 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
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 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -39,28 +37,26 @@ 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/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 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
|
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
||||||
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
|
||||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
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-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
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 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
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 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
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 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-acme/lego/v4 v4.27.0 h1:cIhWd7Uj4BNFLEF3IpwuMkukVVRs5qjlp4KdUGa75yU=
|
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
|
||||||
github.com/go-acme/lego/v4 v4.27.0/go.mod h1:9FfNZHZmg6hf5CWOp4Lzo4gU8aBEvqZvrwdkBboa+4g=
|
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
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-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=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -77,18 +73,17 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
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/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 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
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/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.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
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/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@@ -98,16 +93,14 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
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/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
|
||||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
|
||||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
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 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -118,10 +111,10 @@ 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/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
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-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
|
github.com/luthermonson/go-proxmox v0.4.0 h1:LKXpG9d64zTaQF79wV0kfOnnSwIcdG39m7sc4ga+XZs=
|
||||||
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
github.com/luthermonson/go-proxmox v0.4.0/go.mod h1:U6dAkJ+iiwaeb1g/LMWpWuWN4nmvWeXhmoMuYJMumS4=
|
||||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
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/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
@@ -131,23 +124,19 @@ 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
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 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
|
||||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
@@ -156,21 +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/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 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
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/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
|
||||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
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/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.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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
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.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
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=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
@@ -178,169 +174,92 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
|||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
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 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
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.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
|
||||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
|
||||||
github.com/samber/slog-zerolog/v2 v2.8.0 h1:K3+PJieRyi2rX/eaJZ95EdmpY/pzdeDd3jRnIQZG6kU=
|
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
|
||||||
github.com/samber/slog-zerolog/v2 v2.8.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
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 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
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.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.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.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.7.1/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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
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.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
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 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
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.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||||
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg=
|
||||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
github.com/yusing/gointernals v0.2.0/go.mod h1:xGzNbPGMm5Z8kG0t4JYISMscw+gMQlgghkLxlgRZv5Y=
|
||||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
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/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
|
||||||
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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
|
||||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
|
||||||
golang.org/x/oauth2 v0.32.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/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
|
||||||
golang.org/x/sync v0.17.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-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-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-20210616094352-59db8d763f22/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.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.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.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.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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/sys v0.37.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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
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/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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
|
||||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
|
||||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@@ -349,3 +268,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
|
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||||
|
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||||
|
|||||||
108
agent/pkg/agent/README.md
Normal file
108
agent/pkg/agent/README.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# agent/pkg/agent
|
||||||
|
|
||||||
|
The `agent` package provides the client-side implementation for interacting with GoDoxy agents. It handles agent configuration, secure communication via TLS, and provides utilities for agent deployment and management.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph GoDoxy Server
|
||||||
|
AP[Agent Pool] --> AC[AgentConfig]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Agent Communication
|
||||||
|
AC -->|HTTPS| AI[Agent Info API]
|
||||||
|
AC -->|TLS| ST[Stream Tunneling]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Deployment
|
||||||
|
G[Generator] --> DC[Docker Compose]
|
||||||
|
G --> IS[Install Script]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Security
|
||||||
|
NA[NewAgent] --> Certs[Certificates]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| 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`](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`](config.go:45)
|
||||||
|
|
||||||
|
Contains basic metadata about the agent, including its version, name, and container runtime (Docker or Podman).
|
||||||
|
|
||||||
|
### [`PEMPair`](new_agent.go:53)
|
||||||
|
|
||||||
|
A utility struct for handling PEM-encoded certificate and key pairs, supporting encryption, decryption, and conversion to `tls.Certificate`.
|
||||||
|
|
||||||
|
## Agent Creation and Certificate Management
|
||||||
|
|
||||||
|
### Certificate Generation
|
||||||
|
|
||||||
|
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.
|
||||||
|
- **Client Certificate**: For the GoDoxy server to authenticate with the agent.
|
||||||
|
|
||||||
|
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`](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`](config.go:29) handles the loading of CA and client certificates to establish secure connections.
|
||||||
|
|
||||||
|
### 2. Agent Discovery and Initialization
|
||||||
|
|
||||||
|
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.
|
||||||
|
- Test support for TCP and UDP stream tunneling.
|
||||||
|
|
||||||
|
### 3. Deployment Generators
|
||||||
|
|
||||||
|
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()`](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`](config.go:90) and [`GetAgentAddrFromDockerHost`](config.go:94).
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg := &agent.AgentConfig{}
|
||||||
|
cfg.Parse("192.168.1.100:8081")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := cfg.Init(ctx); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Connected to agent: %s (Version: %s)\n", cfg.Name, cfg.Version)
|
||||||
|
```
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"iter"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
var agentPool = xsync.NewMap[string, *AgentConfig](xsync.WithPresize(10))
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if strings.HasSuffix(os.Args[0], ".test") {
|
|
||||||
agentPool.Store("test-agent", &AgentConfig{
|
|
||||||
Addr: "test-agent",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAgent(agentAddrOrDockerHost string) (*AgentConfig, bool) {
|
|
||||||
if !IsDockerHostAgent(agentAddrOrDockerHost) {
|
|
||||||
return getAgentByAddr(agentAddrOrDockerHost)
|
|
||||||
}
|
|
||||||
return getAgentByAddr(GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAgentByName(name string) (*AgentConfig, bool) {
|
|
||||||
for _, agent := range agentPool.Range {
|
|
||||||
if agent.Name == name {
|
|
||||||
return agent, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddAgent(agent *AgentConfig) {
|
|
||||||
agentPool.Store(agent.Addr, agent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveAgent(agent *AgentConfig) {
|
|
||||||
agentPool.Delete(agent.Addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveAllAgents() {
|
|
||||||
agentPool.Clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListAgents() []*AgentConfig {
|
|
||||||
agents := make([]*AgentConfig, 0, agentPool.Size())
|
|
||||||
for _, agent := range agentPool.Range {
|
|
||||||
agents = append(agents, agent)
|
|
||||||
}
|
|
||||||
return agents
|
|
||||||
}
|
|
||||||
|
|
||||||
func IterAgents() iter.Seq2[string, *AgentConfig] {
|
|
||||||
return agentPool.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func NumAgents() int {
|
|
||||||
return agentPool.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAgentByAddr(addr string) (agent *AgentConfig, ok bool) {
|
|
||||||
agent, ok = agentPool.Load(addr)
|
|
||||||
return agent, ok
|
|
||||||
}
|
|
||||||
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,8 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -13,35 +15,54 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
|
agentstream "github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||||
"github.com/yusing/godoxy/agent/pkg/certs"
|
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/version"
|
"github.com/yusing/goutils/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
Addr string `json:"addr"`
|
AgentInfo
|
||||||
Name string `json:"name"`
|
|
||||||
Version version.Version `json:"version" swaggertype:"string"`
|
|
||||||
Runtime ContainerRuntime `json:"runtime"`
|
|
||||||
|
|
||||||
httpClient *http.Client
|
Addr string `json:"addr"`
|
||||||
fasthttpClientHealthCheck *fasthttp.Client
|
IsTCPStreamSupported bool `json:"supports_tcp_stream"`
|
||||||
tlsConfig tls.Config
|
IsUDPStreamSupported bool `json:"supports_udp_stream"`
|
||||||
l zerolog.Logger
|
|
||||||
|
// for stream
|
||||||
|
caCert *x509.Certificate
|
||||||
|
clientCert *tls.Certificate
|
||||||
|
|
||||||
|
tlsConfig tls.Config
|
||||||
|
|
||||||
|
l zerolog.Logger
|
||||||
} // @name Agent
|
} // @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 (
|
const (
|
||||||
EndpointVersion = "/version"
|
EndpointVersion = "/version"
|
||||||
EndpointName = "/name"
|
EndpointName = "/name"
|
||||||
EndpointRuntime = "/runtime"
|
EndpointRuntime = "/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EndpointInfo = "/info"
|
||||||
EndpointProxyHTTP = "/proxy/http"
|
EndpointProxyHTTP = "/proxy/http"
|
||||||
EndpointHealth = "/health"
|
EndpointHealth = "/health"
|
||||||
EndpointLogs = "/logs"
|
EndpointLogs = "/logs"
|
||||||
EndpointSystemInfo = "/system_info"
|
EndpointSystemInfo = "/system_info"
|
||||||
|
|
||||||
AgentHost = CertsDNSName
|
AgentHost = common.CertsDNSName
|
||||||
|
|
||||||
APIEndpointBase = "/godoxy/agent"
|
APIEndpointBase = "/godoxy/agent"
|
||||||
APIBaseURL = "https://" + AgentHost + APIEndpointBase
|
APIBaseURL = "https://" + AgentHost + APIEndpointBase
|
||||||
@@ -85,11 +106,13 @@ func (cfg *AgentConfig) Parse(addr string) error {
|
|||||||
|
|
||||||
var serverVersion = version.Get()
|
var serverVersion = version.Get()
|
||||||
|
|
||||||
func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte) error {
|
// InitWithCerts initializes the agent config with the given CA, certificate, and key.
|
||||||
|
func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte) error {
|
||||||
clientCert, err := tls.X509KeyPair(crt, key)
|
clientCert, err := tls.X509KeyPair(crt, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
cfg.clientCert = &clientCert
|
||||||
|
|
||||||
// create tls config
|
// create tls config
|
||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
@@ -97,64 +120,105 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
|
|||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("invalid ca certificate")
|
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{
|
cfg.tlsConfig = tls.Config{
|
||||||
Certificates: []tls.Certificate{clientCert},
|
Certificates: []tls.Certificate{clientCert},
|
||||||
RootCAs: caCertPool,
|
RootCAs: caCertPool,
|
||||||
ServerName: CertsDNSName,
|
ServerName: common.CertsDNSName,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
}
|
}
|
||||||
|
|
||||||
// create transport and http client
|
|
||||||
cfg.httpClient = cfg.NewHTTPClient()
|
|
||||||
applyNormalTransportConfig(cfg.httpClient)
|
|
||||||
|
|
||||||
cfg.fasthttpClientHealthCheck = cfg.NewFastHTTPHealthCheckClient()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// get agent name
|
status, err := cfg.fetchJSON(ctx, EndpointInfo, &cfg.AgentInfo)
|
||||||
name, _, err := cfg.fetchString(ctx, EndpointName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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()
|
cfg.l = log.With().Str("agent", cfg.Name).Logger()
|
||||||
|
|
||||||
// check agent version
|
if err := streamUnsupportedErrs.Error(); err != nil {
|
||||||
agentVersion, _, err := cfg.fetchString(ctx, EndpointVersion)
|
cfg.l.Warn().Err(err).Msg("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
if serverVersion.IsNewerThanMajor(cfg.Version) {
|
||||||
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.Name, serverVersion, cfg.Version)
|
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.Name, serverVersion, cfg.Version)
|
||||||
}
|
}
|
||||||
@@ -163,7 +227,8 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) Start(ctx context.Context) error {
|
// Init initializes the agent config with the given context.
|
||||||
|
func (cfg *AgentConfig) Init(ctx context.Context) error {
|
||||||
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid agent host: %s", cfg.Addr)
|
return fmt.Errorf("invalid agent host: %s", cfg.Addr)
|
||||||
@@ -179,32 +244,39 @@ func (cfg *AgentConfig) Start(ctx context.Context) error {
|
|||||||
return fmt.Errorf("failed to extract agent certs: %w", err)
|
return fmt.Errorf("failed to extract agent certs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg.StartWithCerts(ctx, ca, crt, key)
|
return cfg.InitWithCerts(ctx, ca, crt, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
|
// NewTCPClient creates a new TCP client for the agent.
|
||||||
return &http.Client{
|
//
|
||||||
Transport: cfg.Transport(),
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) NewFastHTTPHealthCheckClient() *fasthttp.Client {
|
// NewUDPClient creates a new UDP client for the agent.
|
||||||
return &fasthttp.Client{
|
//
|
||||||
Dial: func(addr string) (net.Conn, error) {
|
// It returns an error if
|
||||||
if addr != AgentHost+":443" {
|
// - the agent is not initialized
|
||||||
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
// - the agent does not support UDP stream tunneling
|
||||||
}
|
// - the agent stream server address is not initialized
|
||||||
return net.Dial("tcp", cfg.Addr)
|
func (cfg *AgentConfig) NewUDPClient(targetAddress string) (net.Conn, error) {
|
||||||
},
|
if cfg.caCert == nil || cfg.clientCert == nil {
|
||||||
TLSConfig: &cfg.tlsConfig,
|
return nil, errors.New("agent is not initialized")
|
||||||
ReadTimeout: 5 * time.Second,
|
|
||||||
WriteTimeout: 3 * time.Second,
|
|
||||||
DisableHeaderNamesNormalizing: true,
|
|
||||||
DisablePathNormalizing: true,
|
|
||||||
NoDefaultUserAgentHeader: true,
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
}
|
}
|
||||||
|
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 {
|
func (cfg *AgentConfig) Transport() *http.Transport {
|
||||||
@@ -222,6 +294,10 @@ func (cfg *AgentConfig) Transport() *http.Transport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) TLSConfig() *tls.Config {
|
||||||
|
return &cfg.tlsConfig
|
||||||
|
}
|
||||||
|
|
||||||
var dialer = &net.Dialer{Timeout: 5 * time.Second}
|
var dialer = &net.Dialer{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
|
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
|
||||||
@@ -232,10 +308,67 @@ func (cfg *AgentConfig) String() string {
|
|||||||
return cfg.Name + "@" + cfg.Addr
|
return cfg.Name + "@" + cfg.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyNormalTransportConfig(client *http.Client) {
|
func (cfg *AgentConfig) do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||||
transport := client.Transport.(*http.Transport)
|
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
|
||||||
transport.MaxIdleConns = 100
|
if err != nil {
|
||||||
transport.MaxIdleConnsPerHost = 100
|
return nil, err
|
||||||
transport.ReadBufferSize = 16384
|
}
|
||||||
transport.WriteBufferSize = 16384
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) fetchString(ctx context.Context, endpoint string) (string, 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
|
||||||
|
}
|
||||||
|
ret := string(data)
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
httputils "github.com/yusing/goutils/http"
|
|
||||||
"github.com/yusing/goutils/http/reverseproxy"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return cfg.httpClient.Do(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
|
||||||
req.URL.Host = AgentHost
|
|
||||||
req.URL.Scheme = "https"
|
|
||||||
req.URL.Path = APIEndpointBase + endpoint
|
|
||||||
req.RequestURI = ""
|
|
||||||
resp, err := cfg.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type HealthCheckResponse struct {
|
|
||||||
Healthy bool `json:"healthy"`
|
|
||||||
Detail string `json:"detail"`
|
|
||||||
Latency time.Duration `json:"latency"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *AgentConfig) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
|
|
||||||
req := fasthttp.AcquireRequest()
|
|
||||||
defer fasthttp.ReleaseRequest(req)
|
|
||||||
|
|
||||||
resp := fasthttp.AcquireResponse()
|
|
||||||
defer fasthttp.ReleaseResponse(resp)
|
|
||||||
|
|
||||||
req.SetRequestURI(APIBaseURL + EndpointHealth + "?" + query)
|
|
||||||
req.Header.SetMethod(fasthttp.MethodGet)
|
|
||||||
req.Header.Set("Accept-Encoding", "identity")
|
|
||||||
req.SetConnectionClose()
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
err = cfg.fasthttpClientHealthCheck.DoTimeout(req, resp, timeout)
|
|
||||||
ret.Latency = time.Since(start)
|
|
||||||
if err != nil {
|
|
||||||
return ret, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if status := resp.StatusCode(); status != http.StatusOK {
|
|
||||||
// clone body since fasthttp response will be released
|
|
||||||
body := resp.Body()
|
|
||||||
cloneBody := make([]byte, len(body))
|
|
||||||
copy(cloneBody, body)
|
|
||||||
return ret, fmt.Errorf("HTTP %d %s", status, cloneBody)
|
|
||||||
} else {
|
|
||||||
err = sonic.Unmarshal(resp.Body(), &ret)
|
|
||||||
if err != nil {
|
|
||||||
return ret, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *AgentConfig) fetchString(ctx context.Context, endpoint string) (string, 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
|
|
||||||
}
|
|
||||||
ret := string(data)
|
|
||||||
release(data)
|
|
||||||
return ret, resp.StatusCode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *AgentConfig) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
|
||||||
transport := cfg.Transport()
|
|
||||||
dialer := websocket.Dialer{
|
|
||||||
NetDialContext: transport.DialContext,
|
|
||||||
NetDialTLSContext: transport.DialTLSContext,
|
|
||||||
}
|
|
||||||
return dialer.DialContext(ctx, APIBaseURL+endpoint, http.Header{
|
|
||||||
"Host": {AgentHost},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReverseProxy reverse proxies the request to the agent
|
|
||||||
//
|
|
||||||
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
|
|
||||||
// If the request has a query, it will be added to the proxy request's URL
|
|
||||||
func (cfg *AgentConfig) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
|
|
||||||
rp := reverseproxy.NewReverseProxy("agent", AgentURL, cfg.Transport())
|
|
||||||
req.URL.Host = AgentHost
|
|
||||||
req.URL.Scheme = "https"
|
|
||||||
req.URL.Path = endpoint
|
|
||||||
req.RequestURI = ""
|
|
||||||
rp.ServeHTTP(w, req)
|
|
||||||
}
|
|
||||||
@@ -17,10 +17,8 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
CertsDNSName = "godoxy.agent"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func toPEMPair(certDER []byte, key *ecdsa.PrivateKey) *PEMPair {
|
func toPEMPair(certDER []byte, key *ecdsa.PrivateKey) *PEMPair {
|
||||||
@@ -156,7 +154,7 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
|||||||
SerialNumber: caSerialNumber,
|
SerialNumber: caSerialNumber,
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
Organization: []string{"GoDoxy"},
|
Organization: []string{"GoDoxy"},
|
||||||
CommonName: CertsDNSName,
|
CommonName: common.CertsDNSName,
|
||||||
},
|
},
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now(),
|
||||||
NotAfter: time.Now().AddDate(1000, 0, 0), // 1000 years
|
NotAfter: time.Now().AddDate(1000, 0, 0), // 1000 years
|
||||||
@@ -196,9 +194,9 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
|||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
Organization: caTemplate.Subject.Organization,
|
Organization: caTemplate.Subject.Organization,
|
||||||
OrganizationalUnit: []string{"Server"},
|
OrganizationalUnit: []string{"Server"},
|
||||||
CommonName: CertsDNSName,
|
CommonName: common.CertsDNSName,
|
||||||
},
|
},
|
||||||
DNSNames: []string{CertsDNSName},
|
DNSNames: []string{common.CertsDNSName},
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now(),
|
||||||
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
|
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
@@ -228,9 +226,9 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
|||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
Organization: caTemplate.Subject.Organization,
|
Organization: caTemplate.Subject.Organization,
|
||||||
OrganizationalUnit: []string{"Client"},
|
OrganizationalUnit: []string{"Client"},
|
||||||
CommonName: CertsDNSName,
|
CommonName: common.CertsDNSName,
|
||||||
},
|
},
|
||||||
DNSNames: []string{CertsDNSName},
|
DNSNames: []string{common.CertsDNSName},
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now(),
|
||||||
NotAfter: time.Now().AddDate(1000, 0, 0),
|
NotAfter: time.Now().AddDate(1000, 0, 0),
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewAgent(t *testing.T) {
|
func TestNewAgent(t *testing.T) {
|
||||||
@@ -72,7 +73,7 @@ func TestServerClient(t *testing.T) {
|
|||||||
clientTLSConfig := &tls.Config{
|
clientTLSConfig := &tls.Config{
|
||||||
Certificates: []tls.Certificate{*clientTLS},
|
Certificates: []tls.Certificate{*clientTLS},
|
||||||
RootCAs: caPool,
|
RootCAs: caPool,
|
||||||
ServerName: CertsDNSName,
|
ServerName: common.CertsDNSName,
|
||||||
}
|
}
|
||||||
|
|
||||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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 @@
|
|||||||
|
# agent/pkg/agent/stream
|
||||||
|
|
||||||
|
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()
|
||||||
119
agent/pkg/agent/stream/header.go
Normal file
119
agent/pkg/agent/stream/header.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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")
|
||||||
|
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")
|
||||||
|
}
|
||||||
95
agent/pkg/agent/stream/tests/mux_test.go
Normal file
95
agent/pkg/agent/stream/tests/mux_test.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
200
agent/pkg/agent/stream/tests/server_flow_test.go
Normal file
200
agent/pkg/agent/stream/tests/server_flow_test.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
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
|
restart: always
|
||||||
{{ if eq .ContainerRuntime "podman" -}}
|
{{ if eq .ContainerRuntime "podman" -}}
|
||||||
ports:
|
ports:
|
||||||
- "{{.Port}}:{{.Port}}"
|
- "{{.Port}}:{{.Port}}/tcp"
|
||||||
|
- "{{.Port}}:{{.Port}}/udp"
|
||||||
{{ else -}}
|
{{ else -}}
|
||||||
network_mode: host # do not change this
|
network_mode: host # do not change this
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
|
|||||||
122
agent/pkg/agentproxy/README.md
Normal file
122
agent/pkg/agentproxy/README.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# agent/pkg/agentproxy
|
||||||
|
|
||||||
|
Package for configuring HTTP proxy connections through the GoDoxy Agent using HTTP headers.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package provides types and functions for parsing and setting agent proxy configuration via HTTP headers. It supports both a modern base64-encoded JSON format and a legacy header-based format for backward compatibility.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[HTTP Request] --> B[ConfigFromHeaders]
|
||||||
|
B --> C{Modern Format?}
|
||||||
|
C -->|Yes| D[Parse X-Proxy-Config Base64 JSON]
|
||||||
|
C -->|No| E[Parse Legacy Headers]
|
||||||
|
D --> F[Config]
|
||||||
|
E --> F
|
||||||
|
|
||||||
|
F --> G[SetAgentProxyConfigHeaders]
|
||||||
|
G --> H[Modern Headers]
|
||||||
|
G --> I[Legacy Headers]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public Types
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Config struct {
|
||||||
|
Scheme string // Proxy scheme (http or https)
|
||||||
|
Host string // Proxy host (hostname or hostname:port)
|
||||||
|
HTTPConfig // Extended HTTP configuration
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `HTTPConfig` embedded type (from `internal/route/types`) includes:
|
||||||
|
|
||||||
|
- `NoTLSVerify` - Skip TLS certificate verification
|
||||||
|
- `ResponseHeaderTimeout` - Timeout for response headers
|
||||||
|
- `DisableCompression` - Disable gzip compression
|
||||||
|
|
||||||
|
## Public Functions
|
||||||
|
|
||||||
|
### ConfigFromHeaders
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ConfigFromHeaders(h http.Header) (Config, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses proxy configuration from HTTP request headers. Tries modern format first, falls back to legacy format if not present.
|
||||||
|
|
||||||
|
### proxyConfigFromHeaders
|
||||||
|
|
||||||
|
```go
|
||||||
|
func proxyConfigFromHeaders(h http.Header) (Config, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses the modern base64-encoded JSON format from `X-Proxy-Config` header.
|
||||||
|
|
||||||
|
### proxyConfigFromHeadersLegacy
|
||||||
|
|
||||||
|
```go
|
||||||
|
func proxyConfigFromHeadersLegacy(h http.Header) Config
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses the legacy header format:
|
||||||
|
|
||||||
|
- `X-Proxy-Host` - Proxy host
|
||||||
|
- `X-Proxy-Https` - Whether to use HTTPS
|
||||||
|
- `X-Proxy-Skip-Tls-Verify` - Skip TLS verification
|
||||||
|
- `X-Proxy-Response-Header-Timeout` - Response timeout in seconds
|
||||||
|
|
||||||
|
### SetAgentProxyConfigHeaders
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header)
|
||||||
|
```
|
||||||
|
|
||||||
|
Sets headers for modern format with base64-encoded JSON config.
|
||||||
|
|
||||||
|
### SetAgentProxyConfigHeadersLegacy
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header)
|
||||||
|
```
|
||||||
|
|
||||||
|
Sets headers for legacy format with individual header fields.
|
||||||
|
|
||||||
|
## Header Constants
|
||||||
|
|
||||||
|
Modern headers:
|
||||||
|
|
||||||
|
- `HeaderXProxyScheme` - Proxy scheme
|
||||||
|
- `HeaderXProxyHost` - Proxy host
|
||||||
|
- `HeaderXProxyConfig` - Base64-encoded JSON config
|
||||||
|
|
||||||
|
Legacy headers (deprecated):
|
||||||
|
|
||||||
|
- `HeaderXProxyHTTPS`
|
||||||
|
- `HeaderXProxySkipTLSVerify`
|
||||||
|
- `HeaderXProxyResponseHeaderTimeout`
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Reading configuration from incoming request headers
|
||||||
|
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg, err := agentproxy.ConfigFromHeaders(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid proxy config", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cfg.Scheme and cfg.Host to proxy the request
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
This package is used by `agent/pkg/handler/proxy_http.go` to configure reverse proxy connections based on request headers.
|
||||||
102
agent/pkg/certs/README.md
Normal file
102
agent/pkg/certs/README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# agent/pkg/certs
|
||||||
|
|
||||||
|
Certificate management package for creating and extracting certificate archives.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package provides utilities for packaging SSL certificates into ZIP archives and extracting them. It is used by the GoDoxy Agent to distribute certificates to clients in a convenient format.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Raw Certs] --> B[ZipCert]
|
||||||
|
B --> C[ZIP Archive]
|
||||||
|
C --> D[ca.pem]
|
||||||
|
C --> E[cert.pem]
|
||||||
|
C --> F[key.pem]
|
||||||
|
|
||||||
|
G[ZIP Archive] --> H[ExtractCert]
|
||||||
|
H --> I[ca, crt, key]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public Functions
|
||||||
|
|
||||||
|
### ZipCert
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ZipCert(ca, crt, key []byte) ([]byte, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a ZIP archive containing three PEM files:
|
||||||
|
|
||||||
|
- `ca.pem` - CA certificate
|
||||||
|
- `cert.pem` - Server/client certificate
|
||||||
|
- `key.pem` - Private key
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `ca` - CA certificate in PEM format
|
||||||
|
- `crt` - Certificate in PEM format
|
||||||
|
- `key` - Private key in PEM format
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
- ZIP archive bytes
|
||||||
|
- Error if packing fails
|
||||||
|
|
||||||
|
### ExtractCert
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ExtractCert(data []byte) (ca, crt, key []byte, err error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Extracts certificates from a ZIP archive created by `ZipCert`.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `data` - ZIP archive bytes
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
- `ca` - CA certificate bytes
|
||||||
|
- `crt` - Certificate bytes
|
||||||
|
- `key` - Private key bytes
|
||||||
|
- Error if extraction fails
|
||||||
|
|
||||||
|
### AgentCertsFilepath
|
||||||
|
|
||||||
|
```go
|
||||||
|
func AgentCertsFilepath(host string) (filepathOut string, ok bool)
|
||||||
|
```
|
||||||
|
|
||||||
|
Generates the file path for storing agent certificates.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `host` - Agent hostname
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
- Full file path within `certs/` directory
|
||||||
|
- `false` if host is invalid (contains path separators or special characters)
|
||||||
|
|
||||||
|
### isValidAgentHost
|
||||||
|
|
||||||
|
```go
|
||||||
|
func isValidAgentHost(host string) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Validates that a host string is safe for use in file paths.
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
```go
|
||||||
|
const AgentCertsBasePath = "certs"
|
||||||
|
```
|
||||||
|
|
||||||
|
Base directory for storing certificate archives.
|
||||||
|
|
||||||
|
## File Format
|
||||||
|
|
||||||
|
The ZIP archive uses `zip.Store` compression (no compression) for fast creation and extraction. Each file is stored with its standard name (`ca.pem`, `cert.pem`, `key.pem`).
|
||||||
52
agent/pkg/env/README.md
vendored
Normal file
52
agent/pkg/env/README.md
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# agent/pkg/env
|
||||||
|
|
||||||
|
Environment configuration package for the GoDoxy Agent.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package manages environment variable parsing and provides a centralized location for all agent configuration options. It is automatically initialized on import.
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Type | Default | Description |
|
||||||
|
| -------------------------- | ---------------- | ---------------------- | --------------------------------------- |
|
||||||
|
| `DockerSocket` | string | `/var/run/docker.sock` | Path to Docker socket |
|
||||||
|
| `AgentName` | string | System hostname | Agent identifier |
|
||||||
|
| `AgentPort` | int | `8890` | Agent server port |
|
||||||
|
| `AgentSkipClientCertCheck` | bool | `false` | Skip mTLS certificate verification |
|
||||||
|
| `AgentCACert` | string | (empty) | Base64 Encoded CA certificate + key |
|
||||||
|
| `AgentSSLCert` | string | (empty) | Base64 Encoded server certificate + key |
|
||||||
|
| `Runtime` | ContainerRuntime | `docker` | Container runtime (docker or podman) |
|
||||||
|
|
||||||
|
## ContainerRuntime Type
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ContainerRuntime string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContainerRuntimeDocker ContainerRuntime = "docker"
|
||||||
|
ContainerRuntimePodman ContainerRuntime = "podman"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public Functions
|
||||||
|
|
||||||
|
### DefaultAgentName
|
||||||
|
|
||||||
|
```go
|
||||||
|
func DefaultAgentName() string
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the system hostname as the default agent name. Falls back to `"agent"` if hostname cannot be determined.
|
||||||
|
|
||||||
|
### Load
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Load()
|
||||||
|
```
|
||||||
|
|
||||||
|
Reloads all environment variables from the environment. Called automatically on package init, but can be called again to refresh configuration.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
The `Load()` function validates that `Runtime` is either `docker` or `podman`. An invalid runtime causes a fatal error.
|
||||||
127
agent/pkg/handler/README.md
Normal file
127
agent/pkg/handler/README.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# agent/pkg/handler
|
||||||
|
|
||||||
|
HTTP request handler package for the GoDoxy Agent.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package provides the HTTP handler for the GoDoxy Agent server, including endpoints for:
|
||||||
|
|
||||||
|
- Version information
|
||||||
|
- Agent name and runtime
|
||||||
|
- Health checks
|
||||||
|
- System metrics (via SSE)
|
||||||
|
- HTTP proxy routing
|
||||||
|
- Docker socket proxying
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[HTTP Request] --> B[NewAgentHandler]
|
||||||
|
B --> C{ServeMux Router}
|
||||||
|
|
||||||
|
C --> D[GET /version]
|
||||||
|
C --> E[GET /name]
|
||||||
|
C --> F[GET /runtime]
|
||||||
|
C --> G[GET /health]
|
||||||
|
C --> H[GET /system-info]
|
||||||
|
C --> I[GET /proxy/http/#123;path...#125;]
|
||||||
|
C --> J[ /#42; Docker Socket]
|
||||||
|
|
||||||
|
H --> K[Gin Router]
|
||||||
|
K --> L[WebSocket Upgrade]
|
||||||
|
L --> M[SystemInfo Poller]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public Types
|
||||||
|
|
||||||
|
### ServeMux
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ServeMux struct{ *http.ServeMux }
|
||||||
|
```
|
||||||
|
|
||||||
|
Wrapper around `http.ServeMux` with agent-specific endpoint helpers.
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
- `HandleEndpoint(method, endpoint string, handler http.HandlerFunc)` - Registers handler with API base path
|
||||||
|
- `HandleFunc(endpoint string, handler http.HandlerFunc)` - Registers GET handler with API base path
|
||||||
|
|
||||||
|
## Public Functions
|
||||||
|
|
||||||
|
### NewAgentHandler
|
||||||
|
|
||||||
|
```go
|
||||||
|
func NewAgentHandler() http.Handler
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates and configures the HTTP handler for the agent server. Sets up:
|
||||||
|
|
||||||
|
- Gin-based metrics handler with WebSocket support for SSE
|
||||||
|
- All standard agent endpoints
|
||||||
|
- HTTP proxy endpoint
|
||||||
|
- Docker socket proxy fallback
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
| ----------------------- | -------- | ------------------------------------ |
|
||||||
|
| `/version` | GET | Returns agent version |
|
||||||
|
| `/name` | GET | Returns agent name |
|
||||||
|
| `/runtime` | GET | Returns container runtime |
|
||||||
|
| `/health` | GET | Health check with scheme query param |
|
||||||
|
| `/system-info` | GET | System metrics via SSE or WebSocket |
|
||||||
|
| `/proxy/http/{path...}` | GET/POST | HTTP proxy with config from headers |
|
||||||
|
| `/*` | \* | Docker socket proxy |
|
||||||
|
|
||||||
|
## Sub-packages
|
||||||
|
|
||||||
|
### proxy_http.go
|
||||||
|
|
||||||
|
Handles HTTP proxy requests by reading configuration from request headers and proxying to the configured upstream.
|
||||||
|
|
||||||
|
**Key Function:**
|
||||||
|
|
||||||
|
- `ProxyHTTP(w, r)` - Proxies HTTP requests based on `X-Proxy-*` headers
|
||||||
|
|
||||||
|
### check_health.go
|
||||||
|
|
||||||
|
Handles health check requests for various schemes.
|
||||||
|
|
||||||
|
**Key Function:**
|
||||||
|
|
||||||
|
- `CheckHealth(w, r)` - Performs health checks with configurable scheme
|
||||||
|
|
||||||
|
**Supported Schemes:**
|
||||||
|
|
||||||
|
- `http`, `https` - HTTP health check
|
||||||
|
- `h2c` - HTTP/2 cleartext health check
|
||||||
|
- `tcp`, `udp`, `tcp4`, `udp4`, `tcp6`, `udp6` - TCP/UDP health check
|
||||||
|
- `fileserver` - File existence check
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/", handler.NewAgentHandler())
|
||||||
|
|
||||||
|
http.ListenAndServe(":8890", mux)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket Support
|
||||||
|
|
||||||
|
The handler includes a permissive WebSocket upgrader for internal use (no origin check). This enables real-time system metrics streaming via Server-Sent Events (SSE).
|
||||||
|
|
||||||
|
## Docker Socket Integration
|
||||||
|
|
||||||
|
All unmatched requests fall through to the Docker socket handler, allowing the agent to proxy Docker API calls when configured.
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
|
healthcheck "github.com/yusing/godoxy/internal/health/check"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
"github.com/yusing/godoxy/internal/watcher/health/monitor"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultHealthConfig = types.DefaultHealthConfig()
|
|
||||||
|
|
||||||
func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
scheme := query.Get("scheme")
|
scheme := query.Get("scheme")
|
||||||
@@ -21,6 +20,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "missing scheme", http.StatusBadRequest)
|
http.Error(w, "missing scheme", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
timeout := parseMsOrDefault(query.Get("timeout"))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
result types.HealthCheckResult
|
result types.HealthCheckResult
|
||||||
@@ -33,24 +33,21 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "missing path", http.StatusBadRequest)
|
http.Error(w, "missing path", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err := os.Stat(path)
|
result, err = healthcheck.FileServer(path)
|
||||||
result = types.HealthCheckResult{Healthy: err == nil}
|
case "http", "https", "h2c": // path is optional
|
||||||
if err != nil {
|
|
||||||
result.Detail = err.Error()
|
|
||||||
}
|
|
||||||
case "http", "https": // path is optional
|
|
||||||
host := query.Get("host")
|
host := query.Get("host")
|
||||||
path := query.Get("path")
|
path := query.Get("path")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
http.Error(w, "missing host", http.StatusBadRequest)
|
http.Error(w, "missing host", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
|
url := url.URL{Scheme: scheme, Host: host}
|
||||||
Scheme: scheme,
|
if scheme == "h2c" {
|
||||||
Host: host,
|
result, err = healthcheck.H2C(r.Context(), &url, http.MethodHead, path, timeout)
|
||||||
Path: path,
|
} else {
|
||||||
}, defaultHealthConfig).CheckHealth()
|
result, err = healthcheck.HTTP(&url, http.MethodHead, path, timeout)
|
||||||
case "tcp", "udp":
|
}
|
||||||
|
case "tcp", "udp", "tcp4", "udp4", "tcp6", "udp6":
|
||||||
host := query.Get("host")
|
host := query.Get("host")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
http.Error(w, "missing host", http.StatusBadRequest)
|
http.Error(w, "missing host", http.StatusBadRequest)
|
||||||
@@ -63,12 +60,10 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if port != "" {
|
if port != "" {
|
||||||
host = fmt.Sprintf("%s:%s", host, port)
|
host = net.JoinHostPort(host, port)
|
||||||
}
|
}
|
||||||
result, err = monitor.NewRawHealthMonitor(&url.URL{
|
url := url.URL{Scheme: scheme, Host: host}
|
||||||
Scheme: scheme,
|
result, err = healthcheck.Stream(r.Context(), &url, timeout)
|
||||||
Host: host,
|
|
||||||
}, defaultHealthConfig).CheckHealth()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,3 +75,16 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
sonic.ConfigDefault.NewEncoder(w).Encode(result)
|
sonic.ConfigDefault.NewEncoder(w).Encode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseMsOrDefault(msStr string) time.Duration {
|
||||||
|
if msStr == "" {
|
||||||
|
return types.HealthCheckTimeoutDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutMs, _ := strconv.ParseInt(msStr, 10, 64)
|
||||||
|
if timeoutMs == 0 {
|
||||||
|
return types.HealthCheckTimeoutDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Duration(timeoutMs) * time.Millisecond
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
@@ -44,14 +44,14 @@ func NewAgentHandler() http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
|
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
|
||||||
mux.HandleEndpoint("GET", agent.EndpointVersion, func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc(agent.EndpointInfo, func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, version.Get())
|
agentInfo := agent.AgentInfo{
|
||||||
})
|
Version: version.Get(),
|
||||||
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
|
Name: env.AgentName,
|
||||||
fmt.Fprint(w, env.AgentName)
|
Runtime: env.Runtime,
|
||||||
})
|
}
|
||||||
mux.HandleEndpoint("GET", agent.EndpointRuntime, func(w http.ResponseWriter, r *http.Request) {
|
w.Header().Set("Content-Type", "application/json")
|
||||||
fmt.Fprint(w, env.Runtime)
|
sonic.ConfigDefault.NewEncoder(w).Encode(agentInfo)
|
||||||
})
|
})
|
||||||
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
||||||
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
@@ -43,10 +44,22 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r.URL.Scheme = ""
|
// Strip the {API_BASE}/proxy/http prefix while preserving URL escaping.
|
||||||
r.URL.Host = ""
|
//
|
||||||
r.URL.Path = r.URL.Path[agent.HTTPProxyURLPrefixLen:] // strip the {API_BASE}/proxy/http prefix
|
// NOTE: `r.URL.Path` is decoded. If we rewrite it without keeping `RawPath`
|
||||||
r.RequestURI = r.URL.String()
|
// in sync, Go may re-escape the path (e.g. turning "%5B" into "%255B"),
|
||||||
|
// which breaks urls with percent-encoded characters, like Next.js static chunk URLs.
|
||||||
|
prefix := agent.APIEndpointBase + agent.EndpointProxyHTTP
|
||||||
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
|
||||||
|
if r.URL.RawPath != "" {
|
||||||
|
if after, ok := strings.CutPrefix(r.URL.RawPath, prefix); ok {
|
||||||
|
r.URL.RawPath = after
|
||||||
|
} else {
|
||||||
|
// RawPath is no longer a valid encoding for Path; force Go to re-derive it.
|
||||||
|
r.URL.RawPath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.RequestURI = ""
|
||||||
|
|
||||||
rp := &httputil.ReverseProxy{
|
rp := &httputil.ReverseProxy{
|
||||||
Director: func(r *http.Request) {
|
Director: func(r *http.Request) {
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
73
cmd/README.md
Normal file
73
cmd/README.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# cmd
|
||||||
|
|
||||||
|
Main entry point package for GoDoxy, a lightweight reverse proxy with WebUI for Docker containers.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package contains the `main.go` entry point that initializes and starts the GoDoxy server. It coordinates the initialization of all core components including configuration loading, API server, authentication, and monitoring services.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[main] --> B[Init Profiling]
|
||||||
|
A --> C[Init Logger]
|
||||||
|
A --> D[Parallel Init]
|
||||||
|
D --> D1[DNS Providers]
|
||||||
|
D --> D2[Icon Cache]
|
||||||
|
D --> D3[System Info Poller]
|
||||||
|
D --> D4[Middleware Compose Files]
|
||||||
|
A --> E[JWT Secret Setup]
|
||||||
|
A --> F[Create Directories]
|
||||||
|
A --> G[Load Config]
|
||||||
|
A --> H[Start Proxy Servers]
|
||||||
|
A --> I[Init Auth]
|
||||||
|
A --> J[Start API Server]
|
||||||
|
A --> K[Debug Server]
|
||||||
|
A --> L[Uptime Poller]
|
||||||
|
A --> M[Watch Changes]
|
||||||
|
A --> N[Wait Exit]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main Function Flow
|
||||||
|
|
||||||
|
The `main()` function performs the following initialization steps:
|
||||||
|
|
||||||
|
1. **Profiling Setup**: Initializes pprof endpoints for performance monitoring
|
||||||
|
1. **Logger Initialization**: Configures zerolog with memory logging
|
||||||
|
1. **Parallel Initialization**: Starts DNS providers, icon cache, system info poller, and middleware
|
||||||
|
1. **JWT Secret**: Ensures API JWT secret is set (generates random if not provided)
|
||||||
|
1. **Directory Preparation**: Creates required directories for logs, certificates, etc.
|
||||||
|
1. **Configuration Loading**: Loads YAML configuration and reports any errors
|
||||||
|
1. **Proxy Servers**: Starts HTTP/HTTPS proxy servers based on configuration
|
||||||
|
1. **Authentication**: Initializes authentication system with access control
|
||||||
|
1. **API Server**: Starts the REST API server with all configured routes
|
||||||
|
1. **Debug Server**: Starts the debug page server (development mode)
|
||||||
|
1. **Monitoring**: Starts uptime and system info polling
|
||||||
|
1. **Change Watcher**: Starts watching for Docker container and configuration changes
|
||||||
|
1. **Graceful Shutdown**: Waits for exit signal with configured timeout
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The main configuration is loaded from `config/config.yml`. Required directories include:
|
||||||
|
|
||||||
|
- `logs/` - Log files
|
||||||
|
- `config/` - Configuration directory
|
||||||
|
- `certs/` - SSL certificates
|
||||||
|
- `proxy/` - Proxy-related files
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `API_JWT_SECRET` - Secret key for JWT authentication (optional, auto-generated if not set)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `internal/api` - REST API handlers
|
||||||
|
- `internal/auth` - Authentication and ACL
|
||||||
|
- `internal/config` - Configuration management
|
||||||
|
- `internal/dnsproviders` - DNS provider integration
|
||||||
|
- `internal/homepage` - WebUI dashboard
|
||||||
|
- `internal/logging` - Logging infrastructure
|
||||||
|
- `internal/metrics` - System metrics collection
|
||||||
|
- `internal/route` - HTTP routing and middleware
|
||||||
|
- `github.com/yusing/goutils/task` - Task lifecycle management
|
||||||
18
cmd/bench_server/Dockerfile
Normal file
18
cmd/bench_server/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM golang:1.26.0-alpine AS builder
|
||||||
|
|
||||||
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
COPY main.go ./
|
||||||
|
|
||||||
|
RUN go build -o bench_server main.go
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY --from=builder /src/bench_server /app/run
|
||||||
|
|
||||||
|
USER 1001:1001
|
||||||
|
|
||||||
|
CMD ["/app/run"]
|
||||||
3
cmd/bench_server/go.mod
Normal file
3
cmd/bench_server/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/yusing/godoxy/cmd/bench_server
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
0
cmd/bench_server/go.sum
Normal file
0
cmd/bench_server/go.sum
Normal file
35
cmd/bench_server/main.go
Normal file
35
cmd/bench_server/main.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"math/rand/v2"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
random = make([]byte, 4096)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for i := range random {
|
||||||
|
random[i] = printables[rand.IntN(len(printables))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(random)
|
||||||
|
})
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: ":80",
|
||||||
|
Handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Bench server listening on :80")
|
||||||
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("ListenAndServe: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
730
cmd/cli/cli.go
Executable file
730
cmd/cli/cli.go
Executable file
@@ -0,0 +1,730 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/yusing/goutils/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Addr string
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringSliceFlag struct {
|
||||||
|
set bool
|
||||||
|
v []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stringSliceFlag) String() string {
|
||||||
|
return strings.Join(s.v, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stringSliceFlag) Set(value string) error {
|
||||||
|
s.set = true
|
||||||
|
if value == "" {
|
||||||
|
s.v = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.v = strings.Split(value, ",")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(args []string) error {
|
||||||
|
cfg, rest, err := parseGlobal(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(rest) == 0 {
|
||||||
|
printHelp()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if rest[0] == "help" {
|
||||||
|
printHelp()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ep, matchedLen := findEndpoint(rest)
|
||||||
|
if ep == nil {
|
||||||
|
ep, matchedLen = findEndpointAlias(rest)
|
||||||
|
}
|
||||||
|
if ep == nil {
|
||||||
|
return unknownCommandError(rest)
|
||||||
|
}
|
||||||
|
cmdArgs := rest[matchedLen:]
|
||||||
|
return executeEndpoint(cfg.Addr, *ep, cmdArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGlobal(args []string) (config, []string, error) {
|
||||||
|
var cfg config
|
||||||
|
fs := flag.NewFlagSet("godoxy", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(io.Discard)
|
||||||
|
fs.StringVar(&cfg.Addr, "addr", "", "API address, e.g. 127.0.0.1:8888 or http://127.0.0.1:8888")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return cfg, nil, err
|
||||||
|
}
|
||||||
|
return cfg, fs.Args(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveBaseURL(addrFlag string) (string, error) {
|
||||||
|
if addrFlag != "" {
|
||||||
|
return normalizeURL(addrFlag), nil
|
||||||
|
}
|
||||||
|
_, _, _, fullURL := env.GetAddrEnv("LOCAL_API_ADDR", "", "http")
|
||||||
|
if fullURL == "" {
|
||||||
|
return "", errors.New("missing LOCAL_API_ADDR (or GODOXY_LOCAL_API_ADDR). set env var or pass --addr")
|
||||||
|
}
|
||||||
|
return normalizeURL(fullURL), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeURL(addr string) string {
|
||||||
|
a := strings.TrimSpace(addr)
|
||||||
|
if strings.Contains(a, "://") {
|
||||||
|
return strings.TrimRight(a, "/")
|
||||||
|
}
|
||||||
|
return "http://" + strings.TrimRight(a, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func findEndpoint(args []string) (*Endpoint, int) {
|
||||||
|
var best *Endpoint
|
||||||
|
bestLen := -1
|
||||||
|
for i := range generatedEndpoints {
|
||||||
|
ep := &generatedEndpoints[i]
|
||||||
|
if len(ep.CommandPath) > len(args) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ok := true
|
||||||
|
for j, tok := range ep.CommandPath {
|
||||||
|
if args[j] != tok {
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok && len(ep.CommandPath) > bestLen {
|
||||||
|
best = ep
|
||||||
|
bestLen = len(ep.CommandPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best, bestLen
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeEndpoint(addrFlag string, ep Endpoint, args []string) error {
|
||||||
|
fs := flag.NewFlagSet(strings.Join(ep.CommandPath, "-"), flag.ContinueOnError)
|
||||||
|
fs.SetOutput(io.Discard)
|
||||||
|
useWS := false
|
||||||
|
if ep.IsWebSocket {
|
||||||
|
fs.BoolVar(&useWS, "ws", false, "use websocket")
|
||||||
|
}
|
||||||
|
typedValues := make(map[string]any, len(ep.Params))
|
||||||
|
isSet := make(map[string]bool, len(ep.Params))
|
||||||
|
for _, p := range ep.Params {
|
||||||
|
switch p.Type {
|
||||||
|
case "integer":
|
||||||
|
v := new(int)
|
||||||
|
fs.IntVar(v, p.FlagName, 0, p.Description)
|
||||||
|
typedValues[p.FlagName] = v
|
||||||
|
case "number":
|
||||||
|
v := new(float64)
|
||||||
|
fs.Float64Var(v, p.FlagName, 0, p.Description)
|
||||||
|
typedValues[p.FlagName] = v
|
||||||
|
case "boolean":
|
||||||
|
v := new(bool)
|
||||||
|
fs.BoolVar(v, p.FlagName, false, p.Description)
|
||||||
|
typedValues[p.FlagName] = v
|
||||||
|
case "array":
|
||||||
|
v := &stringSliceFlag{}
|
||||||
|
fs.Var(v, p.FlagName, p.Description+" (comma-separated)")
|
||||||
|
typedValues[p.FlagName] = v
|
||||||
|
default:
|
||||||
|
v := new(string)
|
||||||
|
fs.StringVar(v, p.FlagName, "", p.Description)
|
||||||
|
typedValues[p.FlagName] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return fmt.Errorf("%w\n\n%s", err, formatEndpointHelp(ep))
|
||||||
|
}
|
||||||
|
if len(fs.Args()) > 0 {
|
||||||
|
return fmt.Errorf("unexpected args: %s\n\n%s", strings.Join(fs.Args(), " "), formatEndpointHelp(ep))
|
||||||
|
}
|
||||||
|
fs.Visit(func(f *flag.Flag) {
|
||||||
|
isSet[f.Name] = true
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, p := range ep.Params {
|
||||||
|
if !p.Required {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isSet[p.FlagName] {
|
||||||
|
return fmt.Errorf("missing required flag --%s\n\n%s", p.FlagName, formatEndpointHelp(ep))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL, err := resolveBaseURL(addrFlag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reqURL, body, err := buildRequest(ep, baseURL, typedValues, isSet)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if useWS {
|
||||||
|
if !ep.IsWebSocket {
|
||||||
|
return errors.New("--ws is only supported for websocket endpoints")
|
||||||
|
}
|
||||||
|
return execWebsocket(ep, reqURL)
|
||||||
|
}
|
||||||
|
return execHTTP(ep, reqURL, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRequest(ep Endpoint, baseURL string, typedValues map[string]any, isSet map[string]bool) (string, []byte, error) {
|
||||||
|
path := ep.Path
|
||||||
|
for _, p := range ep.Params {
|
||||||
|
if p.In != "path" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
raw, err := paramValueString(p, typedValues[p.FlagName], isSet[p.FlagName])
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if raw == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
esc := url.PathEscape(raw)
|
||||||
|
path = strings.ReplaceAll(path, "{"+p.Name+"}", esc)
|
||||||
|
path = strings.ReplaceAll(path, ":"+p.Name, esc)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("invalid base url: %w", err)
|
||||||
|
}
|
||||||
|
u.Path = strings.TrimRight(u.Path, "/") + path
|
||||||
|
|
||||||
|
q := u.Query()
|
||||||
|
for _, p := range ep.Params {
|
||||||
|
if p.In != "query" || !isSet[p.FlagName] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val, err := paramQueryValues(p, typedValues[p.FlagName])
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
for _, v := range val {
|
||||||
|
q.Add(p.Name, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
bodyMap := map[string]any{}
|
||||||
|
rawBody := ""
|
||||||
|
for _, p := range ep.Params {
|
||||||
|
if p.In != "body" || !isSet[p.FlagName] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p.Name == "file" {
|
||||||
|
s, err := paramValueString(p, typedValues[p.FlagName], true)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
rawBody = s
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v, err := paramBodyValue(p, typedValues[p.FlagName])
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
bodyMap[p.Name] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawBody != "" {
|
||||||
|
return u.String(), []byte(rawBody), nil
|
||||||
|
}
|
||||||
|
if len(bodyMap) == 0 {
|
||||||
|
return u.String(), nil, nil
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(bodyMap)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("marshal body: %w", err)
|
||||||
|
}
|
||||||
|
return u.String(), data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func paramValueString(p Param, raw any, wasSet bool) (string, error) {
|
||||||
|
if !wasSet {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case *string:
|
||||||
|
return *v, nil
|
||||||
|
case *int:
|
||||||
|
return strconv.Itoa(*v), nil
|
||||||
|
case *float64:
|
||||||
|
return strconv.FormatFloat(*v, 'f', -1, 64), nil
|
||||||
|
case *bool:
|
||||||
|
if *v {
|
||||||
|
return "true", nil
|
||||||
|
}
|
||||||
|
return "false", nil
|
||||||
|
case *stringSliceFlag:
|
||||||
|
return strings.Join(v.v, ","), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported flag value for %s", p.FlagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func paramQueryValues(p Param, raw any) ([]string, error) {
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case *string:
|
||||||
|
return []string{*v}, nil
|
||||||
|
case *int:
|
||||||
|
return []string{strconv.Itoa(*v)}, nil
|
||||||
|
case *float64:
|
||||||
|
return []string{strconv.FormatFloat(*v, 'f', -1, 64)}, nil
|
||||||
|
case *bool:
|
||||||
|
if *v {
|
||||||
|
return []string{"true"}, nil
|
||||||
|
}
|
||||||
|
return []string{"false"}, nil
|
||||||
|
case *stringSliceFlag:
|
||||||
|
if len(v.v) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return v.v, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported query flag type for %s", p.FlagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func paramBodyValue(p Param, raw any) (any, error) {
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case *string:
|
||||||
|
if p.Type == "object" || p.Type == "array" {
|
||||||
|
var decoded any
|
||||||
|
if err := json.Unmarshal([]byte(*v), &decoded); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid JSON for --%s: %w", p.FlagName, err)
|
||||||
|
}
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
return *v, nil
|
||||||
|
case *int:
|
||||||
|
return *v, nil
|
||||||
|
case *float64:
|
||||||
|
return *v, nil
|
||||||
|
case *bool:
|
||||||
|
return *v, nil
|
||||||
|
case *stringSliceFlag:
|
||||||
|
return v.v, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported body flag type for %s", p.FlagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func execHTTP(ep Endpoint, reqURL string, body []byte) error {
|
||||||
|
var r io.Reader
|
||||||
|
if body != nil {
|
||||||
|
r = bytes.NewReader(body)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(ep.Method, reqURL, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
payload, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return fmt.Errorf("%s %s failed: %s", ep.Method, ep.Path, resp.Status)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s %s failed: %s: %s", ep.Method, ep.Path, resp.Status, strings.TrimSpace(string(payload)))
|
||||||
|
}
|
||||||
|
|
||||||
|
printJSON(payload)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func execWebsocket(ep Endpoint, reqURL string) error {
|
||||||
|
wsURL := strings.Replace(reqURL, "http://", "ws://", 1)
|
||||||
|
wsURL = strings.Replace(wsURL, "https://", "wss://", 1)
|
||||||
|
if strings.ToUpper(ep.Method) != http.MethodGet {
|
||||||
|
return fmt.Errorf("--ws requires GET endpoint, got %s", ep.Method)
|
||||||
|
}
|
||||||
|
c, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
stopPing := make(chan struct{})
|
||||||
|
defer close(stopPing)
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stopPing:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
_ = c.SetWriteDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
if err := c.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, msg, err := c.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) || strings.Contains(err.Error(), "close") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if string(msg) == "pong" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Println(string(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printJSON(payload []byte) {
|
||||||
|
if len(payload) == 0 {
|
||||||
|
fmt.Println("null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
if err := json.Unmarshal(payload, &v); err != nil {
|
||||||
|
fmt.Println(strings.TrimSpace(string(payload)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
_ = enc.Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
fmt.Println("godoxy [--addr ADDR] <command>")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Examples:")
|
||||||
|
fmt.Println(" godoxy version")
|
||||||
|
fmt.Println(" godoxy route list")
|
||||||
|
fmt.Println(" godoxy route route --which whoami")
|
||||||
|
fmt.Println()
|
||||||
|
printGroupedCommands()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printGroupedCommands() {
|
||||||
|
grouped := map[string][]Endpoint{}
|
||||||
|
groupOrder := make([]string, 0)
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, ep := range generatedEndpoints {
|
||||||
|
group := "root"
|
||||||
|
if len(ep.CommandPath) > 1 {
|
||||||
|
group = ep.CommandPath[0]
|
||||||
|
}
|
||||||
|
grouped[group] = append(grouped[group], ep)
|
||||||
|
if !seen[group] {
|
||||||
|
seen[group] = true
|
||||||
|
groupOrder = append(groupOrder, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(groupOrder)
|
||||||
|
for _, group := range groupOrder {
|
||||||
|
fmt.Printf("Commands (%s):\n", group)
|
||||||
|
sort.Slice(grouped[group], func(i, j int) bool {
|
||||||
|
li := strings.Join(grouped[group][i].CommandPath, " ")
|
||||||
|
lj := strings.Join(grouped[group][j].CommandPath, " ")
|
||||||
|
return li < lj
|
||||||
|
})
|
||||||
|
maxCmdWidth := 0
|
||||||
|
for _, ep := range grouped[group] {
|
||||||
|
cmd := strings.Join(ep.CommandPath, " ")
|
||||||
|
if len(cmd) > maxCmdWidth {
|
||||||
|
maxCmdWidth = len(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ep := range grouped[group] {
|
||||||
|
cmd := strings.Join(ep.CommandPath, " ")
|
||||||
|
fmt.Printf(" %-*s %s\n", maxCmdWidth, cmd, ep.Summary)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unknownCommandError(rest []string) error {
|
||||||
|
cmd := strings.Join(rest, " ")
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("unknown command: ")
|
||||||
|
b.WriteString(cmd)
|
||||||
|
if len(rest) > 0 && hasGroup(rest[0]) {
|
||||||
|
if len(rest) > 1 {
|
||||||
|
if hint := nearestForGroup(rest[0], rest[1]); hint != "" {
|
||||||
|
b.WriteString("\nDo you mean ")
|
||||||
|
b.WriteString(hint)
|
||||||
|
b.WriteString("?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(formatGroupHelp(rest[0]))
|
||||||
|
return errors.New(b.String())
|
||||||
|
}
|
||||||
|
if hint := nearestCommand(cmd); hint != "" {
|
||||||
|
b.WriteString("\nDo you mean ")
|
||||||
|
b.WriteString(hint)
|
||||||
|
b.WriteString("?")
|
||||||
|
}
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString("Run `godoxy help` for available commands.")
|
||||||
|
return errors.New(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func findEndpointAlias(args []string) (*Endpoint, int) {
|
||||||
|
var best *Endpoint
|
||||||
|
bestLen := -1
|
||||||
|
for i := range generatedEndpoints {
|
||||||
|
alias := aliasCommandPath(generatedEndpoints[i])
|
||||||
|
if len(alias) == 0 || len(alias) > len(args) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ok := true
|
||||||
|
for j, tok := range alias {
|
||||||
|
if args[j] != tok {
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok && len(alias) > bestLen {
|
||||||
|
best = &generatedEndpoints[i]
|
||||||
|
bestLen = len(alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best, bestLen
|
||||||
|
}
|
||||||
|
|
||||||
|
func aliasCommandPath(ep Endpoint) []string {
|
||||||
|
rawPath := strings.TrimPrefix(ep.Path, "/api/v1/")
|
||||||
|
rawPath = strings.Trim(rawPath, "/")
|
||||||
|
if rawPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts := strings.Split(rawPath, "/")
|
||||||
|
if len(parts) == 1 {
|
||||||
|
if isPathParam(parts[0]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{toKebabToken(parts[0])}
|
||||||
|
}
|
||||||
|
if isPathParam(parts[0]) || isPathParam(parts[1]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{toKebabToken(parts[0]), toKebabToken(parts[1])}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPathParam(s string) bool {
|
||||||
|
return strings.HasPrefix(s, "{") || strings.HasPrefix(s, ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toKebabToken(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "_", "-")
|
||||||
|
return strings.ToLower(strings.Trim(s, "-"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasGroup(group string) bool {
|
||||||
|
for _, ep := range generatedEndpoints {
|
||||||
|
if len(ep.CommandPath) > 1 && ep.CommandPath[0] == group {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func nearestCommand(input string) string {
|
||||||
|
commands := make([]string, 0, len(generatedEndpoints))
|
||||||
|
for _, ep := range generatedEndpoints {
|
||||||
|
commands = append(commands, strings.Join(ep.CommandPath, " "))
|
||||||
|
}
|
||||||
|
return nearestByDistance(input, commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nearestForGroup(group, input string) string {
|
||||||
|
choiceSet := map[string]struct{}{}
|
||||||
|
for _, ep := range generatedEndpoints {
|
||||||
|
if len(ep.CommandPath) < 2 || ep.CommandPath[0] != group {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
choiceSet[ep.CommandPath[1]] = struct{}{}
|
||||||
|
alias := aliasCommandPath(ep)
|
||||||
|
if len(alias) == 2 && alias[0] == group {
|
||||||
|
choiceSet[alias[1]] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
choices := make([]string, 0, len(choiceSet))
|
||||||
|
for choice := range choiceSet {
|
||||||
|
choices = append(choices, choice)
|
||||||
|
}
|
||||||
|
if len(choices) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return group + " " + nearestByDistance(input, choices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatGroupHelp(group string) string {
|
||||||
|
commands := make([]Endpoint, 0)
|
||||||
|
for _, ep := range generatedEndpoints {
|
||||||
|
if len(ep.CommandPath) > 1 && ep.CommandPath[0] == group {
|
||||||
|
commands = append(commands, ep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(commands, func(i, j int) bool {
|
||||||
|
return strings.Join(commands[i].CommandPath, " ") < strings.Join(commands[j].CommandPath, " ")
|
||||||
|
})
|
||||||
|
maxWidth := 0
|
||||||
|
for _, ep := range commands {
|
||||||
|
cmd := strings.Join(ep.CommandPath, " ")
|
||||||
|
if len(cmd) > maxWidth {
|
||||||
|
maxWidth = len(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintf(&b, "Available subcommands for %s:\n", group)
|
||||||
|
for _, ep := range commands {
|
||||||
|
cmd := strings.Join(ep.CommandPath, " ")
|
||||||
|
fmt.Fprintf(&b, " %-*s %s\n", maxWidth, cmd, ep.Summary)
|
||||||
|
}
|
||||||
|
return strings.TrimRight(b.String(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatEndpointHelp(ep Endpoint) string {
|
||||||
|
cmd := "godoxy " + strings.Join(ep.CommandPath, " ")
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintf(&b, "Usage: %s [flags]\n", cmd)
|
||||||
|
if ep.Summary != "" {
|
||||||
|
fmt.Fprintf(&b, "Summary: %s\n", ep.Summary)
|
||||||
|
}
|
||||||
|
if alias := aliasCommandPath(ep); len(alias) > 0 && strings.Join(alias, " ") != strings.Join(ep.CommandPath, " ") {
|
||||||
|
fmt.Fprintf(&b, "Alias: godoxy %s\n", strings.Join(alias, " "))
|
||||||
|
}
|
||||||
|
params := make([]Param, 0, len(ep.Params))
|
||||||
|
params = append(params, ep.Params...)
|
||||||
|
if ep.IsWebSocket {
|
||||||
|
params = append(params, Param{
|
||||||
|
FlagName: "ws",
|
||||||
|
Type: "boolean",
|
||||||
|
Description: "use websocket",
|
||||||
|
Required: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(params) == 0 {
|
||||||
|
return strings.TrimRight(b.String(), "\n")
|
||||||
|
}
|
||||||
|
b.WriteString("Flags:\n")
|
||||||
|
maxWidth := 0
|
||||||
|
flagNames := make([]string, 0, len(params))
|
||||||
|
for _, p := range params {
|
||||||
|
name := "--" + p.FlagName
|
||||||
|
if p.Required {
|
||||||
|
name += " (required)"
|
||||||
|
}
|
||||||
|
flagNames = append(flagNames, name)
|
||||||
|
if len(name) > maxWidth {
|
||||||
|
maxWidth = len(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, p := range params {
|
||||||
|
desc := p.Description
|
||||||
|
if desc == "" {
|
||||||
|
desc = p.In + " " + p.Type
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, " %-*s %s\n", maxWidth, flagNames[i], desc)
|
||||||
|
}
|
||||||
|
return strings.TrimRight(b.String(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func nearestByDistance(input string, choices []string) string {
|
||||||
|
if len(choices) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
nearest := choices[0]
|
||||||
|
minDistance := levenshteinDistance(input, nearest)
|
||||||
|
for _, choice := range choices[1:] {
|
||||||
|
d := levenshteinDistance(input, choice)
|
||||||
|
if d < minDistance {
|
||||||
|
minDistance = d
|
||||||
|
nearest = choice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:intrange
|
||||||
|
func levenshteinDistance(a, b string) int {
|
||||||
|
if a == b {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if len(a) == 0 {
|
||||||
|
return len(b)
|
||||||
|
}
|
||||||
|
if len(b) == 0 {
|
||||||
|
return len(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
v0 := make([]int, len(b)+1)
|
||||||
|
v1 := make([]int, len(b)+1)
|
||||||
|
|
||||||
|
for i := 0; i <= len(b); i++ {
|
||||||
|
v0[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(a); i++ {
|
||||||
|
v1[0] = i + 1
|
||||||
|
|
||||||
|
for j := 0; j < len(b); j++ {
|
||||||
|
cost := 0
|
||||||
|
if a[i] != b[j] {
|
||||||
|
cost = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
v1[j+1] = min3(v1[j]+1, v0[j+1]+1, v0[j]+cost)
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := 0; j <= len(b); j++ {
|
||||||
|
v0[j] = v1[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v1[len(b)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func min3(a, b, c int) int {
|
||||||
|
if a < b && a < c {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
if b < a && b < c {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
366
cmd/cli/gen/main.go
Executable file
366
cmd/cli/gen/main.go
Executable file
@@ -0,0 +1,366 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"go/format"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type swaggerSpec struct {
|
||||||
|
BasePath string `json:"basePath"`
|
||||||
|
Paths map[string]map[string]operation `json:"paths"`
|
||||||
|
Definitions map[string]definition `json:"definitions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type operation struct {
|
||||||
|
OperationID string `json:"operationId"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Parameters []parameter `json:"parameters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type parameter struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
In string `json:"in"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Schema *schemaRef `json:"schema"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type schemaRef struct {
|
||||||
|
Ref string `json:"$ref"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type definition struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Required []string `json:"required"`
|
||||||
|
Properties map[string]definition `json:"properties"`
|
||||||
|
Items *definition `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type endpoint struct {
|
||||||
|
CommandPath []string
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
Summary string
|
||||||
|
IsWebSocket bool
|
||||||
|
Params []param
|
||||||
|
}
|
||||||
|
|
||||||
|
type param struct {
|
||||||
|
FlagName string
|
||||||
|
Name string
|
||||||
|
In string
|
||||||
|
Type string
|
||||||
|
Required bool
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
root := filepath.Join("..", "..")
|
||||||
|
inPath := filepath.Join(root, "internal", "api", "v1", "docs", "swagger.json")
|
||||||
|
outPath := "generated_commands.go"
|
||||||
|
|
||||||
|
raw, err := os.ReadFile(inPath)
|
||||||
|
must(err)
|
||||||
|
|
||||||
|
var spec swaggerSpec
|
||||||
|
must(json.Unmarshal(raw, &spec))
|
||||||
|
|
||||||
|
eps := buildEndpoints(spec)
|
||||||
|
must(writeGenerated(outPath, eps))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildEndpoints(spec swaggerSpec) []endpoint {
|
||||||
|
byCommand := map[string]endpoint{}
|
||||||
|
|
||||||
|
pathKeys := make([]string, 0, len(spec.Paths))
|
||||||
|
for p := range spec.Paths {
|
||||||
|
pathKeys = append(pathKeys, p)
|
||||||
|
}
|
||||||
|
sort.Strings(pathKeys)
|
||||||
|
|
||||||
|
for _, p := range pathKeys {
|
||||||
|
methodMap := spec.Paths[p]
|
||||||
|
methods := make([]string, 0, len(methodMap))
|
||||||
|
for m := range methodMap {
|
||||||
|
methods = append(methods, strings.ToUpper(m))
|
||||||
|
}
|
||||||
|
sort.Strings(methods)
|
||||||
|
|
||||||
|
for _, method := range methods {
|
||||||
|
op := methodMap[strings.ToLower(method)]
|
||||||
|
if op.OperationID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ep := endpoint{
|
||||||
|
CommandPath: commandPathFromOp(p, op.OperationID),
|
||||||
|
Method: method,
|
||||||
|
Path: ensureSlash(spec.BasePath) + normalizePath(p),
|
||||||
|
Summary: op.Summary,
|
||||||
|
IsWebSocket: hasTag(op.Tags, "websocket"),
|
||||||
|
Params: collectParams(spec, op),
|
||||||
|
}
|
||||||
|
key := strings.Join(ep.CommandPath, " ")
|
||||||
|
if existing, ok := byCommand[key]; ok {
|
||||||
|
if betterEndpoint(ep, existing) {
|
||||||
|
byCommand[key] = ep
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byCommand[key] = ep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]endpoint, 0, len(byCommand))
|
||||||
|
for _, ep := range byCommand {
|
||||||
|
out = append(out, ep)
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
ai := strings.Join(out[i].CommandPath, " ")
|
||||||
|
aj := strings.Join(out[j].CommandPath, " ")
|
||||||
|
return ai < aj
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandPathFromOp(path, opID string) []string {
|
||||||
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return []string{toKebab(opID)}
|
||||||
|
}
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return []string{toKebab(parts[0])}
|
||||||
|
}
|
||||||
|
group := toKebab(parts[0])
|
||||||
|
name := toKebab(opID)
|
||||||
|
if name == group {
|
||||||
|
name = "get"
|
||||||
|
}
|
||||||
|
if group == "v1" {
|
||||||
|
return []string{name}
|
||||||
|
}
|
||||||
|
return []string{group, name}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectParams(spec swaggerSpec, op operation) []param {
|
||||||
|
params := make([]param, 0)
|
||||||
|
for _, p := range op.Parameters {
|
||||||
|
switch p.In {
|
||||||
|
case "body":
|
||||||
|
if p.Schema != nil && p.Schema.Ref != "" {
|
||||||
|
defName := strings.TrimPrefix(p.Schema.Ref, "#/definitions/")
|
||||||
|
params = append(params, bodyParamsFromDef(spec.Definitions[defName])...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
params = append(params, param{
|
||||||
|
FlagName: toKebab(p.Name),
|
||||||
|
Name: p.Name,
|
||||||
|
In: "body",
|
||||||
|
Type: defaultType(p.Type),
|
||||||
|
Required: p.Required,
|
||||||
|
Description: p.Description,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
params = append(params, param{
|
||||||
|
FlagName: toKebab(p.Name),
|
||||||
|
Name: p.Name,
|
||||||
|
In: p.In,
|
||||||
|
Type: defaultType(p.Type),
|
||||||
|
Required: p.Required,
|
||||||
|
Description: p.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate by flag name, prefer required entries.
|
||||||
|
byFlag := map[string]param{}
|
||||||
|
for _, p := range params {
|
||||||
|
if cur, ok := byFlag[p.FlagName]; ok {
|
||||||
|
if !cur.Required && p.Required {
|
||||||
|
byFlag[p.FlagName] = p
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byFlag[p.FlagName] = p
|
||||||
|
}
|
||||||
|
out := make([]param, 0, len(byFlag))
|
||||||
|
for _, p := range byFlag {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
if out[i].In != out[j].In {
|
||||||
|
return out[i].In < out[j].In
|
||||||
|
}
|
||||||
|
return out[i].FlagName < out[j].FlagName
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func bodyParamsFromDef(def definition) []param {
|
||||||
|
if def.Type != "object" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
requiredSet := map[string]struct{}{}
|
||||||
|
for _, name := range def.Required {
|
||||||
|
requiredSet[name] = struct{}{}
|
||||||
|
}
|
||||||
|
keys := make([]string, 0, len(def.Properties))
|
||||||
|
for k := range def.Properties {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
out := make([]param, 0, len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
prop := def.Properties[k]
|
||||||
|
_, required := requiredSet[k]
|
||||||
|
t := defaultType(prop.Type)
|
||||||
|
if prop.Type == "array" {
|
||||||
|
t = "array"
|
||||||
|
}
|
||||||
|
if prop.Type == "object" {
|
||||||
|
t = "object"
|
||||||
|
}
|
||||||
|
out = append(out, param{
|
||||||
|
FlagName: toKebab(k),
|
||||||
|
Name: k,
|
||||||
|
In: "body",
|
||||||
|
Type: t,
|
||||||
|
Required: required,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func betterEndpoint(a, b endpoint) bool {
|
||||||
|
// Prefer GET, then fewer path params, then shorter path.
|
||||||
|
if a.Method == "GET" && b.Method != "GET" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if a.Method != "GET" && b.Method == "GET" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ac := countPathParams(a.Path)
|
||||||
|
bc := countPathParams(b.Path)
|
||||||
|
if ac != bc {
|
||||||
|
return ac < bc
|
||||||
|
}
|
||||||
|
return len(a.Path) < len(b.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func countPathParams(path string) int {
|
||||||
|
count := 0
|
||||||
|
for _, seg := range strings.Split(path, "/") {
|
||||||
|
if strings.HasPrefix(seg, "{") || strings.HasPrefix(seg, ":") {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePath(p string) string {
|
||||||
|
parts := strings.Split(p, "/")
|
||||||
|
for i, part := range parts {
|
||||||
|
if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") {
|
||||||
|
name := strings.TrimSuffix(strings.TrimPrefix(part, "{"), "}")
|
||||||
|
parts[i] = "{" + name + "}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasTag(tags []string, want string) bool {
|
||||||
|
for _, t := range tags {
|
||||||
|
if strings.EqualFold(t, want) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeGenerated(outPath string, eps []endpoint) error {
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.WriteString("// Code generated by cmd/cli/gen. DO NOT EDIT.\n")
|
||||||
|
b.WriteString("package main\n\n")
|
||||||
|
b.WriteString("var generatedEndpoints = []Endpoint{\n")
|
||||||
|
for _, ep := range eps {
|
||||||
|
b.WriteString("\t{\n")
|
||||||
|
fmt.Fprintf(&b, "\t\tCommandPath: %#v,\n", ep.CommandPath)
|
||||||
|
fmt.Fprintf(&b, "\t\tMethod: %q,\n", ep.Method)
|
||||||
|
fmt.Fprintf(&b, "\t\tPath: %q,\n", ep.Path)
|
||||||
|
fmt.Fprintf(&b, "\t\tSummary: %q,\n", ep.Summary)
|
||||||
|
fmt.Fprintf(&b, "\t\tIsWebSocket: %t,\n", ep.IsWebSocket)
|
||||||
|
b.WriteString("\t\tParams: []Param{\n")
|
||||||
|
for _, p := range ep.Params {
|
||||||
|
b.WriteString("\t\t\t{\n")
|
||||||
|
fmt.Fprintf(&b, "\t\t\t\tFlagName: %q,\n", p.FlagName)
|
||||||
|
fmt.Fprintf(&b, "\t\t\t\tName: %q,\n", p.Name)
|
||||||
|
fmt.Fprintf(&b, "\t\t\t\tIn: %q,\n", p.In)
|
||||||
|
fmt.Fprintf(&b, "\t\t\t\tType: %q,\n", p.Type)
|
||||||
|
fmt.Fprintf(&b, "\t\t\t\tRequired: %t,\n", p.Required)
|
||||||
|
fmt.Fprintf(&b, "\t\t\t\tDescription: %q,\n", p.Description)
|
||||||
|
b.WriteString("\t\t\t},\n")
|
||||||
|
}
|
||||||
|
b.WriteString("\t\t},\n")
|
||||||
|
b.WriteString("\t},\n")
|
||||||
|
}
|
||||||
|
b.WriteString("}\n")
|
||||||
|
formatted, err := format.Source(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("format generated source: %w", err)
|
||||||
|
}
|
||||||
|
return os.WriteFile(outPath, formatted, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureSlash(s string) string {
|
||||||
|
if strings.HasPrefix(s, "/") {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "/" + s
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultType(t string) string {
|
||||||
|
switch t {
|
||||||
|
case "integer", "number", "boolean", "array", "object", "string":
|
||||||
|
return t
|
||||||
|
default:
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toKebab(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
s = strings.ReplaceAll(s, "_", "-")
|
||||||
|
s = strings.ReplaceAll(s, ".", "-")
|
||||||
|
var out []rune
|
||||||
|
for i, r := range s {
|
||||||
|
if unicode.IsUpper(r) {
|
||||||
|
if i > 0 && out[len(out)-1] != '-' {
|
||||||
|
out = append(out, '-')
|
||||||
|
}
|
||||||
|
out = append(out, unicode.ToLower(r))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, unicode.ToLower(r))
|
||||||
|
}
|
||||||
|
res := strings.Trim(string(out), "-")
|
||||||
|
for strings.Contains(res, "--") {
|
||||||
|
res = strings.ReplaceAll(res, "--", "-")
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func must(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
cmd/cli/go.mod
Normal file
10
cmd/cli/go.mod
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module github.com/yusing/godoxy/cli
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/yusing/goutils v0.7.0
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/yusing/goutils => ../../goutils
|
||||||
10
cmd/cli/go.sum
Executable file
10
cmd/cli/go.sum
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
13
cmd/cli/main.go
Normal file
13
cmd/cli/main.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(os.Args[1:]); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "error:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
cmd/cli/types.go
Executable file
19
cmd/cli/types.go
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type Param struct {
|
||||||
|
FlagName string
|
||||||
|
Name string
|
||||||
|
In string
|
||||||
|
Type string
|
||||||
|
Required bool
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint struct {
|
||||||
|
CommandPath []string
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
Summary string
|
||||||
|
IsWebSocket bool
|
||||||
|
Params []Param
|
||||||
|
}
|
||||||
263
cmd/debug_page.go
Normal file
263
cmd/debug_page.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
//go:build !production
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/godoxy/internal/api"
|
||||||
|
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
||||||
|
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
|
||||||
|
authApi "github.com/yusing/godoxy/internal/api/v1/auth"
|
||||||
|
certApi "github.com/yusing/godoxy/internal/api/v1/cert"
|
||||||
|
dockerApi "github.com/yusing/godoxy/internal/api/v1/docker"
|
||||||
|
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"
|
||||||
|
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
|
||||||
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
|
"github.com/yusing/godoxy/internal/idlewatcher"
|
||||||
|
idlewatcherTypes "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type debugMux struct {
|
||||||
|
endpoints []debugEndpoint
|
||||||
|
mux http.ServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
type debugEndpoint struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDebugMux() *debugMux {
|
||||||
|
return &debugMux{
|
||||||
|
endpoints: make([]debugEndpoint, 0),
|
||||||
|
mux: *http.NewServeMux(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *debugMux) registerEndpoint(name, method, path string) {
|
||||||
|
mux.endpoints = append(mux.endpoints, debugEndpoint{name: name, method: method, path: path})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *debugMux) HandleFunc(name, method, path string, handler http.HandlerFunc) {
|
||||||
|
mux.registerEndpoint(name, method, path)
|
||||||
|
mux.mux.HandleFunc(method+" "+path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *debugMux) Finalize() {
|
||||||
|
mux.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprintln(w, `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #f8f9fa;
|
||||||
|
background-color: #121212;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
color: #e9ecef;
|
||||||
|
}
|
||||||
|
.link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.method {
|
||||||
|
color: #6c757d;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.path {
|
||||||
|
color: #6c757d;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Method</th>
|
||||||
|
<th>Path</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>`)
|
||||||
|
for _, endpoint := range mux.endpoints {
|
||||||
|
fmt.Fprintf(w, "<tr><td><a class='link' href=%q>%s</a></td><td class='method'>%s</td><td class='path'>%s</td></tr>", endpoint.path, endpoint.name, endpoint.method, endpoint.path)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenDebugServer() {
|
||||||
|
mux := newDebugMux()
|
||||||
|
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "image/svg+xml")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprint(w, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
|
||||||
|
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
|
||||||
|
apiHandler := newAPIHandler(mux)
|
||||||
|
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
|
||||||
|
|
||||||
|
mux.Finalize()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
//nolint:gosec
|
||||||
|
err := http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Pragma", "no-cache")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
w.Header().Set("Expires", "0")
|
||||||
|
mux.mux.ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Error starting debug server")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAPIHandler(debugMux *debugMux) *gin.Engine {
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(api.ErrorHandler())
|
||||||
|
r.Use(api.ErrorLoggingMiddleware())
|
||||||
|
r.Use(api.NoCache())
|
||||||
|
|
||||||
|
registerGinRoute := func(router gin.IRouter, method, name string, path string, handler gin.HandlerFunc) {
|
||||||
|
if group, ok := router.(*gin.RouterGroup); ok {
|
||||||
|
debugMux.registerEndpoint(name, method, group.BasePath()+path)
|
||||||
|
} else {
|
||||||
|
debugMux.registerEndpoint(name, method, path)
|
||||||
|
}
|
||||||
|
router.Handle(method, path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerGinRoute(r, "GET", "App version", "/api/v1/version", apiV1.Version)
|
||||||
|
|
||||||
|
v1 := r.Group("/api/v1")
|
||||||
|
if auth.IsEnabled() {
|
||||||
|
v1Auth := v1.Group("/auth")
|
||||||
|
{
|
||||||
|
registerGinRoute(v1Auth, "HEAD", "Auth check", "/check", authApi.Check)
|
||||||
|
registerGinRoute(v1Auth, "POST", "Auth login", "/login", authApi.Login)
|
||||||
|
registerGinRoute(v1Auth, "GET", "Auth callback", "/callback", authApi.Callback)
|
||||||
|
registerGinRoute(v1Auth, "POST", "Auth callback", "/callback", authApi.Callback)
|
||||||
|
registerGinRoute(v1Auth, "POST", "Auth logout", "/logout", authApi.Logout)
|
||||||
|
registerGinRoute(v1Auth, "GET", "Auth logout", "/logout", authApi.Logout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// enable cache for favicon
|
||||||
|
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, "GET", "Route stats", "/stats", apiV1.Stats)
|
||||||
|
|
||||||
|
route := v1.Group("/route")
|
||||||
|
{
|
||||||
|
registerGinRoute(route, "GET", "List routes", "/list", routeApi.Routes)
|
||||||
|
registerGinRoute(route, "GET", "Get route", "/:which", routeApi.Route)
|
||||||
|
registerGinRoute(route, "GET", "List providers", "/providers", routeApi.Providers)
|
||||||
|
registerGinRoute(route, "GET", "List routes by provider", "/by_provider", routeApi.ByProvider)
|
||||||
|
registerGinRoute(route, "POST", "Playground", "/playground", routeApi.Playground)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := v1.Group("/file")
|
||||||
|
{
|
||||||
|
registerGinRoute(file, "GET", "List files", "/list", fileApi.List)
|
||||||
|
registerGinRoute(file, "GET", "Get file", "/content", fileApi.Get)
|
||||||
|
registerGinRoute(file, "PUT", "Set file", "/content", fileApi.Set)
|
||||||
|
registerGinRoute(file, "POST", "Set file", "/content", fileApi.Set)
|
||||||
|
registerGinRoute(file, "POST", "Validate file", "/validate", fileApi.Validate)
|
||||||
|
}
|
||||||
|
|
||||||
|
homepage := v1.Group("/homepage")
|
||||||
|
{
|
||||||
|
registerGinRoute(homepage, "GET", "List categories", "/categories", homepageApi.Categories)
|
||||||
|
registerGinRoute(homepage, "GET", "List items", "/items", homepageApi.Items)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item", "/set/item", homepageApi.SetItem)
|
||||||
|
registerGinRoute(homepage, "POST", "Set items batch", "/set/items_batch", homepageApi.SetItemsBatch)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item visible", "/set/item_visible", homepageApi.SetItemVisible)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item favorite", "/set/item_favorite", homepageApi.SetItemFavorite)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item sort order", "/set/item_sort_order", homepageApi.SetItemSortOrder)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item all sort order", "/set/item_all_sort_order", homepageApi.SetItemAllSortOrder)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item fav sort order", "/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder)
|
||||||
|
registerGinRoute(homepage, "POST", "Set category order", "/set/category_order", homepageApi.SetCategoryOrder)
|
||||||
|
registerGinRoute(homepage, "POST", "Item click", "/item_click", homepageApi.ItemClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := v1.Group("/cert")
|
||||||
|
{
|
||||||
|
registerGinRoute(cert, "GET", "Get cert info", "/info", certApi.Info)
|
||||||
|
registerGinRoute(cert, "GET", "Renew cert", "/renew", certApi.Renew)
|
||||||
|
}
|
||||||
|
|
||||||
|
agent := v1.Group("/agent")
|
||||||
|
{
|
||||||
|
registerGinRoute(agent, "GET", "List agents", "/list", agentApi.List)
|
||||||
|
registerGinRoute(agent, "POST", "Create agent", "/create", agentApi.Create)
|
||||||
|
registerGinRoute(agent, "POST", "Verify agent", "/verify", agentApi.Verify)
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics := v1.Group("/metrics")
|
||||||
|
{
|
||||||
|
registerGinRoute(metrics, "GET", "Get system info", "/system_info", metricsApi.SystemInfo)
|
||||||
|
registerGinRoute(metrics, "GET", "Get all system info", "/all_system_info", metricsApi.AllSystemInfo)
|
||||||
|
registerGinRoute(metrics, "GET", "Get uptime", "/uptime", metricsApi.Uptime)
|
||||||
|
}
|
||||||
|
|
||||||
|
docker := v1.Group("/docker")
|
||||||
|
{
|
||||||
|
registerGinRoute(docker, "GET", "Get container", "/container/:id", dockerApi.GetContainer)
|
||||||
|
registerGinRoute(docker, "GET", "List containers", "/containers", dockerApi.Containers)
|
||||||
|
registerGinRoute(docker, "GET", "Get docker info", "/info", dockerApi.Info)
|
||||||
|
registerGinRoute(docker, "GET", "Get docker logs", "/logs/:id", dockerApi.Logs)
|
||||||
|
registerGinRoute(docker, "POST", "Start docker container", "/start", dockerApi.Start)
|
||||||
|
registerGinRoute(docker, "POST", "Stop docker container", "/stop", dockerApi.Stop)
|
||||||
|
registerGinRoute(docker, "POST", "Restart docker container", "/restart", dockerApi.Restart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthBlockPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
auth.WriteBlockPage(w, http.StatusForbidden, "Forbidden", "Login", "/login")
|
||||||
|
}
|
||||||
7
cmd/debug_page_prod.go
Normal file
7
cmd/debug_page_prod.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build production
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func listenDebugServer() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
18
cmd/h2c_test_server/Dockerfile
Normal file
18
cmd/h2c_test_server/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM golang:1.26.0-alpine AS builder
|
||||||
|
|
||||||
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
COPY main.go ./
|
||||||
|
|
||||||
|
RUN go build -o h2c_test_server main.go
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY --from=builder /src/h2c_test_server /app/run
|
||||||
|
|
||||||
|
USER 1001:1001
|
||||||
|
|
||||||
|
CMD ["/app/run"]
|
||||||
7
cmd/h2c_test_server/go.mod
Normal file
7
cmd/h2c_test_server/go.mod
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module github.com/yusing/godoxy/cmd/h2c_test_server
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require golang.org/x/net v0.50.0
|
||||||
|
|
||||||
|
require golang.org/x/text v0.34.0 // indirect
|
||||||
4
cmd/h2c_test_server/go.sum
Normal file
4
cmd/h2c_test_server/go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
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=
|
||||||
26
cmd/h2c_test_server/main.go
Normal file
26
cmd/h2c_test_server/main.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: ":80",
|
||||||
|
Handler: h2c.NewHandler(handler, &http2.Server{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("H2C server listening on :80")
|
||||||
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("ListenAndServe: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
41
cmd/main.go
41
cmd/main.go
@@ -1,23 +1,21 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/api"
|
|
||||||
"github.com/yusing/godoxy/internal/auth"
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/config"
|
"github.com/yusing/godoxy/internal/config"
|
||||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
"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"
|
||||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
"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/net/gphttp/middleware"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
"github.com/yusing/godoxy/internal/route/rules"
|
||||||
"github.com/yusing/goutils/server"
|
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
"github.com/yusing/goutils/version"
|
"github.com/yusing/goutils/version"
|
||||||
)
|
)
|
||||||
@@ -31,6 +29,16 @@ func parallel(fns ...func()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
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()
|
initProfiling()
|
||||||
|
|
||||||
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
||||||
@@ -38,8 +46,7 @@ func main() {
|
|||||||
log.Trace().Msg("trace enabled")
|
log.Trace().Msg("trace enabled")
|
||||||
parallel(
|
parallel(
|
||||||
dnsproviders.InitProviders,
|
dnsproviders.InitProviders,
|
||||||
homepage.InitIconListCache,
|
iconlist.InitCache,
|
||||||
systeminfo.Poller.Start,
|
|
||||||
middleware.LoadComposeFiles,
|
middleware.LoadComposeFiles,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,23 +61,23 @@ func main() {
|
|||||||
|
|
||||||
err := config.Load()
|
err := config.Load()
|
||||||
if err != nil {
|
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 {
|
if err := auth.Initialize(); err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||||
}
|
}
|
||||||
// API Handler needs to start after auth is initialized.
|
rules.InitAuthHandler(auth.AuthOrProceed)
|
||||||
server.StartServer(task.RootTask("api_server", false), server.Options{
|
|
||||||
Name: "api",
|
listenDebugServer()
|
||||||
HTTPAddr: common.APIHTTPAddr,
|
|
||||||
Handler: api.NewHandler(),
|
|
||||||
})
|
|
||||||
|
|
||||||
uptime.Poller.Start()
|
|
||||||
config.WatchChanges()
|
config.WatchChanges()
|
||||||
|
|
||||||
|
close(done)
|
||||||
|
|
||||||
task.WaitExit(config.Value().TimeoutShutdown)
|
task.WaitExit(config.Value().TimeoutShutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,17 @@ services:
|
|||||||
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
|
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
|
||||||
frontend:
|
frontend:
|
||||||
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
|
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
|
||||||
|
# lite variant
|
||||||
|
# image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}-lite
|
||||||
container_name: godoxy-frontend
|
container_name: godoxy-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
# comment out `user` for lite variant
|
||||||
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /app/.next/cache # next image caching
|
- /tmp:rw
|
||||||
|
- /app/node_modules/.cache:rw
|
||||||
# for lite variant, do not change uid/gid
|
# for lite variant, do not change uid/gid
|
||||||
# - /var/cache/nginx:uid=101,gid=101
|
# - /var/cache/nginx:uid=101,gid=101
|
||||||
# - /run:uid=101,gid=101
|
# - /run:uid=101,gid=101
|
||||||
@@ -73,10 +76,9 @@ services:
|
|||||||
- ./error_pages:/app/error_pages:ro
|
- ./error_pages:/app/error_pages:ro
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
|
||||||
# To use autocert, certs will be stored in "./certs".
|
# This path stores certs obtained from autocert and agent TLS client certs
|
||||||
# You can also use a docker volume to store it
|
|
||||||
- ./certs:/app/certs
|
- ./certs:/app/certs
|
||||||
|
|
||||||
# remove "./certs:/app/certs" and uncomment below to use existing certificate
|
# mount existing certificate
|
||||||
# - /path/to/certs/cert.crt:/app/certs/cert.crt
|
# - /path/to/certs/cert.crt:/app/certs/cert.crt
|
||||||
# - /path/to/certs/priv.key:/app/certs/priv.key
|
# - /path/to/certs/priv.key:/app/certs/priv.key
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
# autocert:
|
# autocert:
|
||||||
# provider: local
|
# provider: local
|
||||||
|
# cert_path: /path/to/cert.crt # default: /app/certs/cert.crt
|
||||||
|
# key_path: /path/to/priv.key # default: /app/certs/priv.key
|
||||||
|
|
||||||
# 2. cloudflare
|
# 2. cloudflare
|
||||||
# autocert:
|
# autocert:
|
||||||
@@ -86,6 +88,12 @@ entrypoint:
|
|||||||
# - name: default
|
# - name: default
|
||||||
# do: proxy http://other-proxy:8080
|
# do: proxy http://other-proxy:8080
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
healthcheck:
|
||||||
|
interval: 5s
|
||||||
|
timeout: 15s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
# include files are standalone yaml files under `config/` directory
|
# include files are standalone yaml files under `config/` directory
|
||||||
#
|
#
|
||||||
|
|||||||
188
dev.compose.yml
188
dev.compose.yml
@@ -1,3 +1,8 @@
|
|||||||
|
x-benchmark: &benchmark
|
||||||
|
restart: no
|
||||||
|
labels:
|
||||||
|
proxy.exclude: true
|
||||||
|
proxy.#1.healthcheck.disable: true
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: godoxy-dev
|
image: godoxy-dev
|
||||||
@@ -54,7 +59,190 @@ services:
|
|||||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||||
labels:
|
labels:
|
||||||
proxy.tinyauth.port: "3000"
|
proxy.tinyauth.port: "3000"
|
||||||
|
jotty: # issue #182
|
||||||
|
image: ghcr.io/fccview/jotty:latest
|
||||||
|
container_name: jotty
|
||||||
|
user: "1000:1000"
|
||||||
|
tmpfs:
|
||||||
|
- /app/data:rw,uid=1000,gid=1000
|
||||||
|
- /app/config:rw,uid=1000,gid=1000
|
||||||
|
- /app/.next/cache:rw,uid=1000,gid=1000
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
labels:
|
||||||
|
proxy.aliases: "jotty.my.app"
|
||||||
|
postgres-test:
|
||||||
|
image: postgres:18-alpine
|
||||||
|
container_name: postgres-test
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_DB=postgres
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
h2c_test_server:
|
||||||
|
build:
|
||||||
|
context: cmd/h2c_test_server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: h2c_test
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
proxy.#1.scheme: h2c
|
||||||
|
proxy.#1.port: 80
|
||||||
|
bench: # returns 4096 bytes of random data
|
||||||
|
<<: *benchmark
|
||||||
|
build:
|
||||||
|
context: cmd/bench_server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: bench
|
||||||
|
godoxy:
|
||||||
|
<<: *benchmark
|
||||||
|
build: .
|
||||||
|
container_name: godoxy-benchmark
|
||||||
|
ports:
|
||||||
|
- 8080:80
|
||||||
|
configs:
|
||||||
|
- source: godoxy_config
|
||||||
|
target: /app/config/config.yml
|
||||||
|
- source: godoxy_provider
|
||||||
|
target: /app/config/providers.yml
|
||||||
|
traefik:
|
||||||
|
<<: *benchmark
|
||||||
|
image: traefik:latest
|
||||||
|
container_name: traefik
|
||||||
|
command:
|
||||||
|
- --api.insecure=true
|
||||||
|
- --entrypoints.web.address=:8081
|
||||||
|
- --providers.file.directory=/etc/traefik/dynamic
|
||||||
|
- --providers.file.watch=true
|
||||||
|
- --log.level=ERROR
|
||||||
|
ports:
|
||||||
|
- 8081:8081
|
||||||
|
configs:
|
||||||
|
- source: traefik_config
|
||||||
|
target: /etc/traefik/dynamic/routes.yml
|
||||||
|
caddy:
|
||||||
|
<<: *benchmark
|
||||||
|
image: caddy:latest
|
||||||
|
container_name: caddy
|
||||||
|
ports:
|
||||||
|
- 8082:80
|
||||||
|
configs:
|
||||||
|
- source: caddy_config
|
||||||
|
target: /etc/caddy/Caddyfile
|
||||||
|
tmpfs:
|
||||||
|
- /data
|
||||||
|
- /config
|
||||||
|
nginx:
|
||||||
|
<<: *benchmark
|
||||||
|
image: nginx:latest
|
||||||
|
container_name: nginx
|
||||||
|
command: nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
|
||||||
|
ports:
|
||||||
|
- 8083:80
|
||||||
|
configs:
|
||||||
|
- source: nginx_config
|
||||||
|
target: /etc/nginx/nginx.conf
|
||||||
|
|
||||||
configs:
|
configs:
|
||||||
|
godoxy_config:
|
||||||
|
content: |
|
||||||
|
providers:
|
||||||
|
include:
|
||||||
|
- providers.yml
|
||||||
|
godoxy_provider:
|
||||||
|
content: |
|
||||||
|
bench.domain.com:
|
||||||
|
host: bench
|
||||||
|
traefik_config:
|
||||||
|
content: |
|
||||||
|
http:
|
||||||
|
routers:
|
||||||
|
bench:
|
||||||
|
rule: "Host(`bench.domain.com`)"
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
service: bench
|
||||||
|
services:
|
||||||
|
bench:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://bench:80"
|
||||||
|
caddy_config:
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
admin off
|
||||||
|
auto_https off
|
||||||
|
default_bind 0.0.0.0
|
||||||
|
|
||||||
|
servers {
|
||||||
|
protocols h1 h2c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http://bench.domain.com {
|
||||||
|
reverse_proxy bench:80
|
||||||
|
}
|
||||||
|
nginx_config:
|
||||||
|
content: |
|
||||||
|
worker_processes auto;
|
||||||
|
worker_rlimit_nofile 65535;
|
||||||
|
error_log /dev/null;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 10240;
|
||||||
|
multi_accept on;
|
||||||
|
use epoll;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
keepalive_requests 10000;
|
||||||
|
|
||||||
|
upstream backend {
|
||||||
|
server bench:80;
|
||||||
|
keepalive 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name bench.domain.com;
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_set_header Host $$host;
|
||||||
|
proxy_set_header X-Real-IP $$remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
parca:
|
parca:
|
||||||
content: |
|
content: |
|
||||||
object_storage:
|
object_storage:
|
||||||
|
|||||||
46
examples/docker-compose/netbird.yml
Normal file
46
examples/docker-compose/netbird.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
services:
|
||||||
|
netbird-dashboard:
|
||||||
|
image: netbirdio/dashboard:latest
|
||||||
|
container_name: netbird-dashboard
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: [netbird-net]
|
||||||
|
env_file: dashboard.env
|
||||||
|
labels:
|
||||||
|
proxy.aliases: netbird
|
||||||
|
proxy.#1.port: 80
|
||||||
|
proxy.#1.scheme: http
|
||||||
|
proxy.#1.homepage.name: NetBird
|
||||||
|
proxy.#1.homepage.icon: "@selfhst/netbird.svg"
|
||||||
|
proxy.#1.homepage.category: networking
|
||||||
|
proxy.#1.rules: | # https://docs.netbird.io/selfhosted/configuration-files
|
||||||
|
path glob(/signalexchange.SignalExchange/**) | path glob(/management.ManagementService/**) | path glob(/management.ProxyService/**) {
|
||||||
|
route netbird-grpc
|
||||||
|
}
|
||||||
|
path glob(/relay*) | path glob(/ws-proxy/**) | path glob(/api*) | path glob(/oauth2*) {
|
||||||
|
route netbird-api
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
pass
|
||||||
|
}
|
||||||
|
netbird-server:
|
||||||
|
image: netbirdio/netbird-server:latest
|
||||||
|
container_name: netbird-server
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- netbird-net
|
||||||
|
volumes:
|
||||||
|
- netbird_data:/var/lib/netbird
|
||||||
|
- ./config.yaml:/etc/netbird/config.yaml
|
||||||
|
command: ["--config", "/etc/netbird/config.yaml"]
|
||||||
|
labels:
|
||||||
|
proxy.aliases: netbird-api, netbird-grpc
|
||||||
|
proxy.*.port: 80
|
||||||
|
proxy.*.homepage.show: false
|
||||||
|
proxy.#1.scheme: http
|
||||||
|
proxy.#2.scheme: h2c
|
||||||
|
|
||||||
|
networks:
|
||||||
|
netbird-net:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
netbird_data:
|
||||||
190
go.mod
190
go.mod
@@ -1,67 +1,83 @@
|
|||||||
module github.com/yusing/godoxy
|
module github.com/yusing/godoxy
|
||||||
|
|
||||||
go 1.25.3
|
go 1.26.0
|
||||||
|
|
||||||
replace github.com/yusing/godoxy/agent => ./agent
|
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/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
|
replace (
|
||||||
|
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
||||||
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
|
||||||
replace github.com/shirou/gopsutil/v4 => ./internal/gopsutil
|
github.com/yusing/godoxy/agent => ./agent
|
||||||
|
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
|
||||||
replace github.com/yusing/goutils => ./goutils
|
github.com/yusing/goutils => ./goutils
|
||||||
|
github.com/yusing/goutils/http/reverseproxy => ./goutils/http/reverseproxy
|
||||||
|
github.com/yusing/goutils/http/websocket => ./goutils/http/websocket
|
||||||
|
github.com/yusing/goutils/server => ./goutils/server
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0 // oidc authentication
|
github.com/cenkalti/backoff/v5 v5.0.3 // backoff for retrying operations
|
||||||
github.com/docker/docker v28.5.1+incompatible // docker daemon
|
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||||
github.com/gin-gonic/gin v1.11.0 // api server
|
github.com/gin-gonic/gin v1.11.0 // api server
|
||||||
github.com/go-acme/lego/v4 v4.27.0 // acme client
|
github.com/go-acme/lego/v4 v4.32.0 // acme client
|
||||||
github.com/go-playground/validator/v10 v10.28.0 // validator
|
github.com/go-playground/validator/v10 v10.30.1 // validator
|
||||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
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/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||||
github.com/gotify/server/v2 v2.7.3 // reference the Message struct for json response
|
github.com/gotify/server/v2 v2.9.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/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/pires/go-proxyproto v0.11.0 // proxy protocol support
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // lock free map for concurrent operations
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
|
||||||
github.com/rs/zerolog v1.34.0 // logging
|
github.com/rs/zerolog v1.34.0 // logging
|
||||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||||
golang.org/x/crypto v0.43.0 // encrypting password with bcrypt
|
golang.org/x/crypto v0.48.0 // encrypting password with bcrypt
|
||||||
golang.org/x/net v0.46.0 // HTTP header utilities
|
golang.org/x/net v0.50.0 // HTTP header utilities
|
||||||
golang.org/x/oauth2 v0.32.0 // oauth2 authentication
|
golang.org/x/oauth2 v0.35.0 // oauth2 authentication
|
||||||
golang.org/x/sync v0.17.0
|
golang.org/x/sync v0.19.0 // errgroup and singleflight for concurrent operations
|
||||||
golang.org/x/time v0.14.0 // time utilities
|
golang.org/x/time v0.14.0 // time utilities
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/docker/cli v28.5.1+incompatible
|
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||||
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
|
github.com/bytedance/sonic v1.15.0 // fast json parsing
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/docker/cli v29.2.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||||
github.com/luthermonson/go-proxmox v0.2.3
|
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1
|
github.com/golang-jwt/jwt/v5 v5.3.1 // jwt authentication
|
||||||
github.com/quic-go/quic-go v0.55.0 // indirect; http3 support
|
github.com/luthermonson/go-proxmox v0.4.0 // proxmox API client
|
||||||
github.com/samber/slog-zerolog/v2 v2.8.0 // indirect
|
github.com/moby/moby/api v1.52.0 // docker API
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/moby/moby/client v0.2.1 // docker client
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
||||||
github.com/yusing/ds v0.3.1
|
github.com/quic-go/quic-go v0.59.0 // http3 support
|
||||||
github.com/yusing/godoxy/agent v0.0.0-20251025144347-1ec2872f3d4c
|
github.com/shirou/gopsutil/v4 v4.26.1 // system information
|
||||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251025144347-1ec2872f3d4c
|
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.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-20260224071728-0eba04510480
|
||||||
|
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260224071728-0eba04510480
|
||||||
|
github.com/yusing/gointernals v0.2.0
|
||||||
github.com/yusing/goutils v0.7.0
|
github.com/yusing/goutils v0.7.0
|
||||||
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260223150038-3be815cb6e3b
|
||||||
|
github.com/yusing/goutils/http/websocket v0.0.0-20260223150038-3be815cb6e3b
|
||||||
|
github.com/yusing/goutils/server v0.0.0-20260223150038-3be815cb6e3b
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/auth v0.17.0 // indirect
|
cloud.google.com/go/auth v0.18.2 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // 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.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/internal v1.11.2 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
@@ -72,9 +88,9 @@ require (
|
|||||||
github.com/djherbis/times v1.6.0 // indirect
|
github.com/djherbis/times v1.6.0 // indirect
|
||||||
github.com/docker/go-connections v0.6.0
|
github.com/docker/go-connections v0.6.0
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.9.0 // indirect
|
github.com/ebitengine/purego v0.10.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // 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/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
@@ -83,8 +99,8 @@ require (
|
|||||||
github.com/gofrs/flock v0.13.0 // indirect
|
github.com/gofrs/flock v0.13.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
github.com/jinzhu/copier v0.4.0 // indirect
|
github.com/jinzhu/copier v0.4.0 // indirect
|
||||||
@@ -94,7 +110,7 @@ require (
|
|||||||
github.com/magefile/mage v1.15.0 // indirect
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/miekg/dns v1.1.68 // indirect
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
@@ -106,81 +122,75 @@ require (
|
|||||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/samber/lo v1.52.0 // indirect
|
github.com/samber/lo v1.52.0 // indirect
|
||||||
github.com/samber/slog-common v0.19.0 // indirect
|
github.com/samber/slog-common v0.20.0 // indirect
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 // indirect
|
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/sony/gobreaker v1.0.0 // indirect
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0
|
go.uber.org/atomic v1.11.0
|
||||||
go.uber.org/ratelimit v0.3.1 // indirect
|
go.uber.org/ratelimit v0.3.1 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.30.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
google.golang.org/api v0.253.0 // indirect
|
google.golang.org/api v0.268.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc // indirect
|
||||||
google.golang.org/grpc v1.76.0 // indirect
|
google.golang.org/grpc v1.79.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // 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.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/bytedance/sonic v1.14.1
|
|
||||||
github.com/shirou/gopsutil/v4 v4.25.9
|
|
||||||
github.com/valyala/fasthttp v1.68.0
|
|
||||||
github.com/yusing/gointernals v0.1.16
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.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
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||||
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
github.com/go-resty/resty/v2 v2.17.2 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/linode/linodego v1.60.0 // indirect
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
github.com/linode/linodego v1.65.0 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1 // indirect
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 // indirect
|
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // 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/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
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.14 // indirect
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/vultr/govultr/v3 v3.24.0 // indirect
|
github.com/vultr/govultr/v3 v3.27.0 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
golang.org/x/arch v0.24.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
316
go.sum
316
go.sum
@@ -1,14 +1,14 @@
|
|||||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
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/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 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
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 v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
|
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.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
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.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s=
|
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.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I=
|
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=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||||
@@ -23,16 +23,14 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourceg
|
|||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=
|
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=
|
||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
|
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
|
||||||
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
|
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
|
||||||
@@ -46,24 +44,27 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
|
|||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||||
|
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||||
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
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 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
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 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -75,32 +76,32 @@ 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/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 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
|
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
||||||
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
|
||||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
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-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
|
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
|
||||||
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
|
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
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 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
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 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
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 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-acme/lego/v4 v4.27.0 h1:cIhWd7Uj4BNFLEF3IpwuMkukVVRs5qjlp4KdUGa75yU=
|
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
|
||||||
github.com/go-acme/lego/v4 v4.27.0/go.mod h1:9FfNZHZmg6hf5CWOp4Lzo4gU8aBEvqZvrwdkBboa+4g=
|
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
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-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=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -119,46 +120,45 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
|
||||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
github.com/go-resty/resty/v2 v2.17.2/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 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
|
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 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
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 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
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/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 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
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.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
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/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
|
||||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
|
||||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
@@ -177,10 +177,12 @@ 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/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 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||||
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -189,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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/linode/linodego v1.60.0 h1:SgsebJFRCi+lSmYy+C40wmKZeJllGGm+W12Qw4+yVdI=
|
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
|
||||||
github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs=
|
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 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
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-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
|
|
||||||
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
|
||||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
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/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
@@ -208,31 +208,29 @@ 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/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 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
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=
|
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/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
|
||||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
|
||||||
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
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/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1 h1:45giryNXrlUHzK/Cd4DDBOhaK0EklXrhjTgv00Zo5po=
|
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 h1:2EthQw4pEN2rbbSLWlF9itV+Ws2xmAmIcfKYsrwCbVA=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1/go.mod h1:xOLJ0zNGmF4M4LqdQclLONwdzjJewNl/7WQiZgrvYR8=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||||
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=
|
||||||
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=
|
||||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
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/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -247,11 +245,16 @@ 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/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 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
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/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
|
||||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
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 h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||||
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||||
@@ -260,12 +263,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
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/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
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=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
@@ -273,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/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 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
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.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
|
||||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
|
||||||
github.com/samber/slog-zerolog/v2 v2.8.0 h1:K3+PJieRyi2rX/eaJZ95EdmpY/pzdeDd3jRnIQZG6kU=
|
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
|
||||||
github.com/samber/slog-zerolog/v2 v2.8.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
|
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.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
|
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 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
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 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
@@ -288,90 +293,85 @@ github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.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.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
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/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
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.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
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 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||||
github.com/vultr/govultr/v3 v3.24.0 h1:fTTTj0VBve+Miy+wGhlb90M2NMDfpGFi6Frlj3HVy6M=
|
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
|
||||||
github.com/vultr/govultr/v3 v3.24.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
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 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
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/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.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||||
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg=
|
||||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
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 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/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
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/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
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 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
||||||
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
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-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.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.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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.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.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.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
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-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-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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
@@ -381,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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
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.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -392,8 +392,8 @@ 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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.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/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
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-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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -402,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-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-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-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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -413,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.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.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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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/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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -433,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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.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.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
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 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -443,29 +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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
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=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.253.0 h1:apU86Eq9Q2eQco3NsUYFpVTfy7DwemojL7LmbAj7g/I=
|
google.golang.org/api v0.268.0 h1:hgA3aS4lt9rpF5RCCkX0Q2l7DvHgvlb53y4T4u6iKkA=
|
||||||
google.golang.org/api v0.253.0/go.mod h1:PX09ad0r/4du83vZVAaGg7OaeyGnaUmT/CYPNvtLCbw=
|
google.golang.org/api v0.268.0/go.mod h1:HXMyMH496wz+dAJwD/GkAPLd3ZL33Kh0zEG32eNvy9w=
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
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-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc h1:51Wupg8spF+5FC6D+iMKbOddFjMckETnNnEiZ+HX37s=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
@@ -474,3 +472,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
|
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||||
|
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||||
|
|||||||
2
goutils
2
goutils
Submodule goutils updated: c0955732e9...4912690d40
282
internal/acl/README.md
Normal file
282
internal/acl/README.md
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
# internal/acl
|
||||||
|
|
||||||
|
Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The ACL package provides network-level access control by wrapping TCP listeners and validating incoming connections against configurable allow/deny rules. It integrates with MaxMind GeoIP for geographic-based filtering and supports access logging with notification batching.
|
||||||
|
|
||||||
|
### Primary consumers
|
||||||
|
|
||||||
|
- `internal/entrypoint` - Wraps the main TCP listener for connection filtering
|
||||||
|
- Operators - Configure rules via YAML configuration
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- HTTP request-level filtering (handled by middleware)
|
||||||
|
- Authentication or authorization (see `internal/auth`)
|
||||||
|
- VPN or tunnel integration
|
||||||
|
|
||||||
|
### Stability
|
||||||
|
|
||||||
|
Stable internal package. The public API is the `Config` struct and its methods.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
### Exported types
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Config struct {
|
||||||
|
Default string // "allow" or "deny" (default: "allow")
|
||||||
|
AllowLocal *bool // Allow private/loopback IPs (default: true)
|
||||||
|
Allow Matchers // Allow rules
|
||||||
|
Deny Matchers // Deny rules
|
||||||
|
Log *accesslog.ACLLoggerConfig // Access logging configuration
|
||||||
|
|
||||||
|
Notify struct {
|
||||||
|
To []string // Notification providers
|
||||||
|
Interval time.Duration // Notification frequency (default: 1m)
|
||||||
|
IncludeAllowed *bool // Include allowed in notifications (default: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Matcher struct {
|
||||||
|
match MatcherFunc
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Matchers []Matcher
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exported functions and methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (c *Config) Validate() error
|
||||||
|
```
|
||||||
|
|
||||||
|
Validates configuration and sets defaults. Must be called before `Start`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (c *Config) Start(parent task.Parent) error
|
||||||
|
```
|
||||||
|
|
||||||
|
Initializes the ACL, starts the logger and notification goroutines.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (c *Config) IPAllowed(ip net.IP) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns true if the IP is allowed based on configured rules. Performs caching and GeoIP lookup if needed.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (c *Config) WrapTCP(lis net.Listener) net.Listener
|
||||||
|
```
|
||||||
|
|
||||||
|
Wraps a `net.Listener` to filter connections by IP.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (matcher *Matcher) Parse(s string) error
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses a matcher string in the format `{type}:{value}`. Supported types: `ip`, `cidr`, `tz`, `country`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core components
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[TCP Listener] --> B[TCPListener Wrapper]
|
||||||
|
B --> C{IP Allowed?}
|
||||||
|
C -->|Yes| D[Accept Connection]
|
||||||
|
C -->|No| E[Close Connection]
|
||||||
|
|
||||||
|
F[Config] --> G[Validate]
|
||||||
|
G --> H[Start]
|
||||||
|
H --> I[Matcher Evaluation]
|
||||||
|
I --> C
|
||||||
|
|
||||||
|
J[MaxMind] -.-> K[IP Lookup]
|
||||||
|
K -.-> I
|
||||||
|
|
||||||
|
L[Access Logger] -.-> M[Log & Notify]
|
||||||
|
M -.-> B
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection filtering flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant TCPListener
|
||||||
|
participant Config
|
||||||
|
participant MaxMind
|
||||||
|
participant Logger
|
||||||
|
|
||||||
|
Client->>TCPListener: Connection Request
|
||||||
|
TCPListener->>Config: IPAllowed(clientIP)
|
||||||
|
|
||||||
|
alt Loopback IP
|
||||||
|
Config-->>TCPListener: true
|
||||||
|
else Private IP (allow_local)
|
||||||
|
Config-->>TCPListener: true
|
||||||
|
else Cached Result
|
||||||
|
Config-->>TCPListener: Cached Result
|
||||||
|
else Evaluate Allow Rules
|
||||||
|
Config->>Config: Check Allow list
|
||||||
|
alt Matches
|
||||||
|
Config->>Config: Cache true
|
||||||
|
Config-->>TCPListener: Allowed
|
||||||
|
else Evaluate Deny Rules
|
||||||
|
Config->>Config: Check Deny list
|
||||||
|
alt Matches
|
||||||
|
Config->>Config: Cache false
|
||||||
|
Config-->>TCPListener: Denied
|
||||||
|
else Default Action
|
||||||
|
Config->>MaxMind: Lookup GeoIP
|
||||||
|
MaxMind-->>Config: IPInfo
|
||||||
|
Config->>Config: Apply default rule
|
||||||
|
Config->>Config: Cache result
|
||||||
|
Config-->>TCPListener: Result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
alt Logging enabled
|
||||||
|
Config->>Logger: Log access attempt
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Matcher types
|
||||||
|
|
||||||
|
| Type | Format | Example |
|
||||||
|
| -------- | ----------------- | --------------------- |
|
||||||
|
| IP | `ip:address` | `ip:192.168.1.1` |
|
||||||
|
| CIDR | `cidr:network` | `cidr:192.168.0.0/16` |
|
||||||
|
| TimeZone | `tz:timezone` | `tz:Asia/Shanghai` |
|
||||||
|
| Country | `country:ISOCode` | `country:GB` |
|
||||||
|
|
||||||
|
## Configuration Surface
|
||||||
|
|
||||||
|
### Config sources
|
||||||
|
|
||||||
|
Configuration is loaded from `config/config.yml` under the `acl` key.
|
||||||
|
|
||||||
|
### Schema
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
acl:
|
||||||
|
default: "allow" # "allow" or "deny"
|
||||||
|
allow_local: true # Allow private/loopback IPs
|
||||||
|
log:
|
||||||
|
log_allowed: false # Log allowed connections
|
||||||
|
notify:
|
||||||
|
to: ["gotify"] # Notification providers
|
||||||
|
interval: "1m" # Notification interval
|
||||||
|
include_allowed: false # Include allowed in notifications
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hot-reloading
|
||||||
|
|
||||||
|
Configuration requires restart. The ACL does not support dynamic rule updates.
|
||||||
|
|
||||||
|
## Dependency and Integration Map
|
||||||
|
|
||||||
|
### Internal dependencies
|
||||||
|
|
||||||
|
- `internal/maxmind` - IP geolocation lookup
|
||||||
|
- `internal/logging/accesslog` - Access logging
|
||||||
|
- `internal/notif` - Notifications
|
||||||
|
- `internal/task/task.go` - Lifetime management
|
||||||
|
|
||||||
|
### Integration points
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Entrypoint uses ACL to wrap the TCP listener
|
||||||
|
aclListener := config.ACL.WrapTCP(listener)
|
||||||
|
http.Server.Serve(aclListener, entrypoint)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
- `ACL started` - Configuration summary on start
|
||||||
|
- `log_notify_loop` - Access attempts (allowed/denied)
|
||||||
|
|
||||||
|
Log levels: `Info` for startup, `Debug` for client closure.
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
No metrics are currently exposed.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Loopback and private IPs are always allowed unless explicitly denied
|
||||||
|
- Cache TTL is 1 minute to limit memory usage
|
||||||
|
- Notification channel has a buffer of 100 to prevent blocking
|
||||||
|
- Failed connections are immediately closed without response
|
||||||
|
|
||||||
|
## Failure Modes and Recovery
|
||||||
|
|
||||||
|
| Failure | Behavior | Recovery |
|
||||||
|
| --------------------------------- | ------------------------------------- | --------------------------------------------- |
|
||||||
|
| Invalid matcher syntax | Validation fails on startup | Fix configuration syntax |
|
||||||
|
| MaxMind database unavailable | GeoIP lookups return unknown location | Default action applies; cache hit still works |
|
||||||
|
| Notification provider unavailable | Notification dropped | Error logged, continues operation |
|
||||||
|
| Cache full | No eviction, uses Go map | No action needed |
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
aclConfig := &acl.Config{
|
||||||
|
Default: "allow",
|
||||||
|
AllowLocal: ptr(true),
|
||||||
|
Allow: acl.Matchers{
|
||||||
|
{match: matchIP(net.ParseIP("192.168.1.0/24"))},
|
||||||
|
},
|
||||||
|
Deny: acl.Matchers{
|
||||||
|
{match: matchISOCode("CN")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := aclConfig.Validate(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := aclConfig.Start(parent); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wrapping a TCP listener
|
||||||
|
|
||||||
|
```go
|
||||||
|
listener, err := net.Listen("tcp", ":443")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap with ACL
|
||||||
|
aclListener := aclConfig.WrapTCP(listener)
|
||||||
|
|
||||||
|
// Use with HTTP server
|
||||||
|
server := &http.Server{}
|
||||||
|
server.Serve(aclListener)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating custom matchers
|
||||||
|
|
||||||
|
```go
|
||||||
|
matcher := &acl.Matcher{}
|
||||||
|
err := matcher.Parse("country:US")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the matcher
|
||||||
|
allowed := matcher.match(ipInfo)
|
||||||
|
```
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
@@ -14,8 +13,8 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
"github.com/yusing/godoxy/internal/maxmind"
|
"github.com/yusing/godoxy/internal/maxmind"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
"github.com/yusing/godoxy/internal/utils"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
aclevents "github.com/yusing/goutils/events/acl"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
@@ -28,9 +27,9 @@ type Config struct {
|
|||||||
Log *accesslog.ACLLoggerConfig `json:"log"`
|
Log *accesslog.ACLLoggerConfig `json:"log"`
|
||||||
|
|
||||||
Notify struct {
|
Notify struct {
|
||||||
To []string `json:"to"` // list of notification providers
|
To []string `json:"to,omitempty"` // list of notification providers
|
||||||
Interval time.Duration `json:"interval"` // interval between notifications
|
Interval time.Duration `json:"interval,omitempty"` // interval between notifications
|
||||||
IncludeAllowed *bool `json:"include_allowed"` // default: false
|
IncludeAllowed *bool `json:"include_allowed,omitzero"` // default: false
|
||||||
} `json:"notify"`
|
} `json:"notify"`
|
||||||
|
|
||||||
config
|
config
|
||||||
@@ -55,7 +54,7 @@ type config struct {
|
|||||||
|
|
||||||
logAllowed bool
|
logAllowed bool
|
||||||
// will be nil if Log is nil
|
// will be nil if Log is nil
|
||||||
logger *accesslog.AccessLogger
|
logger accesslog.AccessLogger
|
||||||
|
|
||||||
// will never tick if Notify.To is empty
|
// will never tick if Notify.To is empty
|
||||||
notifyTicker *time.Ticker
|
notifyTicker *time.Ticker
|
||||||
@@ -68,21 +67,20 @@ type config struct {
|
|||||||
type checkCache struct {
|
type checkCache struct {
|
||||||
*maxmind.IPInfo
|
*maxmind.IPInfo
|
||||||
allow bool
|
allow bool
|
||||||
|
reason string
|
||||||
created time.Time
|
created time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type ipLog struct {
|
type ipLog struct {
|
||||||
info *maxmind.IPInfo
|
info *maxmind.IPInfo
|
||||||
allowed bool
|
allowed bool
|
||||||
|
reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
// could be nil
|
|
||||||
var ActiveConfig atomic.Pointer[Config]
|
|
||||||
|
|
||||||
const cacheTTL = 1 * time.Minute
|
const cacheTTL = 1 * time.Minute
|
||||||
|
|
||||||
func (c *checkCache) Expired() bool {
|
func (c *checkCache) Expired() bool {
|
||||||
return c.created.Add(cacheTTL).Before(utils.TimeNow())
|
return c.created.Add(cacheTTL).Before(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add stats
|
// TODO: add stats
|
||||||
@@ -92,7 +90,7 @@ const (
|
|||||||
ACLDeny = "deny"
|
ACLDeny = "deny"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Config) Validate() gperr.Error {
|
func (c *Config) Validate() error {
|
||||||
switch c.Default {
|
switch c.Default {
|
||||||
case "", ACLAllow:
|
case "", ACLAllow:
|
||||||
c.defaultAllow = true
|
c.defaultAllow = true
|
||||||
@@ -109,7 +107,7 @@ func (c *Config) Validate() gperr.Error {
|
|||||||
c.allowLocal = true
|
c.allowLocal = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Notify.Interval < 0 {
|
if c.Notify.Interval <= 0 {
|
||||||
c.Notify.Interval = defaultNotifyInterval
|
c.Notify.Interval = defaultNotifyInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +134,10 @@ func (c *Config) Valid() bool {
|
|||||||
return c != nil && c.valErr == nil
|
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 {
|
if c.Log != nil {
|
||||||
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -144,9 +145,6 @@ func (c *Config) Start(parent task.Parent) gperr.Error {
|
|||||||
}
|
}
|
||||||
c.logger = logger
|
c.logger = logger
|
||||||
}
|
}
|
||||||
if c.valErr != nil {
|
|
||||||
return c.valErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.needLogOrNotify() {
|
if c.needLogOrNotify() {
|
||||||
c.logNotifyCh = make(chan ipLog, 100)
|
c.logNotifyCh = make(chan ipLog, 100)
|
||||||
@@ -173,14 +171,15 @@ func (c *Config) Start(parent task.Parent) gperr.Error {
|
|||||||
return nil
|
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 {
|
if common.ForceResolveCountry && info.City == nil {
|
||||||
maxmind.LookupCity(info)
|
maxmind.LookupCity(info)
|
||||||
}
|
}
|
||||||
c.ipCache.Store(info.Str, &checkCache{
|
c.ipCache.Store(info.Str, &checkCache{
|
||||||
IPInfo: info,
|
IPInfo: info,
|
||||||
allow: allow,
|
allow: allow,
|
||||||
created: utils.TimeNow(),
|
reason: reason,
|
||||||
|
created: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,23 +215,26 @@ func (c *Config) logNotifyLoop(parent task.Parent) {
|
|||||||
select {
|
select {
|
||||||
case <-parent.Context().Done():
|
case <-parent.Context().Done():
|
||||||
return
|
return
|
||||||
case log := <-c.logNotifyCh:
|
case req := <-c.logNotifyCh:
|
||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
if !log.allowed || c.logAllowed {
|
if !req.allowed || c.logAllowed {
|
||||||
c.logger.LogACL(log.info, !log.allowed)
|
c.logger.LogACL(req.info, !req.allowed, req.reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.needNotify() {
|
if c.needNotify() {
|
||||||
if log.allowed {
|
if req.allowed {
|
||||||
if c.notifyAllowed {
|
if c.notifyAllowed {
|
||||||
c.allowedCount[log.info.Str]++
|
c.allowedCount[req.info.Str]++
|
||||||
c.totalAllowedCount++
|
c.totalAllowedCount++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.blockedCount[log.info.Str]++
|
c.blockedCount[req.info.Str]++
|
||||||
c.totalBlockedCount++
|
c.totalBlockedCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !req.allowed {
|
||||||
|
aclevents.Blocked(req.info.Str, req.reason)
|
||||||
|
}
|
||||||
case <-c.notifyTicker.C: // will never tick when notify is disabled
|
case <-c.notifyTicker.C: // will never tick when notify is disabled
|
||||||
total := len(c.allowedCount) + len(c.blockedCount)
|
total := len(c.allowedCount) + len(c.blockedCount)
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
@@ -264,9 +266,9 @@ func (c *Config) logNotifyLoop(parent task.Parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// log and notify if needed
|
// 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 {
|
if c.logNotifyCh != nil {
|
||||||
c.logNotifyCh <- ipLog{info: info, allowed: allowed}
|
c.logNotifyCh <- ipLog{info: info, allowed: allowed, reason: reason}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,30 +283,36 @@ func (c *Config) IPAllowed(ip net.IP) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.allowLocal && ip.IsPrivate() {
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
ipStr := ip.String()
|
ipStr := ip.String()
|
||||||
record, ok := c.ipCache.Load(ipStr)
|
record, ok := c.ipCache.Load(ipStr)
|
||||||
if ok && !record.Expired() {
|
if ok && !record.Expired() {
|
||||||
c.logAndNotify(record.IPInfo, record.allow)
|
c.logAndNotify(record.IPInfo, record.allow, record.reason)
|
||||||
return record.allow
|
return record.allow
|
||||||
}
|
}
|
||||||
|
|
||||||
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
||||||
if c.Allow.Match(ipAndStr) {
|
if index := c.Deny.MatchedIndex(ipAndStr); index != -1 {
|
||||||
c.logAndNotify(ipAndStr, true)
|
reason := "blocked by deny rule: " + c.Deny[index].raw
|
||||||
c.cacheRecord(ipAndStr, true)
|
c.logAndNotify(ipAndStr, false, reason)
|
||||||
return true
|
c.cacheRecord(ipAndStr, false, reason)
|
||||||
}
|
|
||||||
if c.Deny.Match(ipAndStr) {
|
|
||||||
c.logAndNotify(ipAndStr, false)
|
|
||||||
c.cacheRecord(ipAndStr, false)
|
|
||||||
return false
|
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)
|
reason := "denied by default"
|
||||||
c.cacheRecord(ipAndStr, c.defaultAllow)
|
if c.defaultAllow {
|
||||||
|
reason = "allowed by default"
|
||||||
|
}
|
||||||
|
c.logAndNotify(ipAndStr, c.defaultAllow, reason)
|
||||||
|
c.cacheRecord(ipAndStr, c.defaultAllow, reason)
|
||||||
return c.defaultAllow
|
return c.defaultAllow
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package acl
|
package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ type MatcherFunc func(*maxmind.IPInfo) bool
|
|||||||
|
|
||||||
type Matcher struct {
|
type Matcher struct {
|
||||||
match MatcherFunc
|
match MatcherFunc
|
||||||
|
raw string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Matchers []Matcher
|
type Matchers []Matcher
|
||||||
@@ -36,9 +39,9 @@ var errMatcherFormat = gperr.Multiline().AddLines(
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errSyntax = gperr.New("syntax error")
|
errSyntax = errors.New("syntax error")
|
||||||
errInvalidIP = gperr.New("invalid IP")
|
errInvalidIP = errors.New("invalid IP")
|
||||||
errInvalidCIDR = gperr.New("invalid CIDR")
|
errInvalidCIDR = errors.New("invalid CIDR")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (matcher *Matcher) Parse(s string) error {
|
func (matcher *Matcher) Parse(s string) error {
|
||||||
@@ -46,6 +49,7 @@ func (matcher *Matcher) Parse(s string) error {
|
|||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return errSyntax
|
return errSyntax
|
||||||
}
|
}
|
||||||
|
matcher.raw = s
|
||||||
|
|
||||||
switch parts[0] {
|
switch parts[0] {
|
||||||
case MatcherTypeIP:
|
case MatcherTypeIP:
|
||||||
@@ -79,6 +83,27 @@ func (matchers Matchers) Match(ip *maxmind.IPInfo) bool {
|
|||||||
return false
|
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 {
|
func matchIP(ip net.IP) MatcherFunc {
|
||||||
return func(ip2 *maxmind.IPInfo) bool {
|
return func(ip2 *maxmind.IPInfo) bool {
|
||||||
return ip.Equal(ip2.IP)
|
return ip.Equal(ip2.IP)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TCPListener struct {
|
type TCPListener struct {
|
||||||
@@ -44,6 +46,7 @@ func (s *TCPListener) Accept() (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
addr, ok := c.RemoteAddr().(*net.TCPAddr)
|
addr, ok := c.RemoteAddr().(*net.TCPAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
log.Error().Msgf("unexpected remote address type: %T, addr: %s", c.RemoteAddr(), c.RemoteAddr().String())
|
||||||
// Not a TCPAddr, drop
|
// Not a TCPAddr, drop
|
||||||
c.Close()
|
c.Close()
|
||||||
return noConn{}, nil
|
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(key any, value 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"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UDPListener struct {
|
type UDPListener struct {
|
||||||
@@ -33,6 +35,7 @@ func (s *UDPListener) ReadFrom(p []byte) (int, net.Addr, error) {
|
|||||||
}
|
}
|
||||||
udpAddr, ok := addr.(*net.UDPAddr)
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
log.Error().Msgf("unexpected remote address type: %T, addr: %s", addr, addr.String())
|
||||||
// Not a UDPAddr, drop
|
// Not a UDPAddr, drop
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -52,6 +55,7 @@ func (s *UDPListener) WriteTo(p []byte, addr net.Addr) (int, error) {
|
|||||||
}
|
}
|
||||||
udpAddr, ok := addr.(*net.UDPAddr)
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
log.Error().Msgf("unexpected remote address type: %T, addr: %s", addr, addr.String())
|
||||||
// Not a UDPAddr, drop
|
// Not a UDPAddr, drop
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
281
internal/agentpool/README.md
Normal file
281
internal/agentpool/README.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# internal/agentpool
|
||||||
|
|
||||||
|
Thread-safe pool for managing remote Docker agent connections.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The agentpool package provides a centralized pool for storing and retrieving remote agent configurations. It enables GoDoxy to connect to Docker hosts via agent connections instead of direct socket access, enabling secure remote container management.
|
||||||
|
|
||||||
|
### Primary consumers
|
||||||
|
|
||||||
|
- `internal/route/provider` - Creates agent-based route providers
|
||||||
|
- `internal/docker` - Manages agent-based Docker client connections
|
||||||
|
- Configuration loading during startup
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Agent lifecycle management (handled by `agent/pkg/agent`)
|
||||||
|
- Agent health monitoring
|
||||||
|
- Agent authentication/authorization
|
||||||
|
|
||||||
|
### Stability
|
||||||
|
|
||||||
|
Stable internal package. The pool uses `xsync.Map` for lock-free concurrent access.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
### Exported types
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Agent struct {
|
||||||
|
*agent.AgentConfig
|
||||||
|
httpClient *http.Client
|
||||||
|
fasthttpHcClient *fasthttp.Client
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exported functions
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Add(cfg *agent.AgentConfig) (added bool)
|
||||||
|
```
|
||||||
|
|
||||||
|
Adds an agent to the pool. Returns `true` if added, `false` if already exists. Uses `LoadOrCompute` to prevent duplicates.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Has(cfg *agent.AgentConfig) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Checks if an agent exists in the pool.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Remove(cfg *agent.AgentConfig)
|
||||||
|
```
|
||||||
|
|
||||||
|
Removes an agent from the pool.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func RemoveAll()
|
||||||
|
```
|
||||||
|
|
||||||
|
Removes all agents from the pool. Called during configuration reload.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Get(agentAddrOrDockerHost string) (*Agent, bool)
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieves an agent by address or Docker host URL. Automatically detects if the input is an agent address or Docker host URL and resolves accordingly.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func GetAgent(name string) (*Agent, bool)
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieves an agent by name. O(n) iteration over pool contents.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func List() []*Agent
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns all agents as a slice. Creates a new copy for thread safety.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Iter() iter.Seq2[string, *Agent]
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns an iterator over all agents. Uses `xsync.Map.Range`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Num() int
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the number of agents in the pool.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (agent *Agent) HTTPClient() *http.Client
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns an HTTP client configured for the agent.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core components
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Agent Config] --> B[Add to Pool]
|
||||||
|
B --> C[xsync.Map Storage]
|
||||||
|
C --> D{Get Request}
|
||||||
|
D -->|By Address| E[Load from map]
|
||||||
|
D -->|By Docker Host| F[Resolve agent addr]
|
||||||
|
D -->|By Name| G[Iterate & match]
|
||||||
|
|
||||||
|
H[Docker Client] --> I[Get Agent]
|
||||||
|
I --> C
|
||||||
|
I --> J[HTTP Client]
|
||||||
|
J --> K[Agent Connection]
|
||||||
|
|
||||||
|
L[Route Provider] --> M[List Agents]
|
||||||
|
M --> C
|
||||||
|
```
|
||||||
|
|
||||||
|
### Thread safety model
|
||||||
|
|
||||||
|
The pool uses `xsync.Map[string, *Agent]` for concurrent-safe operations:
|
||||||
|
|
||||||
|
- `Add`: `LoadOrCompute` prevents race conditions and duplicates
|
||||||
|
- `Get`: Lock-free read operations
|
||||||
|
- `Iter`: Consistent snapshot iteration via `Range`
|
||||||
|
- `Remove`: Thread-safe deletion
|
||||||
|
|
||||||
|
### Test mode
|
||||||
|
|
||||||
|
When running tests (binary ends with `.test`), a test agent is automatically added:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
if strings.HasSuffix(os.Args[0], ".test") {
|
||||||
|
agentPool.Store("test-agent", &Agent{
|
||||||
|
AgentConfig: &agent.AgentConfig{
|
||||||
|
Addr: "test-agent",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Surface
|
||||||
|
|
||||||
|
No direct configuration. Agents are added via configuration loading from `config/config.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
providers:
|
||||||
|
agents:
|
||||||
|
- addr: agent.example.com:443
|
||||||
|
name: remote-agent
|
||||||
|
tls:
|
||||||
|
ca_file: /path/to/ca.pem
|
||||||
|
cert_file: /path/to/cert.pem
|
||||||
|
key_file: /path/to/key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency and Integration Map
|
||||||
|
|
||||||
|
### Internal dependencies
|
||||||
|
|
||||||
|
- `agent/pkg/agent` - Agent configuration and connection settings
|
||||||
|
- `xsync/v4` - Concurrent map implementation
|
||||||
|
|
||||||
|
### External dependencies
|
||||||
|
|
||||||
|
- `valyala/fasthttp` - Fast HTTP client for agent communication
|
||||||
|
|
||||||
|
### Integration points
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Docker package uses agent pool for remote connections
|
||||||
|
if agent.IsDockerHostAgent(host) {
|
||||||
|
a, ok := agentpool.Get(host)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Errorf("agent %q not found", host))
|
||||||
|
}
|
||||||
|
opt := []client.Opt{
|
||||||
|
client.WithHost(agent.DockerHost),
|
||||||
|
client.WithHTTPClient(a.HTTPClient()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
No specific logging in the agentpool package. Client creation/destruction is logged in the docker package.
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
No metrics are currently exposed.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- TLS configuration is loaded from agent configuration
|
||||||
|
- Connection credentials are not stored in the pool after agent creation
|
||||||
|
- HTTP clients are created per-request to ensure credential freshness
|
||||||
|
|
||||||
|
## Failure Modes and Recovery
|
||||||
|
|
||||||
|
| Failure | Behavior | Recovery |
|
||||||
|
| -------------------- | -------------------- | ---------------------------- |
|
||||||
|
| Agent not found | Returns `nil, false` | Add agent to pool before use |
|
||||||
|
| Duplicate add | Returns `false` | Existing agent is preserved |
|
||||||
|
| Test mode activation | Test agent added | Only during test binaries |
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
- O(1) lookup by address
|
||||||
|
- O(n) iteration for name-based lookup
|
||||||
|
- Pre-sized to 10 entries via `xsync.WithPresize(10)`
|
||||||
|
- No locks required for read operations
|
||||||
|
- HTTP clients are created per-call to ensure fresh connections
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Adding an agent
|
||||||
|
|
||||||
|
```go
|
||||||
|
agentConfig := &agent.AgentConfig{
|
||||||
|
Addr: "agent.example.com:443",
|
||||||
|
Name: "my-agent",
|
||||||
|
}
|
||||||
|
|
||||||
|
added := agentpool.Add(agentConfig)
|
||||||
|
if !added {
|
||||||
|
log.Println("Agent already exists")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retrieving an agent
|
||||||
|
|
||||||
|
```go
|
||||||
|
// By address
|
||||||
|
agent, ok := agentpool.Get("agent.example.com:443")
|
||||||
|
if !ok {
|
||||||
|
log.Fatal("Agent not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// By Docker host URL
|
||||||
|
agent, ok := agentpool.Get("http://docker-host:2375")
|
||||||
|
if !ok {
|
||||||
|
log.Fatal("Agent not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// By name
|
||||||
|
agent, ok := agentpool.GetAgent("my-agent")
|
||||||
|
if !ok {
|
||||||
|
log.Fatal("Agent not found")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Iterating over all agents
|
||||||
|
|
||||||
|
```go
|
||||||
|
for addr, agent := range agentpool.Iter() {
|
||||||
|
log.Printf("Agent: %s at %s", agent.Name, addr)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using with Docker client
|
||||||
|
|
||||||
|
```go
|
||||||
|
// When creating a Docker client with an agent host
|
||||||
|
if agent.IsDockerHostAgent(host) {
|
||||||
|
a, ok := agentpool.Get(host)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Errorf("agent %q not found", host))
|
||||||
|
}
|
||||||
|
opt := []client.Opt{
|
||||||
|
client.WithHost(agent.DockerHost),
|
||||||
|
client.WithHTTPClient(a.HTTPClient()),
|
||||||
|
}
|
||||||
|
dockerClient, err := client.New(opt...)
|
||||||
|
}
|
||||||
|
```
|
||||||
58
internal/agentpool/agent.go
Normal file
58
internal/agentpool/agent.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package agentpool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Agent struct {
|
||||||
|
*agent.AgentConfig
|
||||||
|
|
||||||
|
httpClient *http.Client
|
||||||
|
fasthttpHcClient *fasthttp.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAgent(cfg *agent.AgentConfig) *Agent {
|
||||||
|
transport := cfg.Transport()
|
||||||
|
transport.MaxIdleConns = 100
|
||||||
|
transport.MaxIdleConnsPerHost = 100
|
||||||
|
transport.ReadBufferSize = 16384
|
||||||
|
transport.WriteBufferSize = 16384
|
||||||
|
|
||||||
|
return &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) {
|
||||||
|
if addr != agent.AgentHost+":443" {
|
||||||
|
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
||||||
|
}
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
return dialer.Dial("tcp", cfg.Addr)
|
||||||
|
},
|
||||||
|
TLSConfig: cfg.TLSConfig(),
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 3 * time.Second,
|
||||||
|
DisableHeaderNamesNormalizing: true,
|
||||||
|
DisablePathNormalizing: true,
|
||||||
|
NoDefaultUserAgentHeader: true,
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (agent *Agent) HTTPClient() *http.Client {
|
||||||
|
return &http.Client{
|
||||||
|
Transport: agent.Transport(),
|
||||||
|
}
|
||||||
|
}
|
||||||
96
internal/agentpool/http_requests.go
Normal file
96
internal/agentpool/http_requests.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package agentpool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/goutils/http/reverseproxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (agent *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, agentPkg.APIBaseURL+endpoint, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return agent.httpClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (agent *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
||||||
|
req.URL.Host = agentPkg.AgentHost
|
||||||
|
req.URL.Scheme = "https"
|
||||||
|
req.URL.Path = agentPkg.APIEndpointBase + endpoint
|
||||||
|
req.RequestURI = ""
|
||||||
|
resp, err := agent.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealthCheckResponse struct {
|
||||||
|
Healthy bool `json:"healthy"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
Latency time.Duration `json:"latency"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
|
||||||
|
req := fasthttp.AcquireRequest()
|
||||||
|
defer fasthttp.ReleaseRequest(req)
|
||||||
|
|
||||||
|
resp := fasthttp.AcquireResponse()
|
||||||
|
defer fasthttp.ReleaseResponse(resp)
|
||||||
|
|
||||||
|
req.SetRequestURI(agentPkg.APIBaseURL + agentPkg.EndpointHealth + "?" + query)
|
||||||
|
req.Header.SetMethod(fasthttp.MethodGet)
|
||||||
|
req.Header.Set("Accept-Encoding", "identity")
|
||||||
|
req.SetConnectionClose()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
err = agent.fasthttpHcClient.DoTimeout(req, resp, timeout)
|
||||||
|
ret.Latency = time.Since(start)
|
||||||
|
if err != nil {
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status := resp.StatusCode(); status != http.StatusOK {
|
||||||
|
ret.Detail = fmt.Sprintf("HTTP %d %s", status, resp.Body())
|
||||||
|
return ret, nil
|
||||||
|
} else {
|
||||||
|
err = sonic.Unmarshal(resp.Body(), &ret)
|
||||||
|
if err != nil {
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
||||||
|
transport := agent.Transport()
|
||||||
|
dialer := websocket.Dialer{
|
||||||
|
NetDialContext: transport.DialContext,
|
||||||
|
NetDialTLSContext: transport.DialTLSContext,
|
||||||
|
}
|
||||||
|
return dialer.DialContext(ctx, agentPkg.APIBaseURL+endpoint, http.Header{
|
||||||
|
"Host": {agentPkg.AgentHost},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReverseProxy reverse proxies the request to the agent
|
||||||
|
//
|
||||||
|
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
|
||||||
|
// If the request has a query, it will be added to the proxy request's URL
|
||||||
|
func (agent *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
|
||||||
|
rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport())
|
||||||
|
req.URL.Host = agentPkg.AgentHost
|
||||||
|
req.URL.Scheme = "https"
|
||||||
|
req.URL.Path = endpoint
|
||||||
|
req.RequestURI = ""
|
||||||
|
rp.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
79
internal/agentpool/pool.go
Normal file
79
internal/agentpool/pool.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package agentpool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"iter"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
)
|
||||||
|
|
||||||
|
var agentPool = xsync.NewMap[string, *Agent](xsync.WithPresize(10))
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if strings.HasSuffix(os.Args[0], ".test") {
|
||||||
|
agentPool.Store("test-agent", &Agent{
|
||||||
|
AgentConfig: &agent.AgentConfig{
|
||||||
|
Addr: "test-agent",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(agentAddrOrDockerHost string) (*Agent, bool) {
|
||||||
|
if !agent.IsDockerHostAgent(agentAddrOrDockerHost) {
|
||||||
|
return getAgentByAddr(agentAddrOrDockerHost)
|
||||||
|
}
|
||||||
|
return getAgentByAddr(agent.GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAgent(name string) (*Agent, bool) {
|
||||||
|
for _, agent := range agentPool.Range {
|
||||||
|
if agent.Name == name {
|
||||||
|
return agent, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Add(cfg *agent.AgentConfig) (added bool) {
|
||||||
|
_, loaded := agentPool.LoadOrCompute(cfg.Addr, func() (*Agent, bool) {
|
||||||
|
return newAgent(cfg), false
|
||||||
|
})
|
||||||
|
return !loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
func Has(cfg *agent.AgentConfig) bool {
|
||||||
|
_, ok := agentPool.Load(cfg.Addr)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func Remove(cfg *agent.AgentConfig) {
|
||||||
|
agentPool.Delete(cfg.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveAll() {
|
||||||
|
agentPool.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
func List() []*Agent {
|
||||||
|
agents := make([]*Agent, 0, agentPool.Size())
|
||||||
|
for _, agent := range agentPool.Range {
|
||||||
|
agents = append(agents, agent)
|
||||||
|
}
|
||||||
|
return agents
|
||||||
|
}
|
||||||
|
|
||||||
|
func Iter() iter.Seq2[string, *Agent] {
|
||||||
|
return agentPool.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
func Num() int {
|
||||||
|
return agentPool.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAgentByAddr(addr string) (agent *Agent, ok bool) {
|
||||||
|
agent, ok = agentPool.Load(addr)
|
||||||
|
return agent, ok
|
||||||
|
}
|
||||||
@@ -16,29 +16,31 @@ import (
|
|||||||
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
|
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
|
||||||
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
|
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
|
||||||
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
|
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"
|
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
|
||||||
"github.com/yusing/godoxy/internal/auth"
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NewHandler creates a new Gin engine for the API.
|
||||||
|
//
|
||||||
// @title GoDoxy API
|
// @title GoDoxy API
|
||||||
// @version 1.0
|
// @version 1.0
|
||||||
// @description GoDoxy API
|
// @description GoDoxy API
|
||||||
// @termsOfService https://github.com/yusing/godoxy/blob/main/LICENSE
|
// @termsOfService https://github.com/yusing/godoxy/blob/main/LICENSE
|
||||||
|
//
|
||||||
// @contact.name Yusing
|
// @contact.name Yusing
|
||||||
// @contact.url https://github.com/yusing/godoxy/issues
|
// @contact.url https://github.com/yusing/godoxy/issues
|
||||||
|
//
|
||||||
// @license.name MIT
|
// @license.name MIT
|
||||||
// @license.url https://github.com/yusing/godoxy/blob/main/LICENSE
|
// @license.url https://github.com/yusing/godoxy/blob/main/LICENSE
|
||||||
|
//
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
|
//
|
||||||
// @externalDocs.description GoDoxy Docs
|
// @externalDocs.description GoDoxy Docs
|
||||||
// @externalDocs.url https://docs.godoxy.dev
|
// @externalDocs.url https://docs.godoxy.dev
|
||||||
func NewHandler() *gin.Engine {
|
func NewHandler(requireAuth bool) *gin.Engine {
|
||||||
if !common.IsDebug {
|
if !common.IsDebug {
|
||||||
gin.SetMode("release")
|
gin.SetMode("release")
|
||||||
}
|
}
|
||||||
@@ -51,7 +53,7 @@ func NewHandler() *gin.Engine {
|
|||||||
|
|
||||||
r.GET("/api/v1/version", apiV1.Version)
|
r.GET("/api/v1/version", apiV1.Version)
|
||||||
|
|
||||||
if auth.IsEnabled() {
|
if auth.IsEnabled() && requireAuth {
|
||||||
v1Auth := r.Group("/api/v1/auth")
|
v1Auth := r.Group("/api/v1/auth")
|
||||||
{
|
{
|
||||||
v1Auth.HEAD("/check", authApi.Check)
|
v1Auth.HEAD("/check", authApi.Check)
|
||||||
@@ -64,7 +66,7 @@ func NewHandler() *gin.Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
v1 := r.Group("/api/v1")
|
v1 := r.Group("/api/v1")
|
||||||
if auth.IsEnabled() {
|
if auth.IsEnabled() && requireAuth {
|
||||||
v1.Use(AuthMiddleware())
|
v1.Use(AuthMiddleware())
|
||||||
}
|
}
|
||||||
if common.APISkipOriginCheck {
|
if common.APISkipOriginCheck {
|
||||||
@@ -75,8 +77,8 @@ func NewHandler() *gin.Engine {
|
|||||||
v1.GET("/favicon", apiV1.FavIcon)
|
v1.GET("/favicon", apiV1.FavIcon)
|
||||||
v1.GET("/health", apiV1.Health)
|
v1.GET("/health", apiV1.Health)
|
||||||
v1.GET("/icons", apiV1.Icons)
|
v1.GET("/icons", apiV1.Icons)
|
||||||
v1.POST("/reload", apiV1.Reload)
|
|
||||||
v1.GET("/stats", apiV1.Stats)
|
v1.GET("/stats", apiV1.Stats)
|
||||||
|
v1.GET("/events", apiV1.Events)
|
||||||
|
|
||||||
route := v1.Group("/route")
|
route := v1.Group("/route")
|
||||||
{
|
{
|
||||||
@@ -85,6 +87,8 @@ func NewHandler() *gin.Engine {
|
|||||||
route.GET("/providers", routeApi.Providers)
|
route.GET("/providers", routeApi.Providers)
|
||||||
route.GET("/by_provider", routeApi.ByProvider)
|
route.GET("/by_provider", routeApi.ByProvider)
|
||||||
route.POST("/playground", routeApi.Playground)
|
route.POST("/playground", routeApi.Playground)
|
||||||
|
route.GET("/validate", routeApi.Validate) // websocket
|
||||||
|
route.POST("/validate", routeApi.Validate)
|
||||||
}
|
}
|
||||||
|
|
||||||
file := v1.Group("/file")
|
file := v1.Group("/file")
|
||||||
@@ -140,6 +144,21 @@ func NewHandler() *gin.Engine {
|
|||||||
docker.POST("/start", dockerApi.Start)
|
docker.POST("/start", dockerApi.Start)
|
||||||
docker.POST("/stop", dockerApi.Stop)
|
docker.POST("/stop", dockerApi.Stop)
|
||||||
docker.POST("/restart", dockerApi.Restart)
|
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) {
|
return func(c *gin.Context) {
|
||||||
c.Next()
|
c.Next()
|
||||||
if len(c.Errors) > 0 {
|
if len(c.Errors) > 0 {
|
||||||
logger := log.With().Str("uri", c.Request.RequestURI).Logger()
|
|
||||||
for _, err := range c.Errors {
|
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() {
|
if !c.IsWebsocket() {
|
||||||
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
||||||
|
|||||||
199
internal/api/v1/README.md
Normal file
199
internal/api/v1/README.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# internal/api/v1
|
||||||
|
|
||||||
|
Implements the v1 REST API handlers for GoDoxy, exposing endpoints for managing routes, Docker containers, certificates, metrics, and system configuration.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `internal/api/v1` package implements the HTTP handlers that power GoDoxy's REST API. It uses the Gin web framework and provides endpoints for route management, container operations, certificate handling, system metrics, and configuration.
|
||||||
|
|
||||||
|
### Primary Consumers
|
||||||
|
|
||||||
|
- **WebUI**: The homepage dashboard and admin interface consume these endpoints
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Authentication and authorization logic (delegated to `internal/auth`)
|
||||||
|
- Route proxying and request handling (handled by `internal/route`)
|
||||||
|
- Docker container lifecycle management (delegated to `internal/docker`)
|
||||||
|
- Certificate issuance and storage (handled by `internal/autocert`)
|
||||||
|
|
||||||
|
### Stability
|
||||||
|
|
||||||
|
This package is stable. Public API endpoints follow semantic versioning for request/response contracts. Internal implementation may change between minor versions.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
### Exported Types
|
||||||
|
|
||||||
|
Types are defined in `goutils/apitypes`:
|
||||||
|
|
||||||
|
| Type | Purpose |
|
||||||
|
| -------------------------- | -------------------------------- |
|
||||||
|
| `apitypes.ErrorResponse` | Standard error response format |
|
||||||
|
| `apitypes.SuccessResponse` | Standard success response format |
|
||||||
|
|
||||||
|
### Handler Subpackages
|
||||||
|
|
||||||
|
| Package | Purpose |
|
||||||
|
| ---------- | ---------------------------------------------- |
|
||||||
|
| `route` | Route listing, details, and playground testing |
|
||||||
|
| `docker` | Docker container management and monitoring |
|
||||||
|
| `cert` | Certificate information and renewal |
|
||||||
|
| `metrics` | System metrics and uptime information |
|
||||||
|
| `homepage` | Homepage items and category management |
|
||||||
|
| `file` | Configuration file read/write operations |
|
||||||
|
| `auth` | Authentication and session management |
|
||||||
|
| `agent` | Remote agent creation and management |
|
||||||
|
| `proxmox` | Proxmox API management and monitoring |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Handler Organization
|
||||||
|
|
||||||
|
Package structure mirrors the API endpoint paths (e.g., `auth/login.go` handles `/auth/login`).
|
||||||
|
|
||||||
|
### Request Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant GinRouter
|
||||||
|
participant Handler
|
||||||
|
participant Service
|
||||||
|
participant Response
|
||||||
|
|
||||||
|
Client->>GinRouter: HTTP Request
|
||||||
|
GinRouter->>Handler: Route to handler
|
||||||
|
Handler->>Service: Call service layer
|
||||||
|
Service-->>Handler: Data or error
|
||||||
|
Handler->>Response: Format JSON response
|
||||||
|
Response-->>Client: JSON or redirect
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Surface
|
||||||
|
|
||||||
|
API listening address is configured with `GODOXY_API_ADDR` environment variable.
|
||||||
|
|
||||||
|
## Dependency and Integration Map
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
| `internal/proxmox` | Proxmox API management and monitoring |
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
|
||||||
|
| Package | Purpose |
|
||||||
|
| ------------------------------ | --------------------------- |
|
||||||
|
| `github.com/gin-gonic/gin` | HTTP routing and middleware |
|
||||||
|
| `github.com/gorilla/websocket` | WebSocket support |
|
||||||
|
| `github.com/moby/moby/client` | Docker API client |
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
Handlers log at `INFO` level for requests and `ERROR` level for failures. Logs include:
|
||||||
|
|
||||||
|
- Request path and method
|
||||||
|
- Response status code
|
||||||
|
- Error details (when applicable)
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
No dedicated metrics exposed by handlers. Request metrics collected by middleware.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- All endpoints (except `/api/v1/version`) require authentication
|
||||||
|
- Input validation using Gin binding tags
|
||||||
|
- Path traversal prevention in file operations
|
||||||
|
- WebSocket connections use same auth middleware as HTTP
|
||||||
|
|
||||||
|
## Failure Modes and Recovery
|
||||||
|
|
||||||
|
| Failure | Behavior |
|
||||||
|
| ----------------------------------- | ------------------------------------------ |
|
||||||
|
| Docker host unreachable | Returns partial results with errors logged |
|
||||||
|
| Certificate provider not configured | Returns 404 |
|
||||||
|
| Invalid request body | Returns 400 with error details |
|
||||||
|
| Authentication failure | Returns 302 redirect to login |
|
||||||
|
| Agent not found | Returns 404 |
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Listing All Routes via WebSocket
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func watchRoutes(provider string) error {
|
||||||
|
url := "ws://localhost:8888/api/v1/route/list"
|
||||||
|
if provider != "" {
|
||||||
|
url += "?provider=" + provider
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, message, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// message contains JSON array of routes
|
||||||
|
processRoutes(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Container Status
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
Server string `json:"server"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func listContainers() ([]Container, error) {
|
||||||
|
resp, err := http.Get("http://localhost:8888/api/v1/docker/containers")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var containers []Container
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return containers, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8888/health
|
||||||
|
```
|
||||||
|
|
||||||
|
)
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/godoxy/internal/agentpool"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ func Create(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port))
|
hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port))
|
||||||
if _, ok := agent.GetAgent(hostport); ok {
|
if _, ok := agentpool.Get(hostport); ok {
|
||||||
c.JSON(http.StatusConflict, apitypes.Error("agent already exists"))
|
c.JSON(http.StatusConflict, apitypes.Error("agent already exists"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/internal/agentpool"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
|
||||||
@@ -19,15 +19,15 @@ import (
|
|||||||
// @Tags agent,websocket
|
// @Tags agent,websocket
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} Agent
|
// @Success 200 {array} agent.AgentConfig
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
// @Router /agent/list [get]
|
// @Router /agent/list [get]
|
||||||
func List(c *gin.Context) {
|
func List(c *gin.Context) {
|
||||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||||
websocket.PeriodicWrite(c, 10*time.Second, func() (any, error) {
|
websocket.PeriodicWrite(c, 10*time.Second, func() (any, error) {
|
||||||
return agent.ListAgents(), nil
|
return agentpool.List(), nil
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, agent.ListAgents())
|
c.JSON(http.StatusOK, agentpool.List())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package agentapi
|
package agentapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -8,10 +10,10 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/agent/pkg/certs"
|
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||||
|
"github.com/yusing/godoxy/internal/agentpool"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/route/provider"
|
"github.com/yusing/godoxy/internal/route/provider"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type VerifyNewAgentRequest struct {
|
type VerifyNewAgentRequest struct {
|
||||||
@@ -35,6 +37,9 @@ type VerifyNewAgentRequest struct {
|
|||||||
// @Failure 500 {object} ErrorResponse
|
// @Failure 500 {object} ErrorResponse
|
||||||
// @Router /agent/verify [post]
|
// @Router /agent/verify [post]
|
||||||
func Verify(c *gin.Context) {
|
func Verify(c *gin.Context) {
|
||||||
|
// avoid timeout waiting for response headers
|
||||||
|
c.Status(http.StatusContinue)
|
||||||
|
|
||||||
var request VerifyNewAgentRequest
|
var request VerifyNewAgentRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
@@ -59,7 +64,7 @@ func Verify(c *gin.Context) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
return
|
return
|
||||||
@@ -79,35 +84,45 @@ func Verify(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
|
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
var errAgentAlreadyExists = errors.New("agent already exists")
|
||||||
cfgState := config.ActiveState.Load()
|
|
||||||
for _, a := range cfgState.Value().Providers.Agents {
|
|
||||||
if a.Addr == host {
|
|
||||||
return 0, gperr.New("agent already exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, error) {
|
||||||
var agentCfg agent.AgentConfig
|
var agentCfg agent.AgentConfig
|
||||||
agentCfg.Addr = host
|
agentCfg.Addr = host
|
||||||
agentCfg.Runtime = containerRuntime
|
agentCfg.Runtime = containerRuntime
|
||||||
|
|
||||||
err := agentCfg.StartWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
|
// check if agent host exists in the config
|
||||||
|
cfgState := config.ActiveState.Load()
|
||||||
|
for _, a := range cfgState.Value().Providers.Agents {
|
||||||
|
if a.Addr == host {
|
||||||
|
return 0, errAgentAlreadyExists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if agent host exists in the agent pool
|
||||||
|
if agentpool.Has(&agentCfg) {
|
||||||
|
return 0, errAgentAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
err := agentCfg.InitWithCerts(ctx, ca.Cert, client.Cert, client.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, gperr.Wrap(err, "failed to start agent")
|
return 0, fmt.Errorf("failed to initialize agent config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider := provider.NewAgentProvider(&agentCfg)
|
provider := provider.NewAgentProvider(&agentCfg)
|
||||||
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
|
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
|
// agent must be added before loading routes
|
||||||
agent.AddAgent(&agentCfg)
|
added := agentpool.Add(&agentCfg)
|
||||||
|
if !added {
|
||||||
|
return 0, errAgentAlreadyExists
|
||||||
|
}
|
||||||
err = provider.LoadRoutes()
|
err = provider.LoadRoutes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfgState.DeleteProvider(provider.String())
|
cfgState.DeleteProvider(provider.String())
|
||||||
agent.RemoveAgent(&agentCfg)
|
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
|
return provider.NumRoutes(), nil
|
||||||
|
|||||||
@@ -1,53 +1,42 @@
|
|||||||
package certapi
|
package certapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
|
autocertctx "github.com/yusing/godoxy/internal/autocert/types"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type 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
|
|
||||||
|
|
||||||
// @x-id "info"
|
// @x-id "info"
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
// @Summary Get cert info
|
// @Summary Get cert info
|
||||||
// @Description Get cert info
|
// @Description Get cert info
|
||||||
// @Tags cert
|
// @Tags cert
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} CertInfo
|
// @Success 200 {array} autocert.CertInfo
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||||
// @Failure 404 {object} apitypes.ErrorResponse
|
// @Failure 404 {object} apitypes.ErrorResponse "No certificates found or autocert is not enabled"
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
// @Router /cert/info [get]
|
// @Router /cert/info [get]
|
||||||
func Info(c *gin.Context) {
|
func Info(c *gin.Context) {
|
||||||
autocert := autocert.ActiveProvider.Load()
|
provider := autocertctx.FromCtx(c.Request.Context())
|
||||||
if autocert == nil {
|
if provider == nil {
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := autocert.GetCert(nil)
|
certInfos, err := provider.GetCertInfos()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, autocert.ErrNoCertificates) {
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("no certificate found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to get cert info"))
|
c.Error(apitypes.InternalServerError(err, "failed to get cert info"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
certInfo := CertInfo{
|
c.JSON(http.StatusOK, certInfos)
|
||||||
Subject: cert.Leaf.Subject.CommonName,
|
|
||||||
Issuer: cert.Leaf.Issuer.CommonName,
|
|
||||||
NotBefore: cert.Leaf.NotBefore.Unix(),
|
|
||||||
NotAfter: cert.Leaf.NotAfter.Unix(),
|
|
||||||
DNSNames: cert.Leaf.DNSNames,
|
|
||||||
EmailAddresses: cert.Leaf.EmailAddresses,
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, certInfo)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"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"
|
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,8 +23,8 @@ import (
|
|||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /cert/renew [get]
|
// @Router /cert/renew [get]
|
||||||
func Renew(c *gin.Context) {
|
func Renew(c *gin.Context) {
|
||||||
autocert := autocert.ActiveProvider.Load()
|
provider := autocertctx.FromCtx(c.Request.Context())
|
||||||
if autocert == nil {
|
if provider == nil {
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -40,33 +39,33 @@ func Renew(c *gin.Context) {
|
|||||||
logs, cancel := memlogger.Events()
|
logs, cancel := memlogger.Events()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(done)
|
// Stream logs until WebSocket connection closes (renewal runs in background)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-manager.Context().Done():
|
||||||
|
return
|
||||||
|
case l := <-logs:
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = autocert.ObtainCert()
|
err = manager.WriteData(websocket.TextMessage, l, 10*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gperr.LogError("failed to obtain cert", err)
|
return
|
||||||
_ = manager.WriteData(websocket.TextMessage, []byte(err.Error()), 10*time.Second)
|
}
|
||||||
} else {
|
}
|
||||||
log.Info().Msg("cert obtained successfully")
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
// renewal happens in background
|
||||||
select {
|
ok := provider.ForceExpiryAll()
|
||||||
case l := <-logs:
|
if !ok {
|
||||||
if err != nil {
|
log.Error().Msg("cert renewal already in progress")
|
||||||
return
|
time.Sleep(1 * time.Second) // wait for the log above to be sent
|
||||||
}
|
return
|
||||||
|
|
||||||
err = manager.WriteData(websocket.TextMessage, l, 10*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
log.Info().Msg("cert force renewal requested")
|
||||||
|
|
||||||
|
provider.WaitRenewalDone(manager.Context())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/moby/moby/client"
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
@@ -28,36 +29,36 @@ func GetContainer(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||||
if !ok {
|
if !ok {
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := docker.NewClient(dockerHost)
|
dockerClient, err := docker.NewClient(dockerCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer client.Close()
|
defer dockerClient.Close()
|
||||||
|
|
||||||
cont, err := client.ContainerInspect(c.Request.Context(), id)
|
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id, client.ContainerInspectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var state ContainerState
|
var state ContainerState
|
||||||
if cont.State != nil {
|
if cont.Container.State != nil {
|
||||||
state = cont.State.Status
|
state = cont.Container.State.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &Container{
|
c.JSON(http.StatusOK, &Container{
|
||||||
Server: dockerHost,
|
Server: dockerCfg.URL,
|
||||||
Name: cont.Name,
|
Name: cont.Container.Name,
|
||||||
ID: cont.ID,
|
ID: cont.Container.ID,
|
||||||
Image: cont.Image,
|
Image: cont.Container.Image,
|
||||||
State: state,
|
State: state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/gin-gonic/gin"
|
"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"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
|
||||||
_ "github.com/yusing/goutils/apitypes"
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
@@ -35,18 +37,18 @@ func Containers(c *gin.Context) {
|
|||||||
serveHTTP[Container](c, GetContainers)
|
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")
|
errs := gperr.NewBuilder("failed to get containers")
|
||||||
containers := make([]Container, 0)
|
containers := make([]Container, 0)
|
||||||
for server, dockerClient := range dockerClients {
|
for name, dockerClient := range dockerClients {
|
||||||
conts, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Add(err)
|
errs.AddSubject(err, name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, cont := range conts {
|
for _, cont := range conts.Items {
|
||||||
containers = append(containers, Container{
|
containers = append(containers, Container{
|
||||||
Server: server,
|
Server: name,
|
||||||
Name: cont.Names[0],
|
Name: cont.Names[0],
|
||||||
ID: cont.ID,
|
ID: cont.ID,
|
||||||
Image: cont.Image,
|
Image: cont.Image,
|
||||||
@@ -58,11 +60,10 @@ func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Containe
|
|||||||
return containers[i].Name < containers[j].Name
|
return containers[i].Name < containers[j].Name
|
||||||
})
|
})
|
||||||
if err := errs.Error(); err != nil {
|
if err := errs.Error(); err != nil {
|
||||||
gperr.LogError("failed to get containers", err)
|
if len(containers) > 0 {
|
||||||
if len(containers) == 0 {
|
log.Err(err).Msg("failed to get containers from some servers")
|
||||||
return nil, err
|
return containers, nil
|
||||||
}
|
}
|
||||||
return containers, nil
|
|
||||||
}
|
}
|
||||||
return containers, nil
|
return containers, errs.Error()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
dockerSystem "github.com/docker/docker/api/types/system"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
dockerSystem "github.com/moby/moby/api/types/system"
|
||||||
|
"github.com/moby/moby/client"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
|
|
||||||
@@ -58,19 +59,19 @@ func Info(c *gin.Context) {
|
|||||||
serveHTTP[dockerInfo](c, GetDockerInfo)
|
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")
|
errs := gperr.NewBuilder("failed to get docker info")
|
||||||
dockerInfos := make([]dockerInfo, len(dockerClients))
|
dockerInfos := make([]dockerInfo, len(dockerClients))
|
||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
for name, dockerClient := range dockerClients {
|
for name, dockerClient := range dockerClients {
|
||||||
info, err := dockerClient.Info(ctx)
|
info, err := dockerClient.Info(ctx, client.InfoOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Add(err)
|
errs.AddSubject(err, name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info.Name = name
|
info.Info.Name = name
|
||||||
dockerInfos[i] = toDockerInfo(info)
|
dockerInfos[i] = toDockerInfo(info.Info)
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/moby/moby/api/pkg/stdcopy"
|
||||||
|
"github.com/moby/moby/client"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
@@ -22,6 +23,7 @@ type LogsQueryParams struct {
|
|||||||
Since string `form:"from"`
|
Since string `form:"from"`
|
||||||
Until string `form:"to"`
|
Until string `form:"to"`
|
||||||
Levels string `form:"levels"`
|
Levels string `form:"levels"`
|
||||||
|
Limit int `form:"limit,default=100" binding:"min=1,max=1000"`
|
||||||
} // @name LogsQueryParams
|
} // @name LogsQueryParams
|
||||||
|
|
||||||
// @x-id "logs"
|
// @x-id "logs"
|
||||||
@@ -34,9 +36,10 @@ type LogsQueryParams struct {
|
|||||||
// @Param id path string true "container id"
|
// @Param id path string true "container id"
|
||||||
// @Param stdout query bool false "show stdout"
|
// @Param stdout query bool false "show stdout"
|
||||||
// @Param stderr query bool false "show stderr"
|
// @Param stderr query bool false "show stderr"
|
||||||
// @Param from query string false "from timestamp"
|
// @Param from query string false "from timestamp"
|
||||||
// @Param to query string false "to timestamp"
|
// @Param to query string false "to timestamp"
|
||||||
// @Param levels query string false "levels"
|
// @Param levels query string false "levels"
|
||||||
|
// @Param limit query int false "limit"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse
|
// @Failure 400 {object} apitypes.ErrorResponse
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
@@ -57,27 +60,27 @@ func Logs(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement levels
|
// TODO: implement levels
|
||||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||||
if !ok {
|
if !ok {
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id)))
|
c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerClient, err := docker.NewClient(dockerHost)
|
dockerClient, err := docker.NewClient(dockerCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to get docker client"))
|
c.Error(apitypes.InternalServerError(err, "failed to get docker client"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer dockerClient.Close()
|
defer dockerClient.Close()
|
||||||
|
|
||||||
opts := container.LogsOptions{
|
opts := client.ContainerLogsOptions{
|
||||||
ShowStdout: queryParams.Stdout,
|
ShowStdout: queryParams.Stdout,
|
||||||
ShowStderr: queryParams.Stderr,
|
ShowStderr: queryParams.Stderr,
|
||||||
Since: queryParams.Since,
|
Since: queryParams.Since,
|
||||||
Until: queryParams.Until,
|
Until: queryParams.Until,
|
||||||
Timestamps: true,
|
Timestamps: true,
|
||||||
Follow: true,
|
Follow: true,
|
||||||
Tail: "100",
|
Tail: strconv.Itoa(queryParams.Limit),
|
||||||
}
|
}
|
||||||
if queryParams.Levels != "" {
|
if queryParams.Levels != "" {
|
||||||
opts.Details = true
|
opts.Details = true
|
||||||
@@ -105,7 +108,7 @@ func Logs(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Err(err).
|
log.Err(err).
|
||||||
Str("server", dockerHost).
|
Str("server", dockerCfg.URL).
|
||||||
Str("container", id).
|
Str("container", id).
|
||||||
Msg("failed to de-multiplex logs")
|
Msg("failed to de-multiplex logs")
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user