Compare commits

...

217 Commits
0.4.6 ... 0.7.6

Author SHA1 Message Date
yusing
ba26e6a5d6 grafana dashboard template 2024-11-10 06:53:31 +08:00
yusing
6194bac4c4 metric unregistration on route removal, fixed multi-ips as visitor label detected from x headers 2024-11-10 06:47:59 +08:00
yusing
a1d1325ad6 updated health status impl 2024-11-10 06:35:56 +08:00
yusing
cceebff93a fixed CIDR whitelist shared its IP cache map when it should not 2024-11-10 03:25:33 +08:00
yusing
f97e3f65fe go version and deps update, fixed middlewares and metrics
- fixed "API JWT secret empty" warning output format
- fixed metrics initialized when it should not
- fixed middlewares.modifyRequest Host header not working properly
2024-11-08 06:14:08 +08:00
yusing
5214ae1760 uptime metrics 2024-11-07 11:44:01 +08:00
yusing
6be3aef367 changed req time elapsed to count on status code sent 2024-11-07 11:43:49 +08:00
yusing
6712e9b109 initial prometheus metrics support, simplfied some code 2024-11-06 12:24:12 +08:00
yusing
50a0686648 rebrand update Makefile 2024-11-06 05:06:17 +08:00
yusing
d47afa3081 removed extra ` from README 2024-11-06 05:05:46 +08:00
Yuzerion
1ddfe2fb92 Merge pull request #30 from codekoala/update-setup-instructions
Update setup instructions
2024-11-05 23:52:13 +08:00
Josh VanderLinden
3ae3d18566 Update setup instructions 2024-11-05 08:49:33 -05:00
yusing
5fdb171d65 rebrand changed startup message, built script and Dockerfile 2024-11-04 03:47:37 +08:00
yusing
99e43fe340 (non-breaking) rebrand changed environment variables 2024-11-04 03:29:26 +08:00
yusing
cf1ecbc826 added option to disable default app categories 2024-11-04 01:44:58 +08:00
yusing
5ff27b9e3d fixed extra output for ls-* commands 2024-11-04 01:32:26 +08:00
yusing
291304af75 readme update 2024-11-04 00:44:54 +08:00
yusing
b63ebfcb3b disabled auth by default (when no JWT secret is specified) 2024-11-04 00:32:19 +08:00
yusing
c6a9a816f6 copied default config into docker image, fixed ls-routes 2024-11-04 00:31:34 +08:00
yusing
f5cf716a91 rebrand as GoDoxy 2024-11-04 00:06:59 +08:00
yusing
6dbee61742 fixed wrong closing tag in example error page 2024-11-03 23:45:23 +08:00
yusing
d89d97b61f added predefined homepage categories 2024-11-03 11:44:30 +08:00
yusing
01b7ec2a99 API server now protected with rate limiter, fixed extra dot on URL on frontend 2024-11-03 10:11:27 +08:00
yusing
ddbee9ec19 added example error pages, removed compose examples 2024-11-03 09:56:47 +08:00
yusing
0bbadc6d6d removed default values 2024-11-03 07:34:16 +08:00
yusing
64584c73b2 rate limiter check host instead of full remote address 2024-11-03 07:16:59 +08:00
yusing
8df28628ec removed unnecessary pointer indirection, added rate limiter middleware 2024-11-03 07:07:30 +08:00
yusing
3bf520541b fixed stats websocket endpoint when no match_domains configured 2024-11-03 06:04:35 +08:00
yusing
a531896bd6 updated docker compose example 2024-11-03 05:27:27 +08:00
yusing
e005b42d18 readme indentation 2024-11-03 04:50:40 +08:00
yusing
1f6573b6da readme update 2024-11-03 04:43:17 +08:00
yusing
73af381c4c updated auto and manual setup guide 2024-11-03 04:38:09 +08:00
yusing
625bf4dfdc improved HTTP performance, especially when match_domains are used; api json string fix 2024-11-02 07:14:03 +08:00
yusing
46b4090629 moved env to .env.example, update setup method 2024-11-02 03:36:55 +08:00
yusing
91e012987e added option for jwt token ttl 2024-11-02 03:21:47 +08:00
yusing
a86d316d07 refactor and typo fixes 2024-11-02 03:14:47 +08:00
yusing
76454df5e6 remove useless dummytask 2024-11-02 03:14:25 +08:00
yusing
67b6e40f85 remove unused code 2024-11-02 03:04:15 +08:00
yusing
9889b5a8d3 correcting startup behavior 2024-11-02 03:00:14 +08:00
yusing
00fc75b61b update setup default branch 2024-10-31 03:07:03 +08:00
yusing
4ee93a1351 added logout endpoint 2024-10-31 00:58:35 +08:00
yusing
669d13b89a fixed crash on some base64 jwt secret 2024-10-30 11:09:52 +08:00
yusing
5fa86b5eb7 fixed auth internal server error 2024-10-30 10:19:30 +08:00
yusing
369cdf8c4f fixed config reload 2024-10-30 06:52:18 +08:00
yusing
0397f69853 fixed notification not being sent 2024-10-30 06:44:10 +08:00
yusing
81177926ff implemented login and jwt auth 2024-10-30 06:25:32 +08:00
yusing
e5bbb18414 migrated from logrus to zerolog, improved error formatting, fixed concurrent map write, fixed crash on rapid page refresh for idle containers, fixed infinite recursion on gotfiy error, fixed websocket connection problem when using idlewatcher 2024-10-29 11:34:58 +08:00
yusing
cfa74d69ae fixed out of range error on gotify message being sent 2024-10-22 16:46:12 +08:00
yusing
bee26f43d4 initial gotify support 2024-10-22 05:38:09 +08:00
yusing
a3ab32e9ab reload no longer be skipped when there're errors 2024-10-20 15:05:04 +08:00
yusing
c847fe4747 fixed schema and json tag, hide http://:0 2024-10-20 11:04:44 +08:00
yusing
a278711421 fixed loadbalancer with idlewatcher, fixed reload issue 2024-10-20 09:46:02 +08:00
yusing
01ffe0d97c simplified error messages 2024-10-19 14:25:28 +08:00
yusing
bd732dfa0a fixed homepage item not showing 2024-10-19 01:30:34 +08:00
yusing
8b8e1773e8 fixed loadbalanced routes with same alias cause conflict 2024-10-19 01:20:08 +08:00
yusing
b296fb2965 fixed healthcheck failed to disable and nil dereference 2024-10-19 00:13:55 +08:00
yusing
53557e38b6 Fixed a few issues:
- Incorrect name being shown on dashboard "Proxies page"
- Apps being shown when homepage.show is false
- Load balanced routes are shown on homepage instead of the load balancer
- Route with idlewatcher will now be removed on container destroy
- Idlewatcher panic
- Performance improvement
- Idlewatcher infinitely loading
- Reload stucked / not working properly
- Streams stuck on shutdown / reload
- etc...
Added:
- support idlewatcher for loadbalanced routes
- partial implementation for stream type idlewatcher
Issues:
- graceful shutdown
2024-10-18 16:47:01 +08:00
yusing
c0c61709ca fixed idlewatcher panic, dashboard app name, route not removing on container destroy 2024-10-16 00:57:10 +08:00
yusing
56b778f19c fixed json output for ls-routes and its API and homepage api 2024-10-15 16:23:46 +08:00
yusing
f4d532598c Improved healthcheck, idlewatcher support for loadbalanced routes, bug fixes 2024-10-15 15:34:27 +08:00
yusing
53fa28ae77 graceful shutdown and ref count related 2024-10-14 10:31:27 +08:00
yusing
f38b3abdbc improved health check 2024-10-14 10:02:53 +08:00
yusing
99207ae606 routes in loadbalance pool no longer listed in ls-route and its API, the loadbalancer is listed instead. improved context handling and grateful shutdown 2024-10-14 09:28:54 +08:00
yusing
d3b8cb8cba fixed possible memory leak for UDP stream 2024-10-14 08:57:46 +08:00
yusing
51c6eb4597 fixed problems related to reload and port selection 2024-10-14 08:55:49 +08:00
yusing
d47b672aa5 refactored some stuff, added healthcheck support, fixed 'include file' reload not showing in log 2024-10-12 13:56:38 +08:00
yusing
64e30f59e8 added missing json tags 2024-10-11 10:38:38 +08:00
yusing
cef7b3d396 updated tests for new behavior 2024-10-11 10:00:10 +08:00
yusing
7184c9cfe9 correcting some behaviors for $DOCKER_HOST, now uses container's private IP instead of localhost 2024-10-11 09:13:38 +08:00
yusing
da04a0dff4 added golangci-linting, refactor, simplified error msgs and fixed some error handling 2024-10-10 11:52:09 +08:00
yusing
d91b66ae87 performance improvement and small fix on loadbalancer 2024-10-09 18:10:51 +08:00
yusing
5c40f4aa84 added round_robin, least_conn and ip_hash load balance support, small refactoring 2024-10-09 10:39:07 +08:00
yusing
1797896fa6 fixed typos and formatting, fixed loading page not being shown (idlewaker) 2024-10-08 13:15:23 +08:00
yusing
d1c9e18c97 improved idlewatcher support for API-like services, fixed idlewaker proxying to zero port 2024-10-07 18:50:51 +08:00
yusing
ef83ed0596 improved idlewatcher and content type matching, update CI 2024-10-07 17:41:08 +08:00
yusing
d89155a6ee idlewatcher fixed idlewatcher incorrect respond haviour, keep url path 2024-10-07 16:44:18 +08:00
yusing
921ce23dde refactored http import name, fixed and simplified idlewatcher/idlewaker implementation, dependencies update 2024-10-07 12:45:07 +08:00
yusing
929b7f7059 get back aa6fafd5, accidentally reverted in 03cad9f3 2024-10-07 00:06:29 +08:00
yusing
de7805f281 fixed idlewatcher panics and incorrect behavior, update screenshot 2024-10-06 16:17:52 +08:00
yusing
03cad9f315 added package version api, dependencies upgrade 2024-10-06 09:23:41 +08:00
yusing
aa6fafd52f improved tracing for debug 2024-10-06 06:06:29 +08:00
yusing
01ff63a007 fix forward auth attempt#1 2024-10-06 03:18:06 +08:00
yusing
99746bad8e fix attempt#1: int64 not assignable to int 2024-10-06 02:02:13 +08:00
yusing
21b67e97af websocket fix attempt#2 2024-10-06 01:21:35 +08:00
yusing
668639e484 websocket fix attempt 2024-10-06 00:09:14 +08:00
yusing
e9b2079599 duration formatting update 2024-10-05 09:58:56 +08:00
yusing
5fb7d21c80 fixed that error message with sensitive info shouldn't be shown to end user 2024-10-05 03:42:09 +08:00
yusing
f5e00a6ef4 oops, adding back proxy.exclude=1 2024-10-04 19:07:48 +08:00
yusing
b06cbc0fee fixed dashboard stats update 2024-10-04 18:52:31 +08:00
yusing
abbcbad5e9 readme updates, docs moved to wiki 2024-10-04 11:27:11 +08:00
yusing
fab39a461f added ls-icons command 2024-10-04 10:04:18 +08:00
yusing
9c3edff92b databases without explicit alias(es) are now excluded by default 2024-10-04 09:17:45 +08:00
yusing
e8f4cd18a4 refactor: moved models/ to types/ 2024-10-04 08:47:53 +08:00
yusing
e566fd9b57 fixed homepage not respecting homepage.show field, disabled schema validation for included file 2024-10-04 08:36:32 +08:00
yusing
6211ddcdf0 show docker provider name instead of address in log 2024-10-04 07:21:49 +08:00
yusing
245f073350 tuned some http settings, refactor 2024-10-04 07:13:52 +08:00
yusing
dd629f516b omit EOF and contextCanceled error on non-debug mode 2024-10-04 06:55:43 +08:00
yusing
31080edd59 fixed event name missing 2024-10-04 06:51:26 +08:00
yusing
b679655cd5 fixed dashboard incorrect stats 2024-10-04 06:38:27 +08:00
yusing
ca3b062f89 updated schema for homepage fields 2024-10-04 01:00:06 +08:00
yusing
de6c1be51b improved homepage support, memory leak partial fix 2024-10-03 20:02:43 +08:00
yusing
4f09dbf044 replace - _ with whitespace for default homepage.name 2024-10-03 10:19:31 +08:00
yusing
e6b4630ce9 experimental homepage labels support 2024-10-03 10:10:14 +08:00
yusing
90bababd38 improved homepage labels 2024-10-03 04:00:02 +08:00
yusing
90130411f9 initial support of homepage labels 2024-10-03 02:53:05 +08:00
yusing
ae61a2335d added v1/list/match_domains 2024-10-03 02:13:34 +08:00
yusing
8329a8ea9c replacing label parser map with improved deserialization implementation, API host check now disabled when in debug mode 2024-10-03 01:50:49 +08:00
yusing
ef52ccb929 fixed api, fixed ListFiles function 2024-10-02 17:34:35 +08:00
yusing
ed9d8aab6f fixed docs 2024-10-02 17:33:41 +08:00
yusing
aa16287447 fixed route gone after container restart / Brename 2024-10-02 15:38:36 +08:00
yusing
a7a922308e fixed streams with zero port being served 2024-10-02 14:01:36 +08:00
yusing
ba13b81b0e fixed middleware implementation, added middleware tracing for easier debug 2024-10-02 13:55:41 +08:00
yusing
d172552fb0 fixed docs 2024-10-02 01:33:52 +08:00
yusing
2a8ab27fc1 fixed docs 2024-10-02 01:28:55 +08:00
yusing
e8c3e4c75f added cidr_whitelist middleware 2024-10-02 01:20:25 +08:00
yusing
ed887a5cfc fixed serialization and middleware compose 2024-10-02 01:04:34 +08:00
yusing
1bac96dc2a update docker test 2024-10-02 01:02:59 +08:00
yusing
c3b779a810 containers without port mapped will no longer be served 2024-10-01 17:18:17 +08:00
yusing
44cfd65f6c implement middleware compose 2024-10-01 16:38:07 +08:00
yusing
f5a36f94bb fixed error subject missing in some cases 2024-10-01 05:14:56 +08:00
yusing
e951194bee fixed route not being updated on restart, added experimental middleware compose support 2024-09-30 19:00:27 +08:00
yusing
478311fe9e fixed container routes not being loaded, added X-Forwarded-{Scheme,Proto,Host}, fixed containers with no mapping being served 2024-09-30 18:04:47 +08:00
yusing
48dd1397e8 remove sensitive info from debug logging 2024-09-30 16:32:58 +08:00
yusing
ebedbc931f enables add-x-forwarded by default, added hide-x-forwarded 2024-09-30 16:16:56 +08:00
yusing
9065d990e5 go-proxy ls-route now query api server first, then fallback to read from config file 2024-09-30 15:56:03 +08:00
yusing
b38d7595a7 fixed issue for container not being excluded on restart 2024-09-30 15:19:59 +08:00
yusing
860e914b90 added real_ip and cloudflare_real_ip middlewares, fixed that some middlewares does not work properly 2024-09-30 04:03:48 +08:00
yusing
ac3af49aa7 update compose example 2024-09-29 11:46:54 +08:00
yusing
415f169f48 added explicit only mode for docker provider, updated dependencies 2024-09-29 11:24:41 +08:00
yusing
e2b08d8667 ci speedup 2024-09-29 06:00:52 +08:00
yusing
91e7f4894a fixed some containers being excluded on restart 2024-09-28 12:34:06 +08:00
yusing
a78dba5191 added response message on invalid api request host 2024-09-28 11:55:26 +08:00
yusing
c7208c90c6 updated example 2024-09-28 11:51:47 +08:00
yusing
da6a2756fa custom error page enabled for default for non-exist routes and invalid host 2024-09-28 11:45:01 +08:00
yusing
9a6a66f5a8 fixing dockerfile 2024-09-28 09:58:08 +08:00
yusing
90487bfde6 restructured the project to comply community guideline, for others check release note 2024-09-28 09:51:34 +08:00
yusing
4120fd8d1c fixed unchecked integer conversion, fixed 'invalid host' bug, corrected error message 2024-09-28 01:20:18 +08:00
yusing
6f3a5ebe6e Update documentation for Docker labels and middlewares, now fields works for snake cases, camel cases, pascal cases 2024-09-27 23:44:45 +08:00
yusing
a935f200a3 removed config example from README, check config.example.yml for complete explaination 2024-09-27 09:59:36 +08:00
yusing
f474ae4f75 added support for a few middlewares, added match_domain option, changed index reference prefix from $ to #, etc. 2024-09-27 09:57:57 +08:00
yusing
345a4417a6 changing alias index prefix from '$' to '#', to avoid unquoted/unescaped dollar sign being treated as interpolation 2024-09-27 00:34:14 +08:00
yusing
8cca83723c added discord invite badge 2024-09-27 00:23:46 +08:00
Yuzerion
aa2fcd47c2 Update docker.md 2024-09-26 22:52:56 +08:00
Yuzerion
0580a7d3cd Update config.example.yml 2024-09-26 22:51:29 +08:00
Yuzerion
a43c242c66 fixing wrong format for config example 2024-09-26 22:50:56 +08:00
yusing
45d4b92fc6 Fixed missing error subject 2024-09-26 20:11:05 +08:00
yusing
72df9ff3e4 Initial abstract implementation of middlewares 2024-09-25 14:12:40 +08:00
yusing
48bf31fd0e refactor file names, readme updates, removed frontend submodule as it is being built independently 2024-09-25 11:22:25 +08:00
yusing
4ee5383f7d github ci fix attempt, speedup docker build on CI 2024-09-25 10:46:45 +08:00
yusing
33fb60a32d Refactor Docker CI workflow for multi-platform builds 2024-09-25 10:09:50 +08:00
yusing
d10d0e49fa Update default wake timeout to 30 seconds, fixed port selection, improved idlewatcher 2024-09-25 05:27:12 +08:00
yusing
dc3575c8fd Refactoring 2024-09-25 03:43:47 +08:00
yusing
17115cfb0b Refactor and fixed port and scheme assignment logic in FillMissingFields.
Fixed issue that when a container is stopped or network=host, it will be excluded.
2024-09-25 03:40:57 +08:00
yusing
498082f7e5 no longer exclude a stopped container that have user specified port 2024-09-25 00:56:33 +08:00
yusing
99216ffe59 fixed subdomain matching for sub-sub-subdomain and so on, now return 404 when subdomain is missing 2024-09-24 18:30:25 +08:00
yusing
f426dbc9cf removed save registration.json since it does not work properly 2024-09-24 18:18:37 +08:00
yusing
1c611cc9b9 example fix 2024-09-24 03:13:39 +08:00
yusing
dc43e26770 Format README for better clarity in setup instructions 2024-09-24 03:09:50 +08:00
yusing
79ae26f1b5 new simpler setup method, readme and doc update 2024-09-23 22:10:13 +08:00
yusing
109c2460fa fixed container being excluded in host network_mode 2024-09-23 20:34:46 +08:00
yusing
71e8e4a462 smarter scheme and port detection 2024-09-23 20:16:38 +08:00
yusing
8e2cc56afb Fixed nil dereferencing and added missing fields validation 2024-09-23 16:14:34 +08:00
yusing
6728bc39d2 fixed nil dereferencing 2024-09-23 07:19:47 +08:00
yusing
daca4b7735 shrink docker image size in half, adding back ForceColor for logrus 2024-09-23 05:34:50 +08:00
yusing
3b597eea29 support '0' listening port, readme update, showcase added 2024-09-23 04:09:56 +08:00
yusing
090b73d287 fixed tcp/udp I/O, deadlock, nil dereference; improved docker watcher, idlewatcher, loading page 2024-09-23 00:49:46 +08:00
yusing
96bce79e4b changed env GOPROXY_*_PORT to GOPROXY_*_ADDR, changed api server default to listen on localhost only, readme update 2024-09-22 06:06:24 +08:00
yusing
d9fd399e43 fix stuck loading in some scenerios for ls-* command line options 2024-09-22 05:01:36 +08:00
yusing
46281aa3b0 renamed ProxyEntry to RawEntry to avoid confusion with src/proxy/entry.go 2024-09-22 04:13:42 +08:00
yusing
d39b68bfd8 fixed possible resource leak 2024-09-22 04:11:02 +08:00
yusing
a11ce46028 added some docker compose examples; fixed defaults to wrong host; updated watcher behavior to retry connection every 3 secs until success or until cancelled 2024-09-22 04:00:08 +08:00
yusing
6388d9d44d fixed outputing error in ls-config, ls-routes, etc. 2024-09-21 18:47:38 +08:00
yusing
69361aea1b fixed host set to localhost even on remote docker, fixed one error in provider causing all routes not to load 2024-09-21 18:23:20 +08:00
yusing
26e2154c64 fixed startup crash for file provider 2024-09-21 17:22:17 +08:00
Yuzerion
a29bf880bc Update docker.md
Too sleepy...
2024-09-21 16:08:11 +08:00
Yuzerion
1f6d03bdbb Update compose.example.yml 2024-09-21 16:07:12 +08:00
Yuzerion
4a7d898b8e Update docker.md 2024-09-21 16:06:32 +08:00
Yuzerion
521b694aec Update docker.md 2024-09-21 15:56:39 +08:00
yusing
a351de7441 github CI fix attempt 2024-09-21 14:32:52 +08:00
yusing
ab2dc26b76 fixing udp stream listening on wrong port 2024-09-21 14:18:29 +08:00
yusing
9a81b13b67 fixing tcp/udp error on closing 2024-09-21 13:40:20 +08:00
yusing
626bd9666b check release 2024-09-21 12:45:56 +08:00
yusing
d7eab2ebcd fixing idlewatcher 2024-09-21 09:42:40 +08:00
yusing
e48b9bbb0a 新增繁中README (未完成) 2024-09-19 21:16:38 +08:00
yusing
339411530b v0.5.0-rc5: merge 2024-09-19 20:42:12 +08:00
yusing
4a2d42bfa9 v0.5.0-rc5: check release 2024-09-19 20:40:03 +08:00
Yuzerion
81da9ad83a small fix 2024-09-18 09:10:41 +08:00
yusing
be7a766cb2 v0.5.0-rc5: added proxy.exclude label, refactored some code 2024-09-17 17:56:41 +08:00
yusing
83d1d027c6 added TZ env to docker compose example 2024-09-17 12:36:13 +08:00
yusing
21fcceb391 v0.5.0-rc4: initial support for ovh, provider generator implementation update, replaced all interface{} to any 2024-09-17 12:06:58 +08:00
yusing
82f06374f7 v0.5.0-rc4: fixing autocert issue, cache ACME registration, added ls-config option 2024-09-17 08:41:36 +08:00
yusing
04fd6543fd README update for sonarcloud badges, simplify some test code, fixed some sonarlint issues 2024-09-17 04:51:26 +08:00
yusing
409a18df38 update default branch for setup script 2024-09-17 03:54:55 +08:00
yusing
4e5a8d0985 v0.5-rc3: version bump 2024-09-17 03:19:21 +08:00
yusing
16b507bc7c v0.5-rc3: update docker port detect mechanism, docker compose file and doc update 2024-09-17 03:11:04 +08:00
yusing
1120991019 v0.5-rc2: fixed port being overridden to 80 or 443 2024-09-17 00:30:26 +08:00
yusing
c0ebd9f8c0 v0.5-rc2: added reload cooldown, fixed auto reload, updated API 2024-09-17 00:10:25 +08:00
yusing
996b418ea9 v0.5-rc1: updated Dockerfile to conform latest format 2024-09-16 13:24:53 +08:00
yusing
4cddd4ff71 v0.5-rc1: schema fixes, provider file example update 2024-09-16 13:19:24 +08:00
yusing
7a0478164f v0.5: (BREAKING) replacing path with path_patterns, improved docker monitoring mechanism, bug fixes 2024-09-16 13:05:04 +08:00
yusing
2e7ba51521 v0.5: (BREAKING) new syntax for set_headers and hide_headers, updated label parser, error.Nil().String() will now return 'nil', better readme 2024-09-16 07:21:45 +08:00
yusing
5be8659a99 v0.5: (BREAKING) simplified config format, improved output formatting, fixed docker watcher 2024-09-16 03:48:39 +08:00
default
719693deb7 v0.5: (BREAKING) simplified config format, improved error output, updated proxy entry default value for 'port' 2024-08-14 02:41:11 +08:00
default
23e7d06081 v0.5: removed system service env, log output format fix 2024-08-13 06:00:22 +08:00
default
85fb637551 v0.5: fixed nil dereference for empty autocert config, fixed and simplified 'error' module, small readme and docs update 2024-08-13 04:59:34 +08:00
default
2fc82c3790 v0.5: remove binary build 2024-08-09 07:09:42 +08:00
default
a5a31a0d63 v0.5: readme and dockerfile update, removed old panel sources, added new frontend as submodule 2024-08-09 07:03:24 +08:00
default
73e481bc96 v0.5: dependencies update, EOF fix for PUT/POST /v1/file 2024-08-09 02:10:48 +08:00
default
93359110a2 preparing for v0.5 2024-08-01 10:06:42 +08:00
yusing
24778d1093 doc fix 2024-04-09 06:53:54 +00:00
yusing
830d0bdadd revert github workflow 2024-04-08 05:34:41 +00:00
yusing w
e12b356d0d Merge branch 'dev' into 'main'
Dev

See merge request yusing/go-proxy!3
2024-04-08 05:07:27 +00:00
yusing w
52549b6446 Dev 2024-04-08 05:07:27 +00:00
yusing
8694987ef9 version bump 2024-04-01 04:09:22 +00:00
yusing
b125b14bf6 added license 2024-04-01 04:08:48 +00:00
yusing
c782f365f9 readme update 2024-04-01 03:29:34 +00:00
yusing
72418a2056 added host network mode support, docs update, UDP fix 2024-04-01 03:23:30 +00:00
261 changed files with 17955 additions and 5789 deletions

25
.env.example Normal file
View File

@@ -0,0 +1,25 @@
# set timezone to get correct log timestamp
TZ=ETC/UTC
# generate secret with `openssl rand -base64 32`
GODOXY_API_JWT_SECRET=
# the JWT token time-to-live
GODOXY_API_JWT_TOKEN_TTL=1h
# API/WebUI login credentials
GODOXY_API_USER=admin
GODOXY_API_PASSWORD=password
# Proxy listening address
GODOXY_HTTP_ADDR=:80
GODOXY_HTTPS_ADDR=:443
# API listening address
GODOXY_API_ADDR=127.0.0.1:8888
# Prometheus Metrics listening address (uncomment to enable)
#GODOXY_PROMETHEUS_ADDR=:8889
# Debug mode
GODOXY_DEBUG=false

View File

@@ -1,14 +1,128 @@
name: Docker Image CI
on:
push:
tags:
- "*"
push:
tags: ["*"]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Build and Push Container to ghcr.io
uses: GlueOps/github-actions-build-push-containers@v0.3.7
with:
tags: latest,${{ github.ref_name }}
build:
name: Build multi-platform Docker image
runs-on: ubuntu-22.04
permissions:
contents: read
packages: write
id-token: write
attestations: write
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
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- 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: 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: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${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: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- 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: /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: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}

View File

@@ -1,30 +0,0 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.22.1"
- name: Build
run: make build
- name: Release
uses: softprops/action-gh-release@v2
with:
files: bin/go-proxy
#- name: Test
# run: go test -v ./...

27
.gitignore vendored
View File

@@ -1,9 +1,28 @@
compose.yml
*.compose.yml
config/
certs/
config
certs
config*/
certs*/
bin/
templates/codemirror/
error_pages/
!examples/error_pages/
logs/
log/
log/
.vscode/settings.json
go.work.sum
!cmd/**/
!internal/**/
todo.md
.*.swp
.aider*
mtrace.json
.env
test.Dockerfile

15
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,15 @@
build-image:
image: docker
rules:
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
variables:
CI_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE:latest
- if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH
variables:
CI_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH
before_script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
script:
- echo building $CI_REGISTRY_IMAGE
- docker build --no-cache --build-arg VERSION=$CI_COMMIT_REF_NAME -t $CI_REGISTRY_IMAGE .
- docker push $CI_REGISTRY_IMAGE

137
.golangci.yml Normal file
View File

@@ -0,0 +1,137 @@
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
linters:
enable-all: true
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
- 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

9
.trunk/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
*out
*logs
*actions
*notifications
*tools
plugins
user_trunk.yaml
user.yaml
tmp

42
.trunk/trunk.yaml Normal file
View File

@@ -0,0 +1,42 @@
# This file controls the behavior of Trunk: https://docs.trunk.io/cli
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
version: 0.1
cli:
version: 1.22.6
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
plugins:
sources:
- id: trunk
ref: v1.6.3
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.12.1
- python@3.10.8
- go@1.23.2
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
lint:
disabled:
- markdownlint
- yamllint
enabled:
- hadolint@2.12.0
- actionlint@1.7.3
- checkov@3.2.257
- git-diff-check
- gofmt@1.20.4
- golangci-lint@1.61.0
- osv-scanner@1.9.0
- oxipng@9.1.2
- prettier@3.3.3
- shellcheck@0.10.0
- shfmt@3.6.0
- trufflehog@3.82.7
actions:
disabled:
- trunk-announce
- trunk-check-pre-push
- trunk-fmt-pre-commit
enabled:
- trunk-upgrade-available

11
.vscode/settings.example.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"yaml.schemas": {
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
"config.example.yml",
"config.yml"
],
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
"providers.example.yml"
]
}
}

16
.vscode/settings.json vendored
View File

@@ -1,16 +0,0 @@
{
"go.inferGopath": false,
"yaml.schemas": {
// "https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
// "config.example.yml",
// "config.yml"
// ],
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
"providers.example.yml",
"*.providers.yml"
],
"file:///config/workspace/go-proxy/schema/config.schema.json": [
"file:///config/workspace/go-proxy/config.example.yml"
]
}
}

View File

@@ -1,39 +1,61 @@
FROM alpine:latest AS codemirror
RUN apk add --no-cache unzip wget make
COPY Makefile .
RUN make setup-codemirror
# Stage 1: Builder
FROM golang:1.23.3-alpine AS builder
RUN apk add --no-cache tzdata make
FROM golang:1.22.1-alpine as builder
COPY src/ /src
COPY go.mod go.sum /src/go-proxy
WORKDIR /src/go-proxy
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
go mod download -x
ENV GOCACHE=/root/.cache/go-build
ARG VERSION
ENV VERSION=${VERSION}
COPY scripts /src/scripts
COPY Makefile /src/
RUN --mount=type=cache,target="/go/pkg/mod" \
--mount=type=cache,target="/root/.cache/go-build" \
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy
--mount=type=bind,src=cmd,dst=/src/cmd \
--mount=type=bind,src=internal,dst=/src/internal \
--mount=type=bind,src=pkg,dst=/src/pkg \
make build && \
mkdir -p /app/error_pages /app/certs && \
mv bin/godoxy /app/godoxy
FROM alpine:latest
# Stage 2: Final image
FROM scratch
LABEL maintainer="yusing@6uo.me"
LABEL proxy.exclude=1
RUN apk add --no-cache tzdata
RUN mkdir -p /app/templates
COPY --from=codemirror templates/codemirror/ /app/templates/codemirror
COPY templates/ /app/templates
COPY schema/ /app/schema
COPY --from=builder /src/go-proxy /app/
# copy timezone data
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
RUN chmod +x /app/go-proxy
ENV DOCKER_HOST unix:///var/run/docker.sock
ENV GOPROXY_DEBUG 0
# copy binary
COPY --from=builder /app /app
# copy schema directory
COPY schema/ /app/schema/
# copy example config
COPY config.example.yml /app/config/config.yml
# 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 8080
EXPOSE 8888
EXPOSE 443
EXPOSE 8443
WORKDIR /app
CMD ["/app/go-proxy"]
CMD ["/app/godoxy"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
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.

View File

@@ -1,41 +1,68 @@
.PHONY: all build up quick-restart restart logs get udp-server
VERSION ?= $(shell git describe --tags --abbrev=0)
BUILD_FLAGS ?= -s -w -X github.com/yusing/go-proxy/pkg.version=${VERSION}
export VERSION
export BUILD_FLAGS
export CGO_ENABLED = 0
export GOOS = linux
all: build quick-restart logs
.PHONY: all setup build test up restart logs get debug run archive repush rapid-crash debug-list-containers
setup:
mkdir -p config certs
[ -f config/config.yml ] || cp config.example.yml config/config.yml
[ -f config/providers.yml ] || touch config/providers.yml
setup-codemirror:
wget https://codemirror.net/5/codemirror.zip
unzip codemirror.zip
rm codemirror.zip
mkdir -p templates
mv codemirror-* templates/codemirror
all: debug
build:
mkdir -p bin
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy src/go-proxy/*.go
scripts/build.sh
test:
GODOXY_TEST=1 go test ./internal/...
up:
docker compose up -d --build app
docker compose up -d
restart:
docker kill go-proxy
docker compose up -d app
docker compose restart -t 0
logs:
tail -f log/go-proxy.log
docker compose logs -f
get:
go get -d -u ./src/go-proxy
go get -u ./cmd && go mod tidy
udp-server:
docker run -it --rm \
-p 9999:9999/udp \
--label proxy.test-udp.scheme=udp \
--label proxy.test-udp.port=20003:9999 \
--network host \
--name test-udp \
$$(docker build -q -f udp-test-server.Dockerfile .)
debug:
GODOXY_DEBUG=1 make run
debug-trace:
GODOXY_DEBUG=1 GODOXY_TRACE=1 run
profile:
GODEBUG=gctrace=1 make debug
run: build
sudo setcap CAP_NET_BIND_SERVICE=+eip bin/godoxy
bin/godoxy
mtrace:
bin/godoxy debug-ls-mtrace > mtrace.json
archive:
git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip
repush:
git reset --soft HEAD^
git add -A
git commit -m "repush"
git push gitlab dev --force
rapid-crash:
sudo docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
sleep 3 &&\
sudo docker rm -f test_crash
debug-list-containers:
bash -c 'echo -e "GET /containers/json HTTP/1.0\r\n" | sudo netcat -U /var/run/docker.sock | tail -n +9 | jq'
ci-test:
mkdir -p /tmp/artifacts
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
cloc:
cloc --not-match-f '_test.go$$' cmd internal pkg

480
README.md
View File

@@ -1,408 +1,160 @@
# go-proxy
# GoDoxy
A simple auto docker reverse proxy for home use. **Written in _Go_**
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![](https://dcbadge.limes.pink/api/server/umReR62nRd)](https://discord.gg/umReR62nRd)
In the examples domain `x.y.z` is used, replace them with your domain
[繁體中文文檔請看此](README_CHT.md)
A lightweight, easy-to-use, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with a Web UI and dashboard.
![Screenshot](screenshots/webui.png)
_Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
## Table of content
- [go-proxy](#go-proxy)
<!-- TOC -->
- [GoDoxy](#godoxy)
- [Table of content](#table-of-content)
- [Key Points](#key-points)
- [How to use](#how-to-use)
- [Command-line args](#command-line-args)
- [Commands](#commands)
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
- [Configuration](#configuration)
- [Labels (docker)](#labels-docker)
- [Environment variables](#environment-variables)
- [Config File](#config-file)
- [Fields](#fields)
- [Provider Kinds](#provider-kinds)
- [Provider File](#provider-file)
- [Supported DNS Challenge Providers](#supported-dns-challenge-providers)
- [Examples](#examples)
- [Single port configuration example](#single-port-configuration-example)
- [Multiple ports configuration example](#multiple-ports-configuration-example)
- [TCP/UDP configuration example](#tcpudp-configuration-example)
- [Load balancing Configuration Example](#load-balancing-configuration-example)
- [Troubleshooting](#troubleshooting)
- [Benchmarks](#benchmarks)
- [Known issues](#known-issues)
- [Memory usage](#memory-usage)
- [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)
## Key Points
## Key Features
- Fast (See [benchmarks](#benchmarks))
- Auto certificate obtaining and renewal (See [Config File](#config-file) and [Supported DNS Challenge Providers](#supported-dns-challenge-providers))
- Auto detect reverse proxies from docker
- Auto hot-reload on container `start` / `die` / `stop` or config file changes
- Custom proxy entries with `config.yml` and additional provider files
- Subdomain matching + Path matching **(domain name doesn't matter)**
- HTTP(s) proxy + TCP/UDP Proxy (UDP is _experimental_)
- HTTP(s) round robin load balance support (same subdomain and path across different hosts)
- Web UI on port 8080 (http) and port 8443 (https)
- 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
- [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**
- Supports linux/amd64, linux/arm64
- Written in **[Go](https://go.dev)**
- a simple panel to see all reverse proxies and health
[🔼Back to top](#table-of-content)
![panel screenshot](screenshots/panel.png)
## Getting Started
- a config editor to edit config and provider files with validation
### Prerequisites
**Validate and save file with Ctrl+S**
Setup DNS Records point to machine which runs `GoDoxy`, e.g.
![config editor screenshot](screenshots/config_editor.png)
- A Record: `*.y.z` -> `10.0.10.1`
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
## How to use
### Setup
1. Setup DNS Records to your machine's IP address
1. Pull the latest docker images
```shell
docker pull ghcr.io/yusing/go-proxy:latest
```
- A Record: `*.y.z` -> `10.0.10.1`
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
2. Create new directory, `cd` into it, then run setup, or [set up manually](#manual-setup)
2. Start `go-proxy` (see [Binary](docs/binary.md) or [docker](docs/docker.md))
```shell
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/godoxy setup
```
3. Start editing config files
- with text editor (i.e. Visual Studio Code)
- or with web config editor by navigate to `ip:8080`
3. _(Optional)_ setup WebUI login
## Command-line args
- set random JWT secret
```shell
sed -i "s|API_JWT_SECRET=.*|API_JWT_SECRET=$(openssl rand -base64 32)|g" .env
```
`go-proxy [command]`
- change username and password for WebUI authentication
```shell
sed -i "s|API_USERNAME=.*|API_USERNAME=admin|g" .env
sed -i "s|API_PASSWORD=.*|API_PASSWORD=some-strong-password|g" .env
```
### Commands
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`
- empty: start proxy server
- validate: validate config and exit
- reload: trigger a force reload of config
5. Start the container `docker compose up -d`
Examples:
6. You may now do some extra configuration
- With text editor (e.g. Visual Studio Code)
- With Web UI via `http://localhost:3000` or `https://gp.y.z`
- For more info, [See Wiki]([wiki](https://github.com/yusing/go-proxy/wiki))
- Binary: `go-proxy reload`
- Docker: `docker exec -it go-proxy /app/go-proxy reload`
[🔼Back to top](#table-of-content)
## Use JSON Schema in VSCode
### Manual Setup
Modify `.vscode/settings.json` to fit your needs
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
```json
{
"yaml.schemas": {
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
"config.example.yml",
"config.yml"
],
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
"providers.example.yml",
"*.providers.yml"
]
}
}
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.7/config.example.yml -O config/config.yml`
2. Grab `.env.example` into `.env`
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.7/.env.example -O .env`
3. Grab `compose.example.yml` into `compose.yml`
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.7/compose.example.yml -O compose.yml`
### Folder structrue
```shell
├── certs
│ ├── cert.crt
│ └── priv.key
├── compose.yml
├── config
│ ├── config.yml
│ ├── middlewares
│ │ ├── middleware1.yml
│ │ ├── middleware2.yml
│ ├── provider1.yml
│ └── provider2.yml
└── .env
```
## Configuration
### Use JSON Schema in VSCode
With container name, no label needs to be added _(most of the time)_.
Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify it to fit your needs
### Labels (docker)
[🔼Back to top](#table-of-content)
See [compose.example.yml](compose.example.yml) for more
## Screenshots
- `proxy.aliases`: comma separated aliases for subdomain matching
### idlesleeper
- default: container name
![idlesleeper](screenshots/idlesleeper.webp)
- `proxy.*.<field>`: wildcard label for all aliases
Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.nginx.scheme: http`)
- `scheme`: proxy protocol
- default: `http`
- allowed: `http`, `https`, `tcp`, `udp`
- `host`: proxy host
- default: `container_name`
- `port`: proxy port
- default: first expose port (declared in `Dockerfile` or `docker-compose.yml`)
- `http(s)`: number in range og `0 - 65535`
- `tcp/udp`: `[<listeningPort>:]<targetPort>`
- `listeningPort`: number, when it is omitted (not suggested), a free port starting from 20000 will be used.
- `targetPort`: number, or predefined names (see [constants.go:14](src/go-proxy/constants.go#L14))
- `no_tls_verify`: whether skip tls verify when scheme is https
- default: `false`
- `path`: proxy path _(http(s) proxy only)_
- default: empty
- `path_mode`: mode for path handling
- default: empty
- allowed: empty, `forward`, `sub`
- `empty`: remove path prefix from URL when proxying
1. apps.y.z/webdav -> webdav:80
2. apps.y.z./webdav/path/to/file -> webdav:80/path/to/file
- `forward`: path remain unchanged
1. apps.y.z/webdav -> webdav:80/webdav
2. apps.y.z./webdav/path/to/file -> webdav:80/webdav/path/to/file
- `sub`: (experimental) remove path prefix from URL and also append path to HTML link attributes (`src`, `href` and `action`) and Javascript `fetch(url)` by response body substitution
e.g. apps.y.z/app1 -> webdav:80, `href="/app1/path/to/file"` -> `href="/path/to/file"`
- `load_balance`: enable load balance (docker only)
- allowed: `1`, `true`
### Environment variables
- `GOPROXY_DEBUG`: set to `1` or `true` to enable debug behaviors (i.e. output, etc.)
### Config File
See [config.example.yml](config.example.yml) for more
#### Fields
- `autocert`: autocert configuration
- `email`: ACME Email
- `domains`: a list of domains for cert registration
- `provider`: DNS Challenge provider, see [Supported DNS Challenge Providers](#supported-dns-challenge-providers)
- `options`: provider specific options
- `providers`: reverse proxy providers configuration
- `kind`: provider kind (string), see [Provider Kinds](#provider-kinds)
- `value`: provider specific value
#### Provider Kinds
- `docker`: load reverse proxies from docker
values:
- `FROM_ENV`: value from environment (`DOCKER_HOST`)
- full url to docker host (i.e. `tcp://host:2375`)
- `file`: load reverse proxies from provider file
value: relative path of file to `config/`
### Provider File
Fields are same as [docker labels](#labels-docker) starting from `scheme`
See [providers.example.yml](providers.example.yml) for examples
### Supported DNS Challenge Providers
- Cloudflare
- `auth_token`: your zone API token
Follow [this guide](https://cloudkul.com/blog/automcatic-renew-and-generate-ssl-on-your-website-using-lego-client/) to create a new token with `Zone.DNS` read and edit permissions
To add more provider support, see [this](docs/add_dns_provider.md)
## Examples
See [docker.md](docs/docker.md#docker-compose-example) for complete examples
### Single port configuration example
```yaml
# (default) https://<container_name>.y.z
whoami:
image: traefik/whoami
container_name: whoami # => whoami.y.z
# enable both subdomain and path matching:
whoami:
image: traefik/whoami
container_name: whoami
labels:
- proxy.aliases=whoami,apps
- proxy.apps.path=/whoami
# 1. visit https://whoami.y.z
# 2. visit https://apps.y.z/whoami
```
### Multiple ports configuration example
```yaml
minio:
image: quay.io/minio/minio
container_name: minio
...
labels:
- proxy.aliases=minio,minio-console
- proxy.minio.port=9000
- proxy.minio-console.port=9001
# visit https://minio.y.z to access minio
# visit https://minio-console.y.z/whoami to access minio console
```
### TCP/UDP configuration example
```yaml
# In the app
app-db:
image: postgres:15
container_name: app-db
...
labels:
# Optional (postgres is in the known image map)
- proxy.app-db.scheme=tcp
# Optional (first free port will be used for listening port)
- proxy.app-db.port=20000:postgres
# In go-proxy
go-proxy:
...
ports:
- 80:80
...
- <your desired port>:20000/tcp
# or 20000-20010:20000-20010/tcp to declare large range at once
# access app-db via <*>.y.z:20000
```
## Load balancing Configuration Example
```yaml
nginx:
...
deploy:
mode: replicated
replicas: 3
labels:
- proxy.nginx.load_balance=1 # allowed: [1, true]
```
## Troubleshooting
Q: How to fix when it shows "no matching route for subdomain \<subdomain>"?
A: Make sure the container is running, and \<subdomain> matches any container name / alias
## Benchmarks
Benchmarked with `wrk` connecting `traefik/whoami`'s `/bench` endpoint
Remote benchmark (client running wrk and `go-proxy` server are different devices)
- Direct connection
```shell
root@yusing-pc:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.100.3:8003/bench
Running 10s test @ http://10.0.100.3:8003/bench
10 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 94.75ms 199.92ms 1.68s 91.27%
Req/Sec 4.24k 1.79k 18.79k 72.13%
Latency Distribution
50% 1.14ms
75% 120.23ms
90% 245.63ms
99% 1.03s
423444 requests in 10.10s, 50.88MB read
Socket errors: connect 0, read 0, write 0, timeout 29
Requests/sec: 41926.32
Transfer/sec: 5.04MB
```
- With reverse proxy
```shell
root@yusing-pc:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.1.7/bench
Running 10s test @ http://10.0.1.7/bench
10 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 79.35ms 169.79ms 1.69s 92.55%
Req/Sec 4.27k 1.90k 19.61k 75.81%
Latency Distribution
50% 1.12ms
75% 105.66ms
90% 200.22ms
99% 814.59ms
409836 requests in 10.10s, 49.25MB read
Socket errors: connect 0, read 0, write 0, timeout 18
Requests/sec: 40581.61
Transfer/sec: 4.88MB
```
Local benchmark (client running wrk and `go-proxy` server are under same proxmox host but different LXCs)
- Direct connection
```shell
root@http-benchmark-client:~# wrk -t 10 -c 200 -d 10s --latency http://10.0.100.1/bench
Running 10s test @ http://10.0.100.1/bench
10 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 434.08us 539.35us 8.76ms 85.28%
Req/Sec 67.71k 6.31k 87.21k 71.20%
Latency Distribution
50% 153.00us
75% 646.00us
90% 1.18ms
99% 2.38ms
6739591 requests in 10.01s, 809.85MB read
Requests/sec: 673608.15
Transfer/sec: 80.94MB
```
- With `go-proxy` reverse proxy
```shell
root@http-benchmark-client:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.1.7/bench
Running 10s test @ http://10.0.1.7/bench
10 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.23ms 0.96ms 11.43ms 72.09%
Req/Sec 17.48k 1.76k 21.48k 70.20%
Latency Distribution
50% 0.98ms
75% 1.76ms
90% 2.54ms
99% 4.24ms
1739079 requests in 10.01s, 208.97MB read
Requests/sec: 173779.44
Transfer/sec: 20.88MB
```
- With `traefik-v3`
```shell
root@traefik-benchmark:~# wrk -t10 -c200 -d10s -H "Host: benchmark.whoami" --latency http://127.0.0.1:8000/bench
Running 10s test @ http://127.0.0.1:8000/bench
10 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.81ms 10.36ms 180.26ms 98.57%
Req/Sec 11.35k 1.74k 13.76k 85.54%
Latency Distribution
50% 1.59ms
75% 2.27ms
90% 3.17ms
99% 37.91ms
1125723 requests in 10.01s, 109.50MB read
Requests/sec: 112499.59
Transfer/sec: 10.94MB
```
## Known issues
UDP proxy does not work for PalWorld Dedicated Server
## Memory usage
It takes ~15 MB for 50 proxy entries
[🔼Back to top](#table-of-content)
## Build it yourself
1. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
1. Clone the repository `git clone https://github.com/yusing/go-proxy --depth=1`
2. Clear cache if you have built this before (go < 1.22) with `go clean -cache`
2. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
3. get dependencies with `make get`
3. Clear cache if you have built this before (go < 1.22) with `go clean -cache`
4. build binary with `make build`
4. get dependencies with `make get`
5. start your container with `make up` (docker) or `bin/go-proxy` (binary)
5. build binary with `make build`
[🔼Back to top](#table-of-content)

130
README_CHT.md Normal file
View File

@@ -0,0 +1,130 @@
# go-proxy
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![](https://dcbadge.limes.pink/api/server/umReR62nRd)](https://discord.gg/umReR62nRd)
一個輕量化、易用且[高效]([docs/benchmark_result.md](https://github.com/yusing/go-proxy/wiki/Benchmarks)))的反向代理和端口轉發工具
## 目錄
<!-- TOC -->
- [go-proxy](#go-proxy)
- [目錄](#目錄)
- [重點](#重點)
- [入門指南](#入門指南)
- [安裝](#安裝)
- [命令行參數](#命令行參數)
- [環境變量](#環境變量)
- [VSCode 中使用 JSON Schema](#vscode-中使用-json-schema)
- [展示](#展示)
- [idlesleeper](#idlesleeper)
- [源碼編譯](#源碼編譯)
## 重點
- 易用
- 不需花費太多時間就能輕鬆配置
- 支持多個docker節點
- 除錯簡單
- 自動配置 SSL 證書(參見[可用的 DNS 供應商](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers)
- 透過 Docker 容器自動配置
- 容器狀態變更時自動熱重載
- **idlesleeper** 容器閒置時自動暫停/停止,入站時自動喚醒 (可選, 參見 [展示](#idlesleeper))
- HTTP(s) 反向代理
- [HTTP middleware](https://github.com/yusing/go-proxy/wiki/Middlewares)
- [自訂 error pages](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
- TCP/UDP 端口轉發
- Web 面板 (內置App dashboard)
- 支持 linux/amd64、linux/arm64 平台
- 使用 **[Go](https://go.dev)** 編寫
[🔼 返回頂部](#目錄)
## 入門指南
### 安裝
1. 抓取Docker鏡像
```shell
docker pull ghcr.io/yusing/go-proxy:latest
```
2. 建立新的目錄,並切換到該目錄,並執行
```shell
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/go-proxy setup
```
3. 設置 DNS 記錄,例如:
- A 記錄: `*.y.z` -> `10.0.10.1`
- AAAA 記錄: `*.y.z` -> `::ffff:a00:a01`
4. 配置 `docker-socket-proxy` 其他 Docker 節點(如有) (參見 [範例](docs/docker_socket_proxy.md)) 然後加到 `config.yml` 中
5. 大功告成,你可以做一些額外的配置
- 使用文本編輯器 (推薦 Visual Studio Code [參見 VSCode 使用 schema](#vscode-中使用-json-schema))
- 或通過 `http://localhost:3000` 使用網頁配置編輯器
- 詳情請參閱 [docker.md](docs/docker.md)
[🔼 返回頂部](#目錄)
### 命令行參數
| 參數 | 描述 | 示例 |
| ------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------- |
| 空 | 啟動代理服務器 | |
| `validate` | 驗證配置並退出 | |
| `reload` | 強制刷新配置 | |
| `ls-config` | 列出配置並退出 | `go-proxy ls-config \| jq` |
| `ls-route` | 列出路由並退出 | `go-proxy ls-route \| jq` |
| `go-proxy ls-route \| jq` |
| `ls-icons` | 列出 [dashboard-icons](https://github.com/walkxcode/dashboard-icons/tree/main) 並退出 | `go-proxy ls-icons \| grep adguard` |
| `debug-ls-mtrace` | 列出middleware追蹤 **(僅限於 debug 模式)** | `go-proxy debug-ls-mtrace \| jq` |
**使用 `docker exec go-proxy /app/go-proxy <參數>` 運行**
### 環境變量
| 環境變量 | 描述 | 默認 | 格式 |
| ------------------------------ | ---------------- | ---------------- | ------------- |
| `GOPROXY_NO_SCHEMA_VALIDATION` | 禁用 schema 驗證 | `false` | boolean |
| `GOPROXY_DEBUG` | 啟用調試輸出 | `false` | boolean |
| `GOPROXY_HTTP_ADDR` | http 收聽地址 | `:80` | `[host]:port` |
| `GOPROXY_HTTPS_ADDR` | https 收聽地址 | `:443` | `[host]:port` |
| `GOPROXY_API_ADDR` | api 收聽地址 | `127.0.0.1:8888` | `[host]:port` |
### VSCode 中使用 JSON Schema
複製 [`.vscode/settings.example.json`](.vscode/settings.example.json) 到 `.vscode/settings.json` 並根據需求修改
[🔼 返回頂部](#目錄)
## 展示
### idlesleeper
![idlesleeper](screenshots/idlesleeper.webp)
[🔼 返回頂部](#目錄)
## 源碼編譯
1. 獲取源碼 `git clone https://github.com/yusing/go-proxy --depth=1`
2. 安裝/升級 [go 版本 (>=1.22)](https://go.dev/doc/install) 和 `make`(如果尚未安裝)
3. 如果之前編譯過go 版本 < 1.22),請使用 `go clean -cache` 清除緩存
4. 使用 `make get` 獲取依賴項
5. 使用 `make build` 編譯
[🔼 返回頂部](#目錄)

184
cmd/main.go Executable file
View File

@@ -0,0 +1,184 @@
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/yusing/go-proxy/internal"
"github.com/yusing/go-proxy/internal/api"
"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/logging"
"github.com/yusing/go-proxy/internal/metrics"
"github.com/yusing/go-proxy/internal/net/http/middleware"
R "github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/server"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg"
)
func main() {
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)
}
logging.Info().Msg("ok")
return
case common.CommandListIcons:
icons, err := internal.ListAvailableIcons()
if err != nil {
log.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
}
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
}
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)
}
switch args.Command {
case common.CommandListRoutes:
cfg.StartProxyProviders()
printJSON(config.RoutesByAlias())
return
case common.CommandListConfigs:
printJSON(config.Value())
return
case common.CommandDebugListEntries:
printJSON(config.DumpEntries())
return
case common.CommandDebugListProviders:
printJSON(config.DumpProviders())
return
}
if common.APIJWTSecret == nil {
logging.Warn().Msg("API JWT secret is empty, authentication is disabled")
}
cfg.StartProxyProviders()
config.WatchChanges()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT)
signal.Notify(sig, syscall.SIGTERM)
signal.Notify(sig, syscall.SIGHUP)
autocert := config.GetAutoCertProvider()
if autocert != nil {
if err := autocert.Setup(); err != nil {
E.LogFatal("autocert setup error", err)
}
} else {
logging.Info().Msg("autocert not configured")
}
server.StartServer(server.Options{
Name: "proxy",
CertProvider: autocert,
HTTPAddr: common.ProxyHTTPAddr,
HTTPSAddr: common.ProxyHTTPSAddr,
Handler: http.HandlerFunc(R.ProxyHandler),
RedirectToHTTPS: config.Value().RedirectToHTTPS,
})
server.StartServer(server.Options{
Name: "api",
CertProvider: autocert,
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(),
RedirectToHTTPS: config.Value().RedirectToHTTPS,
})
if common.PrometheusEnabled {
server.StartServer(server.Options{
Name: "metrics",
CertProvider: autocert,
HTTPAddr: common.MetricsHTTPAddr,
Handler: metrics.NewHandler(),
RedirectToHTTPS: config.Value().RedirectToHTTPS,
})
}
// wait for signal
<-sig
// grafully shutdown
logging.Info().Msg("shutting down")
task.CancelGlobalContext()
task.GlobalContextWait(time.Second * time.Duration(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)
}
}
}
func printJSON(obj any) {
j, err := json.MarshalIndent(obj, "", " ")
if err != nil {
logging.Fatal().Err(err).Send()
}
rawLogger := log.New(os.Stdout, "", 0)
rawLogger.Print(string(j)) // raw output for convenience using "jq"
}

View File

@@ -1,45 +1,42 @@
version: '3'
---
services:
app:
image: ghcr.io/yusing/go-proxy:latest
container_name: go-proxy
restart: always
networks: # ^also add here
- default
ports:
- 80:80 # http proxy
- 8080:8080 # http panel
# - 443:443 # optional, https proxy
# - 8443:8443 # optional, https panel
frontend:
image: ghcr.io/yusing/go-proxy-frontend:latest
container_name: godoxy-frontend
restart: unless-stopped
network_mode: host
env_file: .env
depends_on:
- app
# modify below to fit your needs
labels:
proxy.aliases: gp
proxy.#1.port: 3000
proxy.#1.middlewares.cidr_whitelist.status_code: 403
proxy.#1.middlewares.cidr_whitelist.message: IP not allowed
proxy.#1.middlewares.cidr_whitelist.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
restart: always
network_mode: host
env_file: .env
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./config:/app/config
- ./error_pages:/app/error_pages
# optional, if you declared any tcp/udp proxy, set a range you want to use
# - 20000:20100/tcp
# - 20000:20100/udp
volumes:
- ./config:/app/config
# (Optional) choose one of below to enable https
# 1. use existing certificate
# if local docker provider is used
- /var/run/docker.sock:/var/run/docker.sock:ro
# use existing certificate
# - /path/to/cert.pem:/app/certs/cert.crt:ro
# - /path/to/privkey.pem:/app/certs/priv.key:ro
# - /path/to/certs/cert.crt:/app/certs/cert.crt
# - /path/to/certs/priv.key:/app/certs/priv.key
# store autocert obtained cert
# - ./certs:/app/certs
# workaround for "lookup: no such host"
# dns:
# - 127.0.0.1
# 2. use autocert, certs will be stored in ./certs
# you can also use a docker volume to store it
# if you have container running in "host" network mode
# extra_hosts:
# - host.docker.internal:host-gateway
logging:
driver: 'json-file'
options:
max-file: '1'
max-size: 128k
networks: # ^you may add other external networks
default:
driver: bridge
# - ./certs:/app/certs

View File

@@ -1,21 +1,75 @@
# Autocert (uncomment to enable)
# autocert: # (optional, if you need autocert feature)
# email: "user@domain.com" # (required) email for acme certificate
# domains: # (required)
# - "*.y.z" # domain for acme certificate, use wild card to allow all subdomains
# provider: cloudflare # (required) dns challenge provider (string)
# options: # provider specific options
# auth_token: "YOUR_ZONE_API_TOKEN"
providers:
local:
kind: docker
# for value format, see https://docs.docker.com/reference/cli/dockerd/
# i.e. FROM_ENV, ssh://user@10.0.1.1:22, tcp://10.0.2.1:2375
value: FROM_ENV
providers:
kind: file
value: providers.yml
# Autocert (choose one below and uncomment to enable)
#
# 1. use existing cert
#
# autocert:
# provider: local
#
# cert_path: certs/cert.crt # optional, uncomment only if you need to change it
# key_path: certs/priv.key # optional, uncomment only if you need to change it
#
# 2. cloudflare
#
# autocert:
# provider: cloudflare
# email: abc@gmail.com # ACME Email
# domains: # a list of domains for cert registration
# - "*.y.z" # remember to use double quotes to surround wildcard domain
# options:
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
#
# 3. other providers, check docs/dns_providers.md for more
# Fixed options (optional, non hot-reloadable)
# timeout_shutdown: 5
# redirect_to_https: false
providers:
# include files are standalone yaml files under `config/` directory
#
# include:
# - file1.yml
# - file2.yml
docker:
# $DOCKER_HOST implies environment variable `DOCKER_HOST` or unix:///var/run/docker.sock by default
local: $DOCKER_HOST
# explicit only mode
# only containers with explicit aliases will be proxied
# add "!" after provider name to enable explicit only mode
#
# local!: $DOCKER_HOST
#
# add more docker providers if needed
# for value format, see https://docs.docker.com/reference/cli/dockerd/
#
# remote-1: tcp://10.0.2.1:2375
# remote-2: ssh://root:1234@10.0.2.2
# if match_domains not defined
# any host = alias+[any domain] will match
# i.e. https://app1.y.z will match alias app1 for any domain y.z
# but https://app1.node1.y.z will only match alias "app.node1"
#
# if match_domains defined
# only host = alias+[one of match_domains] will match
# i.e. match_domains = [node1.my.app, my.site]
# https://app1.my.app, https://app1.my.net, etc. will not match even if app1 exists
# only https://*.node1.my.app and https://*.my.site will match
#
#
# match_domains:
# - my.site
# - node1.my.app
# homepage config
#
homepage:
# use default app categories detected from alias or docker image name
use_default_categories: true
# Below are fixed options (non hot-reloadable)
# timeout for shutdown (in seconds)
#
timeout_shutdown: 5
# global setting redirect http requests to https (if https available, otherwise this will be ignored)
# proxy.<alias>.middlewares.redirect_http will override this
#
redirect_to_https: false

View File

@@ -1,41 +0,0 @@
# Adding provider support
## **CloudDNS** as an example
1. Fork this repo, modify [autocert.go](../src/go-proxy/autocert.go#L305)
```go
var providersGenMap = map[string]ProviderGenerator{
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
// add here, i.e.
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
}
```
2. Go to [https://go-acme.github.io/lego/dns/clouddns](https://go-acme.github.io/lego/dns/clouddns/) and check for required config
3. Build `go-proxy` with `make build`
4. Set required config in `config.yml` `autocert` -> `options` section
```shell
# From https://go-acme.github.io/lego/dns/clouddns/
CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \
CLOUDDNS_EMAIL=you@example.com \
CLOUDDNS_PASSWORD=b9841238feb177a84330f \
lego --email you@example.com --dns clouddns --domains my.example.org run
```
Should turn into:
```yaml
autocert:
...
options:
client_id: bLsdFAks23429841238feb177a572aX
email: you@example.com
password: b9841238feb177a84330f
```
5. Run and test if it works
6. Commit and create pull request

View File

@@ -1,59 +0,0 @@
# Getting started with `go-proxy` (binary)
## Setup
1. Install `bash`, `make` and `wget` if not already
2. Run setup script
To specitfy a version _(optional)_
```shell
export VERSION=latest # will be resolved into real version number
export VERSION=<version>
```
If you don't need web config editor
```shell
export SETUP_CODEMIRROR=0
```
Setup
```shell
wget -qO- https://6uo.me/go-proxy-setup-binary | sudo bash
```
What it does:
- Download source file and binary into /opt/go-proxy/$VERSION
- Setup `config.yml` and `providers.yml`
- Setup `template/codemirror` which is a dependency for web config editor
- Create a systemd service (if available) in `/etc/systemd/system/go-proxy.service`
- Enable and start `go-proxy` service
3. Start editing config files in `http://<ip>:8080`
4. Check logs / status with `systemctl status go-proxy`
## Setup (alternative method)
1. Download the latest release and extract somewhere
2. Run `make setup` and _(optional) `make setup-codemirror`_
3. Enable HTTPS _(optional)_
- To use autocert feature
complete `autocert` in `config/config.yml`
- To use existing certificate
Prepare your wildcard (`*.y.z`) SSL cert in `certs/`
- cert / chain / fullchain: `certs/cert.crt`
- private key: `certs/priv.key`
4. Run the binary `bin/go-proxy`

View File

@@ -1,124 +0,0 @@
# Getting started with `go-proxy` docker container
## Setup
1. Install `wget` if not already
2. Run setup script
`bash <(wget -qO- https://6uo.me/go-proxy-setup-docker)`
What it does:
- Create required directories
- Setup `config.yml` and `compose.yml`
3. Verify folder structure and then `cd go-proxy`
```plain
go-proxy
├── certs
├── compose.yml
└── config
├── config.yml
└── providers.yml
```
4. Enable HTTPs _(optional)_
- To use autocert feature
- completing `autocert` section in `config/config.yml`
- mount `certs/` to `/app/certs` to store obtained certs
- To use existing certificate
mount your wildcard (`*.y.z`) SSL cert
- cert / chain / fullchain -> `/app/certs/cert.crt`
- private key -> `/app/certs/priv.key`
5. Modify `compose.yml` fit your needs
Add networks to make sure it is in the same network with other containers, or make sure `proxy.<alias>.host` is reachable
6. Run `docker compose up -d` to start the container
7. Start editing config files in `http://<ip>:8080`
## Troubleshooting
- Firewall issues
If you are using `ufw` with vpn that drop all inbound traffic except vpn, run below:
`sudo ufw allow from 172.16.0.0/16 to 100.64.0.0/10`
Explaination:
Docker network is usually `172.16.0.0/16`
Tailscale is used as an example, `100.64.0.0/10` will be the CIDR
You can also list CIDRs of all docker bridge networks by:
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
## Docker compose example
```yaml
volumes:
adg-work:
adg-conf:
mc-data:
services:
adg:
image: adguard/adguardhome
restart: unless-stopped
labels:
- proxy.aliases=adg,adg-dns,adg-setup
- proxy.adg.port=80
- proxy.adg-setup.port=3000
- proxy.adg-dns.scheme=udp
- proxy.adg-dns.port=20000:dns
volumes:
- adg-work:/opt/adguardhome/work
- adg-conf:/opt/adguardhome/conf
mc:
image: itzg/minecraft-server
tty: true
stdin_open: true
container_name: mc
restart: unless-stopped
labels:
- proxy.mc.scheme=tcp
- proxy.mc.port=20001:25565
environment:
EULA: "TRUE"
volumes:
- mc-data:/data
go-proxy:
image: ghcr.io/yusing/go-proxy
container_name: go-proxy
restart: always
ports:
- 80:80 # http
- 443:443 # optional, https
- 8080:8080 # http panel
- 8443:8443 # optional, https panel
- 53:20000/udp # adguardhome
- 25565:20001/tcp # minecraft
volumes:
- ./config:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- proxy.aliases=gp
- proxy.panel.port=8080
```
### Services URLs
- `gp.yourdomain.com`: go-proxy web panel
- `adg-setup.yourdomain.com`: adguard setup (first time running)
- `adg.yourdomain.com`: adguard dashboard
- `yourdomain.com:53`: adguard dns
- `yourdomain.com:25565`: minecraft server

View File

@@ -0,0 +1,288 @@
@import url("https://fonts.googleapis.com/css?family=Audiowide&display=swap");
html,
body {
margin: 0px;
overflow: hidden;
}
div {
position: absolute;
top: 0%;
left: 0%;
height: 100%;
width: 100%;
margin: 0px;
background: radial-gradient(circle, #240015 0%, #12000b 100%);
overflow: hidden;
}
.wrap {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
h2 {
position: absolute;
top: 50%;
left: 50%;
margin-top: 150px;
font-size: 32px;
text-transform: uppercase;
transform: translate(-50%, -50%);
display: block;
color: #12000a;
font-weight: 300;
font-family: Audiowide;
text-shadow: 0px 0px 4px #12000a;
animation: fadeInText 3s ease-in 3.5s forwards,
flicker4 5s linear 7.5s infinite, hueRotate 6s ease-in-out 3s infinite;
}
#svgWrap_1,
#svgWrap_2 {
position: absolute;
height: auto;
width: 600px;
max-width: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#svgWrap_1,
#svgWrap_2,
div {
animation: hueRotate 6s ease-in-out 3s infinite;
}
#id1_1,
#id2_1,
#id3_1 {
stroke: #ff005d;
stroke-width: 3px;
fill: transparent;
filter: url(#glow);
}
#id1_2,
#id2_2,
#id3_2 {
stroke: #12000a;
stroke-width: 3px;
fill: transparent;
filter: url(#glow);
}
#id3_1 {
stroke-dasharray: 940px;
stroke-dashoffset: -940px;
animation: drawLine3 2.5s ease-in-out 0s forwards,
flicker3 4s linear 4s infinite;
}
#id2_1 {
stroke-dasharray: 735px;
stroke-dashoffset: -735px;
animation: drawLine2 2.5s ease-in-out 0.5s forwards,
flicker2 4s linear 4.5s infinite;
}
#id1_1 {
stroke-dasharray: 940px;
stroke-dashoffset: -940px;
animation: drawLine1 2.5s ease-in-out 1s forwards,
flicker1 4s linear 5s infinite;
}
@keyframes drawLine1 {
0% {
stroke-dashoffset: -940px;
}
100% {
stroke-dashoffset: 0px;
}
}
@keyframes drawLine2 {
0% {
stroke-dashoffset: -735px;
}
100% {
stroke-dashoffset: 0px;
}
}
@keyframes drawLine3 {
0% {
stroke-dashoffset: -940px;
}
100% {
stroke-dashoffset: 0px;
}
}
@keyframes flicker1 {
0% {
stroke: #ff005d;
}
1% {
stroke: transparent;
}
3% {
stroke: transparent;
}
4% {
stroke: #ff005d;
}
6% {
stroke: #ff005d;
}
7% {
stroke: transparent;
}
13% {
stroke: transparent;
}
14% {
stroke: #ff005d;
}
100% {
stroke: #ff005d;
}
}
@keyframes flicker2 {
0% {
stroke: #ff005d;
}
50% {
stroke: #ff005d;
}
51% {
stroke: transparent;
}
61% {
stroke: transparent;
}
62% {
stroke: #ff005d;
}
100% {
stroke: #ff005d;
}
}
@keyframes flicker3 {
0% {
stroke: #ff005d;
}
1% {
stroke: transparent;
}
10% {
stroke: transparent;
}
11% {
stroke: #ff005d;
}
40% {
stroke: #ff005d;
}
41% {
stroke: transparent;
}
45% {
stroke: transparent;
}
46% {
stroke: #ff005d;
}
100% {
stroke: #ff005d;
}
}
@keyframes flicker4 {
0% {
color: #ff005d;
text-shadow: 0px 0px 4px #ff005d;
}
30% {
color: #ff005d;
text-shadow: 0px 0px 4px #ff005d;
}
31% {
color: #12000a;
text-shadow: 0px 0px 4px #12000a;
}
32% {
color: #ff005d;
text-shadow: 0px 0px 4px #ff005d;
}
36% {
color: #ff005d;
text-shadow: 0px 0px 4px #ff005d;
}
37% {
color: #12000a;
text-shadow: 0px 0px 4px #12000a;
}
41% {
color: #12000a;
text-shadow: 0px 0px 4px #12000a;
}
42% {
color: #ff005d;
text-shadow: 0px 0px 4px #ff005d;
}
85% {
color: #ff005d;
text-shadow: 0px 0px 4px #ff005d;
}
86% {
color: #12000a;
text-shadow: 0px 0px 4px #12000a;
}
95% {
color: #12000a;
text-shadow: 0px 0px 4px #12000a;
}
96% {
color: #ff005d;
text-shadow: 0px 0px 4px #ff005d;
}
100% {
color: #ff005d;
text-shadow: 0px 0px 4px #ff005d;
}
}
@keyframes fadeInText {
1% {
color: #12000a;
text-shadow: 0px 0px 4px #12000a;
}
70% {
color: #ff005d;
text-shadow: 0px 0px 14px #ff005d;
}
100% {
color: #ff005d;
text-shadow: 0px 0px 4px #ff005d;
}
}
@keyframes hueRotate {
0% {
filter: hue-rotate(0deg);
}
50% {
filter: hue-rotate(-120deg);
}
100% {
filter: hue-rotate(0deg);
}
}

View File

@@ -0,0 +1,51 @@
{{/* Credit: https://codepen.io/code2rithik/pen/XWpVvYL */}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Page Not Found</title>
<link rel="stylesheet" href="/$gperrorpage/404.css" type="text/css">
<!-- <script src="/$gperrorpage/404.js"> </script> -->
</head>
<body>
<script>0</script>
<div></div>
<svg id="svgWrap_2" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 700 250">
<g>
<path id="id3_2"
d="M195.7 232.67h-37.1V149.7H27.76c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98H158.6V29.62h37.1v203.05z" />
<path id="id2_2"
d="M470.69 147.71c0 8.31-1.06 16.17-3.19 23.58-2.12 7.41-5.12 14.28-8.99 20.6-3.87 6.33-8.45 11.99-13.74 16.99-5.29 5-11.07 9.28-17.35 12.81a85.146 85.146 0 0 1-20.04 8.14 83.637 83.637 0 0 1-21.67 2.83H319.3c-7.46 0-14.73-.94-21.81-2.83-7.08-1.89-13.76-4.6-20.04-8.14a88.292 88.292 0 0 1-17.35-12.81c-5.29-5-9.84-10.67-13.66-16.99-3.82-6.32-6.8-13.19-8.92-20.6-2.12-7.41-3.19-15.27-3.19-23.58v-33.13c0-12.46 2.34-23.88 7.01-34.27 4.67-10.38 10.92-19.33 18.76-26.83 7.83-7.5 16.87-13.36 27.12-17.56 10.24-4.2 20.93-6.3 32.07-6.3h66.41c7.36 0 14.58.94 21.67 2.83 7.08 1.89 13.76 4.6 20.04 8.14a88.292 88.292 0 0 1 17.35 12.81c5.29 5 9.86 10.67 13.74 16.99 3.87 6.33 6.87 13.19 8.99 20.6 2.13 7.41 3.19 15.27 3.19 23.58v33.14zm-37.1-33.13c0-7.27-1.32-13.88-3.96-19.82-2.64-5.95-6.16-11.04-10.55-15.29-4.39-4.25-9.46-7.5-15.22-9.77-5.76-2.27-11.8-3.35-18.13-3.26h-66.41c-6.14-.09-12.11.97-17.91 3.19-5.81 2.22-10.95 5.43-15.44 9.63-4.48 4.2-8.07 9.3-10.76 15.29-2.69 6-4.04 12.67-4.04 20.04v33.13c0 7.36 1.32 14.02 3.96 19.97 2.64 5.95 6.18 11.02 10.62 15.22 4.44 4.2 9.56 7.43 15.36 9.7 5.8 2.27 11.87 3.35 18.2 3.26h66.41c7.27 0 13.85-1.2 19.75-3.61s10.93-5.73 15.08-9.98 7.36-9.32 9.63-15.22c2.27-5.9 3.4-12.34 3.4-19.33v-33.15zm-16-26.91a17.89 17.89 0 0 1 2.83 6.73c.47 2.41.47 4.77 0 7.08-.47 2.31-1.39 4.48-2.76 6.51-1.37 2.03-3.14 3.75-5.31 5.17l-99.4 66.41c-1.61 1.23-3.26 2.08-4.96 2.55-1.7.47-3.45.71-5.24.71-3.02 0-5.9-.71-8.64-2.12-2.74-1.42-4.96-3.44-6.66-6.09a17.89 17.89 0 0 1-2.83-6.73c-.47-2.41-.5-4.77-.07-7.08.43-2.31 1.3-4.48 2.62-6.51 1.32-2.03 3.07-3.75 5.24-5.17l99.69-66.41a17.89 17.89 0 0 1 6.73-2.83c2.41-.47 4.77-.47 7.08 0 2.31.47 4.48 1.37 6.51 2.69 2.03 1.32 3.75 3.02 5.17 5.09z" />
<path id="id1_2"
d="M688.33 232.67h-37.1V149.7H520.39c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98h112.57V29.62h37.1v203.05z" />
</g>
</svg>
<svg id="svgWrap_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 700 250">
<g>
<path id="id3_1"
d="M195.7 232.67h-37.1V149.7H27.76c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98H158.6V29.62h37.1v203.05z" />
<path id="id2_1"
d="M470.69 147.71c0 8.31-1.06 16.17-3.19 23.58-2.12 7.41-5.12 14.28-8.99 20.6-3.87 6.33-8.45 11.99-13.74 16.99-5.29 5-11.07 9.28-17.35 12.81a85.146 85.146 0 0 1-20.04 8.14 83.637 83.637 0 0 1-21.67 2.83H319.3c-7.46 0-14.73-.94-21.81-2.83-7.08-1.89-13.76-4.6-20.04-8.14a88.292 88.292 0 0 1-17.35-12.81c-5.29-5-9.84-10.67-13.66-16.99-3.82-6.32-6.8-13.19-8.92-20.6-2.12-7.41-3.19-15.27-3.19-23.58v-33.13c0-12.46 2.34-23.88 7.01-34.27 4.67-10.38 10.92-19.33 18.76-26.83 7.83-7.5 16.87-13.36 27.12-17.56 10.24-4.2 20.93-6.3 32.07-6.3h66.41c7.36 0 14.58.94 21.67 2.83 7.08 1.89 13.76 4.6 20.04 8.14a88.292 88.292 0 0 1 17.35 12.81c5.29 5 9.86 10.67 13.74 16.99 3.87 6.33 6.87 13.19 8.99 20.6 2.13 7.41 3.19 15.27 3.19 23.58v33.14zm-37.1-33.13c0-7.27-1.32-13.88-3.96-19.82-2.64-5.95-6.16-11.04-10.55-15.29-4.39-4.25-9.46-7.5-15.22-9.77-5.76-2.27-11.8-3.35-18.13-3.26h-66.41c-6.14-.09-12.11.97-17.91 3.19-5.81 2.22-10.95 5.43-15.44 9.63-4.48 4.2-8.07 9.3-10.76 15.29-2.69 6-4.04 12.67-4.04 20.04v33.13c0 7.36 1.32 14.02 3.96 19.97 2.64 5.95 6.18 11.02 10.62 15.22 4.44 4.2 9.56 7.43 15.36 9.7 5.8 2.27 11.87 3.35 18.2 3.26h66.41c7.27 0 13.85-1.2 19.75-3.61s10.93-5.73 15.08-9.98 7.36-9.32 9.63-15.22c2.27-5.9 3.4-12.34 3.4-19.33v-33.15zm-16-26.91a17.89 17.89 0 0 1 2.83 6.73c.47 2.41.47 4.77 0 7.08-.47 2.31-1.39 4.48-2.76 6.51-1.37 2.03-3.14 3.75-5.31 5.17l-99.4 66.41c-1.61 1.23-3.26 2.08-4.96 2.55-1.7.47-3.45.71-5.24.71-3.02 0-5.9-.71-8.64-2.12-2.74-1.42-4.96-3.44-6.66-6.09a17.89 17.89 0 0 1-2.83-6.73c-.47-2.41-.5-4.77-.07-7.08.43-2.31 1.3-4.48 2.62-6.51 1.32-2.03 3.07-3.75 5.24-5.17l99.69-66.41a17.89 17.89 0 0 1 6.73-2.83c2.41-.47 4.77-.47 7.08 0 2.31.47 4.48 1.37 6.51 2.69 2.03 1.32 3.75 3.02 5.17 5.09z" />
<path id="id1_1"
d="M688.33 232.67h-37.1V149.7H520.39c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98h112.57V29.62h37.1v203.05z" />
</g>
</svg>
<svg>
<defs>
<filter id="glow">
<fegaussianblur class="blur" result="coloredBlur" stddeviation="4"></fegaussianblur>
<femerge>
<femergenode in="coloredBlur"></femergenode>
<femergenode in="SourceGraphic"></femergenode>
</femerge>
</filter>
</defs>
</svg>
<h2>Page Not Found</h2>
</body>
</html>

File diff suppressed because it is too large Load Diff

82
go.mod Executable file → Normal file
View File

@@ -1,62 +1,72 @@
module github.com/yusing/go-proxy
go 1.22
go 1.23.3
require (
github.com/docker/cli v26.0.0+incompatible
github.com/docker/docker v26.0.0+incompatible
github.com/fsnotify/fsnotify v1.7.0
github.com/go-acme/lego/v4 v4.16.1
github.com/coder/websocket v1.8.12
github.com/docker/cli v27.3.1+incompatible
github.com/docker/docker v27.3.1+incompatible
github.com/fsnotify/fsnotify v1.8.0
github.com/go-acme/lego/v4 v4.19.2
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/gotify/server/v2 v2.5.0
github.com/prometheus/client_golang v1.20.5
github.com/puzpuzpuz/xsync/v3 v3.4.0
github.com/rs/zerolog v1.33.0
github.com/santhosh-tekuri/jsonschema v1.2.4
github.com/sirupsen/logrus v1.9.3
golang.org/x/net v0.22.0
golang.org/x/net v0.30.0
golang.org/x/text v0.19.0
golang.org/x/time v0.7.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudflare/cloudflare-go v0.92.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/cloudflare-go v0.109.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.62 // 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/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/pkg/errors v0.9.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.19.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/grpc v1.61.1 // indirect
google.golang.org/protobuf v1.32.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
go.opentelemetry.io/otel v1.31.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
go.opentelemetry.io/otel/metric v1.31.0 // indirect
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
go.opentelemetry.io/otel/trace v1.31.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/tools v0.26.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gotest.tools/v3 v3.5.1 // indirect
)

209
go.sum Executable file → Normal file
View File

@@ -1,175 +1,208 @@
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=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
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/cloudflare/cloudflare-go v0.92.0 h1:ltJvGvqZ4G6Fm2hHOYZ5RWpJQcrM0oDrsjjZydZhFJQ=
github.com/cloudflare/cloudflare-go v0.92.0/go.mod h1:nUqvBUUDRxNzsDSQjbqUNWHEIYAoUlgRmcAzMKlFdKs=
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.109.0 h1:Wjp+RfJD1lidIFUlrTBqUQnCBrUnmVsLxgzWYiURueg=
github.com/cloudflare/cloudflare-go v0.109.0/go.mod h1:m492eNahT/9MsN7Ppnoge8AaI7QhVFtEgVm3I9HJFeU=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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/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 v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I=
github.com/docker/cli v26.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v26.0.0+incompatible h1:Ng2qi+gdKADUa/VM+6b6YaY2nlZhk/lVJiKR/2bMudU=
github.com/docker/docker v26.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.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/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/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
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.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-acme/lego/v4 v4.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ=
github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
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/go-acme/lego/v4 v4.19.2 h1:Y8hrmMvWETdqzzkRly7m98xtPJJivWFsgWi8fcvZo+Y=
github.com/go-acme/lego/v4 v4.19.2/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ=
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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/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/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
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/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
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 v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
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.5.0 h1:tJd+a5bb17X52f0EV2KxqLuyjQFKmVK1+t/iNUkP16Y=
github.com/gotify/server/v2 v2.5.0/go.mod h1:DKPMQI/FZ69iKbZvrOL6VWwRaoB9O+HDvJWVd/kiGbc=
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/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
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/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.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.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
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.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
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/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/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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
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.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
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.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.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/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
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.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
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.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-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.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
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-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao=
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM=
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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.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=

73
internal/api/handler.go Normal file
View File

@@ -0,0 +1,73 @@
package api
import (
"net"
"net/http"
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/utils"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/http/middleware"
)
type ServeMux struct{ *http.ServeMux }
func NewServeMux() ServeMux {
return ServeMux{http.NewServeMux()}
}
func (mux ServeMux) HandleFunc(method, endpoint string, handler http.HandlerFunc) {
mux.ServeMux.HandleFunc(method+" "+endpoint, checkHost(rateLimited(handler)))
}
func NewHandler() http.Handler {
mux := NewServeMux()
mux.HandleFunc("GET", "/v1", v1.Index)
mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
mux.HandleFunc("POST", "/v1/login", auth.LoginHandler)
mux.HandleFunc("GET", "/v1/logout", auth.LogoutHandler)
mux.HandleFunc("POST", "/v1/logout", auth.LogoutHandler)
mux.HandleFunc("POST", "/v1/reload", v1.Reload)
mux.HandleFunc("GET", "/v1/list", auth.RequireAuth(v1.List))
mux.HandleFunc("GET", "/v1/list/{what}", auth.RequireAuth(v1.List))
mux.HandleFunc("GET", "/v1/list/{what}/{which}", auth.RequireAuth(v1.List))
mux.HandleFunc("GET", "/v1/file", auth.RequireAuth(v1.GetFileContent))
mux.HandleFunc("GET", "/v1/file/{filename...}", auth.RequireAuth(v1.GetFileContent))
mux.HandleFunc("POST", "/v1/file/{filename...}", auth.RequireAuth(v1.SetFileContent))
mux.HandleFunc("PUT", "/v1/file/{filename...}", auth.RequireAuth(v1.SetFileContent))
mux.HandleFunc("GET", "/v1/stats", v1.Stats)
mux.HandleFunc("GET", "/v1/stats/ws", v1.StatsWS)
return mux
}
// allow only requests to API server with localhost.
func checkHost(f http.HandlerFunc) http.HandlerFunc {
if common.IsDebug {
return f
}
return func(w http.ResponseWriter, r *http.Request) {
host, _, _ := net.SplitHostPort(r.RemoteAddr)
if host != "127.0.0.1" && host != "localhost" && host != "[::1]" {
LogWarn(r).Msgf("blocked API request from %s", host)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
LogInfo(r).Interface("headers", r.Header).Msg("API request")
f(w, r)
}
}
func rateLimited(f http.HandlerFunc) http.HandlerFunc {
m, err := middleware.RateLimiter.WithOptionsClone(middleware.OptionsRaw{
"average": 10,
"burst": 10,
})
if err != nil {
logging.Fatal().Err(err).Msg("unable to create API rate limiter")
}
return func(w http.ResponseWriter, r *http.Request) {
m.ModifyRequest(f, w, r)
}
}

View File

@@ -0,0 +1,135 @@
package auth
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
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"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type (
Credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
)
var (
ErrInvalidUsername = E.New("invalid username")
ErrInvalidPassword = E.New("invalid password")
)
func validatePassword(cred *Credentials) error {
if cred.Username != common.APIUser {
return ErrInvalidUsername.Subject(cred.Username)
}
if !bytes.Equal(common.HashPassword(cred.Password), common.APIPasswordHash) {
return ErrInvalidPassword.Subject(cred.Password)
}
return nil
}
func LoginHandler(w http.ResponseWriter, r *http.Request) {
var creds Credentials
err := json.NewDecoder(r.Body).Decode(&creds)
if err != nil {
U.HandleErr(w, r, err, http.StatusBadRequest)
return
}
if err := validatePassword(&creds); err != nil {
U.HandleErr(w, r, err, http.StatusUnauthorized)
return
}
expiresAt := time.Now().Add(common.APIJWTTokenTTL)
claim := &Claims{
Username: creds.Username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claim)
tokenStr, err := token.SignedString(common.APIJWTSecret)
if err != nil {
U.HandleErr(w, r, err)
return
}
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: tokenStr,
Expires: expiresAt,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
})
w.WriteHeader(http.StatusOK)
}
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: "",
Expires: time.Unix(0, 0),
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
})
w.Header().Set("location", "/login")
w.WriteHeader(http.StatusTemporaryRedirect)
}
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
if common.IsDebugSkipAuth || common.APIJWTSecret == nil {
return next
}
return func(w http.ResponseWriter, r *http.Request) {
if checkToken(w, r) {
next(w, r)
}
}
}
func checkToken(w http.ResponseWriter, r *http.Request) (ok bool) {
tokenCookie, err := r.Cookie("token")
if err != nil {
U.HandleErr(w, r, E.PrependSubject("token", err), http.StatusUnauthorized)
return false
}
var claims Claims
token, err := jwt.ParseWithClaims(tokenCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return common.APIJWTSecret, nil
})
switch {
case err != nil:
break
case !token.Valid:
err = E.New("invalid token")
case claims.Username != common.APIUser:
err = E.New("username mismatch").Subject(claims.Username)
case claims.ExpiresAt.Before(time.Now()):
err = E.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
}
if err != nil {
U.HandleErr(w, r, err, http.StatusForbidden)
return false
}
return true
}

61
internal/api/v1/file.go Normal file
View File

@@ -0,0 +1,61 @@
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"
"github.com/yusing/go-proxy/internal/config"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/route/provider"
)
func GetFileContent(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename")
if filename == "" {
filename = common.ConfigFileName
}
content, err := os.ReadFile(path.Join(common.ConfigBasePath, filename))
if err != nil {
U.HandleErr(w, r, err)
return
}
U.WriteBody(w, content)
}
func SetFileContent(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename")
if filename == "" {
U.HandleErr(w, r, U.ErrMissingKey("filename"), http.StatusBadRequest)
return
}
content, err := io.ReadAll(r.Body)
if err != nil {
U.HandleErr(w, r, err)
return
}
var valErr E.Error
if filename == common.ConfigFileName {
valErr = config.Validate(content)
} else if !strings.HasPrefix(filename, path.Base(common.MiddlewareComposeBasePath)) {
valErr = provider.Validate(content)
}
// no validation for include files
if valErr != nil {
U.RespondJSON(w, r, valErr, http.StatusBadRequest)
return
}
err = os.WriteFile(path.Join(common.ConfigBasePath, filename), content, 0o644)
if err != nil {
U.HandleErr(w, r, err)
return
}
w.WriteHeader(http.StatusOK)
}

11
internal/api/v1/index.go Normal file
View File

@@ -0,0 +1,11 @@
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"))
}

86
internal/api/v1/list.go Normal file
View File

@@ -0,0 +1,86 @@
package v1
import (
"net/http"
"strings"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config"
"github.com/yusing/go-proxy/internal/net/http/middleware"
"github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
)
const (
ListRoute = "route"
ListRoutes = "routes"
ListConfigFiles = "config_files"
ListMiddlewares = "middlewares"
ListMiddlewareTraces = "middleware_trace"
ListMatchDomains = "match_domains"
ListHomepageConfig = "homepage_config"
ListTasks = "tasks"
)
func List(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.Error(w, "not found", http.StatusNotFound)
return
} else {
U.RespondJSON(w, r, route)
}
case ListRoutes:
U.RespondJSON(w, r, config.RoutesByAlias(route.RouteType(r.FormValue("type"))))
case ListConfigFiles:
listConfigFiles(w, r)
case ListMiddlewares:
U.RespondJSON(w, r, middleware.All())
case ListMiddlewareTraces:
U.RespondJSON(w, r, middleware.GetAllTrace())
case ListMatchDomains:
U.RespondJSON(w, r, config.Value().MatchDomains)
case ListHomepageConfig:
U.RespondJSON(w, r, config.HomepageConfig())
case ListTasks:
U.RespondJSON(w, r, task.DebugTaskMap())
default:
U.HandleErr(w, r, U.ErrInvalidKey("what"), http.StatusBadRequest)
}
}
func listRoute(which string) any {
if which == "" {
which = "all"
}
if which == "all" {
return config.RoutesByAlias()
}
routes := config.RoutesByAlias()
route, ok := routes[which]
if !ok {
return nil
}
return route
}
func listConfigFiles(w http.ResponseWriter, r *http.Request) {
files, err := utils.ListFiles(common.ConfigBasePath, 1)
if err != nil {
U.HandleErr(w, r, err)
return
}
for i := range files {
files[i] = strings.TrimPrefix(files[i], common.ConfigBasePath+"/")
}
U.RespondJSON(w, r, files)
}

View File

@@ -0,0 +1,64 @@
package query
import (
"encoding/json"
"fmt"
"io"
"net/http"
v1 "github.com/yusing/go-proxy/internal/api/v1"
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"
"github.com/yusing/go-proxy/internal/net/http/middleware"
)
func ReloadServer() E.Error {
resp, err := U.Post(common.APIHTTPURL+"/v1/reload", "", nil)
if err != nil {
return E.From(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
failure := E.Errorf("server reload status %v", resp.StatusCode)
body, err := io.ReadAll(resp.Body)
if err != nil {
return failure.With(err)
}
reloadErr := string(body)
return failure.Withf(reloadErr)
}
return nil
}
func List[T any](what string) (_ T, outErr E.Error) {
resp, err := U.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, what))
if err != nil {
outErr = E.From(err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
outErr = E.Errorf("list %s: failed, status %v", what, resp.StatusCode)
return
}
var res T
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
outErr = E.From(err)
return
}
return res, nil
}
func ListRoutes() (map[string]map[string]any, E.Error) {
return List[map[string]map[string]any](v1.ListRoutes)
}
func ListMiddlewareTraces() (middleware.Traces, E.Error) {
return List[middleware.Traces](v1.ListMiddlewareTraces)
}
func DebugListTasks() (map[string]any, E.Error) {
return List[map[string]any](v1.ListTasks)
}

16
internal/api/v1/reload.go Normal file
View File

@@ -0,0 +1,16 @@
package v1
import (
"net/http"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/config"
)
func Reload(w http.ResponseWriter, r *http.Request) {
if err := config.Reload(); err != nil {
U.HandleErr(w, r, err)
return
}
U.WriteBody(w, []byte("OK"))
}

70
internal/api/v1/stats.go Normal file
View File

@@ -0,0 +1,70 @@
package v1
import (
"context"
"net/http"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
func Stats(w http.ResponseWriter, r *http.Request) {
U.RespondJSON(w, r, getStats())
}
func StatsWS(w http.ResponseWriter, r *http.Request) {
var originPats []string
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
if len(config.Value().MatchDomains) == 0 {
U.LogWarn(r).Msg("no match domains configured, accepting websocket API request from all origins")
originPats = []string{"*"}
} else {
originPats = make([]string, len(config.Value().MatchDomains))
for i, domain := range config.Value().MatchDomains {
originPats[i] = "*" + domain
}
originPats = append(originPats, localAddresses...)
}
if common.IsDebug {
originPats = []string{"*"}
}
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
OriginPatterns: originPats,
})
if err != nil {
U.LogError(r).Err(err).Msg("failed to upgrade websocket")
return
}
/* trunk-ignore(golangci-lint/errcheck) */
defer conn.CloseNow()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
stats := getStats()
if err := wsjson.Write(ctx, conn, stats); err != nil {
U.LogError(r).Msg("failed to write JSON")
return
}
}
}
var startTime = time.Now()
func getStats() map[string]any {
return map[string]any{
"proxies": config.Statistics(),
"uptime": strutils.FormatDuration(time.Since(startTime)),
}
}

View File

@@ -0,0 +1,36 @@
package utils
import (
"net/http"
E "github.com/yusing/go-proxy/internal/error"
)
// HandleErr logs the error and returns an HTTP error response to the client.
// If code is specified, it will be used as the HTTP status code; otherwise,
// http.StatusInternalServerError is used.
//
// The error is only logged but not returned to the client.
func HandleErr(w http.ResponseWriter, r *http.Request, origErr error, code ...int) {
if origErr == nil {
return
}
LogError(r).Msg(origErr.Error())
statusCode := http.StatusInternalServerError
if len(code) > 0 {
statusCode = code[0]
}
http.Error(w, http.StatusText(statusCode), statusCode)
}
func ErrMissingKey(k string) error {
return E.New("missing key '" + k + "' in query or request body")
}
func ErrInvalidKey(k string) error {
return E.New("invalid key '" + k + "' in query or request body")
}
func ErrNotFound(k, v string) error {
return E.Errorf("key %q with value %q not found", k, v)
}

View File

@@ -0,0 +1,28 @@
package utils
import (
"crypto/tls"
"net"
"net/http"
"github.com/yusing/go-proxy/internal/common"
)
var (
httpClient = &http.Client{
Timeout: common.ConnectionTimeout,
Transport: &http.Transport{
DisableKeepAlives: true,
ForceAttemptHTTP2: false,
DialContext: (&net.Dialer{
Timeout: common.DialTimeout,
KeepAlive: common.KeepAlive, // this is different from DisableKeepAlives
}).DialContext,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
Get = httpClient.Get
Post = httpClient.Post
Head = httpClient.Head
)

View File

@@ -0,0 +1,18 @@
package utils
import (
"net/http"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/logging"
)
func reqLogger(r *http.Request, level zerolog.Level) *zerolog.Event {
return logging.WithLevel(level).Str("module", "api").
Str("method", r.Method).
Str("path", r.RequestURI)
}
func LogError(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.ErrorLevel) }
func LogWarn(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.WarnLevel) }
func LogInfo(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.InfoLevel) }

View File

@@ -0,0 +1,43 @@
package utils
import (
"encoding/json"
"fmt"
"net/http"
"github.com/yusing/go-proxy/internal/logging"
)
func WriteBody(w http.ResponseWriter, body []byte) {
if _, err := w.Write(body); err != nil {
HandleErr(w, nil, err)
}
}
func RespondJSON(w http.ResponseWriter, r *http.Request, data any, code ...int) (canProceed bool) {
if len(code) > 0 {
w.WriteHeader(code[0])
}
w.Header().Set("Content-Type", "application/json")
var j []byte
var err error
switch data := data.(type) {
case string:
j = []byte(fmt.Sprintf("%q", data))
case []byte:
j = data
default:
j, err = json.MarshalIndent(data, "", " ")
if err != nil {
logging.Panic().Err(err).Msg("failed to marshal json")
return false
}
}
_, err = w.Write(j)
if err != nil {
HandleErr(w, r, err)
return false
}
return true
}

View File

@@ -0,0 +1,12 @@
package v1
import (
"net/http"
. "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/pkg"
)
func GetVersion(w http.ResponseWriter, r *http.Request) {
WriteBody(w, []byte(pkg.GetVersion()))
}

View File

@@ -0,0 +1,84 @@
package autocert
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils"
"github.com/yusing/go-proxy/internal/config/types"
)
type Config types.AutoCertConfig
var (
ErrMissingDomain = E.New("missing field 'domains'")
ErrMissingEmail = E.New("missing field 'email'")
ErrMissingProvider = E.New("missing field 'provider'")
ErrUnknownProvider = E.New("unknown provider")
)
func NewConfig(cfg *types.AutoCertConfig) *Config {
if cfg.CertPath == "" {
cfg.CertPath = CertFileDefault
}
if cfg.KeyPath == "" {
cfg.KeyPath = KeyFileDefault
}
if cfg.Provider == "" {
cfg.Provider = ProviderLocal
}
return (*Config)(cfg)
}
func (cfg *Config) GetProvider() (*Provider, E.Error) {
b := E.NewBuilder("autocert errors")
if cfg.Provider != ProviderLocal {
if len(cfg.Domains) == 0 {
b.Add(ErrMissingDomain)
}
if cfg.Provider == "" {
b.Add(ErrMissingProvider)
}
if cfg.Email == "" {
b.Add(ErrMissingEmail)
}
// check if provider is implemented
_, ok := providersGenMap[cfg.Provider]
if !ok {
b.Add(ErrUnknownProvider.
Subject(cfg.Provider).
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, providersGenMap))))
}
}
if b.HasError() {
return nil, b.Error()
}
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
b.Addf("generate private key: %w", err)
return nil, b.Error()
}
user := &User{
Email: cfg.Email,
key: privKey,
}
legoCfg := lego.NewConfig(user)
legoCfg.Certificate.KeyType = certcrypto.RSA2048
return &Provider{
cfg: cfg,
user: user,
legoCfg: legoCfg,
}, nil
}

View File

@@ -0,0 +1,31 @@
package autocert
import (
"github.com/go-acme/lego/v4/providers/dns/clouddns"
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/go-acme/lego/v4/providers/dns/duckdns"
"github.com/go-acme/lego/v4/providers/dns/ovh"
)
const (
certBasePath = "certs/"
CertFileDefault = certBasePath + "cert.crt"
KeyFileDefault = certBasePath + "priv.key"
RegistrationFile = certBasePath + "registration.json"
)
const (
ProviderLocal = "local"
ProviderCloudflare = "cloudflare"
ProviderClouddns = "clouddns"
ProviderDuckdns = "duckdns"
ProviderOVH = "ovh"
)
var providersGenMap = map[string]ProviderGenerator{
ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
}

View File

@@ -0,0 +1,20 @@
package autocert
type DummyConfig struct{}
type DummyProvider struct{}
func NewDummyDefaultConfig() *DummyConfig {
return &DummyConfig{}
}
func NewDummyDNSProviderConfig(*DummyConfig) (*DummyProvider, error) {
return &DummyProvider{}, nil
}
func (DummyProvider) Present(domain, token, keyAuth string) error {
return nil
}
func (DummyProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}

View File

@@ -0,0 +1,5 @@
package autocert
import "github.com/yusing/go-proxy/internal/logging"
var logger = logging.With().Str("module", "autocert").Logger()

View File

@@ -0,0 +1,288 @@
package autocert
import (
"crypto/tls"
"crypto/x509"
"errors"
"os"
"path"
"reflect"
"sort"
"time"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/yusing/go-proxy/internal/config/types"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type (
Provider struct {
cfg *Config
user *User
legoCfg *lego.Config
client *lego.Client
tlsCert *tls.Certificate
certExpiries CertExpiries
}
ProviderGenerator func(types.AutocertProviderOpt) (challenge.Provider, E.Error)
CertExpiries map[string]time.Time
)
var ErrGetCertFailure = errors.New("get certificate failed")
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
if p.tlsCert == nil {
return nil, ErrGetCertFailure
}
return p.tlsCert, nil
}
func (p *Provider) GetName() string {
return p.cfg.Provider
}
func (p *Provider) GetCertPath() string {
return p.cfg.CertPath
}
func (p *Provider) GetKeyPath() string {
return p.cfg.KeyPath
}
func (p *Provider) GetExpiries() CertExpiries {
return p.certExpiries
}
func (p *Provider) ObtainCert() E.Error {
if p.cfg.Provider == ProviderLocal {
return nil
}
if p.client == nil {
if err := p.initClient(); err != nil {
return err
}
}
if p.user.Registration == nil {
if err := p.registerACME(); err != nil {
return E.From(err)
}
}
client := p.client
req := certificate.ObtainRequest{
Domains: p.cfg.Domains,
Bundle: true,
}
cert, err := client.Certificate.Obtain(req)
if err != nil {
return E.From(err)
}
if err = p.saveCert(cert); err != nil {
return E.From(err)
}
tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
if err != nil {
return E.From(err)
}
expiries, err := getCertExpiries(&tlsCert)
if err != nil {
return E.From(err)
}
p.tlsCert = &tlsCert
p.certExpiries = expiries
return nil
}
func (p *Provider) LoadCert() E.Error {
cert, err := tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath)
if err != nil {
return E.Errorf("load SSL certificate: %w", err)
}
expiries, err := getCertExpiries(&cert)
if err != nil {
return E.Errorf("parse SSL certificate: %w", err)
}
p.tlsCert = &cert
p.certExpiries = expiries
logger.Info().Msgf("next renewal in %v", strutils.FormatDuration(time.Until(p.ShouldRenewOn())))
return p.renewIfNeeded()
}
// ShouldRenewOn returns the time at which the certificate should be renewed.
func (p *Provider) ShouldRenewOn() time.Time {
for _, expiry := range p.certExpiries {
return expiry.AddDate(0, -1, 0) // 1 month before
}
// this line should never be reached
panic("no certificate available")
}
func (p *Provider) ScheduleRenewal() {
if p.GetName() == ProviderLocal {
return
}
go func() {
task := task.GlobalTask("cert renew scheduler")
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
defer task.Finish("cert renew scheduler stopped")
for {
select {
case <-task.Context().Done():
return
case <-ticker.C: // check every 5 seconds
if err := p.renewIfNeeded(); err != nil {
E.LogWarn("cert renew failed", err, &logger)
}
}
}
}()
}
func (p *Provider) initClient() E.Error {
legoClient, err := lego.NewClient(p.legoCfg)
if err != nil {
return E.From(err)
}
generator := providersGenMap[p.cfg.Provider]
legoProvider, pErr := generator(p.cfg.Options)
if pErr != nil {
return pErr
}
err = legoClient.Challenge.SetDNS01Provider(legoProvider)
if err != nil {
return E.From(err)
}
p.client = legoClient
return nil
}
func (p *Provider) registerACME() error {
if p.user.Registration != nil {
return nil
}
reg, err := p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return err
}
p.user.Registration = reg
return nil
}
func (p *Provider) saveCert(cert *certificate.Resource) error {
/* This should have been done in setup
but double check is always a good choice.*/
_, err := os.Stat(path.Dir(p.cfg.CertPath))
if err != nil {
if os.IsNotExist(err) {
if err = os.MkdirAll(path.Dir(p.cfg.CertPath), 0o755); err != nil {
return err
}
} else {
return err
}
}
err = os.WriteFile(p.cfg.KeyPath, cert.PrivateKey, 0o600) // -rw-------
if err != nil {
return err
}
err = os.WriteFile(p.cfg.CertPath, cert.Certificate, 0o644) // -rw-r--r--
if err != nil {
return err
}
return nil
}
func (p *Provider) certState() CertState {
if time.Now().After(p.ShouldRenewOn()) {
return CertStateExpired
}
certDomains := make([]string, len(p.certExpiries))
wantedDomains := make([]string, len(p.cfg.Domains))
i := 0
for domain := range p.certExpiries {
certDomains[i] = domain
i++
}
copy(wantedDomains, p.cfg.Domains)
sort.Strings(wantedDomains)
sort.Strings(certDomains)
if !reflect.DeepEqual(certDomains, wantedDomains) {
logger.Info().Msgf("cert domains mismatch: %v != %v", certDomains, p.cfg.Domains)
return CertStateMismatch
}
return CertStateValid
}
func (p *Provider) renewIfNeeded() E.Error {
if p.cfg.Provider == ProviderLocal {
return nil
}
switch p.certState() {
case CertStateExpired:
logger.Info().Msg("certs expired, renewing")
case CertStateMismatch:
logger.Info().Msg("cert domains mismatch with config, renewing")
default:
return nil
}
return p.ObtainCert()
}
func getCertExpiries(cert *tls.Certificate) (CertExpiries, error) {
r := make(CertExpiries, len(cert.Certificate))
for _, cert := range cert.Certificate {
x509Cert, err := x509.ParseCertificate(cert)
if err != nil {
return nil, err
}
if x509Cert.IsCA {
continue
}
r[x509Cert.Subject.CommonName] = x509Cert.NotAfter
for i := range x509Cert.DNSNames {
r[x509Cert.DNSNames[i]] = x509Cert.NotAfter
}
}
return r, nil
}
func providerGenerator[CT any, PT challenge.Provider](
defaultCfg func() *CT,
newProvider func(*CT) (PT, error),
) ProviderGenerator {
return func(opt types.AutocertProviderOpt) (challenge.Provider, E.Error) {
cfg := defaultCfg()
err := U.Deserialize(opt, cfg)
if err != nil {
return nil, err
}
p, pErr := newProvider(cfg)
return p, E.From(pErr)
}
}

View File

@@ -0,0 +1,50 @@
package provider_test
import (
"testing"
"github.com/go-acme/lego/v4/providers/dns/ovh"
U "github.com/yusing/go-proxy/internal/utils"
. "github.com/yusing/go-proxy/internal/utils/testing"
"gopkg.in/yaml.v3"
)
// type Config struct {
// APIEndpoint string
// ApplicationKey string
// ApplicationSecret string
// ConsumerKey string
// OAuth2Config *OAuth2Config
// PropagationTimeout time.Duration
// PollingInterval time.Duration
// TTL int
// HTTPClient *http.Client
// }
func TestOVH(t *testing.T) {
cfg := &ovh.Config{}
testYaml := `
api_endpoint: https://eu.api.ovh.com
application_key: <application_key>
application_secret: <application_secret>
consumer_key: <consumer_key>
oauth2_config:
client_id: <client_id>
client_secret: <client_secret>
`
cfgExpected := &ovh.Config{
APIEndpoint: "https://eu.api.ovh.com",
ApplicationKey: "<application_key>",
ApplicationSecret: "<application_secret>",
ConsumerKey: "<consumer_key>",
OAuth2Config: &ovh.OAuth2Config{ClientID: "<client_id>", ClientSecret: "<client_secret>"},
}
testYaml = testYaml[1:] // remove first \n
opt := make(map[string]any)
ExpectNoError(t, yaml.Unmarshal([]byte(testYaml), opt))
ExpectNoError(t, U.Deserialize(opt, cfg))
ExpectDeepEqual(t, cfg, cfgExpected)
}

View File

@@ -0,0 +1,28 @@
package autocert
import (
"os"
E "github.com/yusing/go-proxy/internal/error"
)
func (p *Provider) Setup() (err E.Error) {
if err = p.LoadCert(); err != nil {
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist
return err
}
logger.Debug().Msg("obtaining cert due to error loading cert")
if err = p.ObtainCert(); err != nil {
return err
}
}
p.ScheduleRenewal()
for _, expiry := range p.GetExpiries() {
logger.Info().Msg("certificate expire on " + expiry.String())
break
}
return nil
}

View File

@@ -0,0 +1,9 @@
package autocert
type CertState int
const (
CertStateValid CertState = iota
CertStateExpired
CertStateMismatch
)

23
internal/autocert/user.go Normal file
View File

@@ -0,0 +1,23 @@
package autocert
import (
"crypto"
"github.com/go-acme/lego/v4/registration"
)
type User struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
func (u *User) GetEmail() string {
return u.Email
}
func (u *User) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *User) GetPrivateKey() crypto.PrivateKey {
return u.key
}

56
internal/common/args.go Normal file
View File

@@ -0,0 +1,56 @@
package common
import (
"flag"
"fmt"
"log"
)
type Args struct {
Command string
}
const (
CommandStart = ""
CommandSetup = "setup"
CommandValidate = "validate"
CommandListConfigs = "ls-config"
CommandListRoutes = "ls-routes"
CommandListIcons = "ls-icons"
CommandReload = "reload"
CommandDebugListEntries = "debug-ls-entries"
CommandDebugListProviders = "debug-ls-providers"
CommandDebugListMTrace = "debug-ls-mtrace"
)
var ValidCommands = []string{
CommandStart,
CommandSetup,
CommandValidate,
CommandListConfigs,
CommandListRoutes,
CommandListIcons,
CommandReload,
CommandDebugListEntries,
CommandDebugListProviders,
CommandDebugListMTrace,
}
func GetArgs() Args {
var args Args
flag.Parse()
args.Command = flag.Arg(0)
if err := validateArg(args.Command); err != nil {
log.Fatalf("invalid command: %s", err)
}
return args
}
func validateArg(arg string) error {
for _, v := range ValidCommands {
if arg == v {
return nil
}
}
return fmt.Errorf("invalid command %q", arg)
}

View File

@@ -0,0 +1,56 @@
package common
import (
"time"
)
const (
ConnectionTimeout = 5 * time.Second
DialTimeout = 3 * time.Second
KeepAlive = 60 * time.Second
)
// file, folder structure
const (
DotEnvPath = ".env"
DotEnvExamplePath = ".env.example"
ConfigBasePath = "config"
ConfigFileName = "config.yml"
ConfigExampleFileName = "config.example.yml"
ConfigPath = ConfigBasePath + "/" + ConfigFileName
JWTKeyPath = ConfigBasePath + "/jwt.key"
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
SchemaBasePath = "schema"
ConfigSchemaPath = SchemaBasePath + "/config.schema.json"
FileProviderSchemaPath = SchemaBasePath + "/providers.schema.json"
ComposeFileName = "compose.yml"
ComposeExampleFileName = "compose.example.yml"
ErrorPagesBasePath = "error_pages"
)
var RequiredDirectories = []string{
ConfigBasePath,
SchemaBasePath,
ErrorPagesBasePath,
MiddlewareComposeBasePath,
}
const DockerHostFromEnv = "$DOCKER_HOST"
const (
HealthCheckIntervalDefault = 5 * time.Second
HealthCheckTimeoutDefault = 5 * time.Second
WakeTimeoutDefault = "30s"
StopTimeoutDefault = "10s"
StopMethodDefault = "stop"
)
const HeaderCheckRedirect = "X-Goproxy-Check-Redirect"

34
internal/common/crypto.go Normal file
View File

@@ -0,0 +1,34 @@
package common
import (
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"github.com/rs/zerolog/log"
)
func HashPassword(pwd string) []byte {
h := sha512.New()
h.Write([]byte(pwd))
return h.Sum(nil)
}
func generateJWTKey(size int) string {
bytes := make([]byte, size)
if _, err := rand.Read(bytes); err != nil {
log.Panic().Err(err).Msg("failed to generate jwt key")
}
return base64.StdEncoding.EncodeToString(bytes)
}
func decodeJWTKey(key string) []byte {
if key == "" {
return nil
}
bytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
log.Panic().Err(err).Msg("failed to decode jwt key")
}
return bytes
}

99
internal/common/env.go Normal file
View File

@@ -0,0 +1,99 @@
package common
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/rs/zerolog/log"
)
var (
prefixes = []string{"GODOXY_", "GOPROXY_", ""}
NoSchemaValidation = GetEnvBool("NO_SCHEMA_VALIDATION", true)
IsTest = GetEnvBool("TEST", false) || strings.HasSuffix(os.Args[0], ".test")
IsDebug = GetEnvBool("DEBUG", IsTest)
IsDebugSkipAuth = GetEnvBool("DEBUG_SKIP_AUTH", false)
IsTrace = GetEnvBool("TRACE", false) && IsDebug
IsProduction = !IsTest && !IsDebug
ProxyHTTPAddr,
ProxyHTTPHost,
ProxyHTTPPort,
ProxyHTTPURL = GetAddrEnv("HTTP_ADDR", ":80", "http")
ProxyHTTPSAddr,
ProxyHTTPSHost,
ProxyHTTPSPort,
ProxyHTTPSURL = GetAddrEnv("HTTPS_ADDR", ":443", "https")
APIHTTPAddr,
APIHTTPHost,
APIHTTPPort,
APIHTTPURL = GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
MetricsHTTPAddr,
MetricsHTTPHost,
MetricsHTTPPort,
MetricsHTTPURL = GetAddrEnv("PROMETHEUS_ADDR", "", "http")
PrometheusEnabled = MetricsHTTPURL != ""
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", time.Hour)
APIUser = GetEnvString("API_USER", "admin")
APIPasswordHash = HashPassword(GetEnvString("API_PASSWORD", "password"))
)
func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T {
var value string
var ok bool
for _, prefix := range prefixes {
value, ok = os.LookupEnv(prefix + key)
if ok && value != "" {
break
}
}
if !ok || value == "" {
return defaultValue
}
parsed, err := parser(value)
if err == nil {
return parsed
}
log.Fatal().Err(err).Msgf("env %s: invalid %T value: %s", key, parsed, value)
return defaultValue
}
func GetEnvString(key string, defaultValue string) string {
return GetEnv(key, defaultValue, func(s string) (string, error) {
return s, nil
})
}
func GetEnvBool(key string, defaultValue bool) bool {
return GetEnv(key, defaultValue, strconv.ParseBool)
}
func GetAddrEnv(key, defaultValue, scheme string) (addr, host, port, fullURL string) {
addr = GetEnvString(key, defaultValue)
if addr == "" {
return
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
log.Fatal().Msgf("env %s: invalid address: %s", key, addr)
}
if host == "" {
host = "localhost"
}
fullURL = fmt.Sprintf("%s://%s:%s", scheme, host, port)
return
}
func GetDurationEnv(key string, defaultValue time.Duration) time.Duration {
return GetEnv(key, defaultValue, time.ParseDuration)
}

75
internal/common/ports.go Normal file
View File

@@ -0,0 +1,75 @@
package common
var (
WellKnownHTTPPorts = map[string]bool{
"80": true,
"8000": true,
"8008": true,
"8080": true,
"3000": true,
}
ServiceNamePortMapTCP = map[string]int{
"mssql": 1433,
"mysql": 3306,
"mariadb": 3306,
"postgres": 5432,
"rabbitmq": 5672,
"redis": 6379,
"memcached": 11211,
"mongo": 27017,
"minecraft-server": 25565,
"ssh": 22,
"ftp": 21,
"smtp": 25,
"dns": 53,
"pop3": 110,
"imap": 143,
}
ImageNamePortMap = func() (m map[string]int) {
m = make(map[string]int, len(ServiceNamePortMapTCP)+len(imageNamePortMap))
for k, v := range ServiceNamePortMapTCP {
m[k] = v
}
for k, v := range imageNamePortMap {
m[k] = v
}
return
}()
imageNamePortMap = map[string]int{
"adguardhome": 3000,
"bazarr": 6767,
"calibre-web": 8083,
"changedetection.io": 3000,
"dockge": 5001,
"gitea": 3000,
"gogs": 3000,
"grafana": 3000,
"home-assistant": 8123,
"homebridge": 8581,
"httpd": 80,
"immich": 3001,
"jellyfin": 8096,
"lidarr": 8686,
"microbin": 8080,
"nginx": 80,
"nginx-proxy-manager": 81,
"open-webui": 8080,
"plex": 32400,
"portainer-be": 9443,
"portainer-ce": 9443,
"prometheus": 9090,
"prowlarr": 9696,
"radarr": 7878,
"radarr-sma": 7878,
"rsshub": 1200,
"rss-bridge": 80,
"sonarr": 8989,
"sonarr-sma": 8989,
"uptime-kuma": 3001,
"whisparr": 6969,
}
)

249
internal/config/config.go Normal file
View File

@@ -0,0 +1,249 @@
package config
import (
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/yusing/go-proxy/internal/autocert"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config/types"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/notif"
"github.com/yusing/go-proxy/internal/route"
proxy "github.com/yusing/go-proxy/internal/route/provider"
"github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils"
F "github.com/yusing/go-proxy/internal/utils/functional"
"github.com/yusing/go-proxy/internal/watcher"
"github.com/yusing/go-proxy/internal/watcher/events"
"gopkg.in/yaml.v3"
)
type Config struct {
value *types.Config
providers F.Map[string, *proxy.Provider]
autocertProvider *autocert.Provider
task task.Task
}
var (
instance *Config
cfgWatcher watcher.Watcher
logger = logging.With().Str("module", "config").Logger()
reloadMu sync.Mutex
)
const configEventFlushInterval = 500 * time.Millisecond
const (
cfgRenameWarn = `Config file renamed, not reloading.
Make sure you rename it back before next time you start.`
cfgDeleteWarn = `Config file deleted, not reloading.
You may run "ls-config" to show or dump the current config.`
)
func GetInstance() *Config {
return instance
}
func newConfig() *Config {
return &Config{
value: types.DefaultConfig(),
providers: F.NewMapOf[string, *proxy.Provider](),
task: task.GlobalTask("config"),
}
}
func Load() (*Config, E.Error) {
if instance != nil {
return instance, nil
}
instance = newConfig()
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
return instance, instance.load()
}
func Validate(data []byte) E.Error {
return U.ValidateYaml(U.GetSchema(common.ConfigSchemaPath), data)
}
func MatchDomains() []string {
return instance.value.MatchDomains
}
func WatchChanges() {
task := task.GlobalTask("Config watcher")
eventQueue := events.NewEventQueue(
task,
configEventFlushInterval,
OnConfigChange,
func(err E.Error) {
E.LogError("config reload error", err, &logger)
},
)
eventQueue.Start(cfgWatcher.Events(task.Context()))
}
func OnConfigChange(flushTask task.Task, ev []events.Event) {
defer flushTask.Finish("config reload complete")
// no matter how many events during the interval
// just reload once and check the last event
switch ev[len(ev)-1].Action {
case events.ActionFileRenamed:
logger.Warn().Msg(cfgRenameWarn)
return
case events.ActionFileDeleted:
logger.Warn().Msg(cfgDeleteWarn)
return
}
if err := Reload(); err != nil {
// recovered in event queue
panic(err)
}
}
func Reload() E.Error {
// avoid race between config change and API reload request
reloadMu.Lock()
defer reloadMu.Unlock()
newCfg := newConfig()
err := newCfg.load()
if err != nil {
return err
}
// cancel all current subtasks -> wait
// -> replace config -> start new subtasks
instance.task.Finish("config changed")
instance.task.Wait()
*instance = *newCfg
instance.StartProxyProviders()
return nil
}
func Value() types.Config {
return *instance.value
}
func GetAutoCertProvider() *autocert.Provider {
return instance.autocertProvider
}
func (cfg *Config) Task() task.Task {
return cfg.task
}
func (cfg *Config) StartProxyProviders() {
errs := cfg.providers.CollectErrorsParallel(
func(_ string, p *proxy.Provider) error {
subtask := cfg.task.Subtask(p.String())
return p.Start(subtask)
})
if err := E.Join(errs...); err != nil {
E.LogError("route provider errors", err, &logger)
}
}
func (cfg *Config) load() E.Error {
const errMsg = "config load error"
data, err := os.ReadFile(common.ConfigPath)
if err != nil {
E.LogFatal(errMsg, err, &logger)
}
if !common.NoSchemaValidation {
if err := Validate(data); err != nil {
E.LogFatal(errMsg, err, &logger)
}
}
model := types.DefaultConfig()
if err := E.From(yaml.Unmarshal(data, model)); err != nil {
E.LogFatal(errMsg, err, &logger)
}
// errors are non fatal below
errs := E.NewBuilder(errMsg)
errs.Add(cfg.initNotification(model.Providers.Notification))
errs.Add(cfg.initAutoCert(&model.AutoCert))
errs.Add(cfg.loadRouteProviders(&model.Providers))
cfg.value = model
for i, domain := range model.MatchDomains {
if !strings.HasPrefix(domain, ".") {
model.MatchDomains[i] = "." + domain
}
}
route.SetFindMuxDomains(model.MatchDomains)
return errs.Error()
}
func (cfg *Config) initNotification(notifCfgMap types.NotificationConfigMap) (err E.Error) {
if len(notifCfgMap) == 0 {
return
}
errs := E.NewBuilder("notification providers load errors")
for name, notifCfg := range notifCfgMap {
_, err := notif.RegisterProvider(cfg.task.Subtask(name), notifCfg)
errs.Add(err)
}
return errs.Error()
}
func (cfg *Config) initAutoCert(autocertCfg *types.AutoCertConfig) (err E.Error) {
if cfg.autocertProvider != nil {
return
}
cfg.autocertProvider, err = autocert.NewConfig(autocertCfg).GetProvider()
return
}
func (cfg *Config) loadRouteProviders(providers *types.Providers) E.Error {
subtask := cfg.task.Subtask("load route providers")
defer subtask.Finish("done")
errs := E.NewBuilder("route provider errors")
results := E.NewBuilder("loaded route providers")
lenLongestName := 0
for _, filename := range providers.Files {
p, err := proxy.NewFileProvider(filename)
if err != nil {
errs.Add(E.PrependSubject(filename, err))
continue
}
cfg.providers.Store(p.GetName(), p)
if len(p.GetName()) > lenLongestName {
lenLongestName = len(p.GetName())
}
}
for name, dockerHost := range providers.Docker {
p, err := proxy.NewDockerProvider(name, dockerHost)
if err != nil {
errs.Add(E.PrependSubject(name, err))
continue
}
cfg.providers.Store(p.GetName(), p)
if len(p.GetName()) > lenLongestName {
lenLongestName = len(p.GetName())
}
}
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
if err := p.LoadRoutes(); err != nil {
errs.Add(err.Subject(p.String()))
}
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.GetName(), p.NumRoutes())
})
logger.Info().Msg(results.String())
return errs.Error()
}

153
internal/config/query.go Normal file
View File

@@ -0,0 +1,153 @@
package config
import (
"fmt"
"strings"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/proxy/entry"
"github.com/yusing/go-proxy/internal/route"
proxy "github.com/yusing/go-proxy/internal/route/provider"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
func DumpEntries() map[string]*entry.RawEntry {
entries := make(map[string]*entry.RawEntry)
instance.providers.RangeAll(func(_ string, p *proxy.Provider) {
p.RangeRoutes(func(alias string, r *route.Route) {
entries[alias] = r.Entry
})
})
return entries
}
func DumpProviders() map[string]*proxy.Provider {
entries := make(map[string]*proxy.Provider)
instance.providers.RangeAll(func(name string, p *proxy.Provider) {
entries[name] = p
})
return entries
}
func HomepageConfig() homepage.Config {
var proto, port string
domains := instance.value.MatchDomains
cert, _ := instance.autocertProvider.GetCert(nil)
if cert != nil {
proto = "https"
port = common.ProxyHTTPSPort
} else {
proto = "http"
port = common.ProxyHTTPPort
}
hpCfg := homepage.NewHomePageConfig()
route.GetReverseProxies().RangeAll(func(alias string, r *route.HTTPRoute) {
en := r.Raw
item := en.Homepage
if item == nil {
item = new(homepage.Item)
item.Show = true
}
if !item.IsEmpty() {
item.Show = true
}
if !item.Show {
return
}
if item.Name == "" {
item.Name = strutils.Title(
strings.ReplaceAll(
strings.ReplaceAll(alias, "-", " "),
"_", " ",
),
)
}
if instance.value.Homepage.UseDefaultCategories {
if en.Container != nil && item.Category == "" {
if category, ok := homepage.PredefinedCategories[en.Container.ImageName]; ok {
item.Category = category
}
}
if item.Category == "" {
if category, ok := homepage.PredefinedCategories[strings.ToLower(alias)]; ok {
item.Category = category
}
}
}
switch {
case entry.IsDocker(r):
if item.Category == "" {
item.Category = "Docker"
}
item.SourceType = string(proxy.ProviderTypeDocker)
case entry.UseLoadBalance(r):
if item.Category == "" {
item.Category = "Load-balanced"
}
item.SourceType = "loadbalancer"
default:
if item.Category == "" {
item.Category = "Others"
}
item.SourceType = string(proxy.ProviderTypeFile)
}
if item.URL == "" {
if len(domains) > 0 {
item.URL = fmt.Sprintf("%s://%s%s:%s", proto, strings.ToLower(alias), domains[0], port)
}
}
item.AltURL = r.TargetURL().String()
hpCfg.Add(item)
})
return hpCfg
}
func RoutesByAlias(typeFilter ...route.RouteType) map[string]any {
routes := make(map[string]any)
if len(typeFilter) == 0 || typeFilter[0] == "" {
typeFilter = []route.RouteType{route.RouteTypeReverseProxy, route.RouteTypeStream}
}
for _, t := range typeFilter {
switch t {
case route.RouteTypeReverseProxy:
route.GetReverseProxies().RangeAll(func(alias string, r *route.HTTPRoute) {
routes[alias] = r
})
case route.RouteTypeStream:
route.GetStreamProxies().RangeAll(func(alias string, r *route.StreamRoute) {
routes[alias] = r
})
}
}
return routes
}
func Statistics() map[string]any {
nTotalStreams := 0
nTotalRPs := 0
providerStats := make(map[string]proxy.ProviderStats)
instance.providers.RangeAll(func(name string, p *proxy.Provider) {
stats := p.Statistics()
providerStats[name] = stats
nTotalRPs += stats.NumRPs
nTotalStreams += stats.NumStreams
})
return map[string]any{
"num_total_streams": nTotalStreams,
"num_total_reverse_proxies": nTotalRPs,
"providers": providerStats,
}
}

View File

@@ -0,0 +1,13 @@
package types
type (
AutoCertConfig struct {
Email string `json:"email,omitempty" yaml:"email"`
Domains []string `json:"domains,omitempty" yaml:",flow"`
CertPath string `json:"cert_path,omitempty" yaml:"cert_path"`
KeyPath string `json:"key_path,omitempty" yaml:"key_path"`
Provider string `json:"provider,omitempty" yaml:"provider"`
Options AutocertProviderOpt `json:"options,omitempty" yaml:",flow"`
}
AutocertProviderOpt map[string]any
)

View File

@@ -0,0 +1,28 @@
package types
type (
Config struct {
Providers Providers `json:"providers" yaml:",flow"`
AutoCert AutoCertConfig `json:"autocert" yaml:",flow"`
ExplicitOnly bool `json:"explicit_only" yaml:"explicit_only"`
MatchDomains []string `json:"match_domains" yaml:"match_domains"`
Homepage HomepageConfig `json:"homepage" yaml:"homepage"`
TimeoutShutdown int `json:"timeout_shutdown" yaml:"timeout_shutdown"`
RedirectToHTTPS bool `json:"redirect_to_https" yaml:"redirect_to_https"`
}
Providers struct {
Files []string `json:"include" yaml:"include"`
Docker map[string]string `json:"docker" yaml:"docker"`
Notification NotificationConfigMap `json:"notification" yaml:"notification"`
}
)
func DefaultConfig() *Config {
return &Config{
TimeoutShutdown: 3,
Homepage: HomepageConfig{
UseDefaultCategories: true,
},
RedirectToHTTPS: false,
}
}

View File

@@ -0,0 +1,5 @@
package types
type HomepageConfig struct {
UseDefaultCategories bool `json:"use_default_categories" yaml:"use_default_categories"`
}

View File

@@ -0,0 +1,5 @@
package types
import "github.com/yusing/go-proxy/internal/notif"
type NotificationConfigMap map[string]notif.ProviderConfig

139
internal/docker/client.go Normal file
View File

@@ -0,0 +1,139 @@
package docker
import (
"errors"
"net/http"
"sync"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/docker/client"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils"
F "github.com/yusing/go-proxy/internal/utils/functional"
)
type (
Client = *SharedClient
SharedClient struct {
*client.Client
key string
refCount *U.RefCount
l zerolog.Logger
}
)
var (
clientMap F.Map[string, Client] = F.NewMapOf[string, Client]()
clientMapMu sync.Mutex
clientOptEnvHost = []client.Opt{
client.WithHostFromEnv(),
client.WithAPIVersionNegotiation(),
}
)
func init() {
task.GlobalTask("close docker clients").OnFinished("", func() {
clientMap.RangeAllParallel(func(_ string, c Client) {
if c.Connected() {
c.Client.Close()
}
})
})
}
func (c *SharedClient) Connected() bool {
return c != nil && c.Client != nil
}
// if the client is still referenced, this is no-op.
func (c *SharedClient) Close() {
if c.Connected() {
c.refCount.Sub()
}
}
// ConnectClient creates a new Docker client connection to the specified host.
//
// Returns existing client if available.
//
// Parameters:
// - host: the host to connect to (either a URL or common.DockerHostFromEnv).
//
// Returns:
// - Client: the Docker client connection.
// - error: an error if the connection failed.
func ConnectClient(host string) (Client, error) {
clientMapMu.Lock()
defer clientMapMu.Unlock()
// check if client exists
if client, ok := clientMap.Load(host); ok {
client.refCount.Add()
return client, nil
}
// create client
var opt []client.Opt
switch host {
case "":
return nil, errors.New("empty docker host")
case common.DockerHostFromEnv:
opt = clientOptEnvHost
default:
helper, err := connhelper.GetConnectionHelper(host)
if err != nil {
logging.Panic().Err(err).Msg("failed to get connection helper")
}
if helper != nil {
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
opt = []client.Opt{
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithAPIVersionNegotiation(),
client.WithDialContext(helper.Dialer),
}
} else {
opt = []client.Opt{
client.WithHost(host),
client.WithAPIVersionNegotiation(),
}
}
}
client, err := client.NewClientWithOpts(opt...)
if err != nil {
return nil, err
}
c := &SharedClient{
Client: client,
key: host,
refCount: U.NewRefCounter(),
l: logger.With().Str("address", client.DaemonHost()).Logger(),
}
c.l.Trace().Msg("client connected")
clientMap.Store(host, c)
go func() {
<-c.refCount.Zero()
clientMap.Delete(c.key)
if c.Connected() {
c.Client.Close()
c.l.Trace().Msg("client closed")
}
}()
return c, nil
}

View File

@@ -0,0 +1,144 @@
package docker
import (
"net/url"
"strconv"
"strings"
"github.com/docker/docker/api/types"
U "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type (
PortMapping = map[string]types.Port
Container struct {
_ U.NoCopy
DockerHost string `json:"docker_host" yaml:"-"`
ContainerName string `json:"container_name" yaml:"-"`
ContainerID string `json:"container_id" yaml:"-"`
ImageName string `json:"image_name" yaml:"-"`
Labels map[string]string `json:"-" yaml:"-"`
PublicPortMapping PortMapping `json:"public_ports" yaml:"-"` // non-zero publicPort:types.Port
PrivatePortMapping PortMapping `json:"private_ports" yaml:"-"` // privatePort:types.Port
PublicIP string `json:"public_ip" yaml:"-"`
PrivateIP string `json:"private_ip" yaml:"-"`
NetworkMode string `json:"network_mode" yaml:"-"`
Aliases []string `json:"aliases" yaml:"-"`
IsExcluded bool `json:"is_excluded" yaml:"-"`
IsExplicit bool `json:"is_explicit" yaml:"-"`
IsDatabase bool `json:"is_database" yaml:"-"`
IdleTimeout string `json:"idle_timeout,omitempty" yaml:"-"`
WakeTimeout string `json:"wake_timeout,omitempty" yaml:"-"`
StopMethod string `json:"stop_method,omitempty" yaml:"-"`
StopTimeout string `json:"stop_timeout,omitempty" yaml:"-"` // stop_method = "stop" only
StopSignal string `json:"stop_signal,omitempty" yaml:"-"` // stop_method = "stop" | "kill" only
Running bool `json:"running" yaml:"-"`
}
)
var DummyContainer = new(Container)
func FromDocker(c *types.Container, dockerHost string) (res *Container) {
isExplicit := c.Labels[LabelAliases] != ""
helper := containerHelper{c}
res = &Container{
DockerHost: dockerHost,
ContainerName: helper.getName(),
ContainerID: c.ID,
ImageName: helper.getImageName(),
Labels: c.Labels,
PublicPortMapping: helper.getPublicPortMapping(),
PrivatePortMapping: helper.getPrivatePortMapping(),
NetworkMode: c.HostConfig.NetworkMode,
Aliases: helper.getAliases(),
IsExcluded: strutils.ParseBool(helper.getDeleteLabel(LabelExclude)),
IsExplicit: isExplicit,
IsDatabase: helper.isDatabase(),
IdleTimeout: helper.getDeleteLabel(LabelIdleTimeout),
WakeTimeout: helper.getDeleteLabel(LabelWakeTimeout),
StopMethod: helper.getDeleteLabel(LabelStopMethod),
StopTimeout: helper.getDeleteLabel(LabelStopTimeout),
StopSignal: helper.getDeleteLabel(LabelStopSignal),
Running: c.Status == "running" || c.State == "running",
}
res.setPrivateIP(helper)
res.setPublicIP()
return
}
func FromJSON(json types.ContainerJSON, dockerHost string) *Container {
ports := make([]types.Port, 0)
for k, bindings := range json.NetworkSettings.Ports {
privPortStr, proto := k.Port(), k.Proto()
privPort, _ := strconv.ParseUint(privPortStr, 10, 16)
ports = append(ports, types.Port{
PrivatePort: uint16(privPort),
Type: proto,
})
for _, v := range bindings {
pubPort, _ := strconv.ParseUint(v.HostPort, 10, 16)
ports = append(ports, types.Port{
IP: v.HostIP,
PublicPort: uint16(pubPort),
PrivatePort: uint16(privPort),
Type: proto,
})
}
}
cont := FromDocker(&types.Container{
ID: json.ID,
Names: []string{strings.TrimPrefix(json.Name, "/")},
Image: json.Image,
Ports: ports,
Labels: json.Config.Labels,
State: json.State.Status,
Status: json.State.Status,
Mounts: json.Mounts,
NetworkSettings: &types.SummaryNetworkSettings{
Networks: json.NetworkSettings.Networks,
},
}, dockerHost)
cont.NetworkMode = string(json.HostConfig.NetworkMode)
return cont
}
func (c *Container) setPublicIP() {
if !c.Running {
return
}
if strings.HasPrefix(c.DockerHost, "unix://") {
c.PublicIP = "127.0.0.1"
return
}
url, err := url.Parse(c.DockerHost)
if err != nil {
logger.Err(err).Msgf("invalid docker host %q, falling back to 127.0.0.1", c.DockerHost)
c.PublicIP = "127.0.0.1"
return
}
c.PublicIP = url.Hostname()
}
func (c *Container) setPrivateIP(helper containerHelper) {
if !strings.HasPrefix(c.DockerHost, "unix://") {
return
}
if helper.NetworkSettings == nil {
return
}
for _, v := range helper.NetworkSettings.Networks {
if v.IPAddress == "" {
continue
}
c.PrivateIP = v.IPAddress
return
}
}

View File

@@ -0,0 +1,90 @@
package docker
import (
"strings"
"github.com/docker/docker/api/types"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type containerHelper struct {
*types.Container
}
// getDeleteLabel gets the value of a label and then deletes it from the container.
// If the label does not exist, an empty string is returned.
func (c containerHelper) getDeleteLabel(label string) string {
if l, ok := c.Labels[label]; ok {
delete(c.Labels, label)
return l
}
return ""
}
func (c containerHelper) getAliases() []string {
if l := c.getDeleteLabel(LabelAliases); l != "" {
return strutils.CommaSeperatedList(l)
}
return []string{c.getName()}
}
func (c containerHelper) getName() string {
return strings.TrimPrefix(c.Names[0], "/")
}
func (c containerHelper) getImageName() string {
colonSep := strings.Split(c.Image, ":")
slashSep := strings.Split(colonSep[0], "/")
return slashSep[len(slashSep)-1]
}
func (c containerHelper) getPublicPortMapping() PortMapping {
res := make(PortMapping)
for _, v := range c.Ports {
if v.PublicPort == 0 {
continue
}
res[strutils.PortString(v.PublicPort)] = v
}
return res
}
func (c containerHelper) getPrivatePortMapping() PortMapping {
res := make(PortMapping)
for _, v := range c.Ports {
res[strutils.PortString(v.PrivatePort)] = v
}
return res
}
var databaseMPs = map[string]struct{}{
"/var/lib/postgresql/data": {},
"/var/lib/mysql": {},
"/var/lib/mongodb": {},
"/var/lib/mariadb": {},
"/var/lib/memcached": {},
"/var/lib/rabbitmq": {},
}
var databasePrivPorts = map[uint16]struct{}{
5432: {}, // postgres
3306: {}, // mysql, mariadb
6379: {}, // redis
11211: {}, // memcached
27017: {}, // mongodb
}
func (c containerHelper) isDatabase() bool {
for _, m := range c.Mounts {
if _, ok := databaseMPs[m.Destination]; ok {
return true
}
}
for _, v := range c.Ports {
if _, ok := databasePrivPorts[v.PrivatePort]; ok {
return true
}
}
return false
}

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{.Title}}</title>
<style>
/* Global Styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
color: #fff;
background-color: #212121;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
/* Spinner Styles */
.spinner {
width: 120px;
height: 120px;
border: 16px solid #333;
border-radius: 50%;
border-top: 16px solid #66d9ef;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Error Styles */
.error {
display: inline-block;
text-align: center;
justify-content: center;
}
.error::before {
content: "\26A0"; /* Unicode for warning symbol */
font-size: 40px;
color: #ff9900;
}
/* Message Styles */
.message {
font-size: 24px;
font-weight: bold;
padding-left: 32px;
text-align: center;
}
</style>
</head>
<body>
<script>
window.onload = async function () {
let resp = await fetch(window.location.href, {
headers: {
"{{.CheckRedirectHeader}}": "1",
},
});
if (resp.ok) {
window.location.href = resp.url;
} else {
document.getElementById("message").innerText =
await resp.text();
document
.getElementById("spinner")
.classList.replace("spinner", "error");
}
};
</script>
<div id="spinner" class="spinner"></div>
<div id="message" class="message">{{.Message}}</div>
</body>
</html>

View File

@@ -0,0 +1,36 @@
package idlewatcher
import (
"bytes"
_ "embed"
"strings"
"text/template"
"github.com/yusing/go-proxy/internal/common"
)
type templateData struct {
CheckRedirectHeader string
Title string
Message string
}
//go:embed html/loading_page.html
var loadingPage []byte
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
func (w *Watcher) makeLoadingPageBody() []byte {
msg := w.ContainerName + " is starting..."
data := new(templateData)
data.CheckRedirectHeader = common.HeaderCheckRedirect
data.Title = w.ContainerName
data.Message = strings.ReplaceAll(msg, " ", "&ensp;")
buf := bytes.NewBuffer(make([]byte, len(loadingPage)+len(data.Title)+len(data.Message)+len(common.HeaderCheckRedirect)))
err := loadingPageTmpl.Execute(buf, data)
if err != nil { // should never happen in production
panic(err)
}
return buf.Bytes()
}

View File

@@ -0,0 +1,103 @@
package types
import (
"errors"
"time"
"github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error"
)
type (
Config struct {
IdleTimeout time.Duration `json:"idle_timeout,omitempty"`
WakeTimeout time.Duration `json:"wake_timeout,omitempty"`
StopTimeout int `json:"stop_timeout,omitempty"` // docker api takes integer seconds for timeout argument
StopMethod StopMethod `json:"stop_method,omitempty"`
StopSignal Signal `json:"stop_signal,omitempty"`
DockerHost string `json:"docker_host,omitempty"`
ContainerName string `json:"container_name,omitempty"`
ContainerID string `json:"container_id,omitempty"`
ContainerRunning bool `json:"container_running,omitempty"`
}
StopMethod string
Signal string
)
const (
StopMethodPause StopMethod = "pause"
StopMethodStop StopMethod = "stop"
StopMethodKill StopMethod = "kill"
)
func ValidateConfig(cont *docker.Container) (*Config, E.Error) {
if cont == nil {
return nil, nil
}
if cont.IdleTimeout == "" {
return &Config{
DockerHost: cont.DockerHost,
ContainerName: cont.ContainerName,
ContainerID: cont.ContainerID,
ContainerRunning: cont.Running,
}, nil
}
errs := E.NewBuilder("invalid idlewatcher config")
idleTimeout := E.Collect(errs, validateDurationPostitive, cont.IdleTimeout)
wakeTimeout := E.Collect(errs, validateDurationPostitive, cont.WakeTimeout)
stopTimeout := E.Collect(errs, validateDurationPostitive, cont.StopTimeout)
stopMethod := E.Collect(errs, validateStopMethod, cont.StopMethod)
signal := E.Collect(errs, validateSignal, cont.StopSignal)
if errs.HasError() {
return nil, errs.Error()
}
return &Config{
IdleTimeout: idleTimeout,
WakeTimeout: wakeTimeout,
StopTimeout: int(stopTimeout.Seconds()),
StopMethod: stopMethod,
StopSignal: signal,
DockerHost: cont.DockerHost,
ContainerName: cont.ContainerName,
ContainerID: cont.ContainerID,
ContainerRunning: cont.Running,
}, nil
}
func validateDurationPostitive(value string) (time.Duration, error) {
d, err := time.ParseDuration(value)
if err != nil {
return 0, err
}
if d < 0 {
return 0, errors.New("duration must be positive")
}
return d, nil
}
func validateSignal(s string) (Signal, error) {
switch s {
case "", "SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT",
"INT", "TERM", "HUP", "QUIT":
return Signal(s), nil
}
return "", errors.New("invalid signal " + s)
}
func validateStopMethod(s string) (StopMethod, error) {
sm := StopMethod(s)
switch sm {
case StopMethodPause, StopMethodStop, StopMethodKill:
return sm, nil
default:
return "", errors.New("invalid stop method " + s)
}
}

View File

@@ -0,0 +1,15 @@
package types
import (
"net/http"
net "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/watcher/health"
)
type Waker interface {
health.HealthMonitor
http.Handler
net.Stream
Wake() error
}

View File

@@ -0,0 +1,156 @@
package idlewatcher
import (
"sync/atomic"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/yusing/go-proxy/internal/common"
. "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/metrics"
gphttp "github.com/yusing/go-proxy/internal/net/http"
net "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/proxy/entry"
"github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/watcher/health"
)
type waker struct {
_ U.NoCopy
rp *gphttp.ReverseProxy
stream net.Stream
hc health.HealthChecker
metric *metrics.Gauge
ready atomic.Bool
}
const (
idleWakerCheckInterval = 100 * time.Millisecond
idleWakerCheckTimeout = time.Second
)
// TODO: support stream
func newWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReverseProxy, stream net.Stream) (Waker, E.Error) {
hcCfg := entry.HealthCheckConfig()
hcCfg.Timeout = idleWakerCheckTimeout
waker := &waker{
rp: rp,
stream: stream,
}
watcher, err := registerWatcher(providerSubTask, entry, waker)
if err != nil {
return nil, E.Errorf("register watcher: %w", err)
}
switch {
case rp != nil:
waker.hc = health.NewHTTPHealthChecker(entry.TargetURL(), hcCfg, rp.Transport)
case stream != nil:
waker.hc = health.NewRawHealthChecker(entry.TargetURL(), hcCfg)
default:
panic("both nil")
}
if common.PrometheusEnabled {
m := metrics.GetServiceMetrics()
fqn := providerSubTask.Parent().Name() + "/" + entry.TargetName()
waker.metric = m.HealthStatus.With(metrics.HealthMetricLabels(fqn))
waker.metric.Set(float64(watcher.Status()))
}
return watcher, nil
}
// lifetime should follow route provider.
func NewHTTPWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReverseProxy) (Waker, E.Error) {
return newWaker(providerSubTask, entry, rp, nil)
}
func NewStreamWaker(providerSubTask task.Task, entry entry.Entry, stream net.Stream) (Waker, E.Error) {
return newWaker(providerSubTask, entry, nil, stream)
}
// Start implements health.HealthMonitor.
func (w *Watcher) Start(routeSubTask task.Task) E.Error {
routeSubTask.Finish("ignored")
w.task.OnCancel("stop route and cleanup", func() {
routeSubTask.Parent().Finish(w.task.FinishCause())
if w.metric != nil {
prometheus.Unregister(w.metric)
}
})
return nil
}
// Finish implements health.HealthMonitor.
func (w *Watcher) Finish(reason any) {
if w.stream != nil {
w.stream.Close()
}
}
// Name implements health.HealthMonitor.
func (w *Watcher) Name() string {
return w.String()
}
// String implements health.HealthMonitor.
func (w *Watcher) String() string {
return w.ContainerName
}
// Uptime implements health.HealthMonitor.
func (w *Watcher) Uptime() time.Duration {
return 0
}
func (w *Watcher) Status() health.Status {
status := w.getStatusUpdateReady()
if w.metric != nil {
w.metric.Set(float64(status))
}
return status
}
// Status implements health.HealthMonitor.
func (w *Watcher) getStatusUpdateReady() health.Status {
if !w.ContainerRunning {
return health.StatusNapping
}
if w.ready.Load() {
return health.StatusHealthy
}
healthy, _, err := w.hc.CheckHealth()
switch {
case err != nil:
w.ready.Store(false)
return health.StatusError
case healthy:
w.ready.Store(true)
return health.StatusHealthy
default:
return health.StatusStarting
}
}
// MarshalJSON implements health.HealthMonitor.
func (w *Watcher) MarshalJSON() ([]byte, error) {
var url net.URL
if w.hc.URL().Port() != "0" {
url = w.hc.URL()
}
return (&health.JSONRepresentation{
Name: w.Name(),
Status: w.Status(),
Config: w.hc.Config(),
URL: url,
}).MarshalJSON()
}

View File

@@ -0,0 +1,108 @@
package idlewatcher
import (
"context"
"errors"
"net/http"
"strconv"
"time"
"github.com/yusing/go-proxy/internal/common"
gphttp "github.com/yusing/go-proxy/internal/net/http"
"github.com/yusing/go-proxy/internal/watcher/health"
)
// ServeHTTP implements http.Handler.
func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
shouldNext := w.wakeFromHTTP(rw, r)
if !shouldNext {
return
}
select {
case <-r.Context().Done():
return
default:
w.rp.ServeHTTP(rw, r)
}
}
func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldNext bool) {
w.resetIdleTimer()
// pass through if container is already ready
if w.ready.Load() {
return true
}
if r.Body != nil {
defer r.Body.Close()
}
accept := gphttp.GetAccept(r.Header)
acceptHTML := (r.Method == http.MethodGet && accept.AcceptHTML() || r.RequestURI == "/" && accept.IsEmpty())
isCheckRedirect := r.Header.Get(common.HeaderCheckRedirect) != ""
if !isCheckRedirect && acceptHTML {
// Send a loading response to the client
body := w.makeLoadingPageBody()
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.Header().Set("Content-Length", strconv.Itoa(len(body)))
rw.Header().Add("Cache-Control", "no-cache")
rw.Header().Add("Cache-Control", "no-store")
rw.Header().Add("Cache-Control", "must-revalidate")
rw.Header().Add("Connection", "close")
if _, err := rw.Write(body); err != nil {
w.Err(err).Msg("error writing http response")
}
return false
}
ctx, cancel := context.WithTimeoutCause(r.Context(), w.WakeTimeout, errors.New("wake timeout"))
defer cancel()
checkCanceled := func() (canceled bool) {
select {
case <-ctx.Done():
w.WakeDebug().Str("cause", context.Cause(ctx).Error()).Msg("canceled")
return true
case <-w.task.Context().Done():
w.WakeDebug().Str("cause", w.task.FinishCause().Error()).Msg("canceled")
http.Error(rw, "Service unavailable", http.StatusServiceUnavailable)
return true
default:
return false
}
}
if checkCanceled() {
return false
}
w.WakeTrace().Msg("signal received")
err := w.wakeIfStopped()
if err != nil {
w.WakeError(err)
http.Error(rw, "Error waking container", http.StatusInternalServerError)
return false
}
for {
if checkCanceled() {
return false
}
if w.Status() == health.StatusHealthy {
w.resetIdleTimer()
if isCheckRedirect {
w.Debug().Msgf("redirecting to %s ...", w.hc.URL())
rw.WriteHeader(http.StatusOK)
return false
}
w.Debug().Msgf("passing through to %s ...", w.hc.URL())
return true
}
// retry until the container is ready or timeout
time.Sleep(idleWakerCheckInterval)
}
}

