mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 21:10:30 +01:00
Compare commits
909 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a999c51bf8 | ||
|
|
7ca722b256 | ||
|
|
51295be463 | ||
|
|
51fc5f017a | ||
|
|
e4996733fc | ||
|
|
f76d86dfa2 | ||
|
|
8778f4ea73 | ||
|
|
6f75bb7593 | ||
|
|
964ba1eac1 | ||
|
|
6e7b571946 | ||
|
|
fc7a81faf5 | ||
|
|
488ad160e7 | ||
|
|
1ec2872f3d | ||
|
|
9c3346dd9d | ||
|
|
203faa8e7e | ||
|
|
fbc853fa6a | ||
|
|
3fefbdfded | ||
|
|
48be6def12 | ||
|
|
94d6b7a168 | ||
|
|
1ca4b4939e | ||
|
|
f8716d990e | ||
|
|
5a91db8d10 | ||
|
|
3e73be60a1 | ||
|
|
af9363209b | ||
|
|
ccc35b2a00 | ||
|
|
44536139c1 | ||
|
|
2b4c39a79e | ||
|
|
ddf78aacba | ||
|
|
f5a006ce81 | ||
|
|
290af4e311 | ||
|
|
feafdf05f2 | ||
|
|
b09bfd6c1e | ||
|
|
e13b18621d | ||
|
|
53f3397b7a | ||
|
|
19968834d2 | ||
|
|
d41c6f8d77 | ||
|
|
dcc5ab8952 | ||
|
|
cc8858332d | ||
|
|
82f02ea2bf | ||
|
|
046ff8a020 | ||
|
|
dc9ae32e8f | ||
|
|
5640d5d454 | ||
|
|
c66de99fcb | ||
|
|
eef994082c | ||
|
|
8c670ab92e | ||
|
|
d11ddb7c91 | ||
|
|
78aea4b4d2 | ||
|
|
80dd142861 | ||
|
|
92aa61e732 | ||
|
|
848f26aa86 | ||
|
|
81e500fcfc | ||
|
|
f417e0fa25 | ||
|
|
cb5a8e7b9d | ||
|
|
16cad11e89 | ||
|
|
2bfbdbf519 | ||
|
|
d5e9a7b3b6 | ||
|
|
7ea415078f | ||
|
|
e67704695b | ||
|
|
804c7eec60 | ||
|
|
ea8be56bf8 | ||
|
|
20c77edce5 | ||
|
|
4f2f0f58e2 | ||
|
|
ac8ad149b8 | ||
|
|
14ec80c883 | ||
|
|
5de5f854ce | ||
|
|
3d8994b42e | ||
|
|
66043e4a26 | ||
|
|
d1e403e16f | ||
|
|
e72e20af69 | ||
|
|
ad6201c27a | ||
|
|
c4c9e9300c | ||
|
|
b23c3f1c3b | ||
|
|
38c0419483 | ||
|
|
357ce38b18 | ||
|
|
ef34c3ffdd | ||
|
|
2e411373a2 | ||
|
|
3dedd66ad1 | ||
|
|
98f047d88a | ||
|
|
973a58e982 | ||
|
|
4b55d1c607 | ||
|
|
63eff4707c | ||
|
|
55a74c36b0 | ||
|
|
fbabb7b7fb | ||
|
|
7a1841e9a5 | ||
|
|
d82bfd0ebd | ||
|
|
1f41c035ea | ||
|
|
c2c9f42fb3 | ||
|
|
60cfff3435 | ||
|
|
c93a460043 | ||
|
|
9bf7a0beef | ||
|
|
c89c737ecd | ||
|
|
382fc61a9c | ||
|
|
b2de33e835 | ||
|
|
86644054e6 | ||
|
|
c2dcabe144 | ||
|
|
c59ddc1df6 | ||
|
|
f4db874fd6 | ||
|
|
f334f5c13c | ||
|
|
5acc4c3894 | ||
|
|
a8aa82f687 | ||
|
|
0f3a1ac6e6 | ||
|
|
9fceda6729 | ||
|
|
becb49e864 | ||
|
|
3aed41e078 | ||
|
|
8047067b2b | ||
|
|
c3fa7c66a7 | ||
|
|
cab68807ee | ||
|
|
d08be872a0 | ||
|
|
bb5f0cdf09 | ||
|
|
a150f1a628 | ||
|
|
584db2efce | ||
|
|
c27bc0e129 | ||
|
|
b46b464e65 | ||
|
|
52ec309f6b | ||
|
|
6051f75145 | ||
|
|
f4f104d206 | ||
|
|
448a2fbd6f | ||
|
|
74224c8e87 | ||
|
|
ae57edfcb0 | ||
|
|
fc23e262d7 | ||
|
|
11a3935e0c | ||
|
|
42e7adbf86 | ||
|
|
1e0c7a15d8 | ||
|
|
ba8edb160f | ||
|
|
4852efcf9c | ||
|
|
ef40793301 | ||
|
|
80862bcd2e | ||
|
|
45b16abd68 | ||
|
|
f411e17d80 | ||
|
|
024100aa8c | ||
|
|
9d508c5950 | ||
|
|
2ff5e5c0b6 | ||
|
|
2a05c6a630 | ||
|
|
6776f20332 | ||
|
|
5043ef778f | ||
|
|
22bcf1201b | ||
|
|
acecd827d6 | ||
|
|
b2713a4b83 | ||
|
|
e2aeef3a86 | ||
|
|
9545482a44 | ||
|
|
d406b940d9 | ||
|
|
dc1175ad69 | ||
|
|
1409a4e8b9 | ||
|
|
8ec9752656 | ||
|
|
a932688ca3 | ||
|
|
55c1c918ba | ||
|
|
14e243d245 | ||
|
|
f7149453d6 | ||
|
|
00d137d05c | ||
|
|
f9affba9fc | ||
|
|
6b3bf84148 | ||
|
|
62a667758d | ||
|
|
ddd27156fc | ||
|
|
af8e2d56b2 | ||
|
|
74a215b894 | ||
|
|
ccdc0046fd | ||
|
|
2f7fdc4c51 | ||
|
|
de1f4da126 | ||
|
|
a48ccb4423 | ||
|
|
193fd9a249 | ||
|
|
0bc4c4af77 | ||
|
|
5fa1417add | ||
|
|
b763c92645 | ||
|
|
09b14a47e9 | ||
|
|
83a69322fa | ||
|
|
3aba5a1911 | ||
|
|
ca805edfe0 | ||
|
|
7205bf47de | ||
|
|
b12999210f | ||
|
|
8b8969f033 | ||
|
|
025ebab1ce | ||
|
|
ea7bd0d19a | ||
|
|
f889f5c08d | ||
|
|
932c20f32d | ||
|
|
2a08c55e39 | ||
|
|
93e1d17090 | ||
|
|
d72d403e2c | ||
|
|
b5d70a0592 | ||
|
|
da71dcf058 | ||
|
|
6b17272347 | ||
|
|
98afb02e7f | ||
|
|
103fd3b904 | ||
|
|
59917f52d7 | ||
|
|
24fb2e07e6 | ||
|
|
8f1c02ca72 | ||
|
|
e359bc8fd9 | ||
|
|
7b028adaa9 | ||
|
|
f3913e1f6f | ||
|
|
b72f3bde53 | ||
|
|
6077a1d70b | ||
|
|
59cae0967a | ||
|
|
1e1999b0af | ||
|
|
b64725f2f8 | ||
|
|
124069aaa4 | ||
|
|
d56663d3f9 | ||
|
|
d1476edf91 | ||
|
|
4ed6c7c74d | ||
|
|
f31b1b5ed3 | ||
|
|
e0d25e475c | ||
|
|
ef65481394 | ||
|
|
1e9303b1ef | ||
|
|
2c290a3916 | ||
|
|
58a2dc73dd | ||
|
|
1c080e067d | ||
|
|
2717dc963a | ||
|
|
4509622dde | ||
|
|
60c13a797b | ||
|
|
5e1da915dc | ||
|
|
3288624cf2 | ||
|
|
190d5e1ece | ||
|
|
0d2229cca0 | ||
|
|
493c0afdfa | ||
|
|
99c1922342 | ||
|
|
a483e15a20 | ||
|
|
fbe82c3082 | ||
|
|
24bcc2d2d2 | ||
|
|
d8c8cff8b7 | ||
|
|
ef54d336a2 | ||
|
|
0a5df1bd7f | ||
|
|
205928a741 | ||
|
|
11d18091fd | ||
|
|
3be72e5c68 | ||
|
|
a9847b6f81 | ||
|
|
04d823d616 | ||
|
|
1be2ea44a2 | ||
|
|
978407ae7e | ||
|
|
81f8bad77d | ||
|
|
f7de703c15 | ||
|
|
acf7490991 | ||
|
|
7770ce7025 | ||
|
|
c9c5677b35 | ||
|
|
226ee2e5e5 | ||
|
|
aec937a114 | ||
|
|
bab9471bde | ||
|
|
4ebd1dbf32 | ||
|
|
82a4a61df0 | ||
|
|
9e56ea5db1 | ||
|
|
719682c99f | ||
|
|
f81a2b6607 | ||
|
|
f47ba0a9b5 | ||
|
|
52e949de85 | ||
|
|
abeb26b556 | ||
|
|
23d392d88b | ||
|
|
d588664bfa | ||
|
|
41ce784a7f | ||
|
|
577169d03c | ||
|
|
b43274e9e6 | ||
|
|
d83c367e7f | ||
|
|
d9fbd53870 | ||
|
|
7f54f50af8 | ||
|
|
8339c42470 | ||
|
|
ed39942d65 | ||
|
|
998488f285 | ||
|
|
aac5016b78 | ||
|
|
d2b4d3e6e3 | ||
|
|
a2d4c468cd | ||
|
|
c550255458 | ||
|
|
6a3e28dfd7 | ||
|
|
4513c221d5 | ||
|
|
245dba034e | ||
|
|
f39896fe30 | ||
|
|
b051987a1c | ||
|
|
c128557c81 | ||
|
|
6405325e56 | ||
|
|
c3d2a90501 | ||
|
|
31d49453a7 | ||
|
|
04657420b8 | ||
|
|
2f0b8b6c09 | ||
|
|
5e15fd4bbe | ||
|
|
a5022e31a2 | ||
|
|
a057f0e956 | ||
|
|
dfe0014609 | ||
|
|
dfc2d5e35c | ||
|
|
d3bfb2488b | ||
|
|
baf5b5eff1 | ||
|
|
1c7e3e42f8 | ||
|
|
beb1913285 | ||
|
|
e14d6baedb | ||
|
|
cfb37d5bd0 | ||
|
|
f53d384533 | ||
|
|
8360aa59d1 | ||
|
|
6ec1016f29 | ||
|
|
35b0dcb418 | ||
|
|
353f818b41 | ||
|
|
b58cabf998 | ||
|
|
231c0c7665 | ||
|
|
9931c10fa6 | ||
|
|
d56a6bc19d | ||
|
|
e0a110cad3 | ||
|
|
d1eb3470b5 | ||
|
|
e52c86e0b7 | ||
|
|
c19d82c876 | ||
|
|
d2f317b44d | ||
|
|
ba9cb083cf | ||
|
|
06669534cd | ||
|
|
07d6f36159 | ||
|
|
55018c8ab6 | ||
|
|
0862920324 | ||
|
|
b32750d545 | ||
|
|
a836920eca | ||
|
|
6b89cd9106 | ||
|
|
11af9d107a | ||
|
|
7a9b8b3fb9 | ||
|
|
90efa36193 | ||
|
|
1e78a0a0a0 | ||
|
|
52324fbef2 | ||
|
|
8b40baa49f | ||
|
|
35a3e3fef6 | ||
|
|
fce9ce21c9 | ||
|
|
475e697490 | ||
|
|
68ac4f952d | ||
|
|
a2e6688056 | ||
|
|
e02cacdf2a | ||
|
|
46c7ee4d84 | ||
|
|
f39513483b | ||
|
|
731121595c | ||
|
|
8025af6067 | ||
|
|
47910774dd | ||
|
|
b6bfd19cc2 | ||
|
|
e3b53a548d | ||
|
|
a954ac8946 | ||
|
|
814ff33352 | ||
|
|
b1d5c4b091 | ||
|
|
72dc783e23 | ||
|
|
1c95bbba6e | ||
|
|
0c552c9cea | ||
|
|
5631b1540a | ||
|
|
24f949f053 | ||
|
|
9d712b91ff | ||
|
|
4189ffa1db | ||
|
|
e906b358fa | ||
|
|
f179de9231 | ||
|
|
1d546624de | ||
|
|
ecc9d306d1 | ||
|
|
5ce1c7865e | ||
|
|
7d17a01de1 | ||
|
|
cabb840a91 | ||
|
|
4825f768f3 | ||
|
|
5fdb023188 | ||
|
|
4abf61a421 | ||
|
|
96b7c3fcec | ||
|
|
f8c57d930f | ||
|
|
880d66c75e | ||
|
|
4649c8d479 | ||
|
|
20021b3cae | ||
|
|
cfa9201f82 | ||
|
|
b5328fe5e7 | ||
|
|
25fbcc4ab9 | ||
|
|
421aaecba4 | ||
|
|
01773976d1 | ||
|
|
2263d6063e | ||
|
|
cfe0f6bb70 | ||
|
|
a90d2b90d1 | ||
|
|
af9629424e | ||
|
|
ee6cf29bc1 | ||
|
|
c4a780e061 | ||
|
|
09c244ef3c | ||
|
|
bd0fe36c53 | ||
|
|
d240da4393 | ||
|
|
9470a14fe8 | ||
|
|
d3568d9c35 | ||
|
|
44ef351840 | ||
|
|
a39d527fc1 | ||
|
|
22ab043e06 | ||
|
|
b670cdbd49 | ||
|
|
45e34d691a | ||
|
|
e82480a639 | ||
|
|
e39407886d | ||
|
|
3135e377a9 | ||
|
|
bdb3343a7c | ||
|
|
b411c6d504 | ||
|
|
966a59b5c9 | ||
|
|
58db228e25 | ||
|
|
e737737415 | ||
|
|
9087c4f195 | ||
|
|
4705989f4b | ||
|
|
cb506120dd | ||
|
|
88aaf956e5 | ||
|
|
ecfd018b0b | ||
|
|
54bf84dcba | ||
|
|
57200bc1e9 | ||
|
|
6f9bb410f5 | ||
|
|
e62e667b49 | ||
|
|
abe81541db | ||
|
|
9e5d33714c | ||
|
|
93a81fd558 | ||
|
|
72923b8cfa | ||
|
|
24ba4c2a46 | ||
|
|
ed07bf42ce | ||
|
|
371e756307 | ||
|
|
32d8292b17 | ||
|
|
717fd0e58c | ||
|
|
2628d9e8a8 | ||
|
|
c90795e614 | ||
|
|
4a6bed7728 | ||
|
|
216c03c5ff | ||
|
|
2e9f113224 | ||
|
|
9d58977fa6 | ||
|
|
8469b6406c | ||
|
|
b163771956 | ||
|
|
c1221e61d4 | ||
|
|
4a8bd48ad5 | ||
|
|
ade93d49a3 | ||
|
|
82ee75daab | ||
|
|
f0ab14cb1e | ||
|
|
5b7c392297 | ||
|
|
1f1ae38e4d | ||
|
|
22d44a6bb0 | ||
|
|
6a5cd1266b | ||
|
|
1cf18657b6 | ||
|
|
63c4bdc73d | ||
|
|
20a1649275 | ||
|
|
0f3b8e68ce | ||
|
|
5a3e3f19c7 | ||
|
|
df193a42fc | ||
|
|
f1e204f7fd | ||
|
|
ff08c40403 | ||
|
|
d8266f779f | ||
|
|
9711867fbe | ||
|
|
fc8592ab45 | ||
|
|
3dbab118af | ||
|
|
1f50ee7f2f | ||
|
|
cee6eaecff | ||
|
|
67a6b89ea5 | ||
|
|
78be9b1c71 | ||
|
|
26856b612a | ||
|
|
36ceba3ae7 | ||
|
|
f45f3fba79 | ||
|
|
4bbff323e3 | ||
|
|
2e68baa93e | ||
|
|
a162371ec5 | ||
|
|
8f9c76daa5 | ||
|
|
8b3e058885 | ||
|
|
023cbc81bc | ||
|
|
b490e8c475 | ||
|
|
8e27886235 | ||
|
|
7435b8e485 | ||
|
|
21724c037f | ||
|
|
44b4cff35e | ||
|
|
1e24765b17 | ||
|
|
a1f2a84a16 | ||
|
|
453262832a | ||
|
|
99e975145c | ||
|
|
e300170c51 | ||
|
|
1382137f20 | ||
|
|
54d7508f5d | ||
|
|
71ca8c738e | ||
|
|
f1eefde964 | ||
|
|
84e7a6591e | ||
|
|
30c76cfc5f | ||
|
|
a8ba42e360 | ||
|
|
cd291556fc | ||
|
|
0d41809630 | ||
|
|
53acf75c04 | ||
|
|
cf30fe6cfc | ||
|
|
55bbcae911 | ||
|
|
b30c0d7dc0 | ||
|
|
198ae2cd02 | ||
|
|
26938eb6ed | ||
|
|
48823a860f | ||
|
|
985ff0a74d | ||
|
|
43b493c60e | ||
|
|
e0e0fab127 | ||
|
|
fc0dbd940c | ||
|
|
0208e6286f | ||
|
|
2c0b68c8c2 | ||
|
|
c05059765d | ||
|
|
a06787593c | ||
|
|
8fe94d6d14 | ||
|
|
4ddfb48b9d | ||
|
|
31dc112591 | ||
|
|
6797897814 | ||
|
|
99eccd0b95 | ||
|
|
0387739b94 | ||
|
|
ead27c72f1 | ||
|
|
455a85e6a0 | ||
|
|
8424fd9f1a | ||
|
|
75ee0e63bd | ||
|
|
1ce607029a | ||
|
|
1e80ad2a44 | ||
|
|
4daefa19d1 | ||
|
|
491231e439 | ||
|
|
c90ec8caa1 | ||
|
|
9eb674029e | ||
|
|
e41c6530ab | ||
|
|
afd35c183d | ||
|
|
f190483b4e | ||
|
|
7b0ed09772 | ||
|
|
4415bffc35 | ||
|
|
ddab2766b4 | ||
|
|
ef95682116 | ||
|
|
dd65a8d04b | ||
|
|
aa23b5b595 | ||
|
|
c55c6c84bc | ||
|
|
a45e5e17db | ||
|
|
b8c0961de3 | ||
|
|
62d3d200e6 | ||
|
|
bf32cafd90 | ||
|
|
1c182b5a7d | ||
|
|
ad60f377ba | ||
|
|
75db09b1f3 | ||
|
|
6dd849f480 | ||
|
|
e2ae29795d | ||
|
|
92fa0f8168 | ||
|
|
b090598b68 | ||
|
|
2cec88d3ce | ||
|
|
4df31263b5 | ||
|
|
9eae809690 | ||
|
|
f1ba554a24 | ||
|
|
f9a8aede20 | ||
|
|
e275ee634c | ||
|
|
797d88772f | ||
|
|
8ef8015a7f | ||
|
|
5fce4b445b | ||
|
|
7552a706a7 | ||
|
|
e1bc6d1f44 | ||
|
|
56850a9580 | ||
|
|
5f780f4902 | ||
|
|
ccb4639f43 | ||
|
|
ac1470d81d | ||
|
|
efaabfa63a | ||
|
|
9043cf25c5 | ||
|
|
98e90d7a0b | ||
|
|
82c829de18 | ||
|
|
2fe4fef779 | ||
|
|
91302ceed7 | ||
|
|
7fa7b55b18 | ||
|
|
69ee8495d8 | ||
|
|
28d9a72908 | ||
|
|
770c698332 | ||
|
|
cd4c843025 | ||
|
|
f0cf89060b | ||
|
|
f79a15bac6 | ||
|
|
2b4a70a550 | ||
|
|
f06741428c | ||
|
|
16e6e72454 | ||
|
|
100d2c392f | ||
|
|
829eb08e37 | ||
|
|
53d54a09b0 | ||
|
|
62c551c7fe | ||
|
|
80e59bb481 | ||
|
|
7a5afc3612 | ||
|
|
2c0349c11c | ||
|
|
8e3c2cc8d4 | ||
|
|
d35afdb3c9 | ||
|
|
ae093ebf40 | ||
|
|
aa8af4185b | ||
|
|
0029cf69d6 | ||
|
|
33e400a17e | ||
|
|
1d22bcfed9 | ||
|
|
978d82060e | ||
|
|
7aa1215491 | ||
|
|
0b69589586 | ||
|
|
bca3cd84d1 | ||
|
|
ce4bf2f646 | ||
|
|
c49016f22c | ||
|
|
8da63daf02 | ||
|
|
c5fd21552e | ||
|
|
27409abc24 | ||
|
|
21c9e46274 | ||
|
|
22a12d3116 | ||
|
|
89d93dd878 | ||
|
|
66853dfc52 | ||
|
|
c72f66d64b | ||
|
|
59bc342a40 | ||
|
|
e11579df10 | ||
|
|
6a8f6fb4b5 | ||
|
|
8f20bd3840 | ||
|
|
f1abb745fe | ||
|
|
cb2990f6e8 | ||
|
|
fb2f850311 | ||
|
|
2b9c0f09ee | ||
|
|
efe3eb4ce7 | ||
|
|
a1c1a79976 | ||
|
|
90ba355d16 | ||
|
|
01179adfa8 | ||
|
|
e4be403bef | ||
|
|
e1cdf4da0f | ||
|
|
5148cb3b8b | ||
|
|
56c6a9f8fe | ||
|
|
be257b0532 | ||
|
|
0534bc38b2 | ||
|
|
604e2481a6 | ||
|
|
4f557043a5 | ||
|
|
03d609e4e1 | ||
|
|
db6fc65876 | ||
|
|
c6a05f7b35 | ||
|
|
9e4aa32120 | ||
|
|
759995972d | ||
|
|
03401488f6 | ||
|
|
1e790be70c | ||
|
|
4410637f8b | ||
|
|
3947152336 | ||
|
|
af8d2c74f6 | ||
|
|
e107f8d476 | ||
|
|
b427ff1f88 | ||
|
|
e513db62b0 | ||
|
|
2f33ee02d9 | ||
|
|
59490dcac0 | ||
|
|
5afa93a8f1 | ||
|
|
c8e9ed8440 | ||
|
|
8363dfe257 | ||
|
|
080bbc18eb | ||
|
|
1a0edc8bfe | ||
|
|
e8d1d524b9 | ||
|
|
edada22ac0 | ||
|
|
76fb0cfdbb | ||
|
|
5df2553774 | ||
|
|
31812430f1 | ||
|
|
d668b03175 | ||
|
|
663a107c06 | ||
|
|
806184e98b | ||
|
|
08ee82d7b0 | ||
|
|
bcc19167d4 | ||
|
|
858f65ee5a | ||
|
|
43566bbcfd | ||
|
|
ec8cca1245 | ||
|
|
4a65de99a8 | ||
|
|
7461344004 | ||
|
|
b815c6fd69 | ||
|
|
28c9a2e9d0 | ||
|
|
9e0bdd964c | ||
|
|
077641beaa | ||
|
|
ef483403da | ||
|
|
0a8aa2b215 | ||
|
|
5a984f5c0c | ||
|
|
d60688c66f | ||
|
|
23482da259 | ||
|
|
62776229cb | ||
|
|
36fab0cd50 | ||
|
|
8f03662982 | ||
|
|
aad44031c4 | ||
|
|
51813e6030 | ||
|
|
f661907268 | ||
|
|
be85633c32 | ||
|
|
392946fe33 | ||
|
|
671024965f | ||
|
|
8d9aef3cd5 | ||
|
|
b5b4f0453a | ||
|
|
8ca6ac2752 | ||
|
|
27f7e08e18 | ||
|
|
f3e08dc9ea | ||
|
|
e3797ea96b | ||
|
|
146e7781be | ||
|
|
d2e2086540 | ||
|
|
d105f866ff | ||
|
|
1c001ed9df | ||
|
|
366c89164f | ||
|
|
36f13c61bb | ||
|
|
c8935102c3 | ||
|
|
a9e4f82e30 | ||
|
|
f966ca8b83 | ||
|
|
2da7ea56d5 | ||
|
|
232f720e77 | ||
|
|
2f476603d3 | ||
|
|
366fede517 | ||
|
|
7ef8354eb0 | ||
|
|
fbb07011f1 | ||
|
|
a7da8ffb90 | ||
|
|
95fe294f7d | ||
|
|
cdb3ffe439 | ||
|
|
7707fc6f36 | ||
|
|
765328affb | ||
|
|
3c515b0258 | ||
|
|
c6f65ba69f | ||
|
|
8c9a2b022b | ||
|
|
2e8248cd5b | ||
|
|
2b91d99ec6 | ||
|
|
f7688a942a | ||
|
|
574056a7e3 | ||
|
|
84e8dc0e06 | ||
|
|
fb8ce6c878 | ||
|
|
d961c11eb7 | ||
|
|
90f8e82f14 | ||
|
|
14bb66d12f | ||
|
|
7093985b57 | ||
|
|
a557684542 | ||
|
|
b0876331e6 | ||
|
|
cba7338d8d | ||
|
|
f72d9aee80 | ||
|
|
480fb4818c | ||
|
|
78a3c8a8e4 | ||
|
|
9cb7cc84ee | ||
|
|
2f24a1db41 | ||
|
|
4a2cc70b52 | ||
|
|
3021672de5 | ||
|
|
5d2df3550b | ||
|
|
c0c6e21a16 | ||
|
|
8c03c5e82e | ||
|
|
dfd2f3962c | ||
|
|
d315710310 | ||
|
|
3424cc4e51 | ||
|
|
361931ed96 | ||
|
|
e4f6994dfc | ||
|
|
827a27911c | ||
|
|
1e39d0b186 | ||
|
|
fd223c7542 | ||
|
|
40aa937f54 | ||
|
|
47ab6b8a92 | ||
|
|
7420abf175 | ||
|
|
e9a8194cf8 | ||
|
|
9006049d33 | ||
|
|
39381a17de | ||
|
|
9460549eff | ||
|
|
5ea82645ef | ||
|
|
597abc5b06 | ||
|
|
350265e31f | ||
|
|
5680a306ff | ||
|
|
16cb09bda5 | ||
|
|
9a3c40f6a6 | ||
|
|
821e4a225a | ||
|
|
939c99b0cf | ||
|
|
79b9c7011d | ||
|
|
e7ff7402b4 | ||
|
|
91f6369ba9 | ||
|
|
17ef5cb9a5 | ||
|
|
e8109f1b78 | ||
|
|
f3840d56af | ||
|
|
4a5e0b8d81 | ||
|
|
4ef29f027e | ||
|
|
d4d2efe925 | ||
|
|
1078731f2d | ||
|
|
1739afae24 | ||
|
|
9f0c29c009 | ||
|
|
6220d02f32 | ||
|
|
c166b12515 | ||
|
|
189c870630 | ||
|
|
cdead9ba8a | ||
|
|
21616f4d42 | ||
|
|
0a348278ca | ||
|
|
98d0c9a4f6 | ||
|
|
34a3739545 | ||
|
|
7bb34b8788 | ||
|
|
f6dc432419 | ||
|
|
9b2ee628aa | ||
|
|
357ad26a0e | ||
|
|
a3e705373c | ||
|
|
71ad13256e | ||
|
|
68929631f2 | ||
|
|
9c04065c33 | ||
|
|
09db57db8f | ||
|
|
f9b7e64d53 | ||
|
|
50262f2acc | ||
|
|
a4d99b54af | ||
|
|
485aa0f52b | ||
|
|
f8b732c9b8 | ||
|
|
ac72f77a74 | ||
|
|
626d48d151 | ||
|
|
07511281b8 | ||
|
|
7c11c9c91a | ||
|
|
2cabe4c416 | ||
|
|
dc88a037eb | ||
|
|
2fe8531e51 | ||
|
|
fddd2651fc | ||
|
|
deb0781871 | ||
|
|
8114b04ab6 | ||
|
|
767560804d | ||
|
|
8074b93992 | ||
|
|
588dd41244 | ||
|
|
61b0147a7c | ||
|
|
0d388a396c | ||
|
|
135c79d2ad | ||
|
|
9925b042d8 | ||
|
|
1d16d514c7 | ||
|
|
bda547198e | ||
|
|
5f1b78ec84 | ||
|
|
b7e9a85be0 | ||
|
|
080c1cee4f | ||
|
|
baebede816 | ||
|
|
f455251645 | ||
|
|
8d06f7cf02 | ||
|
|
4af2eaa6a3 | ||
|
|
f5b8879b87 | ||
|
|
7501fee448 | ||
|
|
b7b5090673 | ||
|
|
4f94a0f08a | ||
|
|
2281c8ac39 | ||
|
|
2cc152d0ab | ||
|
|
7b86bb262c | ||
|
|
ed2a4251f1 | ||
|
|
847811a52c | ||
|
|
d25d5b734c | ||
|
|
bc4792b7fd | ||
|
|
7850cbc4bf | ||
|
|
97fa648b2f | ||
|
|
c5cf867cd9 | ||
|
|
03ea9bb760 | ||
|
|
a1a5bf921e | ||
|
|
3e1a7a0dc5 | ||
|
|
2c21387ad9 | ||
|
|
5e8e4fa4a1 | ||
|
|
a41107d021 | ||
|
|
281523ee06 | ||
|
|
2504510c61 | ||
|
|
7153fc8bb5 | ||
|
|
3af094d788 | ||
|
|
785ea71a20 | ||
|
|
05d2f77c0c | ||
|
|
e22366e524 | ||
|
|
2b51c47846 | ||
|
|
dd6af9b8e0 | ||
|
|
c66b17583f | ||
|
|
3ce3520c45 | ||
|
|
8d1e7f4331 | ||
|
|
f0b04afa11 | ||
|
|
f1bfd13da3 | ||
|
|
161cd84150 | ||
|
|
da39593c15 | ||
|
|
571f36e405 | ||
|
|
a4b1200475 | ||
|
|
43807dcba9 | ||
|
|
99a72451d9 | ||
|
|
b8900999a4 | ||
|
|
e6f77376b9 | ||
|
|
b2a6a20f10 | ||
|
|
265b52dccb | ||
|
|
0c112e1db1 | ||
|
|
8eef7db1c6 | ||
|
|
05cbf99237 | ||
|
|
651a7cf83e | ||
|
|
ee27237083 | ||
|
|
72306e91a2 | ||
|
|
75d272be14 | ||
|
|
a8a209f0b0 | ||
|
|
1b7b6196c5 | ||
|
|
ed7937a026 | ||
|
|
f2de4692ea | ||
|
|
16b046bd44 | ||
|
|
7129e2cc9d | ||
|
|
01432fa778 | ||
|
|
9731d28ec3 | ||
|
|
99fbb31554 | ||
|
|
18d258aaa2 | ||
|
|
1af6dd9cf8 | ||
|
|
0da183f084 | ||
|
|
205726b045 | ||
|
|
9cd5237bb8 | ||
|
|
964e94b3ba | ||
|
|
9f54f40f5a | ||
|
|
7047d37f70 | ||
|
|
5b1d45a8fe | ||
|
|
a319957f3e | ||
|
|
816166a30a | ||
|
|
5dd2ea776a | ||
|
|
3b94c7bb43 | ||
|
|
f0198616ad | ||
|
|
267fd403da | ||
|
|
0a8bb7eae5 | ||
|
|
409048c206 | ||
|
|
f84bd6a1e8 | ||
|
|
d5c0e62be1 | ||
|
|
40c4344f73 | ||
|
|
3bd8aca2d2 | ||
|
|
a21bdedbc1 | ||
|
|
797ebd7771 | ||
|
|
04e9ecbc76 | ||
|
|
41d37579dc | ||
|
|
10d23828a7 | ||
|
|
19e3392825 | ||
|
|
6bf4846ae8 | ||
|
|
afcd37dac6 | ||
|
|
c2ff497cc9 | ||
|
|
decd2c2ded | ||
|
|
02d1c9ce98 | ||
|
|
5c9083a5df | ||
|
|
3c7fafa91f | ||
|
|
fd50f8fcab | ||
|
|
1a93df5886 | ||
|
|
bdc086c285 | ||
|
|
82042e0b99 | ||
|
|
c807b30c8f | ||
|
|
72dc76ec74 | ||
|
|
71619042fd | ||
|
|
429a77de8e | ||
|
|
b1f72620dc | ||
|
|
2a54aed135 | ||
|
|
040c1f6f78 | ||
|
|
07bce90521 | ||
|
|
508b093278 | ||
|
|
9bed5bf872 | ||
|
|
6d0a2cd301 | ||
|
|
e1ee08361d | ||
|
|
3332ce34c5 | ||
|
|
2c57e439d5 | ||
|
|
73e2660e59 | ||
|
|
9120bbea34 | ||
|
|
58ea9750d7 | ||
|
|
a59ad97e5e | ||
|
|
0a7b28caf5 | ||
|
|
eaf191e350 | ||
|
|
ecb89f80a0 | ||
|
|
9626b65593 | ||
|
|
c9b5516330 | ||
|
|
4363ca88aa | ||
|
|
3353060ad4 | ||
|
|
ddc3b8575e | ||
|
|
136a2ec89f | ||
|
|
021c68f2a7 | ||
|
|
989a09274f | ||
|
|
39c5886d7a | ||
|
|
1a5f3735cf | ||
|
|
4d47eb0e91 | ||
|
|
af7c59b5c2 | ||
|
|
693bf68864 | ||
|
|
c9ddf3d165 | ||
|
|
1549b56866 | ||
|
|
2cd1f22e68 | ||
|
|
688f38943d | ||
|
|
043bbd7a11 |
49
.env.example
49
.env.example
@@ -1,24 +1,35 @@
|
||||
# docker image tag (latest, nightly)
|
||||
TAG=latest
|
||||
|
||||
# set timezone to get correct log timestamp
|
||||
TZ=ETC/UTC
|
||||
|
||||
# container uid and gid (must match the owner of mounted directories)
|
||||
GODOXY_UID=1000
|
||||
GODOXY_GID=1000
|
||||
|
||||
# Set GODOXY_API_JWT_SECURE=false to allow http
|
||||
GODOXY_API_JWT_SECURE=true
|
||||
# API JWT Configuration (common)
|
||||
# generate secret with `openssl rand -base64 32`
|
||||
GODOXY_API_JWT_SECRET=
|
||||
# the JWT token time-to-live
|
||||
# leave empty to use default (24 hours)
|
||||
# format: https://pkg.go.dev/time#Duration
|
||||
GODOXY_API_JWT_TOKEN_TTL=
|
||||
|
||||
# API/WebUI user password login credentials (optional)
|
||||
# These fields are not required for OIDC authentication
|
||||
GODOXY_API_USER=admin
|
||||
GODOXY_API_PASSWORD=password
|
||||
# generate secret with `openssl rand -base64 32`
|
||||
GODOXY_API_JWT_SECRET=
|
||||
# the JWT token time-to-live
|
||||
GODOXY_API_JWT_TOKEN_TTL=1h
|
||||
|
||||
# OIDC Configuration (optional)
|
||||
# Uncomment and configure these values to enable OIDC authentication.
|
||||
#
|
||||
# GODOXY_OIDC_ISSUER_URL=https://accounts.google.com
|
||||
# GODOXY_OIDC_CLIENT_ID=your-client-id
|
||||
# GODOXY_OIDC_CLIENT_SECRET=your-client-secret
|
||||
# Keep /api/auth/callback as the redirect URL, change the domain to match your setup.
|
||||
# GODOXY_OIDC_REDIRECT_URL=https://your-domain/api/auth/callback
|
||||
# Comma-separated list of scopes
|
||||
# GODOXY_OIDC_SCOPES=openid, profile, email
|
||||
# GODOXY_OIDC_SCOPES=openid, profile, email, groups # you may also include `offline_access` if your Idp supports it (e.g. Authentik, Pocket ID)
|
||||
#
|
||||
# User definitions: Uncomment and configure these values to restrict access to specific users or groups.
|
||||
# These two fields act as a logical AND operator. For example, given the following membership:
|
||||
@@ -39,11 +50,29 @@ GODOXY_API_JWT_TOKEN_TTL=1h
|
||||
GODOXY_HTTP_ADDR=:80
|
||||
GODOXY_HTTPS_ADDR=:443
|
||||
|
||||
# Enable HTTP3
|
||||
GODOXY_HTTP3_ENABLED=true
|
||||
|
||||
# API listening address
|
||||
GODOXY_API_ADDR=127.0.0.1:8888
|
||||
|
||||
# Prometheus Metrics
|
||||
GODOXY_PROMETHEUS_ENABLED=true
|
||||
# Metrics
|
||||
GODOXY_METRICS_DISABLE_CPU=false
|
||||
GODOXY_METRICS_DISABLE_MEMORY=false
|
||||
GODOXY_METRICS_DISABLE_DISK=false
|
||||
GODOXY_METRICS_DISABLE_NETWORK=false
|
||||
GODOXY_METRICS_DISABLE_SENSORS=false
|
||||
|
||||
# Frontend listening port
|
||||
GODOXY_FRONTEND_PORT=3000
|
||||
|
||||
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
|
||||
GODOXY_FRONTEND_ALIASES=godoxy
|
||||
|
||||
# Docker socket
|
||||
# /var/run/podman/podman.sock for podman
|
||||
DOCKER_SOCKET=/var/run/docker.sock
|
||||
SOCKET_PROXY_LISTEN_ADDR=127.0.0.1:2375
|
||||
|
||||
# Debug mode
|
||||
GODOXY_DEBUG=false
|
||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: yusing # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: yusingwysq # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
50
.github/workflows/agent-binary.yml
vendored
Normal file
50
.github/workflows/agent-binary.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: GoDoxy agent binary
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
paths:
|
||||
- "agent/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
binary_name: godoxy-agent-linux-amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
binary_name: godoxy-agent-linux-arm64
|
||||
name: Build ${{ matrix.platform }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
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
|
||||
run: |
|
||||
make agent=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 }}
|
||||
24
.github/workflows/docker-image-nightly.yml
vendored
Normal file
24
.github/workflows/docker-image-nightly.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Docker Image CI (nightly)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "*" # matches every branch that doesn't contain a '/'
|
||||
- "*/*" # matches every branch containing a single '/'
|
||||
- "**" # matches every branch
|
||||
- "!dependabot/*"
|
||||
- "!main" # excludes main
|
||||
|
||||
jobs:
|
||||
build-nightly:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy
|
||||
tag: nightly
|
||||
target: main
|
||||
build-nightly-agent:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||
tag: nightly
|
||||
target: agent
|
||||
21
.github/workflows/docker-image-prod.yml
vendored
Normal file
21
.github/workflows/docker-image-prod.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build-prod:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy
|
||||
old_image_name: ${{ github.repository_owner }}/go-proxy
|
||||
tag: latest
|
||||
target: main
|
||||
build-prod-agent:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||
tag: latest
|
||||
target: agent
|
||||
23
.github/workflows/docker-image-socket-proxy.yml
vendored
Normal file
23
.github/workflows/docker-image-socket-proxy.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Docker Image CI (socket-proxy)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "socket-proxy/**"
|
||||
tags-ignore:
|
||||
- '**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/socket-proxy
|
||||
tag: latest
|
||||
target: socket-proxy
|
||||
dockerfile: socket-proxy.Dockerfile
|
||||
256
.github/workflows/docker-image.yml
vendored
256
.github/workflows/docker-image.yml
vendored
@@ -1,128 +1,172 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["*"]
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
image_name:
|
||||
required: true
|
||||
type: string
|
||||
old_image_name:
|
||||
required: false
|
||||
type: string
|
||||
target:
|
||||
required: true
|
||||
type: string
|
||||
dockerfile:
|
||||
required: false
|
||||
type: string
|
||||
default: Dockerfile
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
REGISTRY: ghcr.io
|
||||
MAKE_ARGS: ${{ inputs.target }}=1
|
||||
DIGEST_PATH: /tmp/digests/${{ inputs.target }}
|
||||
DIGEST_NAME_SUFFIX: ${{ inputs.target }}
|
||||
DOCKERFILE: ${{ inputs.dockerfile }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build multi-platform Docker image
|
||||
runs-on: ubuntu-22.04
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
name: Build ${{ matrix.platform }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
# - linux/arm/v6
|
||||
# - linux/arm/v7
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ inputs.image_name }}
|
||||
tags: |
|
||||
type=raw,value=${{ inputs.tag }},event=branch
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
file: ${{ env.DOCKERFILE }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}
|
||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
|
||||
cache-to: |
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
|
||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ inputs.image_name }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p ${{ env.DIGEST_PATH }}
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ env.DIGEST_PATH }}/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
merge:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}-${{ env.DIGEST_NAME_SUFFIX }}
|
||||
path: ${{ env.DIGEST_PATH }}/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
merge:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ env.DIGEST_PATH }}
|
||||
pattern: digests-*-${{ env.DIGEST_NAME_SUFFIX }}
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create manifest list and push
|
||||
id: push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ inputs.image_name }}
|
||||
tags: |
|
||||
type=raw,value=${{ inputs.tag }},event=branch
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
id: push
|
||||
working-directory: ${{ env.DIGEST_PATH }}
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(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
|
||||
run: |
|
||||
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 }}
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -9,6 +9,9 @@ certs*/
|
||||
bin/
|
||||
error_pages/
|
||||
!examples/error_pages/
|
||||
profiles/
|
||||
data/
|
||||
debug/
|
||||
|
||||
logs/
|
||||
log/
|
||||
@@ -26,7 +29,14 @@ todo.md
|
||||
.aider*
|
||||
mtrace.json
|
||||
.env
|
||||
*.env
|
||||
.cursorrules
|
||||
.cursor/
|
||||
.windsurfrules
|
||||
test.Dockerfile
|
||||
|
||||
node_modules/
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
!agent.compose.yml
|
||||
!agent/pkg/**
|
||||
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -0,0 +1,9 @@
|
||||
[submodule "internal/gopsutil"]
|
||||
path = internal/gopsutil
|
||||
url = https://github.com/godoxy-app/gopsutil.git
|
||||
[submodule "internal/go-oidc"]
|
||||
path = internal/go-oidc
|
||||
url = https://github.com/godoxy-app/go-oidc.git
|
||||
[submodule "goutils"]
|
||||
path = goutils
|
||||
url = https://github.com/yusing/goutils.git
|
||||
|
||||
282
.golangci.yml
282
.golangci.yml
@@ -1,137 +1,151 @@
|
||||
run:
|
||||
timeout: 10m
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- shadow
|
||||
- fieldalignment
|
||||
gocyclo:
|
||||
min-complexity: 14
|
||||
goconst:
|
||||
min-len: 3
|
||||
min-occurrences: 4
|
||||
misspell:
|
||||
locale: US
|
||||
funlen:
|
||||
lines: -1
|
||||
statements: 120
|
||||
forbidigo:
|
||||
forbid:
|
||||
- ^print(ln)?$
|
||||
godox:
|
||||
keywords:
|
||||
- FIXME
|
||||
tagalign:
|
||||
align: false
|
||||
sort: true
|
||||
order:
|
||||
- description
|
||||
- json
|
||||
- toml
|
||||
- yaml
|
||||
- yml
|
||||
- label
|
||||
- label-slice-as-struct
|
||||
- file
|
||||
- kv
|
||||
- export
|
||||
stylecheck:
|
||||
dot-import-whitelist:
|
||||
- github.com/yusing/go-proxy/internal/utils/testing # go tests only
|
||||
- github.com/yusing/go-proxy/internal/api/v1/utils # api only
|
||||
revive:
|
||||
rules:
|
||||
- name: struct-tag
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: error-naming
|
||||
- name: exported
|
||||
disabled: true
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
- name: var-naming
|
||||
- name: var-declaration
|
||||
- name: package-comments
|
||||
disabled: true
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
- name: indent-error-flow
|
||||
- name: errorf
|
||||
- name: empty-block
|
||||
- name: superfluous-else
|
||||
- name: unused-parameter
|
||||
disabled: true
|
||||
- name: unreachable-code
|
||||
- name: redefines-builtin-id
|
||||
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
|
||||
testifylint:
|
||||
disable:
|
||||
- suite-dont-use-pkg
|
||||
- require-error
|
||||
- go-require
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -SA1019
|
||||
errcheck:
|
||||
exclude-functions:
|
||||
- fmt.Fprintln
|
||||
version: "2"
|
||||
linters:
|
||||
enable-all: true
|
||||
default: all
|
||||
disable:
|
||||
- execinquery # deprecated
|
||||
- gomnd # deprecated
|
||||
- sqlclosecheck # not relevant (SQL)
|
||||
- rowserrcheck # not relevant (SQL)
|
||||
- cyclop # duplicate of gocyclo
|
||||
- depguard # Not relevant
|
||||
- nakedret # Too strict
|
||||
- lll # Not relevant
|
||||
- gocyclo # FIXME must be fixed
|
||||
- gocognit # Too strict
|
||||
- nestif # Too many false-positive.
|
||||
- prealloc # Too many false-positive.
|
||||
- makezero # Not relevant
|
||||
- dupl # Too strict
|
||||
- gci # I don't care
|
||||
- gosec # Too strict
|
||||
- gochecknoinits
|
||||
# - bodyclose
|
||||
- containedctx
|
||||
# - contextcheck
|
||||
- cyclop
|
||||
- depguard
|
||||
# - dupl
|
||||
- err113
|
||||
- exhaustive
|
||||
- exhaustruct
|
||||
- funcorder
|
||||
- forcetypeassert
|
||||
- gochecknoglobals
|
||||
- wsl # Too strict
|
||||
- nlreturn # Not relevant
|
||||
- mnd # Too strict
|
||||
- testpackage # Too strict
|
||||
- tparallel # Not relevant
|
||||
- paralleltest # Not relevant
|
||||
- exhaustive # Not relevant
|
||||
- exhaustruct # Not relevant
|
||||
- err113 # Too strict
|
||||
- wrapcheck # Too strict
|
||||
- noctx # Too strict
|
||||
- bodyclose # too many false-positive
|
||||
- forcetypeassert # Too strict
|
||||
- tagliatelle # Too strict
|
||||
- varnamelen # Not relevant
|
||||
- nilnil # Not relevant
|
||||
- ireturn # Not relevant
|
||||
- contextcheck # too many false-positive
|
||||
- containedctx # too many false-positive
|
||||
- maintidx # kind of duplicate of gocyclo
|
||||
- nonamedreturns # Too strict
|
||||
- gosmopolitan # not relevant
|
||||
- exportloopref # Not relevant since go1.22
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocyclo
|
||||
- godot
|
||||
- gomoddirectives
|
||||
- gosmopolitan
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- makezero
|
||||
- mnd
|
||||
- nakedret
|
||||
- nestif
|
||||
- nlreturn
|
||||
- nonamedreturns
|
||||
- noinlineerr
|
||||
- paralleltest
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- tagalign
|
||||
- tagliatelle
|
||||
- testpackage
|
||||
- tparallel
|
||||
- varnamelen
|
||||
- wrapcheck
|
||||
- wsl
|
||||
- wsl_v5
|
||||
settings:
|
||||
errcheck:
|
||||
exclude-functions:
|
||||
- fmt.Fprintln
|
||||
forbidigo:
|
||||
forbid:
|
||||
- pattern: ^print(ln)?$
|
||||
funlen:
|
||||
lines: -1
|
||||
statements: 120
|
||||
gocyclo:
|
||||
min-complexity: 14
|
||||
godox:
|
||||
keywords:
|
||||
- FIXME
|
||||
gomoddirectives:
|
||||
replace-allow-list:
|
||||
- github.com/abbot/go-http-auth
|
||||
- github.com/gorilla/mux
|
||||
- github.com/mailgun/minheap
|
||||
- github.com/mailgun/multibuf
|
||||
- github.com/jaguilar/vt100
|
||||
- github.com/cucumber/godog
|
||||
- github.com/http-wasm/http-wasm-host-go
|
||||
govet:
|
||||
disable:
|
||||
- shadow
|
||||
enable-all: true
|
||||
misspell:
|
||||
locale: US
|
||||
revive:
|
||||
rules:
|
||||
- name: struct-tag
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: error-naming
|
||||
- name: exported
|
||||
disabled: true
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
- name: var-naming
|
||||
- name: var-declaration
|
||||
- name: package-comments
|
||||
disabled: true
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
- name: indent-error-flow
|
||||
- name: errorf
|
||||
- name: empty-block
|
||||
- name: superfluous-else
|
||||
- name: unused-parameter
|
||||
disabled: true
|
||||
- name: unreachable-code
|
||||
- name: redefines-builtin-id
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -SA1019
|
||||
dot-import-whitelist:
|
||||
- github.com/yusing/godoxy/internal/utils/testing
|
||||
tagalign:
|
||||
align: false
|
||||
sort: true
|
||||
order:
|
||||
- description
|
||||
- json
|
||||
- toml
|
||||
- yaml
|
||||
- yml
|
||||
- label
|
||||
- label-slice-as-struct
|
||||
- file
|
||||
- kv
|
||||
- export
|
||||
testifylint:
|
||||
disable:
|
||||
- suite-dont-use-pkg
|
||||
- require-error
|
||||
- go-require
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
@@ -2,37 +2,37 @@
|
||||
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
|
||||
version: 0.1
|
||||
cli:
|
||||
version: 1.22.8
|
||||
version: 1.25.0
|
||||
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
ref: v1.6.6
|
||||
ref: v1.7.2
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||
runtimes:
|
||||
enabled:
|
||||
- node@18.20.5
|
||||
- node@22.16.0
|
||||
- python@3.10.8
|
||||
- go@1.23.2
|
||||
- go@1.24.3
|
||||
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||
lint:
|
||||
disabled:
|
||||
- markdownlint
|
||||
- yamllint
|
||||
enabled:
|
||||
- hadolint@2.12.1-beta
|
||||
- actionlint@1.7.6
|
||||
- checkov@3.2.352
|
||||
- checkov@3.2.471
|
||||
- golangci-lint2@2.5.0
|
||||
- hadolint@2.14.0
|
||||
- actionlint@1.7.7
|
||||
- git-diff-check
|
||||
- gofmt@1.20.4
|
||||
- golangci-lint@1.63.4
|
||||
- osv-scanner@1.9.2
|
||||
- oxipng@9.1.3
|
||||
- prettier@3.4.2
|
||||
- shellcheck@0.10.0
|
||||
- osv-scanner@2.2.2
|
||||
- oxipng@9.1.5
|
||||
- prettier@3.6.2
|
||||
- shellcheck@0.11.0
|
||||
- shfmt@3.6.0
|
||||
- trufflehog@3.88.2
|
||||
- trufflehog@3.90.8
|
||||
actions:
|
||||
disabled:
|
||||
- trunk-announce
|
||||
|
||||
4
.vscode/settings.example.json
vendored
4
.vscode/settings.example.json
vendored
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"yaml.schemas": {
|
||||
"https://github.com/yusing/go-proxy/raw/v0.9/schemas/config.schema.json": [
|
||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/config.schema.json": [
|
||||
"config.example.yml",
|
||||
"config.yml"
|
||||
],
|
||||
"https://github.com/yusing/go-proxy/raw/v0.9/schemas/routes.schema.json": [
|
||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/routes.schema.json": [
|
||||
"providers.example.yml"
|
||||
]
|
||||
}
|
||||
|
||||
70
Dockerfile
70
Dockerfile
@@ -1,64 +1,68 @@
|
||||
# Stage 1: Builder
|
||||
FROM golang:1.23.5-alpine AS builder
|
||||
# Stage 1: deps
|
||||
FROM golang:1.25.3-alpine AS deps
|
||||
HEALTHCHECK NONE
|
||||
|
||||
# package version does not matter
|
||||
# trunk-ignore(hadolint/DL3018)
|
||||
RUN apk add --no-cache tzdata make libcap-setcap
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Only copy go.mod and go.sum initially for better caching
|
||||
COPY go.mod go.sum /src/
|
||||
|
||||
# Utilize build cache
|
||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
go mod download -x
|
||||
|
||||
ENV GOPATH=/root/go
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
|
||||
COPY Makefile /src/
|
||||
COPY cmd /src/cmd
|
||||
COPY internal /src/internal
|
||||
COPY pkg /src/pkg
|
||||
WORKDIR /src
|
||||
|
||||
COPY goutils/go.mod goutils/go.sum ./goutils/
|
||||
COPY internal/go-oidc/go.mod internal/go-oidc/go.sum ./internal/go-oidc/
|
||||
COPY internal/gopsutil/go.mod internal/gopsutil/go.sum ./internal/gopsutil/
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# remove godoxy stuff from go.mod first
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--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
|
||||
|
||||
# Stage 2: builder
|
||||
FROM deps AS builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
COPY Makefile ./
|
||||
COPY cmd ./cmd
|
||||
COPY internal ./internal
|
||||
COPY pkg ./pkg
|
||||
COPY agent ./agent
|
||||
COPY socket-proxy ./socket-proxy
|
||||
COPY goutils ./goutils
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=${VERSION}
|
||||
|
||||
ARG BUILD_FLAGS
|
||||
ENV BUILD_FLAGS=${BUILD_FLAGS}
|
||||
ARG MAKE_ARGS
|
||||
ENV MAKE_ARGS=${MAKE_ARGS}
|
||||
|
||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
--mount=type=cache,target="/root/.cache/go-build" \
|
||||
make build && \
|
||||
mkdir -p /app/error_pages /app/certs && \
|
||||
mv bin/godoxy /app/godoxy
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/root/go/pkg/mod \
|
||||
make ${MAKE_ARGS} docker=1 build
|
||||
|
||||
# Stage 2: Final image
|
||||
# Stage 3: Final image
|
||||
FROM scratch
|
||||
|
||||
LABEL maintainer="yusing@6uo.me"
|
||||
LABEL proxy.exclude=1
|
||||
LABEL proxy.#1.healthcheck.disable=true
|
||||
|
||||
# copy timezone data
|
||||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
|
||||
# copy binary
|
||||
COPY --from=builder /app /app
|
||||
|
||||
# copy example config
|
||||
COPY config.example.yml /app/config/config.yml
|
||||
COPY --from=builder /app/run /app/run
|
||||
|
||||
# copy certs
|
||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
||||
ENV GODOXY_DEBUG=0
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 8888
|
||||
EXPOSE 443
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["/app/godoxy"]
|
||||
CMD ["/app/run"]
|
||||
26
LICENSE
26
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 [fullname]
|
||||
Copyright (c) 2024 - present Yusing
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -19,3 +19,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
internal/net/gphttp/reverseproxy/reverse_proxy_mod.go is copied from et/http/httputil/reverseproxy.go with modifications to adapt to this project.
|
||||
|
||||
Copyright 2011 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
|
||||
---
|
||||
|
||||
internal/utils/io.go has a modified version of io.Copy with context and HTTP flusher handling.
|
||||
|
||||
Copyright 2009 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
|
||||
---
|
||||
|
||||
internal/utils/strutils/split_join.go is copied from strings.Split and strings.Join with modifications to adapt to this project.
|
||||
|
||||
Copyright 2009 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
|
||||
198
Makefile
198
Makefile
@@ -1,60 +1,132 @@
|
||||
shell := /bin/sh
|
||||
export VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
||||
export GOOS = linux
|
||||
|
||||
LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION}
|
||||
WEBUI_DIR ?= ../godoxy-webui
|
||||
DOCS_DIR ?= ${WEBUI_DIR}/wiki
|
||||
|
||||
ifeq ($(trace), 1)
|
||||
GODOXY_TRACE ?= 1
|
||||
GO_TAGS = sonic
|
||||
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
|
||||
|
||||
ifeq ($(agent), 1)
|
||||
NAME = godoxy-agent
|
||||
PWD = ${shell pwd}/agent
|
||||
else ifeq ($(socket-proxy), 1)
|
||||
NAME = godoxy-socket-proxy
|
||||
PWD = ${shell pwd}/socket-proxy
|
||||
else
|
||||
NAME = godoxy
|
||||
PWD = ${shell pwd}
|
||||
endif
|
||||
|
||||
ifeq ($(debug), 1)
|
||||
CGO_ENABLED = 0
|
||||
GODOXY_DEBUG = 1
|
||||
BUILD_FLAGS = -tags production
|
||||
else ifeq ($(pprof), 1)
|
||||
CGO_ENABLED = 1
|
||||
ifeq ($(trace), 1)
|
||||
debug = 1
|
||||
GODOXY_TRACE ?= 1
|
||||
GODEBUG = gctrace=1 inittrace=1 schedtrace=3000
|
||||
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/
|
||||
BUILD_FLAGS = -race -gcflags=all='-N -l' -tags pprof
|
||||
DOCKER_TAG = pprof
|
||||
VERSION += -pprof
|
||||
endif
|
||||
|
||||
ifeq ($(race), 1)
|
||||
CGO_ENABLED = 1
|
||||
GODOXY_DEBUG = 1
|
||||
GO_TAGS += debug
|
||||
BUILD_FLAGS += -race
|
||||
else ifeq ($(debug), 1)
|
||||
CGO_ENABLED = 1
|
||||
GODOXY_DEBUG = 1
|
||||
GO_TAGS += debug
|
||||
BUILD_FLAGS += -asan # FIXME: -gcflags=all='-N -l'
|
||||
else ifeq ($(pprof), 1)
|
||||
CGO_ENABLED = 0
|
||||
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
||||
GO_TAGS += pprof
|
||||
VERSION := ${VERSION}-pprof
|
||||
else
|
||||
CGO_ENABLED = 0
|
||||
LDFLAGS += -s -w
|
||||
BUILD_FLAGS = -pgo=auto -tags production
|
||||
DOCKER_TAG = latest
|
||||
GO_TAGS += production
|
||||
BUILD_FLAGS += -pgo=auto
|
||||
endif
|
||||
|
||||
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
||||
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
|
||||
BIN_PATH := $(shell pwd)/bin/${NAME}
|
||||
|
||||
export NAME
|
||||
export CGO_ENABLED
|
||||
export GODOXY_DEBUG
|
||||
export GODOXY_TRACE
|
||||
export GODEBUG
|
||||
export GORACE
|
||||
export BUILD_FLAGS
|
||||
export DOCKER_TAG
|
||||
|
||||
ifeq ($(shell id -u), 0)
|
||||
SETCAP_CMD = setcap
|
||||
else
|
||||
SETCAP_CMD = sudo setcap
|
||||
endif
|
||||
|
||||
|
||||
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
|
||||
POST_BUILD = $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
|
||||
ifeq ($(docker), 1)
|
||||
POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run;
|
||||
endif
|
||||
|
||||
.PHONY: debug
|
||||
|
||||
test:
|
||||
GODOXY_TEST=1 go test ./internal/...
|
||||
go test -v -race ./internal/...
|
||||
|
||||
get:
|
||||
go get -u ./cmd && go mod tidy
|
||||
docker-build-test:
|
||||
docker build -t godoxy .
|
||||
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
|
||||
|
||||
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
||||
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
|
||||
gomod_paths := $(shell find . -name go.mod -type f | xargs dirname)
|
||||
|
||||
update-go:
|
||||
for file in ${files}; do \
|
||||
echo "updating $$file"; \
|
||||
sed -i 's|go \([0-9]\+\.[0-9]\+\.[0-9]\+\)|go ${go_ver}|g' $$file; \
|
||||
sed -i 's|FROM golang:.*-alpine|FROM golang:${go_ver}-alpine|g' $$file; \
|
||||
done
|
||||
for path in ${gomod_paths}; do \
|
||||
echo "go mod tidy $$path"; \
|
||||
cd ${PWD}/$$path && go mod tidy; \
|
||||
done
|
||||
|
||||
update-deps:
|
||||
for path in ${gomod_paths}; do \
|
||||
echo "go get -u $$path"; \
|
||||
cd ${PWD}/$$path && go get -u ./... && go mod tidy; \
|
||||
done
|
||||
|
||||
mod-tidy:
|
||||
for path in ${gomod_paths}; do \
|
||||
echo "go mod tidy $$path"; \
|
||||
cd ${PWD}/$$path && go mod tidy; \
|
||||
done
|
||||
|
||||
build:
|
||||
mkdir -p bin
|
||||
go build ${BUILD_FLAGS} -o bin/godoxy ./cmd
|
||||
if [ $(shell id -u) -eq 0 ]; \
|
||||
then setcap CAP_NET_BIND_SERVICE=+eip bin/godoxy; \
|
||||
else sudo setcap CAP_NET_BIND_SERVICE=+eip bin/godoxy; \
|
||||
fi
|
||||
mkdir -p $(shell dirname ${BIN_PATH})
|
||||
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
|
||||
${POST_BUILD}
|
||||
|
||||
run:
|
||||
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
|
||||
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
|
||||
|
||||
dev:
|
||||
docker compose -f dev.compose.yml $(args)
|
||||
|
||||
dev-build: build
|
||||
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
|
||||
|
||||
dev-run: build
|
||||
cd dev-data && ${BIN_PATH}
|
||||
|
||||
mtrace:
|
||||
bin/godoxy debug-ls-mtrace > mtrace.json
|
||||
${BIN_PATH} debug-ls-mtrace > mtrace.json
|
||||
|
||||
rapid-crash:
|
||||
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
||||
@@ -69,57 +141,23 @@ ci-test:
|
||||
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
||||
|
||||
cloc:
|
||||
cloc --not-match-f '_test.go$$' cmd internal pkg
|
||||
|
||||
push-docker-io:
|
||||
BUILDER=build docker buildx build \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
-f Dockerfile \
|
||||
-t docker.io/yusing/godoxy-nightly:${DOCKER_TAG} \
|
||||
-t docker.io/yusing/godoxy-nightly:${VERSION}-${BUILD_DATE} \
|
||||
--build-arg VERSION="${VERSION}-nightly-${BUILD_DATE}" \
|
||||
--build-arg BUILD_FLAGS="${BUILD_FLAGS}" \
|
||||
--push .
|
||||
|
||||
build-docker:
|
||||
docker build -t godoxy-nightly \
|
||||
--build-arg VERSION="${VERSION}-nightly-${BUILD_DATE}" \
|
||||
--build-arg BUILD_FLAGS="${BUILD_FLAGS}" \
|
||||
.
|
||||
|
||||
# To generate schema
|
||||
# comment out this part from typescript-json-schema.js#L884
|
||||
#
|
||||
# if (indexType.flags !== ts.TypeFlags.Number && !isIndexedObject) {
|
||||
# throw new Error("Not supported: IndexSignatureDeclaration with index symbol other than a number or a string");
|
||||
# }
|
||||
|
||||
gen-schema-single:
|
||||
bun --bun run typescript-json-schema --noExtraProps --required --skipLibCheck --tsNodeRegister=true -o schemas/${OUT} schemas/${IN} ${CLASS}
|
||||
# minify
|
||||
python3 -c "import json; f=open('schemas/${OUT}', 'r'); j=json.load(f); f.close(); f=open('schemas/${OUT}', 'w'); json.dump(j, f, separators=(',', ':'));"
|
||||
|
||||
gen-schema:
|
||||
bun --bun tsc
|
||||
make IN=config/config.ts \
|
||||
CLASS=Config \
|
||||
OUT=config.schema.json \
|
||||
gen-schema-single
|
||||
make IN=providers/routes.ts \
|
||||
CLASS=Routes \
|
||||
OUT=routes.schema.json \
|
||||
gen-schema-single
|
||||
make IN=middlewares/middleware_compose.ts \
|
||||
CLASS=MiddlewareCompose \
|
||||
OUT=middleware_compose.schema.json \
|
||||
gen-schema-single
|
||||
make IN=docker.ts \
|
||||
CLASS=DockerRoutes \
|
||||
OUT=docker_routes.schema.json \
|
||||
gen-schema-single
|
||||
|
||||
update-schema-generator:
|
||||
pnpm up -g typescript-json-schema
|
||||
cloc --include-lang=Go --not-match-f '_test.go$$' .
|
||||
|
||||
push-github:
|
||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
gen-swagger:
|
||||
swag init --parseDependency --parseInternal --parseFuncBody -g handler.go -d internal/api -o internal/api/v1/docs
|
||||
python3 scripts/fix-swagger-json.py
|
||||
# we don't need this
|
||||
rm internal/api/v1/docs/docs.go
|
||||
|
||||
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
|
||||
# --disable-throw-on-error
|
||||
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
||||
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
|
||||
235
README.md
235
README.md
@@ -1,120 +1,183 @@
|
||||
# GoDoxy
|
||||
<div align="center">
|
||||
|
||||
<img src="assets/godoxy.png" width="200">
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=go-proxy)
|
||||
|
||||
[繁體中文文檔請看此](README_CHT.md)
|
||||

|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
A lightweight, easy-to-use, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with a Web UI and dashboard.
|
||||
A lightweight, simple, and performant reverse proxy with WebUI.
|
||||
|
||||

|
||||
<h5>
|
||||
<a href="https://docs.godoxy.dev">Website</a> | <a href="https://docs.godoxy.dev/Home.html">Wiki</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
|
||||
</h5>
|
||||
|
||||
_Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
<h5>EN | <a href="README_CHT.md">中文</a></h5>
|
||||
|
||||
<img src="screenshots/webui.jpg" style="max-width: 650">
|
||||
|
||||
Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)! (Thanks to [@ismesid](https://github.com/arevindh))
|
||||
|
||||
</div>
|
||||
|
||||
## Table of content
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [GoDoxy](#godoxy)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Key Features](#key-features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [Manual Setup](#manual-setup)
|
||||
- [Folder structrue](#folder-structrue)
|
||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||
- [Screenshots](#screenshots)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Running demo](#running-demo)
|
||||
- [Key Features](#key-features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||
- [Update / Uninstall system agent](#update--uninstall-system-agent)
|
||||
- [Screenshots](#screenshots)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [Metrics and Logs](#metrics-and-logs)
|
||||
- [Manual Setup](#manual-setup)
|
||||
- [Folder structrue](#folder-structrue)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
- [Star History](#star-history)
|
||||
|
||||
## Running demo
|
||||
|
||||
<https://demo.godoxy.dev>
|
||||
|
||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||
|
||||
## Key Features
|
||||
|
||||
- Easy to use
|
||||
- Effortless configuration
|
||||
- Simple multi-node setup
|
||||
- Error messages is clear and detailed, easy troubleshooting
|
||||
- Auto SSL cert management (See [Supported DNS-01 Challenge Providers](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||
- Auto configuration for docker containers
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [screenshots](#idlesleeper))_
|
||||
- HTTP(s) reserve proxy
|
||||
- OpenID Connect support
|
||||
- [HTTP middleware support](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- TCP and UDP port forwarding
|
||||
- **Web UI with App dashboard and config editor**
|
||||
- Supports linux/amd64, linux/arm64
|
||||
- Written in **[Go](https://go.dev)**
|
||||
- **Simple**
|
||||
- Effortless configuration with [simple labels](https://docs.godoxy.dev/Docker-labels-and-Route-Files) or WebUI
|
||||
- [Simple multi-node setup](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
|
||||
- Detailed error messages for easy troubleshooting.
|
||||
- **ACL**: connection / request level access control
|
||||
- IP/CIDR
|
||||
- Country **(Maxmind account required)**
|
||||
- Timezone **(Maxmind account required)**
|
||||
- **Access logging**
|
||||
- Periodic notification of access summaries for number of allowed and blocked connections
|
||||
- **Advanced Automation**
|
||||
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://docs.godoxy.dev/DNS-01-Providers))
|
||||
- Auto-configuration for Docker containers
|
||||
- Hot-reloading of configurations and container state changes
|
||||
- **Container Runtime Support**
|
||||
- Docker
|
||||
- Podman
|
||||
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||
- Docker containers
|
||||
- Proxmox LXCs
|
||||
- **Traffic Management**
|
||||
- HTTP reserve proxy
|
||||
- TCP/UDP port forwarding
|
||||
- **OpenID Connect support**: SSO and secure your apps easily
|
||||
- **ForwardAuth support**: integrate with any auth provider (e.g. TinyAuth)
|
||||
- **Customization**
|
||||
- [HTTP middlewares](https://docs.godoxy.dev/Middlewares)
|
||||
- [Custom error pages support](https://docs.godoxy.dev/Custom-Error-Pages)
|
||||
- **Web UI**
|
||||
- App Dashboard
|
||||
- Config Editor
|
||||
- Uptime and System Metrics
|
||||
- Docker Logs Viewer
|
||||
- **Cross-Platform support**
|
||||
- Supports **linux/amd64** and **linux/arm64**
|
||||
- **Efficient and Performant**
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
## Prerequisites
|
||||
|
||||
## Getting Started
|
||||
Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
||||
|
||||
For full documentation, **[See Wiki](https://github.com/yusing/go-proxy/wiki)**
|
||||
- A Record: `*.domain.com` -> `10.0.10.1`
|
||||
- AAAA Record (if you use IPv6): `*.domain.com` -> `::ffff:a00:a01`
|
||||
|
||||
### Prerequisites
|
||||
## Setup
|
||||
|
||||
Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
> [!NOTE]
|
||||
> GoDoxy is designed to be running in `host` network mode, do not change it.
|
||||
>
|
||||
> To change listening ports, modify `.env`.
|
||||
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
1. Prepare a new directory for docker compose and config files.
|
||||
|
||||
### Setup
|
||||
2. Run setup script inside the directory, or [set up manually](#manual-setup)
|
||||
|
||||
1. Pull the latest docker images
|
||||
```shell
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||
```
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/yusing/go-proxy:latest
|
||||
```
|
||||
3. Start the docker compose service from generated `compose.yml`:
|
||||
|
||||
2. Create new directory, `cd` into it, then run setup, or [set up manually](#manual-setup)
|
||||
```shell
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
```shell
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/godoxy setup
|
||||
```
|
||||
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
||||
|
||||
3. _(Optional)_ setup WebUI login (skip if you use OIDC)
|
||||
## How does GoDoxy work
|
||||
|
||||
- set random JWT secret
|
||||
1. List all the containers
|
||||
2. Read container name, labels and port configurations for each of them
|
||||
3. Create a route if applicable (a route is like a "Virtual Host" in NPM)
|
||||
4. Watch for container / config changes and update automatically
|
||||
|
||||
```shell
|
||||
sed -i "s|API_JWT_SECRET=.*|API_JWT_SECRET=$(openssl rand -base64 32)|g" .env
|
||||
```
|
||||
> [!NOTE]
|
||||
> GoDoxy uses the label `proxy.aliases` as the subdomain(s), if unset it defaults to the `container_name` field in docker compose.
|
||||
>
|
||||
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
||||
|
||||
- change username and password for WebUI authentication
|
||||
```shell
|
||||
USERNAME=admin
|
||||
PASSWORD=some-password
|
||||
sed -i "s|API_USERNAME=.*|API_USERNAME=${USERNAME}|g" .env
|
||||
sed -i "s|API_PASSWORD=.*|API_PASSWORD=${PASSWORD}|g" .env
|
||||
```
|
||||
## Update / Uninstall system agent
|
||||
|
||||
4. _(Optional)_ setup `docker-socket-proxy` other docker nodes (see [Multi docker nodes setup](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)) then add them inside `config.yml`
|
||||
Update:
|
||||
|
||||
5. Start the container `docker compose up -d`
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- update
|
||||
```
|
||||
|
||||
6. You may now do some extra configuration on WebUI `https://gp.y.z`
|
||||
Uninstall:
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- uninstall
|
||||
```
|
||||
|
||||
### Manual Setup
|
||||
## Screenshots
|
||||
|
||||
### idlesleeper
|
||||
|
||||

|
||||
|
||||
### Metrics and Logs
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
|
||||
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b>Routes</b></td>
|
||||
<td align="center"><b>Servers</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
## Manual Setup
|
||||
|
||||
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
||||
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/config.example.yml -O config/config.yml`
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/godoxy/main/config.example.yml -O config/config.yml`
|
||||
|
||||
2. Grab `.env.example` into `.env`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/.env.example -O .env`
|
||||
`wget https://raw.githubusercontent.com/yusing/godoxy/main/.env.example -O .env`
|
||||
|
||||
3. Grab `compose.example.yml` into `compose.yml`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/compose.example.yml -O compose.yml`
|
||||
`wget https://raw.githubusercontent.com/yusing/godoxy/main/compose.example.yml -O compose.yml`
|
||||
|
||||
### Folder structrue
|
||||
|
||||
@@ -130,26 +193,16 @@ Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
│ │ ├── middleware2.yml
|
||||
│ ├── provider1.yml
|
||||
│ └── provider2.yml
|
||||
├── data
|
||||
│ ├── metrics # metrics data
|
||||
│ │ ├── uptime.json
|
||||
│ │ └── system_info.json
|
||||
└── .env
|
||||
```
|
||||
|
||||
### Use JSON Schema in VSCode
|
||||
|
||||
Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify it to fit your needs
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Screenshots
|
||||
|
||||
### idlesleeper
|
||||
|
||||

|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Build it yourself
|
||||
|
||||
1. Clone the repository `git clone https://github.com/yusing/go-proxy --depth=1`
|
||||
1. Clone the repository `git clone https://github.com/yusing/godoxy --depth=1`
|
||||
|
||||
2. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
|
||||
|
||||
@@ -159,4 +212,8 @@ Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscod
|
||||
|
||||
5. build binary with `make build`
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#yusing/godoxy&Date)
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
209
README_CHT.md
209
README_CHT.md
@@ -1,121 +1,129 @@
|
||||
# GoDoxy
|
||||
<div align="center">
|
||||
|
||||
<img src="assets/godoxy.png" width="200">
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
[English Documentation](README.md)
|
||||

|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
一個輕量級、易於使用且[高效能](https://github.com/yusing/go-proxy/wiki/Benchmarks)的反向代理,具有網頁介面和儀表板。
|
||||
輕量、易用、 高效能,且帶有主頁和配置面板的反向代理
|
||||
|
||||

|
||||
<h5>
|
||||
<a href="https://docs.godoxy.dev">網站</a> | <a href="https://docs.godoxy.dev/Home.html">文檔</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
|
||||
</h5>
|
||||
|
||||
_加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
<h5><a href="README.md">EN</a> | 中文</h5>
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||
|
||||
有疑問? 問 [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)!(鳴謝 [@ismesid](https://github.com/arevindh))
|
||||
|
||||
</div>
|
||||
|
||||
## 目錄
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [GoDoxy](#godoxy)
|
||||
- [目錄](#目錄)
|
||||
- [主要特點](#主要特點)
|
||||
- [入門指南](#入門指南)
|
||||
- [前置需求](#前置需求)
|
||||
- [安裝](#安裝)
|
||||
- [手動安裝](#手動安裝)
|
||||
- [資料夾結構](#資料夾結構)
|
||||
- [在 VSCode 中使用 JSON Schema](#在-vscode-中使用-json-schema)
|
||||
- [截圖](#截圖)
|
||||
- [閒置休眠](#閒置休眠)
|
||||
- [自行編譯](#自行編譯)
|
||||
- [目錄](#目錄)
|
||||
- [運行示例](#運行示例)
|
||||
- [主要特點](#主要特點)
|
||||
- [前置需求](#前置需求)
|
||||
- [安裝](#安裝)
|
||||
- [手動安裝](#手動安裝)
|
||||
- [資料夾結構](#資料夾結構)
|
||||
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
||||
- [截圖](#截圖)
|
||||
- [閒置休眠](#閒置休眠)
|
||||
- [監控](#監控)
|
||||
- [自行編譯](#自行編譯)
|
||||
- [Star History](#star-history)
|
||||
|
||||
## 運行示例
|
||||
|
||||
<https://demo.godoxy.dev>
|
||||
|
||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||
|
||||
## 主要特點
|
||||
|
||||
- 容易使用
|
||||
- 輕鬆配置
|
||||
- 簡單的多節點設置
|
||||
- 錯誤訊息清晰詳細,易於排除故障
|
||||
- 自動 SSL 憑證管理(參見 [支援的 DNS-01 驗證提供商](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||
- 自動配置 Docker 容器
|
||||
- 容器狀態/配置文件變更時自動熱重載
|
||||
- **閒置休眠**:在閒置時停止容器,有流量時喚醒(_可選,參見[截圖](#閒置休眠)_)
|
||||
- HTTP(s) 反向代理
|
||||
- [HTTP 中介軟體支援](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||
- [自訂錯誤頁面支援](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- TCP 和 UDP 埠轉發
|
||||
- **網頁介面,具有應用儀表板和配置編輯器**
|
||||
- 支援 linux/amd64、linux/arm64
|
||||
- 使用 **[Go](https://go.dev)** 編寫
|
||||
- **簡單易用**
|
||||
- 透過 Docker[標籤](https://docs.godoxy.dev/Docker-labels-and-Route-Files)或 WebUI 輕鬆設定
|
||||
- [簡單的多節點設置](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
|
||||
- 詳細的錯誤訊息,便於故障排除
|
||||
- **存取控制 (ACL)**:連線/請求層級存取控制
|
||||
- IP/CIDR
|
||||
- 國家 **(需要 Maxmind 帳戶)**
|
||||
- 時區 **(需要 Maxmind 帳戶)**
|
||||
- **存取日誌記錄**
|
||||
- 定時發送摘要 (允許和拒絕的連線次數)
|
||||
- **自動化**
|
||||
- 使用 Let's Encrypt 自動管理 SSL 憑證 ([使用 DNS-01 驗證](https://docs.godoxy.dev/DNS-01-Providers))
|
||||
- Docker 容器自動配置
|
||||
- 設定檔與容器狀態變更時自動熱重載
|
||||
- **容器運行時支援**
|
||||
- Docker
|
||||
- Podman
|
||||
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
||||
- Docker 容器
|
||||
- Proxmox LXC 容器
|
||||
- **流量管理**
|
||||
- HTTP 反向代理
|
||||
- TCP/UDP 連接埠轉送
|
||||
- **OpenID Connect 支援**:輕鬆實現單點登入 (SSO) 並保護您的應用程式
|
||||
- **ForwardAuth 支援**:整合任何 auth provider (例如 TinyAuth)
|
||||
- **客製化**
|
||||
- [HTTP 中介軟體](https://docs.godoxy.dev/Middlewares)
|
||||
- [支援自訂錯誤頁面](https://docs.godoxy.dev/Custom-Error-Pages)
|
||||
- **網頁使用者介面 (Web UI)**
|
||||
- 應用程式一覽
|
||||
- 設定編輯器
|
||||
- 執行時間與系統指標
|
||||
- Docker 日誌檢視器
|
||||
- **跨平台支援**
|
||||
- 支援 **linux/amd64** 與 **linux/arm64**
|
||||
- **高效能**
|
||||
- 以 **[Go](https://go.dev)** 語言編寫
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
|
||||
## 入門指南
|
||||
|
||||
完整文檔請參見 **[Wiki](https://github.com/yusing/go-proxy/wiki)**
|
||||
|
||||
### 前置需求
|
||||
## 前置需求
|
||||
|
||||
設置 DNS 記錄指向運行 `GoDoxy` 的機器,例如:
|
||||
|
||||
- A 記錄:`*.y.z` -> `10.0.10.1`
|
||||
- AAAA 記錄:`*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
### 安裝
|
||||
## 安裝
|
||||
|
||||
1. 拉取最新的 Docker 映像
|
||||
> [!NOTE]
|
||||
> GoDoxy 僅在 `host` 網路模式下運作,請勿更改。
|
||||
>
|
||||
> 如需更改監聽埠,請修改 `.env`。
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/yusing/go-proxy:latest
|
||||
```
|
||||
1. 準備一個新目錄用於 docker compose 和配置文件。
|
||||
|
||||
2. 建立新目錄,`cd` 進入後運行安裝,或[手動安裝](#手動安裝)
|
||||
2. 在目錄內運行安裝腳本,或[手動安裝](#手動安裝)
|
||||
|
||||
```shell
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/godoxy setup
|
||||
```
|
||||
```shell
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||
```
|
||||
|
||||
3. _(可選)_ 設置網頁介面登入
|
||||
|
||||
- 設置隨機 JWT 密鑰
|
||||
|
||||
```shell
|
||||
sed -i "s|API_JWT_SECRET=.*|API_JWT_SECRET=$(openssl rand -base64 32)|g" .env
|
||||
```
|
||||
|
||||
- 更改網頁介面認證的使用者名稱和密碼
|
||||
```shell
|
||||
USERNAME=admin
|
||||
PASSWORD=some-password
|
||||
sed -i "s|API_USERNAME=.*|API_USERNAME=${USERNAME}|g" .env
|
||||
sed -i "s|API_PASSWORD=.*|API_PASSWORD=${PASSWORD}|g" .env
|
||||
```
|
||||
|
||||
4. _(可選)_ 設置其他 Docker 節點的 `docker-socket-proxy`(參見 [多 Docker 節點設置](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)),然後在 `config.yml` 中添加它們
|
||||
|
||||
5. 啟動容器 `docker compose up -d`
|
||||
|
||||
6. 現在您可以進行額外的配置
|
||||
- 使用文字編輯器(如 Visual Studio Code)
|
||||
- 通過網頁介面 `https://gp.y.z`
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
||||
|
||||
### 手動安裝
|
||||
|
||||
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
|
||||
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/config.example.yml -O config/config.yml`
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/godoxy/main/config.example.yml -O config/config.yml`
|
||||
|
||||
2. 將 `.env.example` 下載到 `.env`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/.env.example -O .env`
|
||||
`wget https://raw.githubusercontent.com/yusing/godoxy/main/.env.example -O .env`
|
||||
|
||||
3. 將 `compose.example.yml` 下載到 `compose.yml`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/compose.example.yml -O compose.yml`
|
||||
`wget https://raw.githubusercontent.com/yusing/godoxy/main/compose.example.yml -O compose.yml`
|
||||
|
||||
### 資料夾結構
|
||||
|
||||
@@ -131,14 +139,26 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
│ │ ├── middleware2.yml
|
||||
│ ├── provider1.yml
|
||||
│ └── provider2.yml
|
||||
├── data
|
||||
│ ├── metrics # metrics data
|
||||
│ │ ├── uptime.json
|
||||
│ │ └── system_info.json
|
||||
└── .env
|
||||
```
|
||||
|
||||
### 在 VSCode 中使用 JSON Schema
|
||||
## 更新 / 卸載系統代理 (System Agent)
|
||||
|
||||
複製 [`.vscode/settings.example.json`](.vscode/settings.example.json) 到 `.vscode/settings.json` 並根據需要修改
|
||||
更新:
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
```bash
|
||||
sudo /bin/bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- update
|
||||
```
|
||||
|
||||
卸載:
|
||||
|
||||
```bash
|
||||
sudo /bin/bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- uninstall
|
||||
```
|
||||
|
||||
## 截圖
|
||||
|
||||
@@ -146,11 +166,24 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
|
||||

|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
### 監控
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
|
||||
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b>路由</b></td>
|
||||
<td align="center"><b>伺服器</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
## 自行編譯
|
||||
|
||||
1. 克隆儲存庫 `git clone https://github.com/yusing/go-proxy --depth=1`
|
||||
1. 克隆儲存庫 `git clone https://github.com/yusing/godoxy --depth=1`
|
||||
|
||||
2. 如果尚未安裝,請安裝/升級 [go (>=1.22)](https://go.dev/doc/install) 和 `make`
|
||||
|
||||
@@ -160,4 +193,8 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
|
||||
5. 使用 `make build` 編譯二進制檔案
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#yusing/godoxy&Date)
|
||||
|
||||
[🔼 回到頂部](#目錄)
|
||||
|
||||
81
agent/cmd/main.go
Normal file
81
agent/cmd/main.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/env"
|
||||
"github.com/yusing/godoxy/agent/pkg/server"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||
httpServer "github.com/yusing/goutils/server"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/task"
|
||||
"github.com/yusing/goutils/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
writer := zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: "01-02 15:04",
|
||||
}
|
||||
zerolog.TimeFieldFormat = writer.TimeFormat
|
||||
log.Logger = zerolog.New(writer).Level(zerolog.InfoLevel).With().Timestamp().Logger()
|
||||
ca := &agent.PEMPair{}
|
||||
err := ca.Load(env.AgentCACert)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("init CA error")
|
||||
}
|
||||
caCert, err := ca.ToTLSCert()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("init CA error")
|
||||
}
|
||||
|
||||
srv := &agent.PEMPair{}
|
||||
srv.Load(env.AgentSSLCert)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("init SSL error")
|
||||
}
|
||||
srvCert, err := srv.ToTLSCert()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("init SSL error")
|
||||
}
|
||||
|
||||
log.Info().Msgf("GoDoxy Agent version %s", version.Get())
|
||||
log.Info().Msgf("Agent name: %s", env.AgentName)
|
||||
log.Info().Msgf("Agent port: %d", env.AgentPort)
|
||||
log.Info().Msgf("Agent runtime: %s", env.Runtime)
|
||||
|
||||
log.Info().Msg(`
|
||||
Tips:
|
||||
1. To change the agent name, you can set the AGENT_NAME environment variable.
|
||||
2. To change the agent port, you can set the AGENT_PORT environment variable.
|
||||
`)
|
||||
|
||||
t := task.RootTask("agent", false)
|
||||
opts := server.Options{
|
||||
CACert: caCert,
|
||||
ServerCert: srvCert,
|
||||
Port: env.AgentPort,
|
||||
}
|
||||
|
||||
server.StartAgentServer(t, opts)
|
||||
|
||||
if socketproxy.ListenAddr != "" {
|
||||
runtime := strutils.Title(string(env.Runtime))
|
||||
|
||||
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
||||
opts := httpServer.Options{
|
||||
Name: runtime,
|
||||
HTTPAddr: socketproxy.ListenAddr,
|
||||
Handler: socketproxy.NewHandler(),
|
||||
}
|
||||
httpServer.StartServer(t, opts)
|
||||
}
|
||||
|
||||
systeminfo.Poller.Start()
|
||||
|
||||
task.WaitExit(3)
|
||||
}
|
||||
115
agent/go.mod
Normal file
115
agent/go.mod
Normal file
@@ -0,0 +1,115 @@
|
||||
module github.com/yusing/godoxy/agent
|
||||
|
||||
go 1.25.3
|
||||
|
||||
replace github.com/yusing/godoxy => ..
|
||||
|
||||
replace github.com/yusing/godoxy/socketproxy => ../socket-proxy
|
||||
|
||||
replace github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
||||
|
||||
replace github.com/yusing/goutils => ../goutils
|
||||
|
||||
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.1
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
github.com/yusing/godoxy v0.19.2
|
||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/goutils v0.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // 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/cascadia v1.3.3 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v28.5.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-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // 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/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.9 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // 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/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
||||
github.com/yusing/ds v0.3.1 // indirect
|
||||
github.com/yusing/gointernals v0.1.16 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // 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/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // 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
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
351
agent/go.sum
Normal file
351
agent/go.sum
Normal file
@@ -0,0 +1,351 @@
|
||||
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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
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/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
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/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/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-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/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.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=
|
||||
github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
|
||||
github.com/docker/cli v28.5.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/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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.27.0 h1:cIhWd7Uj4BNFLEF3IpwuMkukVVRs5qjlp4KdUGa75yU=
|
||||
github.com/go-acme/lego/v4 v4.27.0/go.mod h1:9FfNZHZmg6hf5CWOp4Lzo4gU8aBEvqZvrwdkBboa+4g=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
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/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
||||
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/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.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/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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
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/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
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/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/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0 h1:K3+PJieRyi2rX/eaJZ95EdmpY/pzdeDd3jRnIQZG6kU=
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
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/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
||||
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
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/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
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/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
68
agent/pkg/agent/agent_pool.go
Normal file
68
agent/pkg/agent/agent_pool.go
Normal file
@@ -0,0 +1,68 @@
|
||||
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
|
||||
}
|
||||
33
agent/pkg/agent/bare_metal.go
Normal file
33
agent/pkg/agent/bare_metal.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var (
|
||||
installScript = `AGENT_NAME="{{.Name}}" \
|
||||
AGENT_PORT="{{.Port}}" \
|
||||
AGENT_CA_CERT="{{.CACert}}" \
|
||||
AGENT_SSL_CERT="{{.SSLCert}}" \
|
||||
{{ if eq .ContainerRuntime "nerdctl" -}}
|
||||
DOCKER_SOCKET="/var/run/containerd/containerd.sock" \
|
||||
RUNTIME="nerdctl" \
|
||||
{{ else if eq .ContainerRuntime "podman" -}}
|
||||
DOCKER_SOCKET="/var/run/podman/podman.sock" \
|
||||
RUNTIME="podman" \
|
||||
{{ else -}}
|
||||
DOCKER_SOCKET="/var/run/docker.sock" \
|
||||
RUNTIME="docker" \
|
||||
{{ end -}}
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/install-agent.sh)"`
|
||||
installScriptTemplate = template.Must(template.New("install.sh").Parse(installScript))
|
||||
)
|
||||
|
||||
func (c *AgentEnvConfig) Generate() (string, error) {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
if err := installScriptTemplate.Execute(buf, c); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
241
agent/pkg/agent/config.go
Normal file
241
agent/pkg/agent/config.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||
"github.com/yusing/goutils/version"
|
||||
)
|
||||
|
||||
type AgentConfig struct {
|
||||
Addr string `json:"addr"`
|
||||
Name string `json:"name"`
|
||||
Version version.Version `json:"version" swaggertype:"string"`
|
||||
Runtime ContainerRuntime `json:"runtime"`
|
||||
|
||||
httpClient *http.Client
|
||||
fasthttpClientHealthCheck *fasthttp.Client
|
||||
tlsConfig tls.Config
|
||||
l zerolog.Logger
|
||||
} // @name Agent
|
||||
|
||||
const (
|
||||
EndpointVersion = "/version"
|
||||
EndpointName = "/name"
|
||||
EndpointRuntime = "/runtime"
|
||||
EndpointProxyHTTP = "/proxy/http"
|
||||
EndpointHealth = "/health"
|
||||
EndpointLogs = "/logs"
|
||||
EndpointSystemInfo = "/system_info"
|
||||
|
||||
AgentHost = CertsDNSName
|
||||
|
||||
APIEndpointBase = "/godoxy/agent"
|
||||
APIBaseURL = "https://" + AgentHost + APIEndpointBase
|
||||
|
||||
DockerHost = "https://" + AgentHost
|
||||
|
||||
FakeDockerHostPrefix = "agent://"
|
||||
FakeDockerHostPrefixLen = len(FakeDockerHostPrefix)
|
||||
)
|
||||
|
||||
func mustParseURL(urlStr string) *url.URL {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
var (
|
||||
AgentURL = mustParseURL(APIBaseURL)
|
||||
HTTPProxyURL = mustParseURL(APIBaseURL + EndpointProxyHTTP)
|
||||
HTTPProxyURLPrefixLen = len(APIEndpointBase + EndpointProxyHTTP)
|
||||
)
|
||||
|
||||
func IsDockerHostAgent(dockerHost string) bool {
|
||||
return strings.HasPrefix(dockerHost, FakeDockerHostPrefix)
|
||||
}
|
||||
|
||||
func GetAgentAddrFromDockerHost(dockerHost string) string {
|
||||
return dockerHost[FakeDockerHostPrefixLen:]
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) FakeDockerHost() string {
|
||||
return FakeDockerHostPrefix + cfg.Addr
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Parse(addr string) error {
|
||||
cfg.Addr = addr
|
||||
return nil
|
||||
}
|
||||
|
||||
var serverVersion = version.Get()
|
||||
|
||||
func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte) error {
|
||||
clientCert, err := tls.X509KeyPair(crt, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create tls config
|
||||
caCertPool := x509.NewCertPool()
|
||||
ok := caCertPool.AppendCertsFromPEM(ca)
|
||||
if !ok {
|
||||
return errors.New("invalid ca certificate")
|
||||
}
|
||||
|
||||
cfg.tlsConfig = tls.Config{
|
||||
Certificates: []tls.Certificate{clientCert},
|
||||
RootCAs: caCertPool,
|
||||
ServerName: CertsDNSName,
|
||||
}
|
||||
|
||||
// create transport and http client
|
||||
cfg.httpClient = cfg.NewHTTPClient()
|
||||
applyNormalTransportConfig(cfg.httpClient)
|
||||
|
||||
cfg.fasthttpClientHealthCheck = cfg.NewFastHTTPHealthCheckClient()
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// get agent name
|
||||
name, _, err := cfg.fetchString(ctx, EndpointName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Name = name
|
||||
|
||||
cfg.l = log.With().Str("agent", cfg.Name).Logger()
|
||||
|
||||
// check agent version
|
||||
agentVersion, _, err := cfg.fetchString(ctx, EndpointVersion)
|
||||
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) {
|
||||
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.Name, serverVersion, cfg.Version)
|
||||
}
|
||||
|
||||
log.Info().Msgf("agent %q initialized", cfg.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Start(ctx context.Context) error {
|
||||
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid agent host: %s", cfg.Addr)
|
||||
}
|
||||
|
||||
certData, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read agent certs: %w", err)
|
||||
}
|
||||
|
||||
ca, crt, key, err := certs.ExtractCert(certData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract agent certs: %w", err)
|
||||
}
|
||||
|
||||
return cfg.StartWithCerts(ctx, ca, crt, key)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: cfg.Transport(),
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) NewFastHTTPHealthCheckClient() *fasthttp.Client {
|
||||
return &fasthttp.Client{
|
||||
Dial: func(addr string) (net.Conn, error) {
|
||||
if addr != AgentHost+":443" {
|
||||
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
||||
}
|
||||
return net.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 (cfg *AgentConfig) Transport() *http.Transport {
|
||||
return &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if addr != AgentHost+":443" {
|
||||
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
||||
}
|
||||
if network != "tcp" {
|
||||
return nil, &net.OpError{Op: "dial", Net: network, Source: nil, Addr: nil}
|
||||
}
|
||||
return cfg.DialContext(ctx)
|
||||
},
|
||||
TLSClientConfig: &cfg.tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
var dialer = &net.Dialer{Timeout: 5 * time.Second}
|
||||
|
||||
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, "tcp", cfg.Addr)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) String() string {
|
||||
return cfg.Name + "@" + cfg.Addr
|
||||
}
|
||||
|
||||
func applyNormalTransportConfig(client *http.Client) {
|
||||
transport := client.Transport.(*http.Transport)
|
||||
transport.MaxIdleConns = 100
|
||||
transport.MaxIdleConnsPerHost = 100
|
||||
transport.ReadBufferSize = 16384
|
||||
transport.WriteBufferSize = 16384
|
||||
}
|
||||
28
agent/pkg/agent/docker_compose.go
Normal file
28
agent/pkg/agent/docker_compose.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed templates/agent.compose.yml.tmpl
|
||||
agentComposeYAML string
|
||||
agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml.tmpl").Parse(agentComposeYAML))
|
||||
)
|
||||
|
||||
const (
|
||||
DockerImageProduction = "ghcr.io/yusing/godoxy-agent:latest"
|
||||
DockerImageNightly = "ghcr.io/yusing/godoxy-agent:nightly"
|
||||
)
|
||||
|
||||
func (c *AgentComposeConfig) Generate() (string, error) {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
err := agentComposeYAMLTemplate.Execute(buf, c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
25
agent/pkg/agent/env.go
Normal file
25
agent/pkg/agent/env.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package agent
|
||||
|
||||
type (
|
||||
ContainerRuntime string
|
||||
AgentEnvConfig struct {
|
||||
Name string
|
||||
Port int
|
||||
CACert string
|
||||
SSLCert string
|
||||
ContainerRuntime ContainerRuntime
|
||||
}
|
||||
AgentComposeConfig struct {
|
||||
Image string
|
||||
*AgentEnvConfig
|
||||
}
|
||||
Generator interface {
|
||||
Generate() (string, error)
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
ContainerRuntimeDocker ContainerRuntime = "docker"
|
||||
ContainerRuntimePodman ContainerRuntime = "podman"
|
||||
// ContainerRuntimeNerdctl ContainerRuntime = "nerdctl"
|
||||
)
|
||||
115
agent/pkg/agent/http_requests.go
Normal file
115
agent/pkg/agent/http_requests.go
Normal file
@@ -0,0 +1,115 @@
|
||||
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)
|
||||
}
|
||||
247
agent/pkg/agent/new_agent.go
Normal file
247
agent/pkg/agent/new_agent.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
CertsDNSName = "godoxy.agent"
|
||||
)
|
||||
|
||||
func toPEMPair(certDER []byte, key *ecdsa.PrivateKey) *PEMPair {
|
||||
marshaledKey, err := marshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
// This is a critical internal error during PEM encoding of a newly generated key.
|
||||
// Panicking is acceptable here as it indicates a fundamental issue.
|
||||
panic(fmt.Sprintf("failed to marshal EC private key for PEM encoding: %v", err))
|
||||
}
|
||||
return &PEMPair{
|
||||
Cert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}),
|
||||
Key: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: marshaledKey}),
|
||||
}
|
||||
}
|
||||
|
||||
func marshalECPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) {
|
||||
derBytes, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal EC private key: %w", err)
|
||||
}
|
||||
return derBytes, nil
|
||||
}
|
||||
|
||||
func b64Encode(data []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func b64Decode(data string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(data)
|
||||
}
|
||||
|
||||
type PEMPair struct {
|
||||
Cert, Key []byte
|
||||
}
|
||||
|
||||
func (p *PEMPair) String() string {
|
||||
return b64Encode(p.Cert) + ";" + b64Encode(p.Key)
|
||||
}
|
||||
|
||||
func (p *PEMPair) Load(data string) (err error) {
|
||||
parts := strings.Split(data, ";")
|
||||
if len(parts) != 2 {
|
||||
return errors.New("invalid PEM pair")
|
||||
}
|
||||
p.Cert, err = b64Decode(parts[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Key, err = b64Decode(parts[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PEMPair) Encrypt(encKey []byte) (PEMPair, error) {
|
||||
cert, err := encrypt(p.Cert, encKey)
|
||||
if err != nil {
|
||||
return PEMPair{}, err
|
||||
}
|
||||
key, err := encrypt(p.Key, encKey)
|
||||
if err != nil {
|
||||
return PEMPair{}, err
|
||||
}
|
||||
return PEMPair{Cert: cert, Key: key}, nil
|
||||
}
|
||||
|
||||
func (p *PEMPair) Decrypt(encKey []byte) (PEMPair, error) {
|
||||
cert, err := decrypt(p.Cert, encKey)
|
||||
if err != nil {
|
||||
return PEMPair{}, err
|
||||
}
|
||||
key, err := decrypt(p.Key, encKey)
|
||||
if err != nil {
|
||||
return PEMPair{}, err
|
||||
}
|
||||
return PEMPair{Cert: cert, Key: key}, nil
|
||||
}
|
||||
|
||||
func encrypt(data []byte, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gcm.Seal(nonce, nonce, data, nil), nil
|
||||
}
|
||||
|
||||
func decrypt(data []byte, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := data[:gcm.NonceSize()]
|
||||
ciphertext := data[gcm.NonceSize():]
|
||||
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
|
||||
func (p *PEMPair) ToTLSCert() (*tls.Certificate, error) {
|
||||
cert, err := tls.X509KeyPair(p.Cert, p.Key)
|
||||
return &cert, err
|
||||
}
|
||||
|
||||
func newSerialNumber() (*big.Int, error) {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) // 128-bit random number
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate serial number: %w", err)
|
||||
}
|
||||
return serialNumber, nil
|
||||
}
|
||||
|
||||
func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||
caSerialNumber, err := newSerialNumber()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
// Create the CA's certificate
|
||||
caTemplate := &x509.Certificate{
|
||||
SerialNumber: caSerialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"GoDoxy"},
|
||||
CommonName: CertsDNSName,
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1000, 0, 0), // 1000 years
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
MaxPathLen: 0,
|
||||
MaxPathLenZero: true,
|
||||
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||
}
|
||||
|
||||
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
ca = toPEMPair(caDER, caKey)
|
||||
|
||||
// Generate a new private key for the server certificate
|
||||
serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
serverSerialNumber, err := newSerialNumber()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
srvTemplate := &x509.Certificate{
|
||||
SerialNumber: serverSerialNumber,
|
||||
Issuer: caTemplate.Subject,
|
||||
Subject: pkix.Name{
|
||||
Organization: caTemplate.Subject.Organization,
|
||||
OrganizationalUnit: []string{"Server"},
|
||||
CommonName: CertsDNSName,
|
||||
},
|
||||
DNSNames: []string{CertsDNSName},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||
}
|
||||
|
||||
srvCertDER, err := x509.CreateCertificate(rand.Reader, srvTemplate, caTemplate, &serverKey.PublicKey, caKey)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
srv = toPEMPair(srvCertDER, serverKey)
|
||||
|
||||
clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
clientSerialNumber, err := newSerialNumber()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
clientTemplate := &x509.Certificate{
|
||||
SerialNumber: clientSerialNumber,
|
||||
Issuer: caTemplate.Subject,
|
||||
Subject: pkix.Name{
|
||||
Organization: caTemplate.Subject.Organization,
|
||||
OrganizationalUnit: []string{"Client"},
|
||||
CommonName: CertsDNSName,
|
||||
},
|
||||
DNSNames: []string{CertsDNSName},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1000, 0, 0),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||
}
|
||||
clientCertDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, caTemplate, &clientKey.PublicKey, caKey)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
client = toPEMPair(clientCertDER, clientKey)
|
||||
return ca, srv, client, err
|
||||
}
|
||||
112
agent/pkg/agent/new_agent_test.go
Normal file
112
agent/pkg/agent/new_agent_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewAgent(t *testing.T) {
|
||||
ca, srv, client, err := NewAgent()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ca)
|
||||
require.NotNil(t, srv)
|
||||
require.NotNil(t, client)
|
||||
}
|
||||
|
||||
func TestPEMPair(t *testing.T) {
|
||||
ca, srv, client, err := NewAgent()
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, p := range []*PEMPair{ca, srv, client} {
|
||||
t.Run(fmt.Sprintf("load-%d", i), func(t *testing.T) {
|
||||
var pp PEMPair
|
||||
err := pp.Load(p.String())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, p.Cert, pp.Cert)
|
||||
require.Equal(t, p.Key, pp.Key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPEMPairToTLSCert(t *testing.T) {
|
||||
ca, srv, client, err := NewAgent()
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, p := range []*PEMPair{ca, srv, client} {
|
||||
t.Run(fmt.Sprintf("toTLSCert-%d", i), func(t *testing.T) {
|
||||
cert, err := p.ToTLSCert()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cert)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerClient(t *testing.T) {
|
||||
ca, srv, client, err := NewAgent()
|
||||
require.NoError(t, err)
|
||||
|
||||
srvTLS, err := srv.ToTLSCert()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, srvTLS)
|
||||
|
||||
clientTLS, err := client.ToTLSCert()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, clientTLS)
|
||||
|
||||
caPool := x509.NewCertPool()
|
||||
require.True(t, caPool.AppendCertsFromPEM(ca.Cert))
|
||||
|
||||
srvTLSConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*srvTLS},
|
||||
ClientCAs: caPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
|
||||
clientTLSConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*clientTLS},
|
||||
RootCAs: caPool,
|
||||
ServerName: CertsDNSName,
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
server.TLS = srvTLSConfig
|
||||
server.StartTLS()
|
||||
defer server.Close()
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{TLSClientConfig: clientTLSConfig},
|
||||
}
|
||||
|
||||
resp, err := httpClient.Get(server.URL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestPEMPairEncryptDecrypt(t *testing.T) {
|
||||
encKey := make([]byte, 32)
|
||||
_, err := rand.Read(encKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
ca, _, _, err := NewAgent()
|
||||
require.NoError(t, err)
|
||||
|
||||
encCA, err := ca.Encrypt(encKey)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, encCA)
|
||||
|
||||
decCA, err := encCA.Decrypt(encKey)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decCA)
|
||||
|
||||
require.Equal(t, string(ca.Cert), string(decCA.Cert))
|
||||
require.Equal(t, string(ca.Key), string(decCA.Key))
|
||||
}
|
||||
44
agent/pkg/agent/templates/agent.compose.yml
Normal file
44
agent/pkg/agent/templates/agent.compose.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
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
|
||||
66
agent/pkg/agent/templates/agent.compose.yml.tmpl
Normal file
66
agent/pkg/agent/templates/agent.compose.yml.tmpl
Normal file
@@ -0,0 +1,66 @@
|
||||
services:
|
||||
agent:
|
||||
image: "{{.Image}}"
|
||||
container_name: godoxy-agent
|
||||
restart: always
|
||||
{{ if eq .ContainerRuntime "podman" -}}
|
||||
ports:
|
||||
- "{{.Port}}:{{.Port}}"
|
||||
{{ else -}}
|
||||
network_mode: host # do not change this
|
||||
{{ end -}}
|
||||
environment:
|
||||
{{ if eq .ContainerRuntime "nerdctl" -}}
|
||||
DOCKER_SOCKET: "/var/run/containerd/containerd.sock"
|
||||
RUNTIME: "nerdctl"
|
||||
{{ else if eq .ContainerRuntime "podman" -}}
|
||||
DOCKER_SOCKET: "/var/run/podman/podman.sock"
|
||||
RUNTIME: "podman"
|
||||
{{ else -}}
|
||||
DOCKER_SOCKET: "/var/run/docker.sock"
|
||||
RUNTIME: "docker"
|
||||
{{ end -}}
|
||||
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:
|
||||
{{ if eq .ContainerRuntime "podman" -}}
|
||||
- /var/run/podman/podman.sock:/var/run/podman/podman.sock
|
||||
{{ else if eq .ContainerRuntime "nerdctl" -}}
|
||||
- /var/run/containerd/containerd.sock:/var/run/containerd/containerd.sock
|
||||
- /var/lib/nerdctl:/var/lib/nerdctl:ro # required to read metadata like network info
|
||||
{{ else -}}
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
{{ end -}}
|
||||
- ./data:/app/data
|
||||
73
agent/pkg/agentproxy/config.go
Normal file
73
agent/pkg/agentproxy/config.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package agentproxy
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
route "github.com/yusing/godoxy/internal/route/types"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Scheme string `json:"scheme,omitempty"`
|
||||
Host string `json:"host,omitempty"` // host or host:port
|
||||
|
||||
route.HTTPConfig
|
||||
}
|
||||
|
||||
func ConfigFromHeaders(h http.Header) (Config, error) {
|
||||
cfg, err := proxyConfigFromHeaders(h)
|
||||
if cfg.Host == "" || err != nil {
|
||||
cfg = proxyConfigFromHeadersLegacy(h)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func proxyConfigFromHeadersLegacy(h http.Header) (cfg Config) {
|
||||
cfg.Host = h.Get(HeaderXProxyHost)
|
||||
isHTTPS, _ := strconv.ParseBool(h.Get(HeaderXProxyHTTPS))
|
||||
cfg.NoTLSVerify, _ = strconv.ParseBool(h.Get(HeaderXProxySkipTLSVerify))
|
||||
responseHeaderTimeout, err := strconv.Atoi(h.Get(HeaderXProxyResponseHeaderTimeout))
|
||||
if err != nil {
|
||||
responseHeaderTimeout = 0
|
||||
}
|
||||
cfg.ResponseHeaderTimeout = time.Duration(responseHeaderTimeout) * time.Second
|
||||
|
||||
cfg.Scheme = "http"
|
||||
if isHTTPS {
|
||||
cfg.Scheme = "https"
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func proxyConfigFromHeaders(h http.Header) (cfg Config, err error) {
|
||||
cfg.Scheme = h.Get(HeaderXProxyScheme)
|
||||
cfg.Host = h.Get(HeaderXProxyHost)
|
||||
|
||||
cfgBase64 := h.Get(HeaderXProxyConfig)
|
||||
cfgJSON, err := base64.StdEncoding.DecodeString(cfgBase64)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
err = sonic.Unmarshal(cfgJSON, &cfg)
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header) {
|
||||
h.Set(HeaderXProxyHost, cfg.Host)
|
||||
h.Set(HeaderXProxyHTTPS, strconv.FormatBool(cfg.Scheme == "https"))
|
||||
h.Set(HeaderXProxySkipTLSVerify, strconv.FormatBool(cfg.NoTLSVerify))
|
||||
h.Set(HeaderXProxyResponseHeaderTimeout, strconv.Itoa(int(cfg.ResponseHeaderTimeout.Round(time.Second).Seconds())))
|
||||
}
|
||||
|
||||
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header) {
|
||||
h.Set(HeaderXProxyHost, cfg.Host)
|
||||
h.Set(HeaderXProxyScheme, string(cfg.Scheme))
|
||||
cfgJSON, _ := sonic.Marshal(cfg.HTTPConfig)
|
||||
cfgBase64 := base64.StdEncoding.EncodeToString(cfgJSON)
|
||||
h.Set(HeaderXProxyConfig, cfgBase64)
|
||||
}
|
||||
14
agent/pkg/agentproxy/headers.go
Normal file
14
agent/pkg/agentproxy/headers.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package agentproxy
|
||||
|
||||
const (
|
||||
HeaderXProxyScheme = "X-Proxy-Scheme"
|
||||
HeaderXProxyHost = "X-Proxy-Host"
|
||||
HeaderXProxyConfig = "X-Proxy-Config"
|
||||
)
|
||||
|
||||
// deprecated
|
||||
const (
|
||||
HeaderXProxyHTTPS = "X-Proxy-Https"
|
||||
HeaderXProxySkipTLSVerify = "X-Proxy-Skip-Tls-Verify"
|
||||
HeaderXProxyResponseHeaderTimeout = "X-Proxy-Response-Header-Timeout"
|
||||
)
|
||||
85
agent/pkg/certs/zip.go
Normal file
85
agent/pkg/certs/zip.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
const AgentCertsBasePath = "certs"
|
||||
|
||||
func writeFile(zipWriter *zip.Writer, name string, data []byte) error {
|
||||
w, err := zipWriter.CreateHeader(&zip.FileHeader{
|
||||
Name: name,
|
||||
Method: zip.Store,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
func readFile(f *zip.File) ([]byte, error) {
|
||||
r, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
return io.ReadAll(r)
|
||||
}
|
||||
|
||||
func ZipCert(ca, crt, key []byte) ([]byte, error) {
|
||||
data := bytes.NewBuffer(make([]byte, 0, 6144))
|
||||
zipWriter := zip.NewWriter(data)
|
||||
defer zipWriter.Close()
|
||||
|
||||
if err := writeFile(zipWriter, "ca.pem", ca); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writeFile(zipWriter, "cert.pem", crt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writeFile(zipWriter, "key.pem", key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := zipWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data.Bytes(), nil
|
||||
}
|
||||
|
||||
func isValidAgentHost(host string) bool {
|
||||
return strutils.IsValidFilename(host + ".zip")
|
||||
}
|
||||
|
||||
func AgentCertsFilepath(host string) (filepathOut string, ok bool) {
|
||||
if !isValidAgentHost(host) {
|
||||
return "", false
|
||||
}
|
||||
return filepath.Join(AgentCertsBasePath, host+".zip"), true
|
||||
}
|
||||
|
||||
func ExtractCert(data []byte) (ca, crt, key []byte, err error) {
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
for _, file := range zipReader.File {
|
||||
switch file.Name {
|
||||
case "ca.pem":
|
||||
ca, err = readFile(file)
|
||||
case "cert.pem":
|
||||
crt, err = readFile(file)
|
||||
case "key.pem":
|
||||
key, err = readFile(file)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
return ca, crt, key, nil
|
||||
}
|
||||
20
agent/pkg/certs/zip_test.go
Normal file
20
agent/pkg/certs/zip_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package certs_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||
)
|
||||
|
||||
func TestZipCert(t *testing.T) {
|
||||
ca, crt, key := []byte("test1"), []byte("test2"), []byte("test3")
|
||||
zipData, err := certs.ZipCert(ca, crt, key)
|
||||
require.NoError(t, err)
|
||||
|
||||
ca2, crt2, key2, err := certs.ExtractCert(zipData)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca, ca2)
|
||||
require.Equal(t, crt, crt2)
|
||||
require.Equal(t, key, key2)
|
||||
}
|
||||
49
agent/pkg/env/env.go
vendored
Normal file
49
agent/pkg/env/env.go
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/goutils/env"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func DefaultAgentName() string {
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "agent"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
var (
|
||||
AgentName string
|
||||
AgentPort int
|
||||
AgentSkipClientCertCheck bool
|
||||
AgentCACert string
|
||||
AgentSSLCert string
|
||||
DockerSocket string
|
||||
Runtime agent.ContainerRuntime
|
||||
)
|
||||
|
||||
func init() {
|
||||
Load()
|
||||
}
|
||||
|
||||
func Load() {
|
||||
DockerSocket = env.GetEnvString("DOCKER_SOCKET", "/var/run/docker.sock")
|
||||
AgentName = env.GetEnvString("AGENT_NAME", DefaultAgentName())
|
||||
AgentPort = env.GetEnvInt("AGENT_PORT", 8890)
|
||||
AgentSkipClientCertCheck = env.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false)
|
||||
|
||||
AgentCACert = env.GetEnvString("AGENT_CA_CERT", "")
|
||||
AgentSSLCert = env.GetEnvString("AGENT_SSL_CERT", "")
|
||||
Runtime = agent.ContainerRuntime(env.GetEnvString("RUNTIME", "docker"))
|
||||
|
||||
switch Runtime {
|
||||
case agent.ContainerRuntimeDocker, agent.ContainerRuntimePodman: //, agent.ContainerRuntimeNerdctl:
|
||||
default:
|
||||
log.Fatal().Str("runtime", string(Runtime)).Msg("invalid runtime")
|
||||
}
|
||||
}
|
||||
82
agent/pkg/handler/check_health.go
Normal file
82
agent/pkg/handler/check_health.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"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) {
|
||||
query := r.URL.Query()
|
||||
scheme := query.Get("scheme")
|
||||
if scheme == "" {
|
||||
http.Error(w, "missing scheme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
result types.HealthCheckResult
|
||||
err error
|
||||
)
|
||||
switch scheme {
|
||||
case "fileserver":
|
||||
path := query.Get("path")
|
||||
if path == "" {
|
||||
http.Error(w, "missing path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err := os.Stat(path)
|
||||
result = types.HealthCheckResult{Healthy: err == nil}
|
||||
if err != nil {
|
||||
result.Detail = err.Error()
|
||||
}
|
||||
case "http", "https": // path is optional
|
||||
host := query.Get("host")
|
||||
path := query.Get("path")
|
||||
if host == "" {
|
||||
http.Error(w, "missing host", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: path,
|
||||
}, defaultHealthConfig).CheckHealth()
|
||||
case "tcp", "udp":
|
||||
host := query.Get("host")
|
||||
if host == "" {
|
||||
http.Error(w, "missing host", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hasPort := strings.Contains(host, ":")
|
||||
port := query.Get("port")
|
||||
if port != "" && hasPort {
|
||||
http.Error(w, "port and host with port cannot both be provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if port != "" {
|
||||
host = fmt.Sprintf("%s:%s", host, port)
|
||||
}
|
||||
result, err = monitor.NewRawHealthMonitor(&url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
}, defaultHealthConfig).CheckHealth()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
sonic.ConfigDefault.NewEncoder(w).Encode(result)
|
||||
}
|
||||
226
agent/pkg/handler/check_health_test.go
Normal file
226
agent/pkg/handler/check_health_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
|
||||
func TestCheckHealthHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupServer func() *httptest.Server
|
||||
queryParams map[string]string
|
||||
expectedStatus int
|
||||
expectedHealthy bool
|
||||
}{
|
||||
{
|
||||
name: "Valid",
|
||||
setupServer: func() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
},
|
||||
queryParams: map[string]string{
|
||||
"scheme": "http",
|
||||
"host": "localhost",
|
||||
"path": "/",
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedHealthy: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidQuery",
|
||||
setupServer: nil,
|
||||
queryParams: map[string]string{
|
||||
"scheme": "http",
|
||||
},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "ConnectionError",
|
||||
setupServer: nil,
|
||||
queryParams: map[string]string{
|
||||
"scheme": "http",
|
||||
"host": "localhost:12345",
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedHealthy: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var server *httptest.Server
|
||||
if tt.setupServer != nil {
|
||||
server = tt.setupServer()
|
||||
defer server.Close()
|
||||
u, _ := url.Parse(server.URL)
|
||||
tt.queryParams["scheme"] = u.Scheme
|
||||
tt.queryParams["host"] = u.Host
|
||||
tt.queryParams["path"] = u.Path
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
query := url.Values{}
|
||||
for key, value := range tt.queryParams {
|
||||
query.Set(key, value)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
||||
handler.CheckHealth(recorder, request)
|
||||
|
||||
require.Equal(t, recorder.Code, tt.expectedStatus)
|
||||
|
||||
if tt.expectedStatus == http.StatusOK {
|
||||
var result types.HealthCheckResult
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
||||
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHealthFileServer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expectedStatus int
|
||||
expectedHealthy bool
|
||||
expectedDetail string
|
||||
}{
|
||||
{
|
||||
name: "ValidPath",
|
||||
path: t.TempDir(),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedHealthy: true,
|
||||
expectedDetail: "",
|
||||
},
|
||||
{
|
||||
name: "InvalidPath",
|
||||
path: "/invalid",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedHealthy: false,
|
||||
expectedDetail: "stat /invalid: no such file or directory",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
query := url.Values{}
|
||||
query.Set("scheme", "fileserver")
|
||||
query.Set("path", tt.path)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
||||
handler.CheckHealth(recorder, request)
|
||||
|
||||
require.Equal(t, recorder.Code, tt.expectedStatus)
|
||||
|
||||
var result types.HealthCheckResult
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
||||
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
||||
require.Equal(t, result.Detail, tt.expectedDetail)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHealthTCPUDP(t *testing.T) {
|
||||
tcp, err := net.Listen("tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
go func() {
|
||||
conn, err := tcp.Accept()
|
||||
require.NoError(t, err)
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
udp, err := net.ListenPacket("udp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
go func() {
|
||||
buf := make([]byte, 1024)
|
||||
n, addr, err := udp.ReadFrom(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(buf[:n]), "ping")
|
||||
_, _ = udp.WriteTo([]byte("pong"), addr)
|
||||
udp.Close()
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scheme string
|
||||
host string
|
||||
port int
|
||||
expectedStatus int
|
||||
expectedHealthy bool
|
||||
}{
|
||||
{
|
||||
name: "ValidTCP",
|
||||
scheme: "tcp",
|
||||
host: "localhost",
|
||||
port: tcp.Addr().(*net.TCPAddr).Port,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedHealthy: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidHost",
|
||||
scheme: "tcp",
|
||||
host: "",
|
||||
port: 8080,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedHealthy: false,
|
||||
},
|
||||
{
|
||||
name: "ValidUDP",
|
||||
scheme: "udp",
|
||||
host: "localhost",
|
||||
port: udp.LocalAddr().(*net.UDPAddr).Port,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedHealthy: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidHost",
|
||||
scheme: "udp",
|
||||
host: "",
|
||||
port: 8080,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedHealthy: false,
|
||||
},
|
||||
{
|
||||
name: "Port in both host and port",
|
||||
scheme: "tcp",
|
||||
host: "localhost:1234",
|
||||
port: 1234,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedHealthy: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
query := url.Values{}
|
||||
query.Set("scheme", tt.scheme)
|
||||
query.Set("host", tt.host)
|
||||
query.Set("port", strconv.Itoa(tt.port))
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
||||
handler.CheckHealth(recorder, request)
|
||||
|
||||
require.Equal(t, recorder.Code, tt.expectedStatus)
|
||||
|
||||
if tt.expectedStatus == http.StatusOK {
|
||||
var result types.HealthCheckResult
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
||||
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
60
agent/pkg/handler/handler.go
Normal file
60
agent/pkg/handler/handler.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/env"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||
"github.com/yusing/goutils/version"
|
||||
)
|
||||
|
||||
type ServeMux struct{ *http.ServeMux }
|
||||
|
||||
func (mux ServeMux) HandleEndpoint(method, endpoint string, handler http.HandlerFunc) {
|
||||
mux.ServeMux.HandleFunc(method+" "+agent.APIEndpointBase+endpoint, handler)
|
||||
}
|
||||
|
||||
func (mux ServeMux) HandleFunc(endpoint string, handler http.HandlerFunc) {
|
||||
mux.ServeMux.HandleFunc(agent.APIEndpointBase+endpoint, handler)
|
||||
}
|
||||
|
||||
var upgrader = &websocket.Upgrader{
|
||||
// no origin check needed for internal websocket
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func NewAgentHandler() http.Handler {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
mux := ServeMux{http.NewServeMux()}
|
||||
|
||||
metricsHandler := gin.Default()
|
||||
{
|
||||
metrics := metricsHandler.Group(agent.APIEndpointBase)
|
||||
metrics.GET(agent.EndpointSystemInfo, func(c *gin.Context) {
|
||||
c.Set("upgrader", upgrader)
|
||||
systeminfo.Poller.ServeHTTP(c)
|
||||
})
|
||||
}
|
||||
|
||||
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
|
||||
mux.HandleEndpoint("GET", agent.EndpointVersion, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, version.Get())
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, env.AgentName)
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointRuntime, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, env.Runtime)
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
||||
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
||||
mux.ServeMux.HandleFunc("/", socketproxy.DockerSocketHandler(env.DockerSocket))
|
||||
return mux
|
||||
}
|
||||
59
agent/pkg/handler/proxy_http.go
Normal file
59
agent/pkg/handler/proxy_http.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agentproxy"
|
||||
)
|
||||
|
||||
func NewTransport() *http.Transport {
|
||||
return &http.Transport{
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 60 * time.Second,
|
||||
WriteBufferSize: 16 * 1024, // 16KB
|
||||
ReadBufferSize: 16 * 1024, // 16KB
|
||||
}
|
||||
}
|
||||
|
||||
func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
cfg, err := agentproxy.ConfigFromHeaders(r.Header)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to parse agent proxy config: %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
transport := NewTransport()
|
||||
if cfg.ResponseHeaderTimeout > 0 {
|
||||
transport.ResponseHeaderTimeout = cfg.ResponseHeaderTimeout
|
||||
}
|
||||
if cfg.DisableCompression {
|
||||
transport.DisableCompression = true
|
||||
}
|
||||
|
||||
transport.TLSClientConfig, err = cfg.BuildTLSConfig(r.URL)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to build TLS client config: %s", err.Error()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
r.URL.Scheme = ""
|
||||
r.URL.Host = ""
|
||||
r.URL.Path = r.URL.Path[agent.HTTPProxyURLPrefixLen:] // strip the {API_BASE}/proxy/http prefix
|
||||
r.RequestURI = r.URL.String()
|
||||
|
||||
rp := &httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
r.URL.Scheme = cfg.Scheme
|
||||
r.URL.Host = cfg.Host
|
||||
},
|
||||
Transport: transport,
|
||||
}
|
||||
rp.ServeHTTP(w, r)
|
||||
}
|
||||
43
agent/pkg/server/server.go
Normal file
43
agent/pkg/server/server.go
Normal file
@@ -0,0 +1,43 @@
|
||||
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))
|
||||
}
|
||||
BIN
assets/godoxy.png
Normal file
BIN
assets/godoxy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
120
bun.lock
120
bun.lock
@@ -1,120 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "godoxy-types",
|
||||
"devDependencies": {
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-json-schema": "^0.65.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="],
|
||||
|
||||
"@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="],
|
||||
|
||||
"@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="],
|
||||
|
||||
"@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@18.19.74", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A=="],
|
||||
|
||||
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||
|
||||
"acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="],
|
||||
|
||||
"diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"path-equal": ["path-equal@1.2.5", "", {}, "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="],
|
||||
|
||||
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
||||
|
||||
"typescript-json-schema": ["typescript-json-schema@0.65.1", "", { "dependencies": { "@types/json-schema": "^7.0.9", "@types/node": "^18.11.9", "glob": "^7.1.7", "path-equal": "^1.2.5", "safe-stable-stringify": "^2.2.0", "ts-node": "^10.9.1", "typescript": "~5.5.0", "yargs": "^17.1.1" }, "bin": { "typescript-json-schema": "bin/typescript-json-schema" } }, "sha512-tuGH7ff2jPaUYi6as3lHyHcKpSmXIqN7/mu50x3HlYn0EHzLpmt3nplZ7EuhUkO0eqDRc9GqWNkfjgBPIS9kxg=="],
|
||||
|
||||
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
|
||||
|
||||
"typescript-json-schema/typescript": ["typescript@5.5.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="],
|
||||
}
|
||||
}
|
||||
179
cmd/main.go
179
cmd/main.go
@@ -1,170 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal"
|
||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/query"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/pkg"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/api"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/config"
|
||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/logging"
|
||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
"github.com/yusing/godoxy/internal/metrics/uptime"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/server"
|
||||
"github.com/yusing/goutils/task"
|
||||
"github.com/yusing/goutils/version"
|
||||
)
|
||||
|
||||
var rawLogger = log.New(os.Stdout, "", 0)
|
||||
|
||||
func init() {
|
||||
var out io.Writer = os.Stderr
|
||||
if common.EnableLogStreaming {
|
||||
out = zerolog.MultiLevelWriter(out, v1.GetMemLogger())
|
||||
func parallel(fns ...func()) {
|
||||
var wg sync.WaitGroup
|
||||
for _, fn := range fns {
|
||||
wg.Go(fn)
|
||||
}
|
||||
logging.InitLogger(out)
|
||||
// logging.AddHook(v1.GetMemLogger())
|
||||
internal.InitIconListCache()
|
||||
homepage.InitOverridesConfig()
|
||||
favicon.InitIconCache()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func main() {
|
||||
initProfiling()
|
||||
args := common.GetArgs()
|
||||
|
||||
switch args.Command {
|
||||
case common.CommandSetup:
|
||||
internal.Setup()
|
||||
return
|
||||
case common.CommandReload:
|
||||
if err := query.ReloadServer(); err != nil {
|
||||
E.LogFatal("server reload error", err)
|
||||
}
|
||||
rawLogger.Println("ok")
|
||||
return
|
||||
case common.CommandListIcons:
|
||||
icons, err := internal.ListAvailableIcons()
|
||||
if err != nil {
|
||||
rawLogger.Fatal(err)
|
||||
}
|
||||
printJSON(icons)
|
||||
return
|
||||
case common.CommandListRoutes:
|
||||
routes, err := query.ListRoutes()
|
||||
if err != nil {
|
||||
log.Printf("failed to connect to api server: %s", err)
|
||||
log.Printf("falling back to config file")
|
||||
} else {
|
||||
printJSON(routes)
|
||||
return
|
||||
}
|
||||
case common.CommandDebugListMTrace:
|
||||
trace, err := query.ListMiddlewareTraces()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
printJSON(trace)
|
||||
return
|
||||
}
|
||||
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
||||
log.Info().Msgf("GoDoxy version %s", version.Get())
|
||||
log.Trace().Msg("trace enabled")
|
||||
parallel(
|
||||
dnsproviders.InitProviders,
|
||||
homepage.InitIconListCache,
|
||||
systeminfo.Poller.Start,
|
||||
middleware.LoadComposeFiles,
|
||||
)
|
||||
|
||||
if args.Command == common.CommandStart {
|
||||
logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
|
||||
logging.Trace().Msg("trace enabled")
|
||||
// logging.AddHook(notif.GetDispatcher())
|
||||
} else {
|
||||
logging.DiscardLogger()
|
||||
}
|
||||
|
||||
if args.Command == common.CommandValidate {
|
||||
data, err := os.ReadFile(common.ConfigPath)
|
||||
if err == nil {
|
||||
err = config.Validate(data)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal("config error: ", err)
|
||||
}
|
||||
log.Print("config OK")
|
||||
return
|
||||
if common.APIJWTSecret == nil {
|
||||
log.Warn().Msg("API_JWT_SECRET is not set, using random key")
|
||||
common.APIJWTSecret = common.RandomJWTKey()
|
||||
}
|
||||
|
||||
for _, dir := range common.RequiredDirectories {
|
||||
prepareDirectory(dir)
|
||||
}
|
||||
|
||||
middleware.LoadComposeFiles()
|
||||
|
||||
var cfg *config.Config
|
||||
var err E.Error
|
||||
if cfg, err = config.Load(); err != nil {
|
||||
E.LogWarn("errors in config", err)
|
||||
err := config.Load()
|
||||
if err != nil {
|
||||
gperr.LogWarn("errors in config", err)
|
||||
}
|
||||
|
||||
switch args.Command {
|
||||
case common.CommandListRoutes:
|
||||
cfg.StartProxyProviders()
|
||||
printJSON(routequery.RoutesByAlias())
|
||||
return
|
||||
case common.CommandListConfigs:
|
||||
printJSON(cfg.Value())
|
||||
return
|
||||
case common.CommandDebugListEntries:
|
||||
printJSON(cfg.DumpEntries())
|
||||
return
|
||||
case common.CommandDebugListProviders:
|
||||
printJSON(cfg.DumpRouteProviders())
|
||||
return
|
||||
}
|
||||
|
||||
cfg.Start(&config.StartServersOptions{
|
||||
Proxy: true,
|
||||
})
|
||||
config.StartProxyServers()
|
||||
if err := auth.Initialize(); err != nil {
|
||||
logging.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.
|
||||
cfg.StartServers(&config.StartServersOptions{
|
||||
API: true,
|
||||
server.StartServer(task.RootTask("api_server", false), server.Options{
|
||||
Name: "api",
|
||||
HTTPAddr: common.APIHTTPAddr,
|
||||
Handler: api.NewHandler(),
|
||||
})
|
||||
|
||||
uptime.Poller.Start()
|
||||
config.WatchChanges()
|
||||
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT)
|
||||
signal.Notify(sig, syscall.SIGTERM)
|
||||
signal.Notify(sig, syscall.SIGHUP)
|
||||
|
||||
// wait for signal
|
||||
<-sig
|
||||
|
||||
// gracefully shutdown
|
||||
logging.Info().Msg("shutting down")
|
||||
_ = task.GracefulShutdown(time.Second * time.Duration(cfg.Value().TimeoutShutdown))
|
||||
task.WaitExit(config.Value().TimeoutShutdown)
|
||||
}
|
||||
|
||||
func prepareDirectory(dir string) {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dir, 0o755); err != nil {
|
||||
logging.Fatal().Msgf("failed to create directory %s: %v", dir, err)
|
||||
log.Fatal().Msgf("failed to create directory %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printJSON(obj any) {
|
||||
j, err := json.MarshalIndent(obj, "", " ")
|
||||
if err != nil {
|
||||
logging.Fatal().Err(err).Send()
|
||||
}
|
||||
rawLogger.Print(string(j)) // raw output for convenience using "jq"
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
//go:build pprof
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
func initProfiling() {
|
||||
runtime.GOMAXPROCS(2)
|
||||
debug.SetMemoryLimit(100 * 1024 * 1024)
|
||||
debug.SetMaxStack(15 * 1024 * 1024)
|
||||
go func() {
|
||||
log.Println(http.ListenAndServe(":7777", nil))
|
||||
}()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build production
|
||||
//go:build !pprof
|
||||
|
||||
package main
|
||||
|
||||
48
cmd/pprof_prof.go
Normal file
48
cmd/pprof_prof.go
Normal file
@@ -0,0 +1,48 @@
|
||||
//go:build pprof
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
func initProfiling() {
|
||||
go func() {
|
||||
log.Info().Msgf("pprof server started at http://localhost:7777/debug/pprof/")
|
||||
log.Error().Err(http.ListenAndServe(":7777", nil)).Msg("pprof server failed")
|
||||
}()
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * 10)
|
||||
defer ticker.Stop()
|
||||
|
||||
var m runtime.MemStats
|
||||
var gcStats debug.GCStats
|
||||
|
||||
for range ticker.C {
|
||||
runtime.ReadMemStats(&m)
|
||||
debug.ReadGCStats(&gcStats)
|
||||
|
||||
log.Info().Msgf("-----------------------------------------------------")
|
||||
log.Info().Msgf("Timestamp: %s", time.Now().Format(time.RFC3339))
|
||||
log.Info().Msgf(" Go Heap - In Use (Alloc/HeapAlloc): %s", strutils.FormatByteSize(m.Alloc))
|
||||
log.Info().Msgf(" Go Heap - Reserved from OS (HeapSys): %s", strutils.FormatByteSize(m.HeapSys))
|
||||
log.Info().Msgf(" Go Stacks - In Use (StackInuse): %s", strutils.FormatByteSize(m.StackInuse))
|
||||
log.Info().Msgf(" Go Runtime - Other Sys (MSpanInuse, MCacheInuse, BuckHashSys, GCSys, OtherSys): %s", strutils.FormatByteSize(m.MSpanInuse+m.MCacheInuse+m.BuckHashSys+m.GCSys+m.OtherSys))
|
||||
log.Info().Msgf(" Go Runtime - Total from OS (Sys): %s", strutils.FormatByteSize(m.Sys))
|
||||
log.Info().Msgf(" Go Runtime - Freed from OS (HeapReleased): %s", strutils.FormatByteSize(m.HeapReleased))
|
||||
log.Info().Msgf(" Number of Goroutines: %d", runtime.NumGoroutine())
|
||||
log.Info().Msgf(" Number of completed GC cycles: %d", m.NumGC)
|
||||
log.Info().Msgf(" Number of GCs: %d", gcStats.NumGC)
|
||||
log.Info().Msgf(" Total GC time: %s", gcStats.PauseTotal)
|
||||
log.Info().Msgf(" Last GC time: %s", gcStats.LastGC.Format(time.DateTime))
|
||||
log.Info().Msg("-----------------------------------------------------")
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,35 +1,78 @@
|
||||
---
|
||||
services:
|
||||
socket-proxy:
|
||||
container_name: socket-proxy
|
||||
image: ghcr.io/yusing/socket-proxy:latest
|
||||
environment:
|
||||
- ALLOW_START=1
|
||||
- ALLOW_STOP=1
|
||||
- ALLOW_RESTARTS=1
|
||||
- CONTAINERS=1
|
||||
- EVENTS=1
|
||||
- INFO=1
|
||||
- PING=1
|
||||
- POST=1
|
||||
- VERSION=1
|
||||
volumes:
|
||||
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
restart: unless-stopped
|
||||
tmpfs:
|
||||
- /run
|
||||
ports:
|
||||
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
|
||||
frontend:
|
||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
|
||||
container_name: godoxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
network_mode: host # do not change this
|
||||
env_file: .env
|
||||
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /app/.next/cache # next image caching
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- all
|
||||
depends_on:
|
||||
- app
|
||||
# modify below to fit your needs
|
||||
environment:
|
||||
HOSTNAME: 127.0.0.1
|
||||
PORT: ${GODOXY_FRONTEND_PORT:-3000}
|
||||
labels:
|
||||
proxy.aliases: gp
|
||||
proxy.#1.port: 3000
|
||||
# proxy.#1.middlewares.cidr_whitelist.status: 403
|
||||
# proxy.#1.middlewares.cidr_whitelist.message: IP not allowed
|
||||
# proxy.#1.middlewares.cidr_whitelist.allow: |
|
||||
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
|
||||
proxy.#1.port: ${GODOXY_FRONTEND_PORT:-3000}
|
||||
# proxy.#1.middlewares.cidr_whitelist: |
|
||||
# status: 403
|
||||
# message: IP not allowed
|
||||
# allow:
|
||||
# - 127.0.0.1
|
||||
# - 10.0.0.0/8
|
||||
# - 192.168.0.0/16
|
||||
# - 172.16.0.0/12
|
||||
app:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: godoxy
|
||||
image: ghcr.io/yusing/godoxy:${TAG:-latest}
|
||||
container_name: godoxy-proxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
network_mode: host # do not change this
|
||||
env_file: .env
|
||||
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||
depends_on:
|
||||
socket-proxy:
|
||||
condition: service_started
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- all
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
environment:
|
||||
- DOCKER_HOST=tcp://${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./config:/app/config
|
||||
- ./logs:/app/logs
|
||||
- ./error_pages:/app/error_pages
|
||||
- ./error_pages:/app/error_pages:ro
|
||||
- ./data:/app/data
|
||||
|
||||
# To use autocert, certs will be stored in "./certs".
|
||||
# You can also use a docker volume to store it
|
||||
|
||||
@@ -15,28 +15,76 @@
|
||||
# options:
|
||||
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
|
||||
# 3. other providers, see https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers#supported-dns-01-providers
|
||||
# 3. other providers, see https://docs.godoxy.dev/DNS-01-Providers
|
||||
|
||||
# Access Control
|
||||
# When enabled, it will be applied globally at connection level,
|
||||
# all incoming connections (web, tcp and udp) will be checked against the ACL rules.
|
||||
|
||||
# acl:
|
||||
# default: allow # or deny (default: allow)
|
||||
# allow_local: true # or false (default: true)
|
||||
# allow:
|
||||
# - ip:1.2.3.4
|
||||
# - cidr:1.2.3.4/32
|
||||
# - country:US
|
||||
# - timezone:Asia/Shanghai
|
||||
# deny:
|
||||
# - ip:1.2.3.4
|
||||
# - cidr:1.2.3.4/32
|
||||
# - country:US
|
||||
# - timezone:Asia/Shanghai
|
||||
# log: # warning: logging ACL can be slow based on the number of incoming connections and configured rules
|
||||
# path: /app/logs/acl.log # (default: none)
|
||||
# stdout: false # (default: false)
|
||||
# keep: 30 days # (default: 30 days)
|
||||
# log_allowed: false # (default: false)
|
||||
# notify:
|
||||
# interval: 1m # (default: 1m)
|
||||
# to: [gotify, discord] # names under providers.notification
|
||||
# include_allowed: false # (default: false)
|
||||
|
||||
entrypoint:
|
||||
# Proxy Protocol: https://www.haproxy.com/blog/use-the-proxy-protocol-to-preserve-a-clients-ip-address
|
||||
# When set to true, web entrypoint and all tcp routes will be wrapped with Proxy Protocol listener in order to preserve the client's IP address.
|
||||
# Note that HTTP/3 with proxy protocol is not supported yet.
|
||||
support_proxy_protocol: false
|
||||
|
||||
# Below define an example of middleware config
|
||||
# 1. block non local IP connections
|
||||
# 2. redirect HTTP to HTTPS
|
||||
# 1. set security headers
|
||||
# 2. block non local IP connections
|
||||
# 3. redirect HTTP to HTTPS
|
||||
#
|
||||
# middlewares:
|
||||
# - use: CIDRWhitelist
|
||||
# allow:
|
||||
# - "127.0.0.1"
|
||||
# - "10.0.0.0/8"
|
||||
# - "172.16.0.0/12"
|
||||
# - "192.168.0.0/16"
|
||||
# status: 403
|
||||
# message: "Forbidden"
|
||||
# - use: RedirectHTTP
|
||||
middlewares:
|
||||
- use: CloudflareRealIP
|
||||
- use: ModifyResponse
|
||||
set_headers:
|
||||
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
|
||||
Access-Control-Allow-Headers: "*"
|
||||
Access-Control-Allow-Origin: "*"
|
||||
Access-Control-Max-Age: 180
|
||||
Vary: "*"
|
||||
X-XSS-Protection: 1; mode=block
|
||||
Content-Security-Policy: "object-src 'self'; frame-ancestors 'self';"
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: SAMEORIGIN
|
||||
Referrer-Policy: same-origin
|
||||
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
|
||||
# - use: RedirectHTTP
|
||||
|
||||
# below enables access log
|
||||
access_log:
|
||||
format: combined
|
||||
path: /app/logs/entrypoint.log
|
||||
stdout: false # (default: false)
|
||||
keep: 30 days # (default: 30 days)
|
||||
|
||||
# customize behavior for non-existent routes, e.g. pass over to another proxy
|
||||
#
|
||||
# rules:
|
||||
# not_found:
|
||||
# - name: default
|
||||
# do: proxy http://other-proxy:8080
|
||||
|
||||
providers:
|
||||
# include files are standalone yaml files under `config/` directory
|
||||
@@ -61,9 +109,13 @@ providers:
|
||||
# remote-1: tcp://10.0.2.1:2375
|
||||
# remote-2: ssh://root:1234@10.0.2.2
|
||||
|
||||
# notification providers (notify when service health changes)
|
||||
# notification providers
|
||||
#
|
||||
# notification:
|
||||
# - name: ntfy
|
||||
# provider: ntfy
|
||||
# url: https://ntfy.domain.tld
|
||||
# topic: godoxy
|
||||
# - name: gotify
|
||||
# provider: gotify
|
||||
# url: https://gotify.domain.tld
|
||||
@@ -72,9 +124,22 @@ providers:
|
||||
# provider: webhook
|
||||
# url: https://discord.com/api/webhooks/...
|
||||
# template: discord # this means use payload template from internal/notif/templates/discord.json
|
||||
# - name: pushover
|
||||
# provider: webhook
|
||||
# url: https://api.pushover.net/1/messages.json
|
||||
# mime_type: application/x-www-form-urlencoded
|
||||
# payload: '{"token": "your-app-token", "user": "your-user-key", "title": $title, "message": $message}'
|
||||
|
||||
# Check https://github.com/yusing/go-proxy/wiki/Certificates-and-domain-matching#domain-matching
|
||||
# for explaination of `match_domains`
|
||||
# Proxmox providers (for idlesleep support for proxmox LXCs)
|
||||
#
|
||||
# proxmox:
|
||||
# - url: https://pve.domain.com:8006/api2/json
|
||||
# token_id: root@pam!abcdef
|
||||
# secret: aaaa-bbbb-cccc-dddd
|
||||
# no_tls_verify: true
|
||||
|
||||
# Match domains
|
||||
# See https://docs.godoxy.dev/Certificates-and-domain-matching
|
||||
#
|
||||
# match_domains:
|
||||
# - my.site
|
||||
|
||||
7
dev.Dockerfile
Normal file
7
dev.Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["/app/run"]
|
||||
69
dev.compose.yml
Normal file
69
dev.compose.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
services:
|
||||
app:
|
||||
image: godoxy-dev
|
||||
build:
|
||||
context: .
|
||||
dockerfile: dev.Dockerfile
|
||||
container_name: godoxy-proxy-dev
|
||||
restart: unless-stopped
|
||||
env_file: dev.env
|
||||
environment:
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
TZ: Asia/Hong_Kong
|
||||
API_ADDR: 127.0.0.1:8999
|
||||
API_USER: dev
|
||||
API_PASSWORD: 1234
|
||||
API_SKIP_ORIGIN_CHECK: true
|
||||
API_JWT_TTL: 24h
|
||||
DEBUG: true
|
||||
API_JWT_SECRET: 1234567891234567
|
||||
labels:
|
||||
proxy.exclude: true
|
||||
proxy.#1.healthcheck.disable: true
|
||||
ipc: host
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./bin/godoxy:/app/run:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./dev-data/config:/app/config
|
||||
- ./dev-data/certs:/app/certs
|
||||
- ./dev-data/error_pages:/app/error_pages:ro
|
||||
- ./dev-data/data:/app/data
|
||||
- ./dev-data/logs:/app/logs
|
||||
- ~/certs/myCA.pem:/etc/ssl/certs/ca.crt:ro
|
||||
parca:
|
||||
image: ghcr.io/parca-dev/parca:v0.24.2
|
||||
container_name: godoxy-parca
|
||||
restart: unless-stopped
|
||||
command: [/parca, --config-path, /parca.yaml]
|
||||
network_mode: host
|
||||
# ports:
|
||||
# - 7070:7070
|
||||
configs:
|
||||
- source: parca
|
||||
target: /parca.yaml
|
||||
labels:
|
||||
proxy.#1.port: "7070"
|
||||
tinyauth:
|
||||
image: ghcr.io/steveiliop56/tinyauth:v3
|
||||
container_name: tinyauth
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SECRET=12345678912345671234567891234567
|
||||
- APP_URL=https://tinyauth.my.app
|
||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||
labels:
|
||||
proxy.tinyauth.port: "3000"
|
||||
configs:
|
||||
parca:
|
||||
content: |
|
||||
object_storage:
|
||||
bucket:
|
||||
type: "FILESYSTEM"
|
||||
config:
|
||||
directory: "./data"
|
||||
scrape_configs:
|
||||
- job_name: "parca"
|
||||
scrape_interval: "1s"
|
||||
static_configs:
|
||||
- targets: [ 'localhost:7777' ]
|
||||
224
go.mod
224
go.mod
@@ -1,82 +1,186 @@
|
||||
module github.com/yusing/go-proxy
|
||||
module github.com/yusing/godoxy
|
||||
|
||||
go 1.23.5
|
||||
go 1.25.3
|
||||
|
||||
replace github.com/yusing/godoxy/agent => ./agent
|
||||
|
||||
replace github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
|
||||
|
||||
replace github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
||||
|
||||
replace github.com/shirou/gopsutil/v4 => ./internal/gopsutil
|
||||
|
||||
replace github.com/yusing/goutils => ./goutils
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/coder/websocket v1.8.12
|
||||
github.com/coreos/go-oidc/v3 v3.12.0
|
||||
github.com/docker/cli v27.5.1+incompatible
|
||||
github.com/docker/docker v27.5.1+incompatible
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/go-acme/lego/v4 v4.21.0
|
||||
github.com/go-playground/validator/v10 v10.24.0
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/gotify/server/v2 v2.6.1
|
||||
github.com/lithammer/fuzzysearch v1.1.8
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/vincent-petithory/dataurl v1.0.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/oauth2 v0.25.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/time v0.9.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
||||
github.com/coreos/go-oidc/v3 v3.16.0 // oidc authentication
|
||||
github.com/docker/docker v28.5.1+incompatible // docker daemon
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/gin-gonic/gin v1.11.0 // api server
|
||||
github.com/go-acme/lego/v4 v4.27.0 // acme client
|
||||
github.com/go-playground/validator/v10 v10.28.0 // validator
|
||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||
github.com/gotify/server/v2 v2.7.3 // reference the Message struct for json response
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||
github.com/pires/go-proxyproto v0.8.1 // proxy protocol support
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // lock free map for concurrent operations
|
||||
github.com/rs/zerolog v1.34.0 // logging
|
||||
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/net v0.46.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.32.0 // oauth2 authentication
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/time v0.14.0 // time utilities
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/docker/cli v28.5.1+incompatible
|
||||
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/luthermonson/go-proxmox v0.2.3
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect; http3 support
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0 // indirect
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yusing/ds v0.3.1
|
||||
github.com/yusing/godoxy/agent v0.0.0-20251025144347-1ec2872f3d4c
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251025144347-1ec2872f3d4c
|
||||
github.com/yusing/goutils v0.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 // 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/privatedns/armprivatedns v1.3.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/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/diskfs/go-diskfs v1.7.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magefile/mage v1.15.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/mod v0.29.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/api v0.253.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // 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 (
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.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-resty/resty/v2 v2.16.5 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/linode/linodego v1.60.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // 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/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.14 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vultr/govultr/v3 v3.24.0 // 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.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
|
||||
)
|
||||
|
||||
473
go.sum
473
go.sum
@@ -1,89 +1,186 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
|
||||
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/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s=
|
||||
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/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/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk=
|
||||
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/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/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.5.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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
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/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
|
||||
github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
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/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
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/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
|
||||
github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
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/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
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/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/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-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=
|
||||
github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
|
||||
github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
|
||||
github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
|
||||
github.com/docker/cli v28.5.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/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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.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/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
|
||||
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o=
|
||||
github.com/go-acme/lego/v4 v4.21.0/go.mod h1:HrSWzm3Ckj45Ie3i+p1zKVobbQoMOaGu9m4up0dUeDI=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.27.0 h1:cIhWd7Uj4BNFLEF3IpwuMkukVVRs5qjlp4KdUGa75yU=
|
||||
github.com/go-acme/lego/v4 v4.27.0/go.mod h1:9FfNZHZmg6hf5CWOp4Lzo4gU8aBEvqZvrwdkBboa+4g=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
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/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
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/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gotify/server/v2 v2.6.1 h1:Kf7v5fzBxzELzZa/jonWfwJMkqYqh1LBzBpCmt5QIAI=
|
||||
github.com/gotify/server/v2 v2.6.1/go.mod h1:Dk8HLyTVDqmXM8YEg6tjROBen6mxyHZFRggJFHTwZLc=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
||||
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/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/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
|
||||
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -92,8 +189,16 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/linode/linodego v1.60.0 h1:SgsebJFRCi+lSmYy+C40wmKZeJllGGm+W12Qw4+yVdI=
|
||||
github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.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/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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
@@ -101,96 +206,173 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
||||
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
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/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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
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/oci-go-sdk/common/v1065 v1065.102.1 h1:45giryNXrlUHzK/Cd4DDBOhaK0EklXrhjTgv00Zo5po=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 h1:2EthQw4pEN2rbbSLWlF9itV+Ws2xmAmIcfKYsrwCbVA=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1/go.mod h1:xOLJ0zNGmF4M4LqdQclLONwdzjJewNl/7WQiZgrvYR8=
|
||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
|
||||
github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||
github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
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/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
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/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0 h1:K3+PJieRyi2rX/eaJZ95EdmpY/pzdeDd3jRnIQZG6kU=
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
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/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
||||
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/vultr/govultr/v3 v3.24.0 h1:fTTTj0VBve+Miy+wGhlb90M2NMDfpGFi6Frlj3HVy6M=
|
||||
github.com/vultr/govultr/v3 v3.24.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
||||
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
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/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
||||
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
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=
|
||||
@@ -199,29 +381,31 @@ 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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-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 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -229,8 +413,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
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=
|
||||
@@ -248,40 +432,45 @@ 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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
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/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
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=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.253.0 h1:apU86Eq9Q2eQco3NsUYFpVTfy7DwemojL7LmbAj7g/I=
|
||||
google.golang.org/api v0.253.0/go.mod h1:PX09ad0r/4du83vZVAaGg7OaeyGnaUmT/CYPNvtLCbw=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
|
||||
1
goutils
Submodule
1
goutils
Submodule
Submodule goutils added at c0955732e9
310
internal/acl/config.go
Normal file
310
internal/acl/config.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||
"github.com/yusing/godoxy/internal/maxmind"
|
||||
"github.com/yusing/godoxy/internal/notif"
|
||||
"github.com/yusing/godoxy/internal/utils"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
|
||||
AllowLocal *bool `json:"allow_local"` // default: true
|
||||
Allow Matchers `json:"allow"`
|
||||
Deny Matchers `json:"deny"`
|
||||
Log *accesslog.ACLLoggerConfig `json:"log"`
|
||||
|
||||
Notify struct {
|
||||
To []string `json:"to"` // list of notification providers
|
||||
Interval time.Duration `json:"interval"` // interval between notifications
|
||||
IncludeAllowed *bool `json:"include_allowed"` // default: false
|
||||
} `json:"notify"`
|
||||
|
||||
config
|
||||
valErr gperr.Error
|
||||
}
|
||||
|
||||
const defaultNotifyInterval = 1 * time.Minute
|
||||
|
||||
type config struct {
|
||||
defaultAllow bool
|
||||
allowLocal bool
|
||||
ipCache *xsync.Map[string, *checkCache]
|
||||
|
||||
// will be nil if Notify.To is empty
|
||||
// these are per IP, reset every Notify.Interval
|
||||
allowedCount map[string]uint32
|
||||
blockedCount map[string]uint32
|
||||
|
||||
// these are total, never reset
|
||||
totalAllowedCount uint64
|
||||
totalBlockedCount uint64
|
||||
|
||||
logAllowed bool
|
||||
// will be nil if Log is nil
|
||||
logger *accesslog.AccessLogger
|
||||
|
||||
// will never tick if Notify.To is empty
|
||||
notifyTicker *time.Ticker
|
||||
notifyAllowed bool
|
||||
|
||||
// will be nil if both Log and Notify.To are empty
|
||||
logNotifyCh chan ipLog
|
||||
}
|
||||
|
||||
type checkCache struct {
|
||||
*maxmind.IPInfo
|
||||
allow bool
|
||||
created time.Time
|
||||
}
|
||||
|
||||
type ipLog struct {
|
||||
info *maxmind.IPInfo
|
||||
allowed bool
|
||||
}
|
||||
|
||||
// could be nil
|
||||
var ActiveConfig atomic.Pointer[Config]
|
||||
|
||||
const cacheTTL = 1 * time.Minute
|
||||
|
||||
func (c *checkCache) Expired() bool {
|
||||
return c.created.Add(cacheTTL).Before(utils.TimeNow())
|
||||
}
|
||||
|
||||
// TODO: add stats
|
||||
|
||||
const (
|
||||
ACLAllow = "allow"
|
||||
ACLDeny = "deny"
|
||||
)
|
||||
|
||||
func (c *Config) Validate() gperr.Error {
|
||||
switch c.Default {
|
||||
case "", ACLAllow:
|
||||
c.defaultAllow = true
|
||||
case ACLDeny:
|
||||
c.defaultAllow = false
|
||||
default:
|
||||
c.valErr = gperr.New("invalid default value").Subject(c.Default)
|
||||
return c.valErr
|
||||
}
|
||||
|
||||
if c.AllowLocal != nil {
|
||||
c.allowLocal = *c.AllowLocal
|
||||
} else {
|
||||
c.allowLocal = true
|
||||
}
|
||||
|
||||
if c.Notify.Interval < 0 {
|
||||
c.Notify.Interval = defaultNotifyInterval
|
||||
}
|
||||
|
||||
if c.Log != nil {
|
||||
c.logAllowed = c.Log.LogAllowed
|
||||
}
|
||||
|
||||
if !c.allowLocal && !c.defaultAllow && len(c.Allow) == 0 {
|
||||
c.valErr = gperr.New("allow_local is false and default is deny, but no allow rules are configured")
|
||||
return c.valErr
|
||||
}
|
||||
|
||||
c.ipCache = xsync.NewMap[string, *checkCache]()
|
||||
|
||||
if c.Notify.IncludeAllowed != nil {
|
||||
c.notifyAllowed = *c.Notify.IncludeAllowed
|
||||
} else {
|
||||
c.notifyAllowed = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) Valid() bool {
|
||||
return c != nil && c.valErr == nil
|
||||
}
|
||||
|
||||
func (c *Config) Start(parent task.Parent) gperr.Error {
|
||||
if c.Log != nil {
|
||||
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
||||
if err != nil {
|
||||
return gperr.New("failed to start access logger").With(err)
|
||||
}
|
||||
c.logger = logger
|
||||
}
|
||||
if c.valErr != nil {
|
||||
return c.valErr
|
||||
}
|
||||
|
||||
if c.needLogOrNotify() {
|
||||
c.logNotifyCh = make(chan ipLog, 100)
|
||||
}
|
||||
|
||||
if c.needNotify() {
|
||||
c.allowedCount = make(map[string]uint32)
|
||||
c.blockedCount = make(map[string]uint32)
|
||||
c.notifyTicker = time.NewTicker(c.Notify.Interval)
|
||||
} else {
|
||||
c.notifyTicker = time.NewTicker(time.Duration(math.MaxInt64)) // never tick
|
||||
}
|
||||
|
||||
if c.needLogOrNotify() {
|
||||
go c.logNotifyLoop(parent)
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("default", c.Default).
|
||||
Bool("allow_local", c.allowLocal).
|
||||
Int("allow_rules", len(c.Allow)).
|
||||
Int("deny_rules", len(c.Deny)).
|
||||
Msg("ACL started")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
|
||||
if common.ForceResolveCountry && info.City == nil {
|
||||
maxmind.LookupCity(info)
|
||||
}
|
||||
c.ipCache.Store(info.Str, &checkCache{
|
||||
IPInfo: info,
|
||||
allow: allow,
|
||||
created: utils.TimeNow(),
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Config) needLogOrNotify() bool {
|
||||
return c.needLog() || c.needNotify()
|
||||
}
|
||||
|
||||
func (c *Config) needLog() bool {
|
||||
return c.logger != nil
|
||||
}
|
||||
|
||||
func (c *Config) needNotify() bool {
|
||||
return len(c.Notify.To) > 0
|
||||
}
|
||||
|
||||
func (c *Config) getCachedCity(ip string) string {
|
||||
record, ok := c.ipCache.Load(ip)
|
||||
if ok {
|
||||
if record.City != nil {
|
||||
if record.City.Country.IsoCode != "" {
|
||||
return record.City.Country.IsoCode
|
||||
}
|
||||
return record.City.Location.TimeZone
|
||||
}
|
||||
}
|
||||
return "unknown location"
|
||||
}
|
||||
|
||||
func (c *Config) logNotifyLoop(parent task.Parent) {
|
||||
defer c.notifyTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-parent.Context().Done():
|
||||
return
|
||||
case log := <-c.logNotifyCh:
|
||||
if c.logger != nil {
|
||||
if !log.allowed || c.logAllowed {
|
||||
c.logger.LogACL(log.info, !log.allowed)
|
||||
}
|
||||
}
|
||||
if c.needNotify() {
|
||||
if log.allowed {
|
||||
if c.notifyAllowed {
|
||||
c.allowedCount[log.info.Str]++
|
||||
c.totalAllowedCount++
|
||||
}
|
||||
} else {
|
||||
c.blockedCount[log.info.Str]++
|
||||
c.totalBlockedCount++
|
||||
}
|
||||
}
|
||||
case <-c.notifyTicker.C: // will never tick when notify is disabled
|
||||
total := len(c.allowedCount) + len(c.blockedCount)
|
||||
if total == 0 {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
fieldsBody := make(notif.ListBody, total)
|
||||
i := 0
|
||||
fieldsBody[i] = fmt.Sprintf("Total: allowed %d, blocked %d", c.totalAllowedCount, c.totalBlockedCount)
|
||||
i++
|
||||
for ip, count := range c.allowedCount {
|
||||
fieldsBody[i] = fmt.Sprintf("%s (%s): allowed %d times", ip, c.getCachedCity(ip), count)
|
||||
i++
|
||||
}
|
||||
for ip, count := range c.blockedCount {
|
||||
fieldsBody[i] = fmt.Sprintf("%s (%s): blocked %d times", ip, c.getCachedCity(ip), count)
|
||||
i++
|
||||
}
|
||||
notif.Notify(¬if.LogMessage{
|
||||
Level: zerolog.InfoLevel,
|
||||
Title: "ACL Summary for last " + strutils.FormatDuration(c.Notify.Interval),
|
||||
Body: fieldsBody,
|
||||
To: c.Notify.To,
|
||||
})
|
||||
clear(c.allowedCount)
|
||||
clear(c.blockedCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log and notify if needed
|
||||
func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool) {
|
||||
if c.logNotifyCh != nil {
|
||||
c.logNotifyCh <- ipLog{info: info, allowed: allowed}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) IPAllowed(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// always allow loopback, not logged
|
||||
if ip.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
|
||||
if c.allowLocal && ip.IsPrivate() {
|
||||
c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
|
||||
return true
|
||||
}
|
||||
|
||||
ipStr := ip.String()
|
||||
record, ok := c.ipCache.Load(ipStr)
|
||||
if ok && !record.Expired() {
|
||||
c.logAndNotify(record.IPInfo, record.allow)
|
||||
return record.allow
|
||||
}
|
||||
|
||||
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
||||
if c.Allow.Match(ipAndStr) {
|
||||
c.logAndNotify(ipAndStr, true)
|
||||
c.cacheRecord(ipAndStr, true)
|
||||
return true
|
||||
}
|
||||
if c.Deny.Match(ipAndStr) {
|
||||
c.logAndNotify(ipAndStr, false)
|
||||
c.cacheRecord(ipAndStr, false)
|
||||
return false
|
||||
}
|
||||
|
||||
c.logAndNotify(ipAndStr, c.defaultAllow)
|
||||
c.cacheRecord(ipAndStr, c.defaultAllow)
|
||||
return c.defaultAllow
|
||||
}
|
||||
112
internal/acl/matcher.go
Normal file
112
internal/acl/matcher.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/godoxy/internal/maxmind"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
type MatcherFunc func(*maxmind.IPInfo) bool
|
||||
|
||||
type Matcher struct {
|
||||
match MatcherFunc
|
||||
}
|
||||
|
||||
type Matchers []Matcher
|
||||
|
||||
const (
|
||||
MatcherTypeIP = "ip"
|
||||
MatcherTypeCIDR = "cidr"
|
||||
MatcherTypeTimeZone = "tz"
|
||||
MatcherTypeCountry = "country"
|
||||
)
|
||||
|
||||
// TODO: use this error in the future
|
||||
//
|
||||
//nolint:unused
|
||||
var errMatcherFormat = gperr.Multiline().AddLines(
|
||||
"invalid matcher format, expect {type}:{value}",
|
||||
"Available types: ip|cidr|tz|country",
|
||||
"ip:127.0.0.1",
|
||||
"cidr:127.0.0.0/8",
|
||||
"tz:Asia/Shanghai",
|
||||
"country:GB",
|
||||
)
|
||||
|
||||
var (
|
||||
errSyntax = gperr.New("syntax error")
|
||||
errInvalidIP = gperr.New("invalid IP")
|
||||
errInvalidCIDR = gperr.New("invalid CIDR")
|
||||
)
|
||||
|
||||
func (matcher *Matcher) Parse(s string) error {
|
||||
parts := strings.Split(s, ":")
|
||||
if len(parts) != 2 {
|
||||
return errSyntax
|
||||
}
|
||||
|
||||
switch parts[0] {
|
||||
case MatcherTypeIP:
|
||||
ip := net.ParseIP(parts[1])
|
||||
if ip == nil {
|
||||
return errInvalidIP
|
||||
}
|
||||
matcher.match = matchIP(ip)
|
||||
case MatcherTypeCIDR:
|
||||
_, net, err := net.ParseCIDR(parts[1])
|
||||
if err != nil {
|
||||
return errInvalidCIDR
|
||||
}
|
||||
matcher.match = matchCIDR(net)
|
||||
case MatcherTypeTimeZone:
|
||||
matcher.match = matchTimeZone(parts[1])
|
||||
case MatcherTypeCountry:
|
||||
matcher.match = matchISOCode(parts[1])
|
||||
default:
|
||||
return errSyntax
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (matchers Matchers) Match(ip *maxmind.IPInfo) bool {
|
||||
for _, m := range matchers {
|
||||
if m.match(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchIP(ip net.IP) MatcherFunc {
|
||||
return func(ip2 *maxmind.IPInfo) bool {
|
||||
return ip.Equal(ip2.IP)
|
||||
}
|
||||
}
|
||||
|
||||
func matchCIDR(n *net.IPNet) MatcherFunc {
|
||||
return func(ip *maxmind.IPInfo) bool {
|
||||
return n.Contains(ip.IP)
|
||||
}
|
||||
}
|
||||
|
||||
func matchTimeZone(tz string) MatcherFunc {
|
||||
return func(ip *maxmind.IPInfo) bool {
|
||||
city, ok := maxmind.LookupCity(ip)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return city.Location.TimeZone == tz
|
||||
}
|
||||
}
|
||||
|
||||
func matchISOCode(iso string) MatcherFunc {
|
||||
return func(ip *maxmind.IPInfo) bool {
|
||||
city, ok := maxmind.LookupCity(ip)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return city.Country.IsoCode == iso
|
||||
}
|
||||
}
|
||||
49
internal/acl/matcher_test.go
Normal file
49
internal/acl/matcher_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
)
|
||||
|
||||
func TestMatchers(t *testing.T) {
|
||||
strMatchers := []string{
|
||||
"ip:127.0.0.1",
|
||||
"cidr:10.0.0.0/8",
|
||||
}
|
||||
|
||||
var mathers Matchers
|
||||
err := serialization.Convert(reflect.ValueOf(strMatchers), reflect.ValueOf(&mathers), false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"127.0.0.1", true},
|
||||
{"10.0.0.1", true},
|
||||
{"127.0.0.2", false},
|
||||
{"192.168.0.1", false},
|
||||
{"11.0.0.1", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
ip := net.ParseIP(test.ip)
|
||||
if ip == nil {
|
||||
t.Fatalf("invalid ip: %s", test.ip)
|
||||
}
|
||||
|
||||
got := mathers.Match(&maxmind.IPInfo{
|
||||
IP: ip,
|
||||
Str: test.ip,
|
||||
})
|
||||
if got != test.want {
|
||||
t.Errorf("mathers.Match(%s) = %v, want %v", test.ip, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
75
internal/acl/tcp_listener.go
Normal file
75
internal/acl/tcp_listener.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TCPListener struct {
|
||||
acl *Config
|
||||
lis net.Listener
|
||||
}
|
||||
|
||||
type noConn struct{}
|
||||
|
||||
func (noConn) Read(b []byte) (int, error) { return 0, io.EOF }
|
||||
func (noConn) Write(b []byte) (int, error) { return 0, io.EOF }
|
||||
func (noConn) Close() error { return nil }
|
||||
func (noConn) LocalAddr() net.Addr { return nil }
|
||||
func (noConn) RemoteAddr() net.Addr { return nil }
|
||||
func (noConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (noConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (noConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func (c *Config) WrapTCP(lis net.Listener) net.Listener {
|
||||
if c == nil {
|
||||
return lis
|
||||
}
|
||||
return &TCPListener{
|
||||
acl: c,
|
||||
lis: lis,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPListener) Addr() net.Addr {
|
||||
return s.lis.Addr()
|
||||
}
|
||||
|
||||
func (s *TCPListener) Accept() (net.Conn, error) {
|
||||
c, err := s.lis.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr, ok := c.RemoteAddr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
// Not a TCPAddr, drop
|
||||
c.Close()
|
||||
return noConn{}, nil
|
||||
}
|
||||
if !s.acl.IPAllowed(addr.IP) {
|
||||
c.Close()
|
||||
return noConn{}, nil
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type tcpListener interface {
|
||||
SetDeadline(t time.Time) error
|
||||
}
|
||||
|
||||
var _ tcpListener = (*net.TCPListener)(nil)
|
||||
|
||||
func (s *TCPListener) SetDeadline(t time.Time) error {
|
||||
switch lis := s.lis.(type) {
|
||||
case tcpListener:
|
||||
return lis.SetDeadline(t)
|
||||
default:
|
||||
return errors.New("not a TCPListener")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPListener) Close() error {
|
||||
return s.lis.Close()
|
||||
}
|
||||
105
internal/acl/udp_listener.go
Normal file
105
internal/acl/udp_listener.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UDPListener struct {
|
||||
acl *Config
|
||||
lis net.PacketConn
|
||||
}
|
||||
|
||||
func (c *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
|
||||
if c == nil {
|
||||
return lis
|
||||
}
|
||||
return &UDPListener{
|
||||
acl: c,
|
||||
lis: lis,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPListener) LocalAddr() net.Addr {
|
||||
return s.lis.LocalAddr()
|
||||
}
|
||||
|
||||
func (s *UDPListener) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
for {
|
||||
n, addr, err := s.lis.ReadFrom(p)
|
||||
if err != nil {
|
||||
return n, addr, err
|
||||
}
|
||||
udpAddr, ok := addr.(*net.UDPAddr)
|
||||
if !ok {
|
||||
// Not a UDPAddr, drop
|
||||
continue
|
||||
}
|
||||
if !s.acl.IPAllowed(udpAddr.IP) {
|
||||
// Drop packet from disallowed IP
|
||||
continue
|
||||
}
|
||||
return n, addr, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPListener) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
for {
|
||||
n, err := s.lis.WriteTo(p, addr)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
udpAddr, ok := addr.(*net.UDPAddr)
|
||||
if !ok {
|
||||
// Not a UDPAddr, drop
|
||||
continue
|
||||
}
|
||||
if !s.acl.IPAllowed(udpAddr.IP) {
|
||||
// Drop packet to disallowed IP
|
||||
continue
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPListener) SetDeadline(t time.Time) error {
|
||||
return s.lis.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (s *UDPListener) SetReadDeadline(t time.Time) error {
|
||||
return s.lis.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (s *UDPListener) SetWriteDeadline(t time.Time) error {
|
||||
return s.lis.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
type udpListener interface {
|
||||
SetReadBuffer(bytes int) error
|
||||
SetWriteBuffer(bytes int) error
|
||||
}
|
||||
|
||||
var _ udpListener = (*net.UDPConn)(nil)
|
||||
|
||||
func (s *UDPListener) SetReadBuffer(bytes int) error {
|
||||
switch lis := s.lis.(type) {
|
||||
case udpListener:
|
||||
return lis.SetReadBuffer(bytes)
|
||||
default:
|
||||
return errors.New("not a UDPConn")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPListener) SetWriteBuffer(bytes int) error {
|
||||
switch lis := s.lis.(type) {
|
||||
case udpListener:
|
||||
return lis.SetWriteBuffer(bytes)
|
||||
default:
|
||||
return errors.New("not a UDPConn")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPListener) Close() error {
|
||||
return s.lis.Close()
|
||||
}
|
||||
@@ -2,69 +2,219 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
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/common"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
type ServeMux struct{ *http.ServeMux }
|
||||
// @title GoDoxy API
|
||||
// @version 1.0
|
||||
// @description GoDoxy API
|
||||
// @termsOfService https://github.com/yusing/godoxy/blob/main/LICENSE
|
||||
|
||||
func (mux ServeMux) HandleFunc(methods, endpoint string, handler http.HandlerFunc) {
|
||||
for _, m := range strutils.CommaSeperatedList(methods) {
|
||||
mux.ServeMux.HandleFunc(m+" "+endpoint, handler)
|
||||
// @contact.name Yusing
|
||||
// @contact.url https://github.com/yusing/godoxy/issues
|
||||
|
||||
// @license.name MIT
|
||||
// @license.url https://github.com/yusing/godoxy/blob/main/LICENSE
|
||||
|
||||
// @BasePath /api/v1
|
||||
|
||||
// @externalDocs.description GoDoxy Docs
|
||||
// @externalDocs.url https://docs.godoxy.dev
|
||||
func NewHandler() *gin.Engine {
|
||||
if !common.IsDebug {
|
||||
gin.SetMode("release")
|
||||
}
|
||||
r := gin.New()
|
||||
r.Use(ErrorHandler())
|
||||
r.Use(ErrorLoggingMiddleware())
|
||||
|
||||
r.GET("/api/v1/version", apiV1.Version)
|
||||
|
||||
if auth.IsEnabled() {
|
||||
v1Auth := r.Group("/api/v1/auth")
|
||||
{
|
||||
v1Auth.HEAD("/check", authApi.Check)
|
||||
v1Auth.POST("/login", authApi.Login)
|
||||
v1Auth.GET("/callback", authApi.Callback)
|
||||
v1Auth.POST("/callback", authApi.Callback)
|
||||
v1Auth.POST("/logout", authApi.Logout)
|
||||
v1Auth.GET("/logout", authApi.Logout)
|
||||
}
|
||||
}
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
if auth.IsEnabled() {
|
||||
v1.Use(AuthMiddleware())
|
||||
}
|
||||
if common.APISkipOriginCheck {
|
||||
v1.Use(SkipOriginCheckMiddleware())
|
||||
}
|
||||
{
|
||||
// enable cache for favicon
|
||||
v1.GET("/favicon", apiV1.FavIcon).Use(Cache(time.Hour * 24))
|
||||
v1.GET("/health", apiV1.Health)
|
||||
v1.GET("/icons", apiV1.Icons)
|
||||
v1.POST("/reload", apiV1.Reload)
|
||||
v1.GET("/stats", apiV1.Stats)
|
||||
|
||||
route := v1.Group("/route")
|
||||
{
|
||||
route.GET("/list", routeApi.Routes)
|
||||
route.GET("/:which", routeApi.Route)
|
||||
route.GET("/providers", routeApi.Providers)
|
||||
route.GET("/by_provider", routeApi.ByProvider)
|
||||
route.POST("/playground", routeApi.Playground)
|
||||
}
|
||||
|
||||
file := v1.Group("/file")
|
||||
{
|
||||
file.GET("/list", fileApi.List)
|
||||
file.GET("/content", fileApi.Get)
|
||||
file.PUT("/content", fileApi.Set)
|
||||
file.POST("/content", fileApi.Set)
|
||||
file.POST("/validate", fileApi.Validate)
|
||||
}
|
||||
|
||||
homepage := v1.Group("/homepage")
|
||||
{
|
||||
homepage.GET("/categories", homepageApi.Categories)
|
||||
homepage.GET("/items", homepageApi.Items)
|
||||
homepage.POST("/set/item", homepageApi.SetItem)
|
||||
homepage.POST("/set/items_batch", homepageApi.SetItemsBatch)
|
||||
homepage.POST("/set/item_visible", homepageApi.SetItemVisible)
|
||||
homepage.POST("/set/item_favorite", homepageApi.SetItemFavorite)
|
||||
homepage.POST("/set/item_sort_order", homepageApi.SetItemSortOrder)
|
||||
homepage.POST("/set/item_all_sort_order", homepageApi.SetItemAllSortOrder)
|
||||
homepage.POST("/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder)
|
||||
homepage.POST("/set/category_order", homepageApi.SetCategoryOrder)
|
||||
homepage.POST("/item_click", homepageApi.ItemClick)
|
||||
}
|
||||
|
||||
cert := v1.Group("/cert")
|
||||
{
|
||||
cert.GET("/info", certApi.Info)
|
||||
cert.GET("/renew", certApi.Renew)
|
||||
}
|
||||
|
||||
agent := v1.Group("/agent")
|
||||
{
|
||||
agent.GET("/list", agentApi.List)
|
||||
agent.POST("/create", agentApi.Create)
|
||||
agent.POST("/verify", agentApi.Verify)
|
||||
}
|
||||
|
||||
metrics := v1.Group("/metrics")
|
||||
{
|
||||
metrics.GET("/system_info", metricsApi.SystemInfo)
|
||||
metrics.GET("/all_system_info", metricsApi.AllSystemInfo)
|
||||
metrics.GET("/uptime", metricsApi.Uptime)
|
||||
}
|
||||
|
||||
docker := v1.Group("/docker")
|
||||
{
|
||||
docker.GET("/container/:id", dockerApi.GetContainer)
|
||||
docker.GET("/containers", dockerApi.Containers)
|
||||
docker.GET("/info", dockerApi.Info)
|
||||
docker.GET("/logs/:id", dockerApi.Logs)
|
||||
docker.POST("/start", dockerApi.Start)
|
||||
docker.POST("/stop", dockerApi.Stop)
|
||||
docker.POST("/restart", dockerApi.Restart)
|
||||
}
|
||||
}
|
||||
|
||||
// disable cache by default
|
||||
r.Use(NoCache())
|
||||
return r
|
||||
}
|
||||
|
||||
func NoCache() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// skip cache if Cache-Control header is set or if caching is explicitly enabled
|
||||
if !c.GetBool("cache_enabled") && c.Writer.Header().Get("Cache-Control") == "" {
|
||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("Expires", "0")
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||
mux := ServeMux{http.NewServeMux()}
|
||||
mux.HandleFunc("GET", "/v1", v1.Index)
|
||||
mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
|
||||
mux.HandleFunc("POST", "/v1/reload", useCfg(cfg, v1.Reload))
|
||||
mux.HandleFunc("GET", "/v1/list", auth.RequireAuth(useCfg(cfg, v1.List)))
|
||||
mux.HandleFunc("GET", "/v1/list/{what}", auth.RequireAuth(useCfg(cfg, v1.List)))
|
||||
mux.HandleFunc("GET", "/v1/list/{what}/{which}", auth.RequireAuth(useCfg(cfg, v1.List)))
|
||||
mux.HandleFunc("GET", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.GetFileContent))
|
||||
mux.HandleFunc("POST,PUT", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.SetFileContent))
|
||||
mux.HandleFunc("POST", "/v1/file/validate/{type}", auth.RequireAuth(v1.ValidateFile))
|
||||
mux.HandleFunc("GET", "/v1/stats", useCfg(cfg, v1.Stats))
|
||||
mux.HandleFunc("GET", "/v1/stats/ws", useCfg(cfg, v1.StatsWS))
|
||||
mux.HandleFunc("GET", "/v1/health/ws", auth.RequireAuth(useCfg(cfg, v1.HealthWS)))
|
||||
mux.HandleFunc("GET", "/v1/logs/ws", auth.RequireAuth(useCfg(cfg, v1.LogsWS())))
|
||||
mux.HandleFunc("GET", "/v1/favicon", auth.RequireAuth(favicon.GetFavIcon))
|
||||
mux.HandleFunc("POST", "/v1/homepage/set", auth.RequireAuth(v1.SetHomePageOverrides))
|
||||
|
||||
if common.PrometheusEnabled {
|
||||
mux.Handle("GET /v1/metrics", promhttp.Handler())
|
||||
logging.Info().Msg("prometheus metrics enabled")
|
||||
func Cache(duration time.Duration) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Signal to NoCache middleware that caching is intended
|
||||
c.Set("cache_enabled", true)
|
||||
// skip cache if Cache-Control header is set
|
||||
if c.Writer.Header().Get("Cache-Control") == "" {
|
||||
c.Header("Cache-Control", "public, max-age="+strconv.FormatFloat(duration.Seconds(), 'f', 0, 64)+", immutable")
|
||||
c.Header("Pragma", "public")
|
||||
c.Header("Expires", time.Now().Add(duration).Format(time.RFC1123))
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
defaultAuth := auth.GetDefaultAuth()
|
||||
if defaultAuth != nil {
|
||||
mux.HandleFunc("GET", "/v1/auth/redirect", defaultAuth.RedirectLoginPage)
|
||||
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := defaultAuth.CheckToken(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
err := auth.GetDefaultAuth().CheckToken(c.Request)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, apitypes.Error("Unauthorized", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func SkipOriginCheckMiddleware() gin.HandlerFunc {
|
||||
upgrader := &websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
c.Set("upgrader", upgrader)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func ErrorHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
if len(c.Errors) > 0 {
|
||||
logger := log.With().Str("uri", c.Request.RequestURI).Logger()
|
||||
for _, err := range c.Errors {
|
||||
gperr.LogError("Internal error", err.Err, &logger)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.LoginCallbackHandler)
|
||||
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutCallbackHandler)
|
||||
} else {
|
||||
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
if !c.IsWebsocket() {
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return mux
|
||||
}
|
||||
|
||||
func useCfg(cfg config.ConfigInstance, handler func(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
handler(cfg, w, r)
|
||||
}
|
||||
func ErrorLoggingMiddleware() gin.HandlerFunc {
|
||||
return gin.CustomRecoveryWithWriter(nil, func(c *gin.Context, err any) {
|
||||
log.Error().Any("error", err).Str("uri", c.Request.RequestURI).Msg("Internal error")
|
||||
if !c.IsWebsocket() {
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
67
internal/api/v1/agent/common.go
Normal file
67
internal/api/v1/agent/common.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
)
|
||||
|
||||
type PEMPairResponse struct {
|
||||
Cert string `json:"cert" format:"base64"`
|
||||
Key string `json:"key" format:"base64"`
|
||||
} // @name PEMPairResponse
|
||||
|
||||
var encryptionKey atomic.Value
|
||||
|
||||
const rotateKeyInterval = 15 * time.Minute
|
||||
|
||||
func init() {
|
||||
if err := rotateKey(); err != nil {
|
||||
log.Panic().Err(err).Msg("failed to generate encryption key")
|
||||
}
|
||||
go func() {
|
||||
for range time.Tick(rotateKeyInterval) {
|
||||
if err := rotateKey(); err != nil {
|
||||
log.Error().Err(err).Msg("failed to rotate encryption key")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func getEncryptionKey() []byte {
|
||||
return encryptionKey.Load().([]byte)
|
||||
}
|
||||
|
||||
func rotateKey() error {
|
||||
// generate a random 32 bytes key
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return err
|
||||
}
|
||||
encryptionKey.Store(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func toPEMPairResponse(encPEMPair agent.PEMPair) PEMPairResponse {
|
||||
return PEMPairResponse{
|
||||
Cert: base64.StdEncoding.EncodeToString(encPEMPair.Cert),
|
||||
Key: base64.StdEncoding.EncodeToString(encPEMPair.Key),
|
||||
}
|
||||
}
|
||||
|
||||
func fromEncryptedPEMPairResponse(pemPair PEMPairResponse) (agent.PEMPair, error) {
|
||||
encCert, err := base64.StdEncoding.DecodeString(pemPair.Cert)
|
||||
if err != nil {
|
||||
return agent.PEMPair{}, err
|
||||
}
|
||||
encKey, err := base64.StdEncoding.DecodeString(pemPair.Key)
|
||||
if err != nil {
|
||||
return agent.PEMPair{}, err
|
||||
}
|
||||
pair := agent.PEMPair{Cert: encCert, Key: encKey}
|
||||
return pair.Decrypt(getEncryptionKey())
|
||||
}
|
||||
107
internal/api/v1/agent/create.go
Normal file
107
internal/api/v1/agent/create.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type NewAgentRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
Port int `json:"port" binding:"required,min=1,max=65535"`
|
||||
Type string `json:"type" binding:"required,oneof=docker system"`
|
||||
Nightly bool `json:"nightly" binding:"omitempty"`
|
||||
ContainerRuntime agent.ContainerRuntime `json:"container_runtime" binding:"omitempty,oneof=docker podman" default:"docker"`
|
||||
} // @name NewAgentRequest
|
||||
|
||||
type NewAgentResponse struct {
|
||||
Compose string `json:"compose"`
|
||||
CA PEMPairResponse `json:"ca"`
|
||||
Client PEMPairResponse `json:"client"`
|
||||
} // @name NewAgentResponse
|
||||
|
||||
// @x-id "create"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Create a new agent
|
||||
// @Description Create a new agent and return the docker compose file, encrypted CA and client PEMs
|
||||
// @Description The returned PEMs are encrypted with a random key and will be used for verification when adding a new agent
|
||||
// @Tags agent
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body NewAgentRequest true "Request"
|
||||
// @Success 200 {object} NewAgentResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 409 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /agent/create [post]
|
||||
func Create(c *gin.Context) {
|
||||
var request NewAgentRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port))
|
||||
if _, ok := agent.GetAgent(hostport); ok {
|
||||
c.JSON(http.StatusConflict, apitypes.Error("agent already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
var image string
|
||||
if request.Nightly {
|
||||
image = agent.DockerImageNightly
|
||||
} else {
|
||||
image = agent.DockerImageProduction
|
||||
}
|
||||
|
||||
ca, srv, client, err := agent.NewAgent()
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create agent"))
|
||||
return
|
||||
}
|
||||
|
||||
var cfg agent.Generator = &agent.AgentEnvConfig{
|
||||
Name: request.Name,
|
||||
Port: request.Port,
|
||||
CACert: ca.String(),
|
||||
SSLCert: srv.String(),
|
||||
ContainerRuntime: request.ContainerRuntime,
|
||||
}
|
||||
if request.Type == "docker" {
|
||||
cfg = &agent.AgentComposeConfig{
|
||||
Image: image,
|
||||
AgentEnvConfig: cfg.(*agent.AgentEnvConfig),
|
||||
}
|
||||
}
|
||||
template, err := cfg.Generate()
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to generate agent config"))
|
||||
return
|
||||
}
|
||||
|
||||
key := getEncryptionKey()
|
||||
encCA, err := ca.Encrypt(key)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to encrypt CA PEMs"))
|
||||
return
|
||||
}
|
||||
encClient, err := client.Encrypt(key)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to encrypt client PEMs"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, NewAgentResponse{
|
||||
Compose: template,
|
||||
CA: toPEMPairResponse(encCA),
|
||||
Client: toPEMPairResponse(encClient),
|
||||
})
|
||||
}
|
||||
33
internal/api/v1/agent/list.go
Normal file
33
internal/api/v1/agent/list.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "list"
|
||||
// @BasePath /api/v1
|
||||
// @Summary List agents
|
||||
// @Description List agents
|
||||
// @Tags agent,websocket
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} Agent
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Router /agent/list [get]
|
||||
func List(c *gin.Context) {
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 10*time.Second, func() (any, error) {
|
||||
return agent.ListAgents(), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, agent.ListAgents())
|
||||
}
|
||||
}
|
||||
114
internal/api/v1/agent/verify.go
Normal file
114
internal/api/v1/agent/verify.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"github.com/yusing/godoxy/internal/route/provider"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
type VerifyNewAgentRequest struct {
|
||||
Host string `json:"host"`
|
||||
CA PEMPairResponse `json:"ca"`
|
||||
Client PEMPairResponse `json:"client"`
|
||||
ContainerRuntime agent.ContainerRuntime `json:"container_runtime"`
|
||||
} // @name VerifyNewAgentRequest
|
||||
|
||||
// @x-id "verify"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Verify a new agent
|
||||
// @Description Verify a new agent and return the number of routes added
|
||||
// @Tags agent
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body VerifyNewAgentRequest true "Request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 403 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /agent/verify [post]
|
||||
func Verify(c *gin.Context) {
|
||||
var request VerifyNewAgentRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
filename, ok := certs.AgentCertsFilepath(request.Host)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid host", nil))
|
||||
return
|
||||
}
|
||||
|
||||
ca, err := fromEncryptedPEMPairResponse(request.CA)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid CA", err))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := fromEncryptedPEMPairResponse(request.Client)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid client", err))
|
||||
return
|
||||
}
|
||||
|
||||
nRoutesAdded, err := verifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
zip, err := certs.ZipCert(ca.Cert, client.Cert, client.Key)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to zip certs"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filename, zip, 0o600); err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to write certs"))
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
cfgState := config.ActiveState.Load()
|
||||
for _, a := range cfgState.Value().Providers.Agents {
|
||||
if a.Addr == host {
|
||||
return 0, gperr.New("agent already exists")
|
||||
}
|
||||
}
|
||||
|
||||
var agentCfg agent.AgentConfig
|
||||
agentCfg.Addr = host
|
||||
agentCfg.Runtime = containerRuntime
|
||||
|
||||
err := agentCfg.StartWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
|
||||
if err != nil {
|
||||
return 0, gperr.Wrap(err, "failed to start agent")
|
||||
}
|
||||
|
||||
provider := provider.NewAgentProvider(&agentCfg)
|
||||
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
|
||||
return 0, gperr.Errorf("provider %s already exists", provider.String())
|
||||
}
|
||||
|
||||
// agent must be added before loading routes
|
||||
agent.AddAgent(&agentCfg)
|
||||
err = provider.LoadRoutes()
|
||||
if err != nil {
|
||||
cfgState.DeleteProvider(provider.String())
|
||||
agent.RemoveAgent(&agentCfg)
|
||||
return 0, gperr.Wrap(err, "failed to load routes")
|
||||
}
|
||||
|
||||
return provider.NumRoutes(), nil
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
)
|
||||
|
||||
var defaultAuth Provider
|
||||
|
||||
// Initialize sets up authentication providers.
|
||||
func Initialize() error {
|
||||
if !IsEnabled() {
|
||||
logging.Warn().Msg("authentication is disabled, please set API_JWT_SECRET or OIDC_* to enable authentication")
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
// Initialize OIDC if configured.
|
||||
if common.OIDCIssuerURL != "" {
|
||||
defaultAuth, err = NewOIDCProviderFromEnv()
|
||||
} else {
|
||||
defaultAuth, err = NewUserPassAuthFromEnv()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetDefaultAuth() Provider {
|
||||
return defaultAuth
|
||||
}
|
||||
|
||||
func IsEnabled() bool {
|
||||
return common.APIJWTSecret != nil || IsOIDCEnabled()
|
||||
}
|
||||
|
||||
func IsOIDCEnabled() bool {
|
||||
return common.OIDCIssuerURL != ""
|
||||
}
|
||||
|
||||
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
if IsEnabled() {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := defaultAuth.CheckToken(r); err != nil {
|
||||
U.RespondError(w, err, http.StatusUnauthorized)
|
||||
} else {
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
24
internal/api/v1/auth/callback.go
Normal file
24
internal/api/v1/auth/callback.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//nolint:dupword
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
)
|
||||
|
||||
// @x-id "callback"
|
||||
// @Base /api/v1
|
||||
// @Summary Auth Callback
|
||||
// @Description Handles the callback from the provider after successful authentication
|
||||
// @Tags auth
|
||||
// @Produce plain
|
||||
// @Param body body auth.UserPassAuthCallbackRequest true "Userpass only"
|
||||
// @Success 200 {string} string "Userpass: OK"
|
||||
// @Success 302 {string} string "OIDC: Redirects to home page"
|
||||
// @Failure 400 {string} string "OIDC: invalid request (missing state cookie or oauth state)"
|
||||
// @Failure 400 {string} string "Userpass: invalid request / credentials"
|
||||
// @Failure 500 {string} string "Internal server error"
|
||||
// @Router /auth/callback [post]
|
||||
func Callback(c *gin.Context) {
|
||||
auth.GetDefaultAuth().PostAuthCallbackHandler(c.Writer, c.Request)
|
||||
}
|
||||
19
internal/api/v1/auth/check.go
Normal file
19
internal/api/v1/auth/check.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
)
|
||||
|
||||
// @x-id "check"
|
||||
// @Base /api/v1
|
||||
// @Summary Check authentication status
|
||||
// @Description Checks if the user is authenticated by validating their token
|
||||
// @Tags auth
|
||||
// @Produce plain
|
||||
// @Success 200 {string} string "OK"
|
||||
// @Failure 302 {string} string "Redirects to login page or IdP"
|
||||
// @Router /auth/check [head]
|
||||
func Check(c *gin.Context) {
|
||||
auth.AuthCheckHandler(c.Writer, c.Request)
|
||||
}
|
||||
19
internal/api/v1/auth/login.go
Normal file
19
internal/api/v1/auth/login.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
)
|
||||
|
||||
// @x-id "login"
|
||||
// @Base /api/v1
|
||||
// @Summary Login
|
||||
// @Description Initiates the login process by redirecting the user to the provider's login page
|
||||
// @Tags auth
|
||||
// @Produce plain
|
||||
// @Success 302 {string} string "Redirects to login page or IdP"
|
||||
// @Failure 429 {string} string "Too Many Requests"
|
||||
// @Router /auth/login [post]
|
||||
func Login(c *gin.Context) {
|
||||
auth.GetDefaultAuth().LoginHandler(c.Writer, c.Request)
|
||||
}
|
||||
19
internal/api/v1/auth/logout.go
Normal file
19
internal/api/v1/auth/logout.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
)
|
||||
|
||||
// @x-id "logout"
|
||||
// @Base /api/v1
|
||||
// @Summary Logout
|
||||
// @Description Logs out the user by invalidating the token
|
||||
// @Tags auth
|
||||
// @Produce plain
|
||||
// @Success 302 {string} string "Redirects to home page"
|
||||
// @Router /auth/logout [post]
|
||||
// @Router /auth/logout [get]
|
||||
func Logout(c *gin.Context) {
|
||||
auth.GetDefaultAuth().LogoutHandler(c.Writer, c.Request)
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
CE "github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type OIDCProvider struct {
|
||||
oauthConfig *oauth2.Config
|
||||
oidcProvider *oidc.Provider
|
||||
oidcVerifier *oidc.IDTokenVerifier
|
||||
oidcLogoutURL *url.URL
|
||||
allowedUsers []string
|
||||
allowedGroups []string
|
||||
isMiddleware bool
|
||||
}
|
||||
|
||||
const CookieOauthState = "godoxy_oidc_state"
|
||||
|
||||
const (
|
||||
OIDCMiddlewareCallbackPath = "/auth/callback"
|
||||
OIDCLogoutPath = "/auth/logout"
|
||||
)
|
||||
|
||||
func NewOIDCProvider(issuerURL, clientID, clientSecret, redirectURL, logoutURL string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
|
||||
if len(allowedUsers)+len(allowedGroups) == 0 {
|
||||
return nil, errors.New("OIDC users, groups, or both must not be empty")
|
||||
}
|
||||
|
||||
var logout *url.URL
|
||||
var err error
|
||||
if logoutURL != "" {
|
||||
logout, err = url.Parse(logoutURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse logout URL: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
provider, err := oidc.NewProvider(context.Background(), issuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
|
||||
}
|
||||
|
||||
return &OIDCProvider{
|
||||
oauthConfig: &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectURL,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: strutils.CommaSeperatedList(common.OIDCScopes),
|
||||
},
|
||||
oidcProvider: provider,
|
||||
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||
ClientID: clientID,
|
||||
}),
|
||||
oidcLogoutURL: logout,
|
||||
allowedUsers: allowedUsers,
|
||||
allowedGroups: allowedGroups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewOIDCProviderFromEnv creates a new OIDCProvider from environment variables.
|
||||
func NewOIDCProviderFromEnv() (*OIDCProvider, error) {
|
||||
return NewOIDCProvider(
|
||||
common.OIDCIssuerURL,
|
||||
common.OIDCClientID,
|
||||
common.OIDCClientSecret,
|
||||
common.OIDCRedirectURL,
|
||||
common.OIDCLogoutURL,
|
||||
common.OIDCAllowedUsers,
|
||||
common.OIDCAllowedGroups,
|
||||
)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) TokenCookieName() string {
|
||||
return "godoxy_oidc_token"
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) SetIsMiddleware(enabled bool) {
|
||||
auth.isMiddleware = enabled
|
||||
auth.oauthConfig.RedirectURL = ""
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) SetAllowedUsers(users []string) {
|
||||
auth.allowedUsers = users
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) SetAllowedGroups(groups []string) {
|
||||
auth.allowedGroups = groups
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) CheckToken(r *http.Request) error {
|
||||
token, err := r.Cookie(auth.TokenCookieName())
|
||||
if err != nil {
|
||||
return ErrMissingToken
|
||||
}
|
||||
|
||||
// checks for Expiry, Audience == ClientID, Issuer, etc.
|
||||
idToken, err := auth.oidcVerifier.Verify(r.Context(), token.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify ID token: %w: %w", ErrInvalidToken, err)
|
||||
}
|
||||
|
||||
if len(idToken.Audience) == 0 {
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
var claims struct {
|
||||
Email string `json:"email"`
|
||||
Username string `json:"preferred_username"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return fmt.Errorf("failed to parse claims: %w", err)
|
||||
}
|
||||
|
||||
// Logical AND between allowed users and groups.
|
||||
allowedUser := slices.Contains(auth.allowedUsers, claims.Username)
|
||||
allowedGroup := len(CE.Intersect(claims.Groups, auth.allowedGroups)) > 0
|
||||
if !allowedUser && !allowedGroup {
|
||||
return ErrUserNotAllowed.Subject(claims.Username)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateState generates a random string for OIDC state.
|
||||
const oidcStateLength = 32
|
||||
|
||||
func generateState() (string, error) {
|
||||
b := make([]byte, oidcStateLength)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b)[:oidcStateLength], nil
|
||||
}
|
||||
|
||||
// RedirectOIDC initiates the OIDC login flow.
|
||||
func (auth *OIDCProvider) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := generateState()
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: CookieOauthState,
|
||||
Value: state,
|
||||
MaxAge: 300,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: true,
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
redirURL := auth.oauthConfig.AuthCodeURL(state)
|
||||
if auth.isMiddleware {
|
||||
u, err := r.URL.Parse(redirURL)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("redirect_uri", "https://"+r.Host+OIDCMiddlewareCallbackPath+q.Get("redirect_uri"))
|
||||
u.RawQuery = q.Encode()
|
||||
redirURL = u.String()
|
||||
}
|
||||
http.Redirect(w, r, redirURL, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) exchange(r *http.Request) (*oauth2.Token, error) {
|
||||
if auth.isMiddleware {
|
||||
cfg := *auth.oauthConfig
|
||||
cfg.RedirectURL = "https://" + r.Host + OIDCMiddlewareCallbackPath
|
||||
return cfg.Exchange(r.Context(), r.URL.Query().Get("code"))
|
||||
}
|
||||
return auth.oauthConfig.Exchange(r.Context(), r.URL.Query().Get("code"))
|
||||
}
|
||||
|
||||
// OIDCCallbackHandler handles the OIDC callback.
|
||||
func (auth *OIDCProvider) LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// For testing purposes, skip provider verification
|
||||
if common.IsTest {
|
||||
auth.handleTestCallback(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
state, err := r.Cookie(CookieOauthState)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, E.New("missing state cookie"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
if query.Get("state") != state.Value {
|
||||
U.HandleErr(w, r, E.New("invalid oauth state"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
oauth2Token, err := auth.exchange(r)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, fmt.Errorf("failed to exchange token: %w", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
U.HandleErr(w, r, E.New("missing id_token"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
idToken, err := auth.oidcVerifier.Verify(r.Context(), rawIDToken)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, fmt.Errorf("failed to verify ID token: %w", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
setTokenCookie(w, r, auth.TokenCookieName(), rawIDToken, time.Until(idToken.Expiry))
|
||||
|
||||
// Redirect to home page
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if auth.oidcLogoutURL == nil {
|
||||
DefaultLogoutCallbackHandler(auth, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := r.Cookie(auth.TokenCookieName())
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, E.New("missing token cookie"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
clearTokenCookie(w, r, auth.TokenCookieName())
|
||||
|
||||
logoutURL := *auth.oidcLogoutURL
|
||||
logoutURL.Query().Add("id_token_hint", token.Value)
|
||||
|
||||
http.Redirect(w, r, logoutURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
// handleTestCallback handles OIDC callback in test environment.
|
||||
func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := r.Cookie(CookieOauthState)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, E.New("missing state cookie"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("state") != state.Value {
|
||||
U.HandleErr(w, r, E.New("invalid oauth state"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Create test JWT token
|
||||
setTokenCookie(w, r, auth.TokenCookieName(), "test", time.Hour)
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
TokenCookieName() string
|
||||
CheckToken(r *http.Request) error
|
||||
RedirectLoginPage(w http.ResponseWriter, r *http.Request)
|
||||
LoginCallbackHandler(w http.ResponseWriter, r *http.Request)
|
||||
LogoutCallbackHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingToken = E.New("missing token")
|
||||
ErrInvalidToken = E.New("invalid token")
|
||||
ErrUserNotAllowed = E.New("user not allowed")
|
||||
)
|
||||
|
||||
// cookieFQDN returns the fully qualified domain name of the request host
|
||||
// with subdomain stripped.
|
||||
//
|
||||
// If the request host does not have a subdomain,
|
||||
// an empty string is returned
|
||||
//
|
||||
// "abc.example.com" -> "example.com"
|
||||
// "example.com" -> ""
|
||||
func cookieFQDN(r *http.Request) string {
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
parts := strutils.SplitRune(host, '.')
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
parts[0] = ""
|
||||
return strutils.JoinRune(parts, '.')
|
||||
}
|
||||
|
||||
func setTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
MaxAge: int(ttl.Seconds()),
|
||||
Domain: cookieFQDN(r),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Path: "/",
|
||||
})
|
||||
}
|
||||
|
||||
func clearTokenCookie(w http.ResponseWriter, r *http.Request, name string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name,
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Domain: cookieFQDN(r),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Path: "/",
|
||||
})
|
||||
}
|
||||
|
||||
// DefaultLogoutCallbackHandler clears the token cookie and redirects to the login page..
|
||||
func DefaultLogoutCallbackHandler(auth Provider, w http.ResponseWriter, r *http.Request) {
|
||||
clearTokenCookie(w, r, auth.TokenCookieName())
|
||||
auth.RedirectLoginPage(w, r)
|
||||
}
|
||||
53
internal/api/v1/cert/info.go
Normal file
53
internal/api/v1/cert/info.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package certapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
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"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get cert info
|
||||
// @Description Get cert info
|
||||
// @Tags cert
|
||||
// @Produce json
|
||||
// @Success 200 {object} CertInfo
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /cert/info [get]
|
||||
func Info(c *gin.Context) {
|
||||
autocert := autocert.ActiveProvider.Load()
|
||||
if autocert == nil {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := autocert.GetCert(nil)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get cert info"))
|
||||
return
|
||||
}
|
||||
|
||||
certInfo := CertInfo{
|
||||
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)
|
||||
}
|
||||
72
internal/api/v1/cert/renew.go
Normal file
72
internal/api/v1/cert/renew.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package certapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
// @x-id "renew"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Renew cert
|
||||
// @Description Renew cert
|
||||
// @Tags cert,websocket
|
||||
// @Produce plain
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /cert/renew [get]
|
||||
func Renew(c *gin.Context) {
|
||||
autocert := autocert.ActiveProvider.Load()
|
||||
if autocert == nil {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create websocket manager"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
logs, cancel := memlogger.Events()
|
||||
defer cancel()
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
err = autocert.ObtainCert()
|
||||
if err != nil {
|
||||
gperr.LogError("failed to obtain cert", err)
|
||||
_ = manager.WriteData(websocket.TextMessage, []byte(err.Error()), 10*time.Second)
|
||||
} else {
|
||||
log.Info().Msg("cert obtained successfully")
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case l := <-logs:
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = manager.WriteData(websocket.TextMessage, l, 10*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
63
internal/api/v1/docker/container.go
Normal file
63
internal/api/v1/docker/container.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "container"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get container
|
||||
// @Description Get container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param id path string true "Container ID"
|
||||
// @Success 200 {object} Container
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "ID is required"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/container/{id} [get]
|
||||
func GetContainer(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("id is required"))
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
|
||||
cont, err := client.ContainerInspect(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
||||
return
|
||||
}
|
||||
|
||||
var state ContainerState
|
||||
if cont.State != nil {
|
||||
state = cont.State.Status
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &Container{
|
||||
Server: dockerHost,
|
||||
Name: cont.Name,
|
||||
ID: cont.ID,
|
||||
Image: cont.Image,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
68
internal/api/v1/docker/containers.go
Normal file
68
internal/api/v1/docker/containers.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type ContainerState = container.ContainerState // @name ContainerState
|
||||
|
||||
type Container struct {
|
||||
Server string `json:"server"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Image string `json:"image"`
|
||||
State ContainerState `json:"state,omitempty" extensions:"x-nullable"`
|
||||
} // @name ContainerResponse
|
||||
|
||||
// @x-id "containers"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get containers
|
||||
// @Description Get containers
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Success 200 {array} Container
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/containers [get]
|
||||
func Containers(c *gin.Context) {
|
||||
serveHTTP[Container](c, GetContainers)
|
||||
}
|
||||
|
||||
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
|
||||
errs := gperr.NewBuilder("failed to get containers")
|
||||
containers := make([]Container, 0)
|
||||
for server, dockerClient := range dockerClients {
|
||||
conts, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
for _, cont := range conts {
|
||||
containers = append(containers, Container{
|
||||
Server: server,
|
||||
Name: cont.Names[0],
|
||||
ID: cont.ID,
|
||||
Image: cont.Image,
|
||||
State: cont.State,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(containers, func(i, j int) bool {
|
||||
return containers[i].Name < containers[j].Name
|
||||
})
|
||||
if err := errs.Error(); err != nil {
|
||||
gperr.LogError("failed to get containers", err)
|
||||
if len(containers) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
81
internal/api/v1/docker/info.go
Normal file
81
internal/api/v1/docker/info.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
dockerSystem "github.com/docker/docker/api/types/system"
|
||||
"github.com/gin-gonic/gin"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type containerStats struct {
|
||||
Total int `json:"total"`
|
||||
Running int `json:"running"`
|
||||
Paused int `json:"paused"`
|
||||
Stopped int `json:"stopped"`
|
||||
} // @name ContainerStats
|
||||
|
||||
type dockerInfo struct {
|
||||
Name string `json:"name"`
|
||||
ServerVersion string `json:"version"`
|
||||
Containers containerStats `json:"containers"`
|
||||
Images int `json:"images"`
|
||||
NCPU int `json:"n_cpu"`
|
||||
MemTotal string `json:"memory"`
|
||||
} // @name ServerInfo
|
||||
|
||||
func toDockerInfo(info dockerSystem.Info) dockerInfo {
|
||||
return dockerInfo{
|
||||
Name: info.Name,
|
||||
ServerVersion: info.ServerVersion,
|
||||
Containers: containerStats{
|
||||
Total: info.ContainersRunning,
|
||||
Running: info.ContainersRunning,
|
||||
Paused: info.ContainersPaused,
|
||||
Stopped: info.ContainersStopped,
|
||||
},
|
||||
Images: info.Images,
|
||||
NCPU: info.NCPU,
|
||||
MemTotal: strutils.FormatByteSize(info.MemTotal),
|
||||
}
|
||||
}
|
||||
|
||||
// @x-id "info"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get docker info
|
||||
// @Description Get docker info
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Success 200 {object} dockerInfo
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/info [get]
|
||||
func Info(c *gin.Context) {
|
||||
serveHTTP[dockerInfo](c, GetDockerInfo)
|
||||
}
|
||||
|
||||
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) {
|
||||
errs := gperr.NewBuilder("failed to get docker info")
|
||||
dockerInfos := make([]dockerInfo, len(dockerClients))
|
||||
|
||||
i := 0
|
||||
for name, dockerClient := range dockerClients {
|
||||
info, err := dockerClient.Info(ctx)
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
info.Name = name
|
||||
dockerInfos[i] = toDockerInfo(info)
|
||||
i++
|
||||
}
|
||||
|
||||
sort.Slice(dockerInfos, func(i, j int) bool {
|
||||
return dockerInfos[i].Name < dockerInfos[j].Name
|
||||
})
|
||||
return dockerInfos, errs.Error()
|
||||
}
|
||||
112
internal/api/v1/docker/logs.go
Normal file
112
internal/api/v1/docker/logs.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
type LogsQueryParams struct {
|
||||
Stdout bool `form:"stdout,default=true"`
|
||||
Stderr bool `form:"stderr,default=true"`
|
||||
Since string `form:"from"`
|
||||
Until string `form:"to"`
|
||||
Levels string `form:"levels"`
|
||||
} // @name LogsQueryParams
|
||||
|
||||
// @x-id "logs"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get docker container logs
|
||||
// @Description Get docker container logs by container id
|
||||
// @Tags docker,websocket
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "container id"
|
||||
// @Param stdout query bool false "show stdout"
|
||||
// @Param stderr query bool false "show stderr"
|
||||
// @Param from query string false "from timestamp"
|
||||
// @Param to query string false "to timestamp"
|
||||
// @Param levels query string false "levels"
|
||||
// @Success 200
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "server not found or container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/logs/{id} [get]
|
||||
func Logs(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("container id is required"))
|
||||
return
|
||||
}
|
||||
|
||||
var queryParams LogsQueryParams
|
||||
if err := c.ShouldBindQuery(&queryParams); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid query params"))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: implement levels
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id)))
|
||||
return
|
||||
}
|
||||
|
||||
dockerClient, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get docker client"))
|
||||
return
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
opts := container.LogsOptions{
|
||||
ShowStdout: queryParams.Stdout,
|
||||
ShowStderr: queryParams.Stderr,
|
||||
Since: queryParams.Since,
|
||||
Until: queryParams.Until,
|
||||
Timestamps: true,
|
||||
Follow: true,
|
||||
Tail: "100",
|
||||
}
|
||||
if queryParams.Levels != "" {
|
||||
opts.Details = true
|
||||
}
|
||||
|
||||
logs, err := dockerClient.ContainerLogs(c.Request.Context(), id, opts)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get container logs"))
|
||||
return
|
||||
}
|
||||
defer logs.Close()
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create websocket manager"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
writer := manager.NewWriter(websocket.TextMessage)
|
||||
|
||||
_, err = stdcopy.StdCopy(writer, writer, logs) // de-multiplex logs
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, task.ErrProgramExiting) {
|
||||
return
|
||||
}
|
||||
log.Err(err).
|
||||
Str("server", dockerHost).
|
||||
Str("container", id).
|
||||
Msg("failed to de-multiplex logs")
|
||||
}
|
||||
}
|
||||
52
internal/api/v1/docker/restart.go
Normal file
52
internal/api/v1/docker/restart.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "restart"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Restart container
|
||||
// @Description Restart container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param request body StopRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/restart [post]
|
||||
func Restart(c *gin.Context) {
|
||||
var req StopRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
|
||||
err = client.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apitypes.Success("container restarted"))
|
||||
}
|
||||
58
internal/api/v1/docker/start.go
Normal file
58
internal/api/v1/docker/start.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type StartRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
container.StartOptions
|
||||
}
|
||||
|
||||
// @x-id "start"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Start container
|
||||
// @Description Start container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param request body StartRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/start [post]
|
||||
func Start(c *gin.Context) {
|
||||
var req StartRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
|
||||
err = client.ContainerStart(c.Request.Context(), req.ID, req.StartOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to start container"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apitypes.Success("container started"))
|
||||
}
|
||||
58
internal/api/v1/docker/stop.go
Normal file
58
internal/api/v1/docker/stop.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type StopRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
container.StopOptions
|
||||
}
|
||||
|
||||
// @x-id "stop"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Stop container
|
||||
// @Description Stop container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param request body StopRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/stop [post]
|
||||
func Stop(c *gin.Context) {
|
||||
var req StopRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
|
||||
err = client.ContainerStop(c.Request.Context(), req.ID, req.StopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apitypes.Success("container stopped"))
|
||||
}
|
||||
54
internal/api/v1/docker/utils.go
Normal file
54
internal/api/v1/docker/utils.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
type (
|
||||
DockerClients map[string]*docker.SharedClient
|
||||
ResultType[T any] interface {
|
||||
map[string]T | []T
|
||||
}
|
||||
)
|
||||
|
||||
// closeAllClients closes all docker clients after a delay.
|
||||
//
|
||||
// This is used to ensure that all docker clients are closed after the http handler returns.
|
||||
func closeAllClients(dockerClients DockerClients) {
|
||||
for _, dockerClient := range dockerClients {
|
||||
dockerClient.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func handleResult[V any, T ResultType[V]](c *gin.Context, errs error, result T) {
|
||||
if errs != nil {
|
||||
if len(result) == 0 {
|
||||
c.Error(apitypes.InternalServerError(errs, "docker errors"))
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
|
||||
dockerClients := docker.Clients()
|
||||
defer closeAllClients(dockerClients)
|
||||
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 5*time.Second, func() (any, error) {
|
||||
return getResult(c.Request.Context(), dockerClients)
|
||||
})
|
||||
} else {
|
||||
result, err := getResult(c.Request.Context(), dockerClients)
|
||||
handleResult[V](c, err, result)
|
||||
}
|
||||
}
|
||||
5559
internal/api/v1/docs/swagger.json
Normal file
5559
internal/api/v1/docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
3156
internal/api/v1/docs/swagger.yaml
Normal file
3156
internal/api/v1/docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
94
internal/api/v1/favicon.go
Normal file
94
internal/api/v1/favicon.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
type GetFavIconRequest struct {
|
||||
URL string `form:"url" binding:"required_without=Alias"`
|
||||
Alias string `form:"alias" binding:"required_without=URL"`
|
||||
} // @name GetFavIconRequest
|
||||
|
||||
// @x-id "favicon"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get favicon
|
||||
// @Description Get favicon
|
||||
// @Tags v1
|
||||
// @Accept json
|
||||
// @Produce image/svg+xml,image/x-icon,image/png,image/webp
|
||||
// @Param url query string false "URL of the route"
|
||||
// @Param alias query string false "Alias of the route"
|
||||
// @Success 200 {array} homepage.FetchResult
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Bad Request: alias is empty or route is not HTTPRoute"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden: unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Not Found: route or icon not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal Server Error: internal error"
|
||||
// @Router /favicon [get]
|
||||
func FavIcon(c *gin.Context) {
|
||||
var request GetFavIconRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
// try with url
|
||||
if request.URL != "" {
|
||||
var iconURL homepage.IconURL
|
||||
if err := iconURL.Parse(request.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
|
||||
return
|
||||
}
|
||||
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), &iconURL)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, fetchResult.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon)
|
||||
return
|
||||
}
|
||||
|
||||
// try with alias
|
||||
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, result.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(result.StatusCode, result.ContentType(), result.Icon)
|
||||
}
|
||||
|
||||
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
||||
func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error) {
|
||||
// try with route.Icon
|
||||
r, ok := routes.HTTP.Get(alias)
|
||||
if !ok {
|
||||
return homepage.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
}
|
||||
|
||||
var (
|
||||
result homepage.FetchResult
|
||||
err error
|
||||
)
|
||||
hp := r.HomepageItem()
|
||||
if hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL)
|
||||
} else {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
result, err = homepage.FindIcon(ctx, r, "/")
|
||||
}
|
||||
if result.StatusCode == 0 {
|
||||
result.StatusCode = http.StatusOK
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package favicon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type cacheEntry struct {
|
||||
Icon []byte `json:"icon"`
|
||||
LastAccess time.Time `json:"lastAccess"`
|
||||
}
|
||||
|
||||
// cache key can be absolute url or route name.
|
||||
var (
|
||||
iconCache = make(map[string]*cacheEntry)
|
||||
iconCacheMu sync.RWMutex
|
||||
)
|
||||
|
||||
const (
|
||||
iconCacheTTL = 24 * time.Hour
|
||||
cleanUpInterval = time.Hour
|
||||
)
|
||||
|
||||
func InitIconCache() {
|
||||
err := utils.LoadJSONIfExist(common.IconCachePath, &iconCache)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Msg("failed to load icon cache")
|
||||
} else {
|
||||
logging.Info().Msgf("icon cache loaded (%d icons)", len(iconCache))
|
||||
}
|
||||
|
||||
go func() {
|
||||
cleanupTicker := time.NewTicker(cleanUpInterval)
|
||||
defer cleanupTicker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-task.RootContextCanceled():
|
||||
return
|
||||
case <-cleanupTicker.C:
|
||||
pruneExpiredIconCache()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
task.OnProgramExit("save_favicon_cache", func() {
|
||||
iconCacheMu.Lock()
|
||||
defer iconCacheMu.Unlock()
|
||||
|
||||
if len(iconCache) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err := utils.SaveJSON(common.IconCachePath, &iconCache, 0o644); err != nil {
|
||||
logging.Error().Err(err).Msg("failed to save icon cache")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func pruneExpiredIconCache() {
|
||||
iconCacheMu.Lock()
|
||||
defer iconCacheMu.Unlock()
|
||||
|
||||
nPruned := 0
|
||||
for key, icon := range iconCache {
|
||||
if icon.IsExpired() {
|
||||
delete(iconCache, key)
|
||||
nPruned++
|
||||
}
|
||||
}
|
||||
logging.Info().Int("pruned", nPruned).Msg("pruned expired icon cache")
|
||||
}
|
||||
|
||||
func routeKey(r route.HTTPRoute) string {
|
||||
return r.RawEntry().Provider + ":" + r.TargetName()
|
||||
}
|
||||
|
||||
func PruneRouteIconCache(route route.HTTPRoute) {
|
||||
iconCacheMu.Lock()
|
||||
defer iconCacheMu.Unlock()
|
||||
delete(iconCache, routeKey(route))
|
||||
}
|
||||
|
||||
func loadIconCache(key string) *fetchResult {
|
||||
iconCacheMu.RLock()
|
||||
defer iconCacheMu.RUnlock()
|
||||
|
||||
icon, ok := iconCache[key]
|
||||
if ok && icon != nil {
|
||||
logging.Debug().
|
||||
Str("key", key).
|
||||
Msg("icon found in cache")
|
||||
icon.LastAccess = time.Now()
|
||||
return &fetchResult{icon: icon.Icon}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func storeIconCache(key string, icon []byte) {
|
||||
iconCacheMu.Lock()
|
||||
defer iconCacheMu.Unlock()
|
||||
iconCache[key] = &cacheEntry{Icon: icon, LastAccess: time.Now()}
|
||||
}
|
||||
|
||||
func (e *cacheEntry) IsExpired() bool {
|
||||
return time.Since(e.LastAccess) > iconCacheTTL
|
||||
}
|
||||
|
||||
func (e *cacheEntry) UnmarshalJSON(data []byte) error {
|
||||
attempt := struct {
|
||||
Icon []byte `json:"icon"`
|
||||
LastAccess time.Time `json:"lastAccess"`
|
||||
}{}
|
||||
err := json.Unmarshal(data, &attempt)
|
||||
if err == nil {
|
||||
e.Icon = attempt.Icon
|
||||
e.LastAccess = attempt.LastAccess
|
||||
return nil
|
||||
}
|
||||
// fallback to bytes
|
||||
err = json.Unmarshal(data, &e.Icon)
|
||||
if err == nil {
|
||||
e.LastAccess = time.Now()
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
package favicon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/vincent-petithory/dataurl"
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
)
|
||||
|
||||
type fetchResult struct {
|
||||
icon []byte
|
||||
contentType string
|
||||
statusCode int
|
||||
errMsg string
|
||||
}
|
||||
|
||||
func (res *fetchResult) OK() bool {
|
||||
return res.icon != nil
|
||||
}
|
||||
|
||||
func (res *fetchResult) ContentType() string {
|
||||
if res.contentType == "" {
|
||||
if bytes.HasPrefix(res.icon, []byte("<svg")) || bytes.HasPrefix(res.icon, []byte("<?xml")) {
|
||||
return "image/svg+xml"
|
||||
} else {
|
||||
return "image/x-icon"
|
||||
}
|
||||
}
|
||||
return res.contentType
|
||||
}
|
||||
|
||||
// GetFavIcon returns the favicon of the route
|
||||
//
|
||||
// Returns:
|
||||
// - 200 OK: if icon found
|
||||
// - 400 Bad Request: if alias is empty or route is not HTTPRoute
|
||||
// - 404 Not Found: if route or icon not found
|
||||
// - 500 Internal Server Error: if internal error
|
||||
// - others: depends on route handler response
|
||||
func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||
url, alias := req.FormValue("url"), req.FormValue("alias")
|
||||
if url == "" && alias == "" {
|
||||
U.RespondError(w, U.ErrMissingKey("url or alias"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if url != "" && alias != "" {
|
||||
U.RespondError(w, U.ErrInvalidKey("url and alias are mutually exclusive"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// try with url
|
||||
if url != "" {
|
||||
var iconURL homepage.IconURL
|
||||
if err := iconURL.Parse(url); err != nil {
|
||||
U.RespondError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fetchResult := getFavIconFromURL(&iconURL)
|
||||
if !fetchResult.OK() {
|
||||
http.Error(w, fetchResult.errMsg, fetchResult.statusCode)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", fetchResult.ContentType())
|
||||
U.WriteBody(w, fetchResult.icon)
|
||||
return
|
||||
}
|
||||
|
||||
// try with route.Homepage.Icon
|
||||
r, ok := routes.GetHTTPRoute(alias)
|
||||
if !ok {
|
||||
U.RespondError(w, errors.New("no such route"), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var result *fetchResult
|
||||
hp := r.RawEntry().Homepage.GetOverride()
|
||||
if !hp.IsEmpty() && hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result = findIcon(r, req, hp.Icon.Value)
|
||||
} else {
|
||||
result = getFavIconFromURL(hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
result = findIcon(r, req, "/")
|
||||
}
|
||||
if result.statusCode == 0 {
|
||||
result.statusCode = http.StatusOK
|
||||
}
|
||||
if !result.OK() {
|
||||
http.Error(w, result.errMsg, result.statusCode)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", result.ContentType())
|
||||
U.WriteBody(w, result.icon)
|
||||
}
|
||||
|
||||
func getFavIconFromURL(iconURL *homepage.IconURL) *fetchResult {
|
||||
switch iconURL.IconSource {
|
||||
case homepage.IconSourceAbsolute:
|
||||
return fetchIconAbsolute(iconURL.URL())
|
||||
case homepage.IconSourceRelative:
|
||||
return &fetchResult{statusCode: http.StatusBadRequest, errMsg: "unexpected relative icon"}
|
||||
case homepage.IconSourceWalkXCode, homepage.IconSourceSelfhSt:
|
||||
return fetchKnownIcon(iconURL)
|
||||
}
|
||||
return &fetchResult{statusCode: http.StatusBadRequest, errMsg: "invalid icon source"}
|
||||
}
|
||||
|
||||
func fetchIconAbsolute(url string) *fetchResult {
|
||||
if result := loadIconCache(url); result != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
resp, err := U.Get(url)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
if err == nil {
|
||||
err = errors.New(resp.Status)
|
||||
}
|
||||
logging.Error().Err(err).
|
||||
Str("url", url).
|
||||
Msg("failed to get icon")
|
||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "connection error"}
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
icon, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).
|
||||
Str("url", url).
|
||||
Msg("failed to read icon")
|
||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
||||
}
|
||||
|
||||
storeIconCache(url, icon)
|
||||
return &fetchResult{icon: icon}
|
||||
}
|
||||
|
||||
var nameSanitizer = strings.NewReplacer(
|
||||
"_", "-",
|
||||
" ", "-",
|
||||
"(", "",
|
||||
")", "",
|
||||
)
|
||||
|
||||
func sanitizeName(name string) string {
|
||||
return strings.ToLower(nameSanitizer.Replace(name))
|
||||
}
|
||||
|
||||
func fetchKnownIcon(url *homepage.IconURL) *fetchResult {
|
||||
// if icon isn't in the list, no need to fetch
|
||||
if !url.HasIcon() {
|
||||
logging.Debug().
|
||||
Str("value", url.String()).
|
||||
Str("url", url.URL()).
|
||||
Msg("no such icon")
|
||||
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "no such icon"}
|
||||
}
|
||||
|
||||
return fetchIconAbsolute(url.URL())
|
||||
}
|
||||
|
||||
func fetchIcon(filetype, filename string) *fetchResult {
|
||||
result := fetchKnownIcon(homepage.NewSelfhStIconURL(filename, filetype))
|
||||
if result.icon == nil {
|
||||
return result
|
||||
}
|
||||
return fetchKnownIcon(homepage.NewWalkXCodeIconURL(filename, filetype))
|
||||
}
|
||||
|
||||
func findIcon(r route.HTTPRoute, req *http.Request, uri string) *fetchResult {
|
||||
key := routeKey(r)
|
||||
if result := loadIconCache(key); result != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
result := fetchIcon("png", sanitizeName(r.TargetName()))
|
||||
cont := r.RawEntry().Container
|
||||
if !result.OK() && cont != nil {
|
||||
result = fetchIcon("png", sanitizeName(cont.ImageName))
|
||||
}
|
||||
if !result.OK() {
|
||||
// fallback to parse html
|
||||
result = findIconSlow(r, req, uri)
|
||||
}
|
||||
if result.OK() {
|
||||
storeIconCache(key, result.icon)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findIconSlow(r route.HTTPRoute, req *http.Request, uri string) *fetchResult {
|
||||
ctx, cancel := context.WithTimeoutCause(req.Context(), 3*time.Second, errors.New("favicon request timeout"))
|
||||
defer cancel()
|
||||
newReq := req.WithContext(ctx)
|
||||
newReq.Header.Set("Accept-Encoding", "identity") // disable compression
|
||||
if !strings.HasPrefix(uri, "/") {
|
||||
uri = "/" + uri
|
||||
}
|
||||
u, err := url.ParseRequestURI(uri)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).
|
||||
Str("route", r.TargetName()).
|
||||
Str("path", uri).
|
||||
Msg("failed to parse uri")
|
||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "cannot parse uri"}
|
||||
}
|
||||
newReq.URL.Path = u.Path
|
||||
newReq.URL.RawPath = u.RawPath
|
||||
newReq.URL.RawQuery = u.RawQuery
|
||||
newReq.RequestURI = u.String()
|
||||
|
||||
c := newContent()
|
||||
r.ServeHTTP(c, newReq)
|
||||
if c.status != http.StatusOK {
|
||||
switch c.status {
|
||||
case 0:
|
||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "connection error"}
|
||||
default:
|
||||
if loc := c.Header().Get("Location"); loc != "" {
|
||||
loc = path.Clean(loc)
|
||||
if !strings.HasPrefix(loc, "/") {
|
||||
loc = "/" + loc
|
||||
}
|
||||
if loc == newReq.URL.Path {
|
||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "circular redirect"}
|
||||
}
|
||||
return findIconSlow(r, req, loc)
|
||||
}
|
||||
}
|
||||
return &fetchResult{statusCode: c.status, errMsg: "upstream error: " + string(c.data)}
|
||||
}
|
||||
// return icon data
|
||||
if !gphttp.GetContentType(c.header).IsHTML() {
|
||||
return &fetchResult{icon: c.data, contentType: c.header.Get("Content-Type")}
|
||||
}
|
||||
// try extract from "link[rel=icon]" from path "/"
|
||||
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(c.data))
|
||||
if err != nil {
|
||||
logging.Error().Err(err).
|
||||
Str("route", r.TargetName()).
|
||||
Msg("failed to parse html")
|
||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
||||
}
|
||||
ele := doc.Find("head > link[rel=icon]").First()
|
||||
if ele.Length() == 0 {
|
||||
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "icon element not found"}
|
||||
}
|
||||
href := ele.AttrOr("href", "")
|
||||
if href == "" {
|
||||
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "icon href not found"}
|
||||
}
|
||||
// https://en.wikipedia.org/wiki/Data_URI_scheme
|
||||
if strings.HasPrefix(href, "data:image/") {
|
||||
dataURI, err := dataurl.DecodeString(href)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).
|
||||
Str("route", r.TargetName()).
|
||||
Msg("failed to decode favicon")
|
||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
||||
}
|
||||
return &fetchResult{icon: dataURI.Data, contentType: dataURI.ContentType()}
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(href, "http://"), strings.HasPrefix(href, "https://"):
|
||||
return fetchIconAbsolute(href)
|
||||
default:
|
||||
return findIconSlow(r, req, path.Clean(href))
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
"github.com/yusing/go-proxy/internal/route/provider"
|
||||
)
|
||||
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
FileTypeConfig FileType = "config"
|
||||
FileTypeProvider FileType = "provider"
|
||||
FileTypeMiddleware FileType = "middleware"
|
||||
)
|
||||
|
||||
func fileType(file string) FileType {
|
||||
switch {
|
||||
case strings.HasPrefix(path.Base(file), "config."):
|
||||
return FileTypeConfig
|
||||
case strings.HasPrefix(file, common.MiddlewareComposeBasePath):
|
||||
return FileTypeMiddleware
|
||||
}
|
||||
return FileTypeProvider
|
||||
}
|
||||
|
||||
func (t FileType) IsValid() bool {
|
||||
switch t {
|
||||
case FileTypeConfig, FileTypeProvider, FileTypeMiddleware:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t FileType) GetPath(filename string) string {
|
||||
if t == FileTypeMiddleware {
|
||||
return path.Join(common.MiddlewareComposeBasePath, filename)
|
||||
}
|
||||
return path.Join(common.ConfigBasePath, filename)
|
||||
}
|
||||
|
||||
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
|
||||
fileType = FileType(r.PathValue("type"))
|
||||
if !fileType.IsValid() {
|
||||
err = U.ErrInvalidKey("type")
|
||||
return
|
||||
}
|
||||
filename = r.PathValue("filename")
|
||||
if filename == "" {
|
||||
err = U.ErrMissingKey("filename")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||
fileType, filename, err := getArgs(r)
|
||||
if err != nil {
|
||||
U.RespondError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
content, err := os.ReadFile(fileType.GetPath(filename))
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
U.WriteBody(w, content)
|
||||
}
|
||||
|
||||
func validateFile(fileType FileType, content []byte) error {
|
||||
switch fileType {
|
||||
case FileTypeConfig:
|
||||
return config.Validate(content)
|
||||
case FileTypeMiddleware:
|
||||
errs := E.NewBuilder("middleware errors")
|
||||
middleware.BuildMiddlewaresFromYAML("", content, errs)
|
||||
return errs.Error()
|
||||
}
|
||||
return provider.Validate(content)
|
||||
}
|
||||
|
||||
func ValidateFile(w http.ResponseWriter, r *http.Request) {
|
||||
fileType := FileType(r.PathValue("type"))
|
||||
if !fileType.IsValid() {
|
||||
U.RespondError(w, U.ErrInvalidKey("type"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
content, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
r.Body.Close()
|
||||
err = validateFile(fileType, content)
|
||||
if err != nil {
|
||||
U.RespondError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||
fileType, filename, err := getArgs(r)
|
||||
if err != nil {
|
||||
U.RespondError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
content, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if valErr := validateFile(fileType, content); valErr != nil {
|
||||
U.RespondError(w, valErr, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(fileType.GetPath(filename), content, 0o644)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
73
internal/api/v1/file/get.go
Normal file
73
internal/api/v1/file/get.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package fileapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type FileType string // @name FileType
|
||||
|
||||
const (
|
||||
FileTypeConfig FileType = "config" // @name FileTypeConfig
|
||||
FileTypeProvider FileType = "provider" // @name FileTypeProvider
|
||||
FileTypeMiddleware FileType = "middleware" // @name FileTypeMiddleware
|
||||
)
|
||||
|
||||
type GetFileContentRequest struct {
|
||||
FileType FileType `form:"type" binding:"required,oneof=config provider middleware"`
|
||||
Filename string `form:"filename" binding:"required" format:"filename"`
|
||||
} // @name GetFileContentRequest
|
||||
|
||||
// @x-id "get"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get file content
|
||||
// @Description Get file content
|
||||
// @Tags file
|
||||
// @Accept json
|
||||
// @Produce json,application/godoxy+yaml
|
||||
// @Param query query GetFileContentRequest true "Request"
|
||||
// @Success 200 {string} application/godoxy+yaml "File content"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /file/content [get]
|
||||
func Get(c *gin.Context) {
|
||||
var request GetFileContentRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(request.FileType.GetPath(request.Filename))
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to read file"))
|
||||
return
|
||||
}
|
||||
|
||||
// RFC 9512: https://www.rfc-editor.org/rfc/rfc9512.html
|
||||
// xxx/yyy+yaml
|
||||
c.Data(http.StatusOK, "application/godoxy+yaml", content)
|
||||
}
|
||||
|
||||
func GetFileType(file string) FileType {
|
||||
switch {
|
||||
case strings.HasPrefix(path.Base(file), "config."):
|
||||
return FileTypeConfig
|
||||
case strings.HasPrefix(file, common.MiddlewareComposeBasePath):
|
||||
return FileTypeMiddleware
|
||||
}
|
||||
return FileTypeProvider
|
||||
}
|
||||
|
||||
func (t FileType) GetPath(filename string) string {
|
||||
if t == FileTypeMiddleware {
|
||||
return path.Join(common.MiddlewareComposeBasePath, filename)
|
||||
}
|
||||
return path.Join(common.ConfigBasePath, filename)
|
||||
}
|
||||
62
internal/api/v1/file/list.go
Normal file
62
internal/api/v1/file/list.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package fileapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/utils"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type ListFilesResponse struct {
|
||||
Config []string `json:"config"`
|
||||
Provider []string `json:"provider"`
|
||||
Middleware []string `json:"middleware"`
|
||||
} // @name ListFilesResponse
|
||||
|
||||
// @x-id "list"
|
||||
// @BasePath /api/v1
|
||||
// @Summary List files
|
||||
// @Description List files
|
||||
// @Tags file
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} ListFilesResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /file/list [get]
|
||||
func List(c *gin.Context) {
|
||||
resp := map[FileType][]string{
|
||||
FileTypeConfig: make([]string, 0),
|
||||
FileTypeProvider: make([]string, 0),
|
||||
FileTypeMiddleware: make([]string, 0),
|
||||
}
|
||||
|
||||
// config/
|
||||
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to list files"))
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
t := GetFileType(file)
|
||||
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
|
||||
resp[t] = append(resp[t], file)
|
||||
}
|
||||
|
||||
// config/middlewares/
|
||||
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to list files"))
|
||||
return
|
||||
}
|
||||
for _, mid := range mids {
|
||||
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
|
||||
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
52
internal/api/v1/file/set.go
Normal file
52
internal/api/v1/file/set.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package fileapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type SetFileContentRequest GetFileContentRequest
|
||||
|
||||
// @x-id "set"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set file content
|
||||
// @Description Set file content
|
||||
// @Tags file
|
||||
// @Accept text/plain
|
||||
// @Produce json
|
||||
// @Param type query FileType true "Type"
|
||||
// @Param filename query string true "Filename"
|
||||
// @Param file body string true "File"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /file/content [put]
|
||||
func Set(c *gin.Context) {
|
||||
var request SetFileContentRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
content, err := c.GetRawData()
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to read file"))
|
||||
return
|
||||
}
|
||||
|
||||
if valErr := validateFile(request.FileType, content); valErr != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid file", valErr))
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(request.FileType.GetPath(request.Filename), content, 0o644)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to write file"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, apitypes.Success("file set"))
|
||||
}
|
||||
64
internal/api/v1/file/validate.go
Normal file
64
internal/api/v1/file/validate.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package fileapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
"github.com/yusing/godoxy/internal/route/provider"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
type ValidateFileRequest struct {
|
||||
FileType FileType `form:"type" validate:"required,oneof=config provider middleware"`
|
||||
} // @name ValidateFileRequest
|
||||
|
||||
// @x-id "validate"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Validate file
|
||||
// @Description Validate file
|
||||
// @Tags file
|
||||
// @Accept text/plain
|
||||
// @Produce json
|
||||
// @Param type query FileType true "Type"
|
||||
// @Param file body string true "File content"
|
||||
// @Success 200 {object} apitypes.SuccessResponse "File validated"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Bad request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
||||
// @Failure 417 {object} any "Validation failed"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /file/validate [post]
|
||||
func Validate(c *gin.Context) {
|
||||
var request ValidateFileRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
content, err := c.GetRawData()
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to read file"))
|
||||
return
|
||||
}
|
||||
c.Request.Body.Close()
|
||||
|
||||
if valErr := validateFile(request.FileType, content); valErr != nil {
|
||||
c.JSON(http.StatusExpectationFailed, valErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, apitypes.Success("file validated"))
|
||||
}
|
||||
|
||||
func validateFile(fileType FileType, content []byte) gperr.Error {
|
||||
switch fileType {
|
||||
case FileTypeConfig:
|
||||
return config.Validate(content)
|
||||
case FileTypeMiddleware:
|
||||
errs := gperr.NewBuilder("middleware errors")
|
||||
middleware.BuildMiddlewaresFromYAML("", content, &errs)
|
||||
return errs.Error()
|
||||
}
|
||||
return provider.Validate(content)
|
||||
}
|
||||
@@ -4,15 +4,33 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
func HealthWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
U.PeriodicWS(cfg, w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
||||
return wsjson.Write(r.Context(), conn, routequery.HealthMap())
|
||||
})
|
||||
type HealthMap = map[string]routes.HealthInfo // @name HealthMap
|
||||
|
||||
// @x-id "health"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get routes health info
|
||||
// @Description Get health info by route name
|
||||
// @Tags v1,websocket
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} HealthMap "Health info by route name"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /health [get]
|
||||
func Health(c *gin.Context) {
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
|
||||
return routes.GetHealthInfo(), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, routes.GetHealthInfo())
|
||||
}
|
||||
}
|
||||
|
||||
44
internal/api/v1/homepage/categories.go
Normal file
44
internal/api/v1/homepage/categories.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package homepageapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "categories"
|
||||
// @BasePath /api/v1
|
||||
// @Summary List homepage categories
|
||||
// @Description List homepage categories
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} string
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/categories [get]
|
||||
func Categories(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, HomepageCategories())
|
||||
}
|
||||
|
||||
func HomepageCategories() []string {
|
||||
check := make(map[string]struct{})
|
||||
categories := make([]string, 0)
|
||||
categories = append(categories, homepage.CategoryAll)
|
||||
categories = append(categories, homepage.CategoryFavorites)
|
||||
for _, r := range routes.HTTP.Iter {
|
||||
item := r.HomepageItem()
|
||||
if item.Category == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := check[item.Category]; ok {
|
||||
continue
|
||||
}
|
||||
check[item.Category] = struct{}{}
|
||||
categories = append(categories, item.Category)
|
||||
}
|
||||
return categories
|
||||
}
|
||||
36
internal/api/v1/homepage/item_click.go
Normal file
36
internal/api/v1/homepage/item_click.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package homepageapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type HomepageOverrideItemClickParams struct {
|
||||
Which string `form:"which" binding:"required"`
|
||||
} // @name HomepageOverrideItemClickParams
|
||||
|
||||
// @x-id "item-click"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Increment item click
|
||||
// @Description Increment item click.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request query HomepageOverrideItemClickParams true "Increment item click"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/item_click [post]
|
||||
func ItemClick(c *gin.Context) {
|
||||
var params HomepageOverrideItemClickParams
|
||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.IncrementItemClicks(params.Which)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
133
internal/api/v1/homepage/items.go
Normal file
133
internal/api/v1/homepage/items.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package homepageapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
type HomepageItemsRequest struct {
|
||||
SearchQuery string `form:"search"` // Search query
|
||||
Category string `form:"category"` // Category filter
|
||||
Provider string `form:"provider"` // Provider filter
|
||||
// Sort method
|
||||
SortMethod homepage.SortMethod `form:"sort_method" default:"alphabetical" binding:"omitempty,oneof=clicks alphabetical custom"`
|
||||
} // @name HomepageItemsRequest
|
||||
|
||||
// @x-id "items"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Homepage items
|
||||
// @Description Homepage items
|
||||
// @Tags homepage,websocket
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param query query HomepageItemsRequest false "Query parameters"
|
||||
// @Success 200 {object} homepage.Homepage
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/items [get]
|
||||
func Items(c *gin.Context) {
|
||||
var request HomepageItemsRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
proto := "http"
|
||||
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
||||
proto = "https"
|
||||
}
|
||||
hostname := c.Request.Host
|
||||
if host := c.GetHeader("X-Forwarded-Host"); host != "" {
|
||||
hostname = host
|
||||
}
|
||||
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
|
||||
return HomepageItems(proto, hostname, &request), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request))
|
||||
}
|
||||
}
|
||||
|
||||
func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
|
||||
switch proto {
|
||||
case "http", "https":
|
||||
default:
|
||||
proto = "http"
|
||||
}
|
||||
|
||||
hp := homepage.NewHomepageMap(routes.HTTP.Size())
|
||||
|
||||
if strings.Count(hostname, ".") > 1 {
|
||||
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
|
||||
}
|
||||
|
||||
for _, r := range routes.HTTP.Iter {
|
||||
if request.Provider != "" && r.ProviderName() != request.Provider {
|
||||
continue
|
||||
}
|
||||
item := r.HomepageItem()
|
||||
if request.Category != "" && item.Category != request.Category {
|
||||
continue
|
||||
}
|
||||
if request.SearchQuery != "" && !fuzzy.MatchFold(request.SearchQuery, item.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// clear url if invalid
|
||||
_, err := url.Parse(item.URL)
|
||||
if err != nil {
|
||||
item.URL = ""
|
||||
}
|
||||
|
||||
// append hostname if provided and only if alias is not FQDN
|
||||
if hostname != "" && item.URL == "" {
|
||||
isFQDNAlias := strings.Contains(item.Alias, ".")
|
||||
if !isFQDNAlias {
|
||||
item.URL = fmt.Sprintf("%s://%s.%s", proto, item.Alias, hostname)
|
||||
} else {
|
||||
item.URL = fmt.Sprintf("%s://%s", proto, item.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
// prepend protocol if not exists
|
||||
if !strings.HasPrefix(item.URL, "http://") && !strings.HasPrefix(item.URL, "https://") {
|
||||
item.URL = fmt.Sprintf("%s://%s", proto, item.URL)
|
||||
}
|
||||
|
||||
hp.Add(&item)
|
||||
}
|
||||
|
||||
ret := hp.Values()
|
||||
// sort items in each category
|
||||
for _, category := range ret {
|
||||
category.Sort(request.SortMethod)
|
||||
}
|
||||
// sort categories
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
slices.SortStableFunc(ret, func(a, b *homepage.Category) int {
|
||||
// if category is "Hidden", move it to the end of the list
|
||||
if a.Name == homepage.CategoryHidden {
|
||||
return 1
|
||||
}
|
||||
if b.Name == homepage.CategoryHidden {
|
||||
return -1
|
||||
}
|
||||
// sort categories by order in config
|
||||
return overrides.CategoryOrder[a.Name] - overrides.CategoryOrder[b.Name]
|
||||
})
|
||||
return ret
|
||||
}
|
||||
217
internal/api/v1/homepage/overrides.go
Normal file
217
internal/api/v1/homepage/overrides.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package homepageapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type (
|
||||
HomepageOverrideItemParams struct {
|
||||
Which string `json:"which"`
|
||||
Value homepage.ItemConfig `json:"value"`
|
||||
} // @name HomepageOverrideItemParams
|
||||
HomepageOverrideItemsBatchParams struct {
|
||||
Value map[string]homepage.ItemConfig `json:"value"`
|
||||
} // @name HomepageOverrideItemsBatchParams
|
||||
|
||||
HomepageOverrideCategoryOrderParams struct {
|
||||
Which string `json:"which"`
|
||||
Value int `json:"value"`
|
||||
} // @name HomepageOverrideCategoryOrderParams
|
||||
HomepageOverrideItemSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemSortOrderParams
|
||||
HomepageOverrideItemAllSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemAllSortOrderParams
|
||||
HomepageOverrideItemFavSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemFavSortOrderParams
|
||||
|
||||
HomepageOverrideItemVisibleParams struct {
|
||||
Which []string `json:"which"`
|
||||
Value bool `json:"value"`
|
||||
} // @name HomepageOverrideItemVisibleParams
|
||||
HomepageOverrideItemFavoriteParams HomepageOverrideItemVisibleParams // @name HomepageOverrideItemFavoriteParams
|
||||
)
|
||||
|
||||
// @x-id "set-item"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Override single homepage item
|
||||
// @Description Override single homepage item.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemParams true "Override single item"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/item [post]
|
||||
func SetItem(c *gin.Context) {
|
||||
var params HomepageOverrideItemParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.OverrideItem(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-items-batch"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Override multiple homepage items
|
||||
// @Description Override multiple homepage items.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemsBatchParams true "Override multiple items"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/items_batch [post]
|
||||
func SetItemsBatch(c *gin.Context) {
|
||||
var params HomepageOverrideItemsBatchParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.OverrideItems(params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-item-visible"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set homepage item visibility
|
||||
// @Description POST list of item ids and visibility value.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemVisibleParams true "Set item visibility"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/item_visible [post]
|
||||
func SetItemVisible(c *gin.Context) {
|
||||
var params HomepageOverrideItemVisibleParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetItemsVisibility(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-item-favorite"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set homepage item favorite
|
||||
// @Description Set homepage item favorite.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemFavoriteParams true "Set item favorite"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/item_favorite [post]
|
||||
func SetItemFavorite(c *gin.Context) {
|
||||
var params HomepageOverrideItemFavoriteParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetItemsFavorite(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-item-sort-order"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set homepage item sort order
|
||||
// @Description Set homepage item sort order.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemSortOrderParams true "Set item sort order"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/item_sort_order [post]
|
||||
func SetItemSortOrder(c *gin.Context) {
|
||||
var params HomepageOverrideItemSortOrderParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetSortOrder(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-item-all-sort-order"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set homepage item all sort order
|
||||
// @Description Set homepage item all sort order.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemAllSortOrderParams true "Set item all sort order"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/item_all_sort_order [post]
|
||||
func SetItemAllSortOrder(c *gin.Context) {
|
||||
var params HomepageOverrideItemAllSortOrderParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetAllSortOrder(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-item-fav-sort-order"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set homepage item fav sort order
|
||||
// @Description Set homepage item fav sort order.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemFavSortOrderParams true "Set item fav sort order"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/item_fav_sort_order [post]
|
||||
func SetItemFavSortOrder(c *gin.Context) {
|
||||
var params HomepageOverrideItemFavSortOrderParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetFavSortOrder(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-category-order"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set homepage category order
|
||||
// @Description Set homepage category order.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideCategoryOrderParams true "Override category order"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/category_order [post]
|
||||
func SetCategoryOrder(c *gin.Context) {
|
||||
var params HomepageOverrideCategoryOrderParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetCategoryOrder(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
)
|
||||
|
||||
const (
|
||||
HomepageOverrideItem = "item"
|
||||
HomepageOverrideItemsBatch = "items_batch"
|
||||
HomepageOverrideCategoryOrder = "category_order"
|
||||
HomepageOverrideItemVisible = "item_visible"
|
||||
)
|
||||
|
||||
type (
|
||||
HomepageOverrideItemParams struct {
|
||||
Which string `json:"which"`
|
||||
Value homepage.ItemConfig `json:"value"`
|
||||
}
|
||||
HomepageOverrideItemsBatchParams struct {
|
||||
Value map[string]*homepage.ItemConfig `json:"value"`
|
||||
}
|
||||
HomepageOverrideCategoryOrderParams struct {
|
||||
Which string `json:"which"`
|
||||
Value int `json:"value"`
|
||||
}
|
||||
HomepageOverrideItemVisibleParams struct {
|
||||
Which []string `json:"which"`
|
||||
Value bool `json:"value"`
|
||||
}
|
||||
)
|
||||
|
||||
func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
|
||||
what := r.FormValue("what")
|
||||
if what == "" {
|
||||
http.Error(w, "missing what or which", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
utils.RespondError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
r.Body.Close()
|
||||
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
switch what {
|
||||
case HomepageOverrideItem:
|
||||
var params HomepageOverrideItemParams
|
||||
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||
utils.RespondError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
overrides.OverrideItem(params.Which, ¶ms.Value)
|
||||
case HomepageOverrideItemsBatch:
|
||||
var params HomepageOverrideItemsBatchParams
|
||||
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||
utils.RespondError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
overrides.OverrideItems(params.Value)
|
||||
case HomepageOverrideItemVisible: // POST /v1/item_visible [a,b,c], false => hide a, b, c
|
||||
var params HomepageOverrideItemVisibleParams
|
||||
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||
utils.RespondError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if params.Value {
|
||||
overrides.UnhideItems(params.Which...)
|
||||
} else {
|
||||
overrides.HideItems(params.Which...)
|
||||
}
|
||||
case HomepageOverrideCategoryOrder:
|
||||
var params HomepageOverrideCategoryOrderParams
|
||||
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||
utils.RespondError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
overrides.SetCategoryOrder(params.Which, params.Value)
|
||||
default:
|
||||
http.Error(w, "invalid what", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
37
internal/api/v1/icons.go
Normal file
37
internal/api/v1/icons.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type ListIconsRequest struct {
|
||||
Limit int `form:"limit" validate:"omitempty,min=0"`
|
||||
Keyword string `form:"keyword" validate:"required"`
|
||||
} // @name ListIconsRequest
|
||||
|
||||
// @x-id "icons"
|
||||
// @BasePath /api/v1
|
||||
// @Summary List icons
|
||||
// @Description List icons
|
||||
// @Tags v1
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit"
|
||||
// @Param keyword query string false "Keyword"
|
||||
// @Success 200 {array} homepage.IconMetaSearch
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Router /icons [get]
|
||||
func Icons(c *gin.Context) {
|
||||
var request ListIconsRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
icons := homepage.SearchIcons(request.Keyword, request.Limit)
|
||||
c.JSON(http.StatusOK, icons)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
)
|
||||
|
||||
func Index(w http.ResponseWriter, r *http.Request) {
|
||||
WriteBody(w, []byte("API ready"))
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/internal"
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
ListRoute = "route"
|
||||
ListRoutes = "routes"
|
||||
ListFiles = "files"
|
||||
ListMiddlewares = "middlewares"
|
||||
ListMiddlewareTraces = "middleware_trace"
|
||||
ListMatchDomains = "match_domains"
|
||||
ListHomepageConfig = "homepage_config"
|
||||
ListRouteProviders = "route_providers"
|
||||
ListHomepageCategories = "homepage_categories"
|
||||
ListIcons = "icons"
|
||||
ListTasks = "tasks"
|
||||
)
|
||||
|
||||
func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
what := r.PathValue("what")
|
||||
if what == "" {
|
||||
what = ListRoutes
|
||||
}
|
||||
which := r.PathValue("which")
|
||||
|
||||
switch what {
|
||||
case ListRoute:
|
||||
if route := listRoute(which); route == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
} else {
|
||||
U.RespondJSON(w, r, route)
|
||||
}
|
||||
case ListRoutes:
|
||||
U.RespondJSON(w, r, routequery.RoutesByAlias(route.RouteType(r.FormValue("type"))))
|
||||
case ListFiles:
|
||||
listFiles(w, r)
|
||||
case ListMiddlewares:
|
||||
U.RespondJSON(w, r, middleware.All())
|
||||
case ListMiddlewareTraces:
|
||||
U.RespondJSON(w, r, middleware.GetAllTrace())
|
||||
case ListMatchDomains:
|
||||
U.RespondJSON(w, r, cfg.Value().MatchDomains)
|
||||
case ListHomepageConfig:
|
||||
U.RespondJSON(w, r, routequery.HomepageConfig(cfg.Value().Homepage.UseDefaultCategories, r.FormValue("category"), r.FormValue("provider")))
|
||||
case ListRouteProviders:
|
||||
U.RespondJSON(w, r, cfg.RouteProviderList())
|
||||
case ListHomepageCategories:
|
||||
U.RespondJSON(w, r, routequery.HomepageCategories())
|
||||
case ListIcons:
|
||||
limit, err := strconv.Atoi(r.FormValue("limit"))
|
||||
if err != nil {
|
||||
limit = 0
|
||||
}
|
||||
icons, err := internal.SearchIcons(r.FormValue("keyword"), limit)
|
||||
if err != nil {
|
||||
U.RespondError(w, err)
|
||||
return
|
||||
}
|
||||
if icons == nil {
|
||||
icons = []string{}
|
||||
}
|
||||
U.RespondJSON(w, r, icons)
|
||||
case ListTasks:
|
||||
U.RespondJSON(w, r, task.DebugTaskList())
|
||||
default:
|
||||
U.HandleErr(w, r, U.ErrInvalidKey("what"), http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// if which is "all" or empty, return map[string]Route of all routes
|
||||
// otherwise, return a single Route with alias which or nil if not found.
|
||||
func listRoute(which string) any {
|
||||
if which == "" || which == "all" {
|
||||
return routequery.RoutesByAlias()
|
||||
}
|
||||
routes := routequery.RoutesByAlias()
|
||||
route, ok := routes[which]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return route
|
||||
}
|
||||
|
||||
func listFiles(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
resp := map[FileType][]string{
|
||||
FileTypeConfig: make([]string, 0),
|
||||
FileTypeProvider: make([]string, 0),
|
||||
FileTypeMiddleware: make([]string, 0),
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
t := fileType(file)
|
||||
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
|
||||
resp[t] = append(resp[t], file)
|
||||
}
|
||||
|
||||
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
for _, mid := range mids {
|
||||
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
|
||||
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
|
||||
}
|
||||
U.RespondJSON(w, r, resp)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user