View File

@@ -0,0 +1,90 @@
package idlewatcher
import (
"context"
"errors"
"fmt"
"net"
"time"
"github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/watcher/health"
)
// Setup implements types.Stream.
func (w *Watcher) Addr() net.Addr {
return w.stream.Addr()
}
// Setup implements types.Stream.
func (w *Watcher) Setup() error {
return w.stream.Setup()
}
// Accept implements types.Stream.
func (w *Watcher) Accept() (conn types.StreamConn, err error) {
conn, err = w.stream.Accept()
if err != nil {
return
}
if wakeErr := w.wakeFromStream(); wakeErr != nil {
w.WakeError(wakeErr)
}
return
}
// Handle implements types.Stream.
func (w *Watcher) Handle(conn types.StreamConn) error {
if err := w.wakeFromStream(); err != nil {
return err
}
return w.stream.Handle(conn)
}
// Close implements types.Stream.
func (w *Watcher) Close() error {
return w.stream.Close()
}
func (w *Watcher) wakeFromStream() error {
w.resetIdleTimer()
// pass through if container is already ready
if w.ready.Load() {
return nil
}
w.WakeDebug().Msg("wake signal received")
wakeErr := w.wakeIfStopped()
if wakeErr != nil {
wakeErr = fmt.Errorf("%s failed: %w", w.String(), wakeErr)
w.WakeError(wakeErr)
return wakeErr
}
ctx, cancel := context.WithTimeoutCause(w.task.Context(), w.WakeTimeout, errors.New("wake timeout"))
defer cancel()
for {
select {
case <-w.task.Context().Done():
cause := w.task.FinishCause()
w.WakeDebug().Str("cause", cause.Error()).Msg("canceled")
return cause
case <-ctx.Done():
cause := context.Cause(ctx)
w.WakeDebug().Str("cause", cause.Error()).Msg("timeout")
return cause
default:
}
if w.Status() == health.StatusHealthy {
w.resetIdleTimer()
w.Debug().Msg("container is ready, passing through to " + w.hc.URL().String())
return nil
}
// retry until the container is ready or timeout
time.Sleep(idleWakerCheckInterval)
}
}

View File

@@ -0,0 +1,297 @@
package idlewatcher
import (
"context"
"errors"
"sync"
"time"
"github.com/docker/docker/api/types/container"
"github.com/rs/zerolog"
D "github.com/yusing/go-proxy/internal/docker"
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/proxy/entry"
"github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils"
F "github.com/yusing/go-proxy/internal/utils/functional"
"github.com/yusing/go-proxy/internal/watcher"
"github.com/yusing/go-proxy/internal/watcher/events"
)
type (
Watcher struct {
_ U.NoCopy
zerolog.Logger
*idlewatcher.Config
*waker
client D.Client
stopByMethod StopCallback // send a docker command w.r.t. `stop_method`
ticker *time.Ticker
task task.Task
}
WakeDone <-chan error
WakeFunc func() WakeDone
StopCallback func() error
)
var (
watcherMap = F.NewMapOf[string, *Watcher]()
watcherMapMu sync.Mutex
logger = logging.With().Str("module", "idle_watcher").Logger()
)
const dockerReqTimeout = 3 * time.Second
func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker) (*Watcher, error) {
cfg := entry.IdlewatcherConfig()
if cfg.IdleTimeout == 0 {
panic("should not reach here")
}
watcherMapMu.Lock()
defer watcherMapMu.Unlock()
key := cfg.ContainerID
if w, ok := watcherMap.Load(key); ok {
w.Config = cfg
w.waker = waker
w.resetIdleTimer()
providerSubtask.Finish("used existing watcher")
return w, nil
}
client, err := D.ConnectClient(cfg.DockerHost)
if err != nil {
return nil, err
}
w := &Watcher{
Logger: logger.With().Str("name", cfg.ContainerName).Logger(),
Config: cfg,
waker: waker,
client: client,
task: providerSubtask,
ticker: time.NewTicker(cfg.IdleTimeout),
}
w.stopByMethod = w.getStopCallback()
watcherMap.Store(key, w)
go func() {
cause := w.watchUntilDestroy()
watcherMap.Delete(w.ContainerID)
w.ticker.Stop()
w.client.Close()
w.task.Finish(cause)
}()
return w, nil
}
func (w *Watcher) Wake() error {
return w.wakeIfStopped()
}
// WakeDebug logs a debug message related to waking the container.
func (w *Watcher) WakeDebug() *zerolog.Event {
return w.Debug().Str("action", "wake")
}
func (w *Watcher) WakeTrace() *zerolog.Event {
return w.Trace().Str("action", "wake")
}
func (w *Watcher) WakeError(err error) {
w.Err(err).Str("action", "wake").Msg("error")
}
func (w *Watcher) LogReason(action, reason string) {
w.Info().Str("reason", reason).Msg(action)
}
func (w *Watcher) containerStop(ctx context.Context) error {
return w.client.ContainerStop(ctx, w.ContainerID, container.StopOptions{
Signal: string(w.StopSignal),
Timeout: &w.StopTimeout,
})
}
func (w *Watcher) containerPause(ctx context.Context) error {
return w.client.ContainerPause(ctx, w.ContainerID)
}
func (w *Watcher) containerKill(ctx context.Context) error {
return w.client.ContainerKill(ctx, w.ContainerID, string(w.StopSignal))
}
func (w *Watcher) containerUnpause(ctx context.Context) error {
return w.client.ContainerUnpause(ctx, w.ContainerID)
}
func (w *Watcher) containerStart(ctx context.Context) error {
return w.client.ContainerStart(ctx, w.ContainerID, container.StartOptions{})
}
func (w *Watcher) containerStatus() (string, error) {
if !w.client.Connected() {
return "", errors.New("docker client not connected")
}
ctx, cancel := context.WithTimeoutCause(w.task.Context(), dockerReqTimeout, errors.New("docker request timeout"))
defer cancel()
json, err := w.client.ContainerInspect(ctx, w.ContainerID)
if err != nil {
return "", err
}
return json.State.Status, nil
}
func (w *Watcher) wakeIfStopped() error {
if w.ContainerRunning {
return nil
}
status, err := w.containerStatus()
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(w.task.Context(), w.WakeTimeout)
defer cancel()
// !Hard coded here since theres no constants from Docker API
switch status {
case "exited", "dead":
return w.containerStart(ctx)
case "paused":
return w.containerUnpause(ctx)
case "running":
return nil
default:
panic("should not reach here")
}
}
func (w *Watcher) getStopCallback() StopCallback {
var cb func(context.Context) error
switch w.StopMethod {
case idlewatcher.StopMethodPause:
cb = w.containerPause
case idlewatcher.StopMethodStop:
cb = w.containerStop
case idlewatcher.StopMethodKill:
cb = w.containerKill
default:
panic("should not reach here")
}
return func() error {
ctx, cancel := context.WithTimeout(w.task.Context(), time.Duration(w.StopTimeout)*time.Second)
defer cancel()
return cb(ctx)
}
}
func (w *Watcher) resetIdleTimer() {
w.Trace().Msg("reset idle timer")
w.ticker.Reset(w.IdleTimeout)
}
func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventTask task.Task, eventCh <-chan events.Event, errCh <-chan E.Error) {
eventTask = w.task.Subtask("docker event watcher")
eventCh, errCh = dockerWatcher.EventsWithOptions(eventTask.Context(), watcher.DockerListOptions{
Filters: watcher.NewDockerFilter(
watcher.DockerFilterContainer,
watcher.DockerFilterContainerNameID(w.ContainerID),
watcher.DockerFilterStart,
watcher.DockerFilterStop,
watcher.DockerFilterDie,
watcher.DockerFilterKill,
watcher.DockerFilterDestroy,
watcher.DockerFilterPause,
watcher.DockerFilterUnpause,
),
})
return
}
// watchUntilDestroy waits for the container to be created, started, or unpaused,
// and then reset the idle timer.
//
// When the container is stopped, paused,
// or killed, the idle timer is stopped and the ContainerRunning flag is set to false.
//
// When the idle timer fires, the container is stopped according to the
// stop method.
//
// it exits only if the context is canceled, the container is destroyed,
// errors occurred on docker client, or route provider died (mainly caused by config reload).
func (w *Watcher) watchUntilDestroy() (returnCause error) {
dockerWatcher := watcher.NewDockerWatcherWithClient(w.client)
eventTask, dockerEventCh, dockerEventErrCh := w.getEventCh(dockerWatcher)
defer eventTask.Finish("stopped")
for {
select {
case <-w.task.Context().Done():
return w.task.FinishCause()
case err := <-dockerEventErrCh:
if !err.Is(context.Canceled) {
E.LogError("idlewatcher error", err, &w.Logger)
}
return err
case e := <-dockerEventCh:
switch {
case e.Action == events.ActionContainerDestroy:
w.ContainerRunning = false
w.ready.Store(false)
w.LogReason("watcher stopped", "container destroyed")
return errors.New("container destroyed")
// create / start / unpause
case e.Action.IsContainerWake():
w.ContainerRunning = true
w.resetIdleTimer()
w.Info().Msg("awaken")
case e.Action.IsContainerSleep(): // stop / pause / kil
w.ContainerRunning = false
w.ready.Store(false)
w.ticker.Stop()
default:
w.Error().Msg("unexpected docker event: " + e.String())
}
// container name changed should also change the container id
if w.ContainerName != e.ActorName {
w.Debug().Msgf("renamed %s -> %s", w.ContainerName, e.ActorName)
w.ContainerName = e.ActorName
}
if w.ContainerID != e.ActorID {
w.Debug().Msgf("id changed %s -> %s", w.ContainerID, e.ActorID)
w.ContainerID = e.ActorID
// recreate event stream
eventTask.Finish("recreate event stream")
eventTask, dockerEventCh, dockerEventErrCh = w.getEventCh(dockerWatcher)
}
case <-w.ticker.C:
w.ticker.Stop()
if w.ContainerRunning {
err := w.stopByMethod()
switch {
case errors.Is(err, context.Canceled):
continue
case err != nil:
w.Err(err).Msgf("container stop with method %q failed", w.StopMethod)
default:
w.LogReason("container stopped", "idle timeout")
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
package docker
import (
"context"
"errors"
"time"
)
func Inspect(dockerHost string, containerID string) (*Container, error) {
client, err := ConnectClient(dockerHost)
defer client.Close()
if err != nil {
return nil, err
}
return client.Inspect(containerID)
}
func (c Client) Inspect(containerID string) (*Container, error) {
ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("docker container inspect timeout"))
defer cancel()
json, err := c.ContainerInspect(ctx, containerID)
if err != nil {
return nil, err
}
return FromJSON(json, c.key), nil
}

125
internal/docker/label.go Normal file
View File

@@ -0,0 +1,125 @@
package docker
import (
"reflect"
"strings"
E "github.com/yusing/go-proxy/internal/error"
U "github.com/yusing/go-proxy/internal/utils"
)
/*
Formats:
- namespace.attribute
- namespace.target.attribute
- namespace.target.attribute.namespace2.attribute
*/
type (
Label struct {
Namespace string
Target string
Attribute string
Value any
}
NestedLabelMap map[string]U.SerializedObject
)
var (
ErrApplyToNil = E.New("label value is nil")
ErrFieldNotExist = E.New("field does not exist")
)
func (l *Label) String() string {
if l.Attribute == "" {
return l.Namespace + "." + l.Target
}
return l.Namespace + "." + l.Target + "." + l.Attribute
}
// Apply applies the value of a Label to the corresponding field in the given object.
//
// Parameters:
// - obj: a pointer to the object to which the Label value will be applied.
// - l: a pointer to the Label containing the attribute and value to be applied.
//
// Returns:
// - error: an error if the field does not exist.
func ApplyLabel[T any](obj *T, l *Label) E.Error {
if obj == nil {
return ErrApplyToNil.Subject(l.String())
}
switch nestedLabel := l.Value.(type) {
case *Label:
var field reflect.Value
objType := reflect.TypeFor[T]()
for i := range reflect.TypeFor[T]().NumField() {
if objType.Field(i).Tag.Get("yaml") == l.Attribute {
field = reflect.ValueOf(obj).Elem().Field(i)
break
}
}
if !field.IsValid() {
return ErrFieldNotExist.Subject(l.Attribute).Subject(l.String())
}
dst, ok := field.Interface().(NestedLabelMap)
if !ok {
if field.Kind() == reflect.Ptr {
if field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
}
} else {
field = field.Addr()
}
err := U.Deserialize(U.SerializedObject{nestedLabel.Namespace: nestedLabel.Value}, field.Interface())
if err != nil {
return err.Subject(l.String())
}
return nil
}
if dst == nil {
field.Set(reflect.MakeMap(reflect.TypeFor[NestedLabelMap]()))
dst = field.Interface().(NestedLabelMap)
}
if dst[nestedLabel.Namespace] == nil {
dst[nestedLabel.Namespace] = make(U.SerializedObject)
}
dst[nestedLabel.Namespace][nestedLabel.Attribute] = nestedLabel.Value
return nil
default:
err := U.Deserialize(U.SerializedObject{l.Attribute: l.Value}, obj)
if err != nil {
return err.Subject(l.String())
}
return nil
}
}
func ParseLabel(label string, value string) *Label {
parts := strings.Split(label, ".")
if len(parts) < 2 {
return &Label{
Namespace: label,
Value: value,
}
}
l := &Label{
Namespace: parts[0],
Target: parts[1],
Value: value,
}
switch len(parts) {
case 2:
l.Attribute = l.Target
case 3:
l.Attribute = parts[2]
default:
l.Attribute = parts[2]
nestedLabel := ParseLabel(strings.Join(parts[3:], "."), value)
l.Value = nestedLabel
}
return l
}

View File

@@ -0,0 +1,79 @@
package docker
import (
"fmt"
"testing"
U "github.com/yusing/go-proxy/internal/utils"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
const (
mName = "middleware1"
mAttr = "prop1"
v = "value1"
)
func makeLabel(ns, name, attr string) string {
return fmt.Sprintf("%s.%s.%s", ns, name, attr)
}
func TestNestedLabel(t *testing.T) {
mAttr := "prop1"
lbl := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
sGot := ExpectType[*Label](t, lbl.Value)
ExpectFalse(t, sGot == nil)
ExpectEqual(t, sGot.Namespace, mName)
ExpectEqual(t, sGot.Attribute, mAttr)
}
func TestApplyNestedLabel(t *testing.T) {
entry := new(struct {
Middlewares NestedLabelMap `yaml:"middlewares"`
})
lbl := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
err := ApplyLabel(entry, lbl)
ExpectNoError(t, err)
middleware1, ok := entry.Middlewares[mName]
ExpectTrue(t, ok)
got := ExpectType[string](t, middleware1[mAttr])
ExpectEqual(t, got, v)
}
func TestApplyNestedLabelExisting(t *testing.T) {
checkAttr := "prop2"
checkV := "value2"
entry := new(struct {
Middlewares NestedLabelMap `yaml:"middlewares"`
})
entry.Middlewares = make(NestedLabelMap)
entry.Middlewares[mName] = make(U.SerializedObject)
entry.Middlewares[mName][checkAttr] = checkV
lbl := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
err := ApplyLabel(entry, lbl)
ExpectNoError(t, err)
middleware1, ok := entry.Middlewares[mName]
ExpectTrue(t, ok)
got := ExpectType[string](t, middleware1[mAttr])
ExpectEqual(t, got, v)
// check if prop2 is affected
ExpectFalse(t, middleware1[checkAttr] == nil)
got = ExpectType[string](t, middleware1[checkAttr])
ExpectEqual(t, got, checkV)
}
func TestApplyNestedLabelNoAttr(t *testing.T) {
entry := new(struct {
Middlewares NestedLabelMap `yaml:"middlewares"`
})
entry.Middlewares = make(NestedLabelMap)
entry.Middlewares[mName] = make(U.SerializedObject)
lbl := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s", "middlewares", mName)), v)
err := ApplyLabel(entry, lbl)
ExpectNoError(t, err)
_, ok := entry.Middlewares[mName]
ExpectTrue(t, ok)
}

16
internal/docker/labels.go Normal file
View File

@@ -0,0 +1,16 @@
package docker
const (
WildcardAlias = "*"
NSProxy = "proxy"
NSHomePage = "homepage"
LabelAliases = NSProxy + ".aliases"
LabelExclude = NSProxy + ".exclude"
LabelIdleTimeout = NSProxy + ".idle_timeout"
LabelWakeTimeout = NSProxy + ".wake_timeout"
LabelStopMethod = NSProxy + ".stop_method"
LabelStopTimeout = NSProxy + ".stop_timeout"
LabelStopSignal = NSProxy + ".stop_signal"
)

View File

@@ -0,0 +1,44 @@
package docker
import (
"context"
"errors"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
var listOptions = container.ListOptions{
// created|restarting|running|removing|paused|exited|dead
// Filters: filters.NewArgs(
// filters.Arg("status", "created"),
// filters.Arg("status", "restarting"),
// filters.Arg("status", "running"),
// filters.Arg("status", "paused"),
// filters.Arg("status", "exited"),
// ),
All: true,
}
func ListContainers(clientHost string) ([]types.Container, error) {
dockerClient, err := ConnectClient(clientHost)
if err != nil {
return nil, err
}
defer dockerClient.Close()
ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("list containers timeout"))
defer cancel()
containers, err := dockerClient.ContainerList(ctx, listOptions)
if err != nil {
return nil, err
}
return containers, nil
}
func IsErrConnectionFailed(err error) bool {
return client.IsErrConnectionFailed(err)
}

View File

@@ -0,0 +1,7 @@
package docker
import (
"github.com/yusing/go-proxy/internal/logging"
)
var logger = logging.With().Str("module", "docker").Logger()

46
internal/error/base.go Normal file
View File

@@ -0,0 +1,46 @@
package error
import (
"errors"
"fmt"
)
// baseError is an immutable wrapper around an error.
type baseError struct {
Err error `json:"err"`
}
func (err *baseError) Unwrap() error {
return err.Err
}
func (err *baseError) Is(other error) bool {
if other, ok := other.(*baseError); ok {
return errors.Is(err.Err, other.Err)
}
return errors.Is(err.Err, other)
}
func (err baseError) Subject(subject string) Error {
err.Err = PrependSubject(subject, err.Err)
return &err
}
func (err *baseError) Subjectf(format string, args ...any) Error {
if len(args) > 0 {
return err.Subject(fmt.Sprintf(format, args...))
}
return err.Subject(format)
}
func (err baseError) With(extra error) Error {
return &nestedError{&err, []error{extra}}
}
func (err baseError) Withf(format string, args ...any) Error {
return &nestedError{&err, []error{fmt.Errorf(format, args...)}}
}
func (err *baseError) Error() string {
return err.Err.Error()
}

124
internal/error/builder.go Normal file
View File

@@ -0,0 +1,124 @@
package error
import (
"fmt"
"sync"
)
type Builder struct {
about string
errs []error
sync.Mutex
}
func NewBuilder(about string) *Builder {
return &Builder{about: about}
}
func (b *Builder) About() string {
if !b.HasError() {
return ""
}
return b.about
}
//go:inline
func (b *Builder) HasError() bool {
return len(b.errs) > 0
}
func (b *Builder) error() Error {
if !b.HasError() {
return nil
}
return &nestedError{Err: New(b.about), Extras: b.errs}
}
func (b *Builder) Error() Error {
if len(b.errs) == 1 {
return From(b.errs[0])
}
return b.error()
}
func (b *Builder) String() string {
err := b.error()
if err == nil {
return ""
}
return err.Error()
}
// Add adds an error to the Builder.
//
// adding nil is no-op.
func (b *Builder) Add(err error) *Builder {
if err == nil {
return b
}
b.Lock()
defer b.Unlock()
switch err := err.(type) {
case *baseError:
b.errs = append(b.errs, err.Err)
case *nestedError:
if err.Err == nil {
b.errs = append(b.errs, err.Extras...)
} else {
b.errs = append(b.errs, err)
}
default:
b.errs = append(b.errs, err)
}
return b
}
func (b *Builder) Adds(err string) *Builder {
b.Lock()
defer b.Unlock()
b.errs = append(b.errs, newError(err))
return b
}
func (b *Builder) Addf(format string, args ...any) *Builder {
if len(args) > 0 {
b.Lock()
defer b.Unlock()
b.errs = append(b.errs, fmt.Errorf(format, args...))
} else {
b.Adds(format)
}
return b
}
func (b *Builder) AddFrom(other *Builder, flatten bool) *Builder {
if other == nil || !other.HasError() {
return b
}
b.Lock()
defer b.Unlock()
if flatten {
b.errs = append(b.errs, other.errs...)
} else {
b.errs = append(b.errs, other.error())
}
return b
}
func (b *Builder) AddRange(errs ...error) *Builder {
b.Lock()
defer b.Unlock()
for _, err := range errs {
if err != nil {
b.errs = append(b.errs, err)
}
}
return b
}

View File

@@ -0,0 +1,55 @@
package error_test
import (
"context"
"errors"
"io"
"testing"
. "github.com/yusing/go-proxy/internal/error"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
func TestBuilderEmpty(t *testing.T) {
eb := NewBuilder("foo")
ExpectTrue(t, errors.Is(eb.Error(), nil))
ExpectFalse(t, eb.HasError())
}
func TestBuilderAddNil(t *testing.T) {
eb := NewBuilder("foo")
var err Error
for range 3 {
eb.Add(nil)
}
for range 3 {
eb.Add(err)
}
eb.AddRange(nil, nil, err)
ExpectFalse(t, eb.HasError())
ExpectTrue(t, eb.Error() == nil)
}
func TestBuilderIs(t *testing.T) {
eb := NewBuilder("foo")
eb.Add(context.Canceled)
eb.Add(io.ErrShortBuffer)
ExpectTrue(t, eb.HasError())
ExpectError(t, io.ErrShortBuffer, eb.Error())
ExpectError(t, context.Canceled, eb.Error())
}
func TestBuilderNested(t *testing.T) {
eb := NewBuilder("action failed")
eb.Add(New("Action 1").Withf("Inner: 1").Withf("Inner: 2"))
eb.Add(New("Action 2").Withf("Inner: 3"))
got := eb.String()
expected := `action failed
• Action 1
• Inner: 1
• Inner: 2
• Action 2
• Inner: 3`
ExpectEqual(t, got, expected)
}

31
internal/error/error.go Normal file
View File

@@ -0,0 +1,31 @@
package error
type Error interface {
error
// Is is a wrapper for errors.Is when there is no sub-error.
//
// When there are sub-errors, they will also be checked.
Is(other error) bool
// With appends a sub-error to the error.
With(extra error) Error
// Withf is a wrapper for With(fmt.Errorf(format, args...)).
Withf(format string, args ...any) Error
// Subject prepends the given subject with a colon and space to the error message.
//
// If there is already a subject in the error message, the subject will be
// prepended to the existing subject with " > ".
//
// Subject empty string is ignored.
Subject(subject string) Error
// Subjectf is a wrapper for Subject(fmt.Sprintf(format, args...)).
Subjectf(format string, args ...any) Error
}
// this makes JSON marshaling work,
// as the builtin one doesn't.
type errStr string
func (err errStr) Error() string {
return string(err)
}

View File

@@ -0,0 +1,157 @@
package error
import (
"errors"
"strings"
"testing"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
func TestBaseString(t *testing.T) {
ExpectEqual(t, New("error").Error(), "error")
}
func TestBaseWithSubject(t *testing.T) {
err := New("error")
withSubject := err.Subject("foo")
withSubjectf := err.Subjectf("%s %s", "foo", "bar")
ExpectError(t, err, withSubject)
ExpectStrEqual(t, withSubject.Error(), "foo: error")
ExpectTrue(t, withSubject.Is(err))
ExpectError(t, err, withSubjectf)
ExpectStrEqual(t, withSubjectf.Error(), "foo bar: error")
ExpectTrue(t, withSubjectf.Is(err))
}
func TestBaseWithExtra(t *testing.T) {
err := New("error")
extra := New("bar").Subject("baz")
withExtra := err.With(extra)
ExpectTrue(t, withExtra.Is(extra))
ExpectTrue(t, withExtra.Is(err))
ExpectTrue(t, errors.Is(withExtra, extra))
ExpectTrue(t, errors.Is(withExtra, err))
ExpectTrue(t, strings.Contains(withExtra.Error(), err.Error()))
ExpectTrue(t, strings.Contains(withExtra.Error(), extra.Error()))
ExpectTrue(t, strings.Contains(withExtra.Error(), "baz"))
}
func TestBaseUnwrap(t *testing.T) {
err := errors.New("err")
wrapped := From(err)
ExpectError(t, err, errors.Unwrap(wrapped))
}
func TestNestedUnwrap(t *testing.T) {
err := errors.New("err")
err2 := New("err2")
wrapped := From(err).Subject("foo").With(err2.Subject("bar"))
unwrapper, ok := wrapped.(interface{ Unwrap() []error })
ExpectTrue(t, ok)
ExpectError(t, err, wrapped)
ExpectError(t, err2, wrapped)
ExpectEqual(t, len(unwrapper.Unwrap()), 2)
}
func TestErrorIs(t *testing.T) {
from := errors.New("error")
err := From(from)
ExpectError(t, from, err)
ExpectTrue(t, err.Is(from))
ExpectFalse(t, err.Is(New("error")))
ExpectTrue(t, errors.Is(err.Subject("foo"), from))
ExpectTrue(t, errors.Is(err.Withf("foo"), from))
ExpectTrue(t, errors.Is(err.Subject("foo").Withf("bar"), from))
}
func TestErrorImmutability(t *testing.T) {
err := New("err")
err2 := New("err2")
for range 3 {
// t.Logf("%d: %v %T %s", i, errors.Unwrap(err), err, err)
err.Subject("foo")
ExpectFalse(t, strings.Contains(err.Error(), "foo"))
err.With(err2)
ExpectFalse(t, strings.Contains(err.Error(), "extra"))
ExpectFalse(t, err.Is(err2))
err = err.Subject("bar").Withf("baz")
ExpectTrue(t, err != nil)
}
}
func TestErrorWith(t *testing.T) {
err1 := New("err1")
err2 := New("err2")
err3 := err1.With(err2)
ExpectTrue(t, err3.Is(err1))
ExpectTrue(t, err3.Is(err2))
err2.Subject("foo")
ExpectTrue(t, err3.Is(err1))
ExpectTrue(t, err3.Is(err2))
// check if err3 is affected by err2.Subject
ExpectFalse(t, strings.Contains(err3.Error(), "foo"))
}
func TestErrorStringSimple(t *testing.T) {
errFailure := New("generic failure")
ne := errFailure.Subject("foo bar")
ExpectStrEqual(t, ne.Error(), "foo bar: generic failure")
ne = ne.Subject("baz")
ExpectStrEqual(t, ne.Error(), "baz > foo bar: generic failure")
}
func TestErrorStringNested(t *testing.T) {
errFailure := New("generic failure")
inner := errFailure.Subject("inner").
Withf("1").
Withf("1")
inner2 := errFailure.Subject("inner2").
Subject("action 2").
Withf("2").
Withf("2")
inner3 := errFailure.Subject("inner3").
Subject("action 3").
Withf("3").
Withf("3")
ne := errFailure.
Subject("foo").
Withf("bar").
Withf("baz").
With(inner).
With(inner.With(inner2.With(inner3)))
want := `foo: generic failure
• bar
• baz
• inner: generic failure
• 1
• 1
• inner: generic failure
• 1
• 1
• action 2 > inner2: generic failure
• 2
• 2
• action 3 > inner3: generic failure
• 3
• 3`
ExpectStrEqual(t, ne.Error(), want)
}

43
internal/error/log.go Normal file
View File

@@ -0,0 +1,43 @@
package error
import (
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/logging"
)
func getLogger(logger ...*zerolog.Logger) *zerolog.Logger {
if len(logger) > 0 {
return logger[0]
}
return logging.GetLogger()
}
//go:inline
func LogFatal(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Fatal().Msg(err.Error())
}
//go:inline
func LogError(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Error().Msg(err.Error())
}
//go:inline
func LogWarn(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Warn().Msg(err.Error())
}
//go:inline
func LogPanic(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Panic().Msg(err.Error())
}
//go:inline
func LogInfo(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Info().Msg(err.Error())
}
//go:inline
func LogDebug(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Debug().Msg(err.Error())
}

View File

@@ -0,0 +1,120 @@
package error
import (
"errors"
"fmt"
"strings"
)
type nestedError struct {
Err error `json:"err"`
Extras []error `json:"extras"`
}
func (err nestedError) Subject(subject string) Error {
if err.Err == nil {
err.Err = newError(subject)
} else {
err.Err = PrependSubject(subject, err.Err)
}
return &err
}
func (err *nestedError) Subjectf(format string, args ...any) Error {
if len(args) > 0 {
return err.Subject(fmt.Sprintf(format, args...))
}
return err.Subject(format)
}
func (err nestedError) With(extra error) Error {
if extra != nil {
err.Extras = append(err.Extras, extra)
}
return &err
}
func (err nestedError) Withf(format string, args ...any) Error {
if len(args) > 0 {
err.Extras = append(err.Extras, fmt.Errorf(format, args...))
} else {
err.Extras = append(err.Extras, newError(format))
}
return &err
}
func (err *nestedError) Unwrap() []error {
if err.Err == nil {
if len(err.Extras) == 0 {
return nil
}
return err.Extras
}
return append([]error{err.Err}, err.Extras...)
}
func (err *nestedError) Is(other error) bool {
if errors.Is(err.Err, other) {
return true
}
for _, e := range err.Extras {
if errors.Is(e, other) {
return true
}
}
return false
}
func (err *nestedError) Error() string {
return buildError(err, 0)
}
//go:inline
func makeLine(err string, level int) string {
const bulletPrefix = "• "
const spaces = " "
if level == 0 {
return err
}
return spaces[:2*level] + bulletPrefix + err
}
func makeLines(errs []error, level int) []string {
if len(errs) == 0 {
return nil
}
lines := make([]string, 0, len(errs))
for _, err := range errs {
switch err := err.(type) {
case *nestedError:
if err.Err != nil {
lines = append(lines, makeLine(err.Err.Error(), level))
}
if extras := makeLines(err.Extras, level+1); len(extras) > 0 {
lines = append(lines, extras...)
}
default:
lines = append(lines, makeLine(err.Error(), level))
}
}
return lines
}
func buildError(err error, level int) string {
switch err := err.(type) {
case nil:
return makeLine("<nil>", level)
case *nestedError:
lines := make([]string, 0, 1+len(err.Extras))
if err.Err != nil {
lines = append(lines, makeLine(err.Err.Error(), level))
}
if extras := makeLines(err.Extras, level+1); len(extras) > 0 {
lines = append(lines, extras...)
}
return strings.Join(lines, "\n")
default:
return makeLine(err.Error(), level)
}
}

52
internal/error/subject.go Normal file
View File

@@ -0,0 +1,52 @@
package error
import (
"strings"
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
)
type withSubject struct {
Subject string `json:"subject"`
Err error `json:"err"`
}
const subjectSep = " > "
func highlight(subject string) string {
return ansi.HighlightRed + subject + ansi.Reset
}
func PrependSubject(subject string, err error) error {
switch err := err.(type) {
case nil:
return nil
case *withSubject:
return err.Prepend(subject)
case Error:
return err.Subject(subject)
default:
return &withSubject{subject, err}
}
}
func (err withSubject) Prepend(subject string) *withSubject {
if subject != "" {
err.Subject = subject + subjectSep + err.Subject
}
return &err
}
func (err *withSubject) Is(other error) bool {
return err.Err == other
}
func (err *withSubject) Unwrap() error {
return err.Err
}
func (err *withSubject) Error() string {
subjects := strings.Split(err.Subject, subjectSep)
subjects[len(subjects)-1] = highlight(subjects[len(subjects)-1])
return strings.Join(subjects, subjectSep) + ": " + err.Err.Error()
}

68
internal/error/utils.go Normal file
View File

@@ -0,0 +1,68 @@
package error
import (
"fmt"
)
func newError(message string) error {
return errStr(message)
}
func New(message string) Error {
if message == "" {
return nil
}
return &baseError{newError(message)}
}
func Errorf(format string, args ...any) Error {
return &baseError{fmt.Errorf(format, args...)}
}
func From(err error) Error {
if err == nil {
return nil
}
if err, ok := err.(Error); ok {
return err
}
return &baseError{err}
}
func Must[T any](v T, err error) T {
if err != nil {
LogPanic("must failed", err)
}
return v
}
func Join(errors ...error) Error {
n := 0
for _, err := range errors {
if err != nil {
n++
}
}
if n == 0 {
return nil
}
errs := make([]error, 0, n)
for _, err := range errors {
if err != nil {
errs = append(errs, err)
}
}
return &nestedError{Extras: errs}
}
func Collect[T any, Err error, Arg any, Func func(Arg) (T, Err)](eb *Builder, fn Func, arg Arg) T {
result, err := fn(arg)
eb.Add(err)
return result
}
func Collect2[T any, Err error, Arg1 any, Arg2 any, Func func(Arg1, Arg2) (T, Err)](eb *Builder, fn Func, arg1 Arg1, arg2 Arg2) T {
result, err := fn(arg1, arg2)
eb.Add(err)
return result
}

View File

@@ -0,0 +1,64 @@
package homepage
// PredefinedCategories by alias or docker image name
var PredefinedCategories = map[string]string{
"sonarr": "Torrenting",
"radarr": "Torrenting",
"bazarr": "Torrenting",
"lidarr": "Torrenting",
"readarr": "Torrenting",
"prowlarr": "Torrenting",
"watcharr": "Torrenting",
"qbittorrent": "Torrenting",
"qbit": "Torrenting",
"qbt": "Torrenting",
"transmission": "Torrenting",
"jellyfin": "Media",
"jellyseerr": "Media",
"emby": "Media",
"plex": "Media",
"navidrome": "Media",
"immich": "Media",
"tautulli": "Media",
"nextcloud": "Media",
"invidious": "Media",
"uptime": "Monitoring",
"uptime-kuma": "Monitoring",
"prometheus": "Monitoring",
"grafana": "Monitoring",
"netdata": "Monitoring",
"changedetection.io": "Monitoring",
"changedetection": "Monitoring",
"influxdb": "Monitoring",
"influx": "Monitoring",
"dozzle": "Monitoring",
"adguardhome": "Networking",
"adgh": "Networking",
"adg": "Networking",
"pihole": "Networking",
"flaresolverr": "Networking",
"homebridge": "Home Automation",
"home-assistant": "Home Automation",
"dockge": "Container Management",
"portainer-ce": "Container Management",
"portainer-be": "Container Management",
"rss": "RSS",
"rsshub": "RSS",
"rss-bridge": "RSS",
"miniflux": "RSS",
"freshrss": "RSS",
"paperless": "Documents",
"paperless-ngx": "Documents",
"s-pdf": "Documents",
"minio": "Storage",
"filebrowser": "Storage",
"rclone": "Storage",
}

View File

@@ -0,0 +1,43 @@
package homepage
type (
Config map[string]Category
Category []*Item
Item struct {
Show bool `json:"show" yaml:"show"`
Name string `json:"name" yaml:"name"`
Icon string `json:"icon" yaml:"icon"`
URL string `json:"url" yaml:"url"` // alias + domain
Category string `json:"category" yaml:"category"`
Description string `json:"description" yaml:"description"`
WidgetConfig map[string]any `json:"widget_config" yaml:",flow"`
SourceType string `json:"source_type" yaml:"-"`
AltURL string `json:"alt_url" yaml:"-"` // original proxy target
}
)
func (item *Item) IsEmpty() bool {
return item == nil || (item.Name == "" &&
item.Icon == "" &&
item.URL == "" &&
item.Category == "" &&
item.Description == "" &&
len(item.WidgetConfig) == 0)
}
func NewHomePageConfig() Config {
return Config(make(map[string]Category))
}
func (c *Config) Clear() {
*c = make(Config)
}
func (c Config) Add(item *Item) {
if c[item.Category] == nil {
c[item.Category] = make(Category, 0)
}
c[item.Category] = append(c[item.Category], item)
}

101
internal/list-icons.go Normal file
View File

@@ -0,0 +1,101 @@
package internal
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/yusing/go-proxy/internal/utils"
)
type GitHubContents struct { //! keep this, may reuse in future
Type string `json:"type"`
Path string `json:"path"`
Name string `json:"name"`
Sha string `json:"sha"`
Size int `json:"size"`
}
const (
iconsCachePath = "/tmp/icons_cache.json"
updateInterval = 1 * time.Hour
)
func ListAvailableIcons() ([]string, error) {
owner := "walkxcode"
repo := "dashboard-icons"
ref := "main"
var lastUpdate time.Time
icons := make([]string, 0)
info, err := os.Stat(iconsCachePath)
if err == nil {
lastUpdate = info.ModTime().Local()
}
if time.Since(lastUpdate) < updateInterval {
err := utils.LoadJSON(iconsCachePath, &icons)
if err == nil {
return icons, nil
}
}
contents, err := getRepoContents(http.DefaultClient, owner, repo, ref, "")
if err != nil {
return nil, err
}
for _, content := range contents {
if content.Type != "dir" {
icons = append(icons, content.Path)
}
}
err = utils.SaveJSON(iconsCachePath, &icons, 0o644)
if err != nil {
log.Print("error saving cache", err)
}
return icons, nil
}
func getRepoContents(client *http.Client, owner string, repo string, ref string, path string) ([]GitHubContents, error) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, path, ref), nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var contents []GitHubContents
err = json.Unmarshal(body, &contents)
if err != nil {
return nil, err
}
filesAndDirs := make([]GitHubContents, 0)
for _, content := range contents {
if content.Type == "dir" {
subContents, err := getRepoContents(client, owner, repo, ref, content.Path)
if err != nil {
return nil, err
}
filesAndDirs = append(filesAndDirs, subContents...)
} else {
filesAndDirs = append(filesAndDirs, content)
}
}
return filesAndDirs, nil
}

View File

@@ -0,0 +1,70 @@
package logging
import (
"os"
"strings"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/common"
)
var logger zerolog.Logger
func init() {
var timeFmt string
var level zerolog.Level
var exclude []string
switch {
case common.IsTrace:
timeFmt = "04:05"
level = zerolog.TraceLevel
case common.IsDebug:
timeFmt = "01-02 15:04"
level = zerolog.DebugLevel
default:
timeFmt = "01-02 15:04"
level = zerolog.InfoLevel
exclude = []string{"module"}
}
prefixLength := len(timeFmt) + 5 // level takes 3 + 2 spaces
prefix := strings.Repeat(" ", prefixLength)
logger = zerolog.New(
zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: timeFmt,
FieldsExclude: exclude,
FormatMessage: func(msgI interface{}) string { // pad spaces for each line
msg := msgI.(string)
lines := strings.Split(msg, "\n")
if len(lines) == 1 {
return msg
}
for i := 1; i < len(lines); i++ {
lines[i] = prefix + lines[i]
}
return strings.Join(lines, "\n")
},
},
).Level(level).With().Timestamp().Logger()
}
func DiscardLogger() { zerolog.SetGlobalLevel(zerolog.Disabled) }
func AddHook(h zerolog.Hook) { logger = logger.Hook(h) }
func GetLogger() *zerolog.Logger { return &logger }
func With() zerolog.Context { return logger.With() }
func WithLevel(level zerolog.Level) *zerolog.Event { return logger.WithLevel(level) }
func Info() *zerolog.Event { return logger.Info() }
func Warn() *zerolog.Event { return logger.Warn() }
func Error() *zerolog.Event { return logger.Error() }
func Err(err error) *zerolog.Event { return logger.Err(err) }
func Debug() *zerolog.Event { return logger.Debug() }
func Fatal() *zerolog.Event { return logger.Fatal() }
func Panic() *zerolog.Event { return logger.Panic() }
func Trace() *zerolog.Event { return logger.Trace() }

View File

@@ -0,0 +1,13 @@
package metrics
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func NewHandler() http.Handler {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
return mux
}

View File

@@ -0,0 +1,36 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
type (
HTTPRouteMetricLabels struct {
Service, Method, Host, Visitor, Path string
}
StreamRouteMetricLabels struct {
Service, Visitor string
}
HealthMetricLabels string
)
func (lbl *HTTPRouteMetricLabels) toPromLabels() prometheus.Labels {
return prometheus.Labels{
"service": lbl.Service,
"method": lbl.Method,
"host": lbl.Host,
"visitor": lbl.Visitor,
"path": lbl.Path,
}
}
func (lbl *StreamRouteMetricLabels) toPromLabels() prometheus.Labels {
return prometheus.Labels{
"service": lbl.Service,
"visitor": lbl.Visitor,
}
}
func (lbl HealthMetricLabels) toPromLabels() prometheus.Labels {
return prometheus.Labels{
"service": string(lbl),
}
}

View File

@@ -0,0 +1,73 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
type (
Counter struct {
mv *prometheus.CounterVec
collector prometheus.Counter
}
Gauge struct {
mv *prometheus.GaugeVec
collector prometheus.Gauge
}
Labels interface {
toPromLabels() prometheus.Labels
}
)
func NewCounter(opts prometheus.CounterOpts, labels ...string) *Counter {
m := &Counter{
mv: prometheus.NewCounterVec(opts, labels),
}
if len(labels) == 0 {
m.collector = m.mv.WithLabelValues()
m.collector.Add(0)
}
prometheus.MustRegister(m)
return m
}
func NewGauge(opts prometheus.GaugeOpts, labels ...string) *Gauge {
m := &Gauge{
mv: prometheus.NewGaugeVec(opts, labels),
}
if len(labels) == 0 {
m.collector = m.mv.WithLabelValues()
m.collector.Set(0)
}
prometheus.MustRegister(m)
return m
}
func (c *Counter) Collect(ch chan<- prometheus.Metric) {
c.mv.Collect(ch)
}
func (c *Counter) Describe(ch chan<- *prometheus.Desc) {
c.mv.Describe(ch)
}
func (c *Counter) Inc() {
c.collector.Inc()
}
func (c *Counter) With(l Labels) *Counter {
return &Counter{mv: c.mv, collector: c.mv.With(l.toPromLabels())}
}
func (g *Gauge) Collect(ch chan<- prometheus.Metric) {
g.mv.Collect(ch)
}
func (g *Gauge) Describe(ch chan<- *prometheus.Desc) {
g.mv.Describe(ch)
}
func (g *Gauge) Set(v float64) {
g.collector.Set(v)
}
func (g *Gauge) With(l Labels) *Gauge {
return &Gauge{mv: g.mv, collector: g.mv.With(l.toPromLabels())}
}

105
internal/metrics/metrics.go Normal file
View File

@@ -0,0 +1,105 @@
package metrics
import (
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/yusing/go-proxy/internal/common"
)
type (
RouteMetrics struct {
HTTPReqTotal,
HTTP2xx3xx,
HTTP4xx,
HTTP5xx *Counter
HTTPReqElapsed *Gauge
}
ServiceMetrics struct {
HealthStatus *Gauge
}
)
var (
rm RouteMetrics
sm ServiceMetrics
)
const (
routerNamespace = "router"
routerHTTPSubsystem = "http"
serviceNamespace = "service"
)
func GetRouteMetrics() *RouteMetrics {
return &rm
}
func GetServiceMetrics() *ServiceMetrics {
return &sm
}
func (rm *RouteMetrics) UnregisterService(service string) {
lbls := &HTTPRouteMetricLabels{Service: service}
prometheus.Unregister(rm.HTTP2xx3xx.With(lbls))
prometheus.Unregister(rm.HTTP4xx.With(lbls))
prometheus.Unregister(rm.HTTP5xx.With(lbls))
prometheus.Unregister(rm.HTTPReqElapsed.With(lbls))
}
func init() {
if !common.PrometheusEnabled {
return
}
initRouteMetrics()
initServiceMetrics()
}
func initRouteMetrics() {
lbls := []string{"service", "method", "host", "visitor", "path"}
partitionsHelp := ", partitioned by " + strings.Join(lbls, ", ")
rm = RouteMetrics{
HTTPReqTotal: NewCounter(prometheus.CounterOpts{
Namespace: routerNamespace,
Subsystem: routerHTTPSubsystem,
Name: "req_total",
Help: "How many requests processed" + partitionsHelp,
}),
HTTP2xx3xx: NewCounter(prometheus.CounterOpts{
Namespace: routerNamespace,
Subsystem: routerHTTPSubsystem,
Name: "req_ok_count",
Help: "How many 2xx-3xx requests processed" + partitionsHelp,
}, lbls...),
HTTP4xx: NewCounter(prometheus.CounterOpts{
Namespace: routerNamespace,
Subsystem: routerHTTPSubsystem,
Name: "req_4xx_count",
Help: "How many 4xx requests processed" + partitionsHelp,
}, lbls...),
HTTP5xx: NewCounter(prometheus.CounterOpts{
Namespace: routerNamespace,
Subsystem: routerHTTPSubsystem,
Name: "req_5xx_count",
Help: "How many 5xx requests processed" + partitionsHelp,
}, lbls...),
HTTPReqElapsed: NewGauge(prometheus.GaugeOpts{
Namespace: routerNamespace,
Subsystem: routerHTTPSubsystem,
Name: "req_elapsed_ms",
Help: "How long it took to process the request and respond a status code" + partitionsHelp,
}, lbls...),
}
}
func initServiceMetrics() {
sm = ServiceMetrics{
HealthStatus: NewGauge(prometheus.GaugeOpts{
Namespace: serviceNamespace,
Name: "health_status",
Help: "The health status of the router by service",
}, "service"),
}
}

View File

@@ -0,0 +1,26 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/yusing/go-proxy/internal/common"
)
func InitRouterMetrics(getRPsCount func() int, getStreamsCount func() int) {
if !common.PrometheusEnabled {
return
}
prometheus.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Namespace: "entrypoint",
Name: "num_reverse_proxies",
Help: "The number of reverse proxies",
}, func() float64 {
return float64(getRPsCount())
}))
prometheus.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Namespace: "entrypoint",
Name: "num_streams",
Help: "The number of streams",
}, func() float64 {
return float64(getStreamsCount())
}))
}

View File

@@ -0,0 +1,30 @@
package http
import (
"crypto/tls"
"net"
"net/http"
"time"
)
var (
defaultDialer = net.Dialer{
Timeout: 60 * time.Second,
KeepAlive: 60 * time.Second,
}
DefaultTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: defaultDialer.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConnsPerHost: 100,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
DefaultTransportNoTLS = func() *http.Transport {
clone := DefaultTransport.Clone()
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
return clone
}()
)
const StaticFilePathPrefix = "/$gperrorpage/"

View File

@@ -0,0 +1,78 @@
package http
import (
"mime"
"net/http"
)
type (
ContentType string
AcceptContentType []ContentType
)
func GetContentType(h http.Header) ContentType {
ct := h.Get("Content-Type")
if ct == "" {
return ""
}
ct, _, err := mime.ParseMediaType(ct)
if err != nil {
return ""
}
return ContentType(ct)
}
func GetAccept(h http.Header) AcceptContentType {
var accepts []ContentType
for _, v := range h["Accept"] {
ct, _, err := mime.ParseMediaType(v)
if err != nil {
continue
}
accepts = append(accepts, ContentType(ct))
}
return accepts
}
func (ct ContentType) IsHTML() bool {
return ct == "text/html" || ct == "application/xhtml+xml"
}
func (ct ContentType) IsJSON() bool {
return ct == "application/json"
}
func (ct ContentType) IsPlainText() bool {
return ct == "text/plain"
}
func (act AcceptContentType) IsEmpty() bool {
return len(act) == 0
}
func (act AcceptContentType) AcceptHTML() bool {
for _, v := range act {
if v.IsHTML() || v == "text/*" || v == "*/*" {
return true
}
}
return false
}
func (act AcceptContentType) AcceptJSON() bool {
for _, v := range act {
if v.IsJSON() || v == "*/*" {
return true
}
}
return false
}
func (act AcceptContentType) AcceptPlainText() bool {
for _, v := range act {
if v.IsPlainText() || v == "text/*" || v == "*/*" {
return true
}
}
return false
}

View File

@@ -0,0 +1,41 @@
package http
import (
"net/http"
"testing"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
func TestContentTypes(t *testing.T) {
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsHTML())
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/html; charset=utf-8"}}).IsHTML())
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"application/xhtml+xml"}}).IsHTML())
ExpectFalse(t, GetContentType(http.Header{"Content-Type": {"text/plain"}}).IsHTML())
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"application/json"}}).IsJSON())
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"application/json; charset=utf-8"}}).IsJSON())
ExpectFalse(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsJSON())
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/plain"}}).IsPlainText())
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/plain; charset=utf-8"}}).IsPlainText())
ExpectFalse(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsPlainText())
}
func TestAcceptContentTypes(t *testing.T) {
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain"}}).AcceptPlainText())
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain; charset=utf-8"}}).AcceptPlainText())
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain"}}).AcceptHTML())
ExpectTrue(t, GetAccept(http.Header{"Accept": {"application/json"}}).AcceptJSON())
ExpectTrue(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptPlainText())
ExpectTrue(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptHTML())
ExpectTrue(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptJSON())
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptPlainText())
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptHTML())
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/plain"}}).AcceptHTML())
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/plain; charset=utf-8"}}).AcceptHTML())
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/html"}}).AcceptPlainText())
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/html"}}).AcceptJSON())
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptJSON())
}

View File

@@ -0,0 +1,53 @@
package http
import (
"net/http"
)
func RemoveHop(h http.Header) {
reqUpType := UpgradeType(h)
RemoveHopByHopHeaders(h)
if reqUpType != "" {
h.Set("Connection", "Upgrade")
h.Set("Upgrade", reqUpType)
} else {
h.Del("Connection")
}
}
func CopyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func FilterHeaders(h http.Header, allowed []string) http.Header {
if len(allowed) == 0 {
return h
}
filtered := make(http.Header)
for i, header := range allowed {
values := h.Values(header)
if len(values) == 0 {
continue
}
filtered[http.CanonicalHeaderKey(allowed[i])] = append([]string(nil), values...)
}
return filtered
}
func HeaderToMap(h http.Header) map[string]string {
result := make(map[string]string)
for k, v := range h {
if len(v) > 0 {
result[k] = v[0] // Take the first value
}
}
return result
}

View File

@@ -0,0 +1,91 @@
package loadbalancer
import (
"hash/fnv"
"net"
"net/http"
"sync"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/net/http/middleware"
)
type ipHash struct {
*LoadBalancer
realIP *middleware.Middleware
pool servers
mu sync.Mutex
}
func (lb *LoadBalancer) newIPHash() impl {
impl := &ipHash{LoadBalancer: lb}
if len(lb.Options) == 0 {
return impl
}
var err E.Error
impl.realIP, err = middleware.NewRealIP(lb.Options)
if err != nil {
E.LogError("invalid real_ip options, ignoring", err, &impl.Logger)
}
return impl
}
func (impl *ipHash) OnAddServer(srv *Server) {
impl.mu.Lock()
defer impl.mu.Unlock()
for i, s := range impl.pool {
if s == srv {
return
}
if s == nil {
impl.pool[i] = srv
return
}
}
impl.pool = append(impl.pool, srv)
}
func (impl *ipHash) OnRemoveServer(srv *Server) {
impl.mu.Lock()
defer impl.mu.Unlock()
for i, s := range impl.pool {
if s == srv {
impl.pool[i] = nil
return
}
}
}
func (impl *ipHash) ServeHTTP(_ servers, rw http.ResponseWriter, r *http.Request) {
if impl.realIP != nil {
impl.realIP.ModifyRequest(impl.serveHTTP, rw, r)
} else {
impl.serveHTTP(rw, r)
}
}
func (impl *ipHash) serveHTTP(rw http.ResponseWriter, r *http.Request) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(rw, "Internal error", http.StatusInternalServerError)
impl.Err(err).Msg("invalid remote address " + r.RemoteAddr)
return
}
idx := hashIP(ip) % uint32(len(impl.pool))
srv := impl.pool[idx]
if srv == nil || srv.Status().Bad() {
http.Error(rw, "Service unavailable", http.StatusServiceUnavailable)
}
srv.ServeHTTP(rw, r)
}
func hashIP(ip string) uint32 {
h := fnv.New32a()
h.Write([]byte(ip))
return h.Sum32()
}

View File

@@ -0,0 +1,53 @@
package loadbalancer
import (
"net/http"
"sync/atomic"
F "github.com/yusing/go-proxy/internal/utils/functional"
)
type leastConn struct {
*LoadBalancer
nConn F.Map[*Server, *atomic.Int64]
}
func (lb *LoadBalancer) newLeastConn() impl {
return &leastConn{
LoadBalancer: lb,
nConn: F.NewMapOf[*Server, *atomic.Int64](),
}
}
func (impl *leastConn) OnAddServer(srv *Server) {
impl.nConn.Store(srv, new(atomic.Int64))
}
func (impl *leastConn) OnRemoveServer(srv *Server) {
impl.nConn.Delete(srv)
}
func (impl *leastConn) ServeHTTP(srvs servers, rw http.ResponseWriter, r *http.Request) {
srv := srvs[0]
minConn, ok := impl.nConn.Load(srv)
if !ok {
impl.Error().Msgf("[BUG] server %s not found", srv.Name)
http.Error(rw, "Internal error", http.StatusInternalServerError)
}
for i := 1; i < len(srvs); i++ {
nConn, ok := impl.nConn.Load(srvs[i])
if !ok {
impl.Error().Msgf("[BUG] server %s not found", srv.Name)
http.Error(rw, "Internal error", http.StatusInternalServerError)
}
if nConn.Load() < minConn.Load() {
minConn = nConn
srv = srvs[i]
}
}
minConn.Add(1)
srv.ServeHTTP(rw, r)
minConn.Add(-1)
}

Some files were not shown because too many files have changed in this diff Show More