Compare commits

...

262 Commits

Author SHA1 Message Date
WJQSERVER
5c54ae788c Merge pull request #65 from WJQSERVER-STUDIO/dev
Rewrite path matcher (v2.4.1)
2025-03-13 22:48:27 +08:00
wjqserver
bfcb1c9901 2.4.1 2025-03-13 22:41:13 +08:00
wjqserver
9bfe8517cb rewrite path matcher 2025-03-13 18:16:17 +08:00
WJQSERVER
50ba185aab Merge pull request #63 from WJQSERVER-STUDIO/dev
v2.4.0
2025-03-13 00:34:24 +08:00
wjqserver
6ee928b0c7 update readme.md 2025-03-12 23:36:50 +08:00
wjqserver
979f59545b 2.4.0 2025-03-12 23:33:17 +08:00
wjqserver
da89b3f45e 25w16d 2025-03-12 23:01:52 +08:00
wjqserver
498266e08e 25w16c 2025-03-11 18:07:17 +08:00
wjqserver
e2faa497ab update frontend 2025-03-11 10:20:43 +08:00
wjqserver
8def955151 25w16b 2025-03-11 08:40:19 +08:00
wjqserver
a18660121a 25w16a 2025-03-10 18:53:12 +08:00
wjqserver
d26f6d1e1b update deps 2025-03-09 12:23:37 +08:00
WJQSERVER
60a1f6073d Merge pull request #54 from WJQSERVER-STUDIO/dev 2025-02-28 20:06:27 +08:00
wjqserver
2cc5409dd0 2.3.1 2025-02-28 19:57:25 +08:00
wjqserver
ad9cffe9e2 25w15a 2025-02-26 16:04:08 +08:00
里見 灯花
9af5010b79 Merge pull request #52 from WJQSERVER-STUDIO/dev
2.3.0
2025-02-19 16:10:00 +08:00
wjqserver
19cd77afd2 update readme.md 2025-02-19 16:07:21 +08:00
wjqserver
91cd76e541 2.3.0 2025-02-19 13:42:11 +08:00
WJQSERVER
0faddce474 change License to WJQserver Studio License 2.0 2025-02-18 11:59:43 +08:00
WJQSERVER
f7ba0c28b4 remove docker frontend & change to internel 2025-02-18 10:38:36 +08:00
WJQSERVER
146dedea21 25w14a 2025-02-18 10:34:24 +08:00
WJQSERVER
8336896979 feat(pages): add page theme configuration and static files 2025-02-18 10:19:11 +08:00
WJQSERVER
785a74dfeb 25w14t-2 2025-02-16 19:48:53 +08:00
WJQSERVER
bd666e08d1 25w14t-1 2025-02-14 15:50:42 +08:00
WJQSERVER
459aa24f89 Merge pull request #49 from liangshengmoran/frontend-refactoring
前端页面大型变动
2025-02-14 09:31:30 +08:00
WJQSERVER
a1e8e3e373 add 404 HTTP Error Handling 2025-02-14 07:20:34 +08:00
WJQSERVER
40c9ca5f38 optimize blacklist 2025-02-14 07:19:17 +08:00
WJQSERVER
97ae0044e7 Merge pull request #50 from WJQSERVER-STUDIO/dev
2.2.0
2025-02-12 19:30:46 +08:00
WJQSERVER
55afe7676c fix drak mode 2025-02-12 19:26:10 +08:00
里見 灯花
b95582ae1a fix wrong transport 2025-02-12 19:14:13 +08:00
WJQSERVER
9aaa3e64d3 2.2.0 2025-02-12 18:45:13 +08:00
WJQSERVER
9e0f222125 25w13b 2025-02-12 18:26:58 +08:00
清韵's
0d6c1d7e35 动态适配示例链接协议 2025-02-12 09:04:26 +08:00
清韵's
5c14aeb48d Refactor the front-end page. 2025-02-11 12:06:43 +08:00
WJQSERVER
21d30dee53 update deploy bash 2025-02-10 23:25:16 +08:00
WJQSERVER
a061b8d369 fix deploy/config.toml 2025-02-10 19:10:58 +08:00
WJQSERVER
68346717a5 25w13a 2025-02-10 18:54:20 +08:00
WJQSERVER
2b7fbd2a0d update outbound 2025-02-10 18:53:16 +08:00
三千
4c5d288f03 Support using proxy dial-up connection to GitHub. (#46) 2025-02-10 00:45:37 +08:00
WJQSERVER
09163ed4df 2.1.0 (#47)
2.1.0
---
- RELEASE: v2.1.0正式版发布;
- CHANGE: 加入`FreeBSD`与`Darwin`系统支持
- CHANGE: 更新安全政策, v1和24w版本序列生命周期正式结束
- ADD: 加入`timing`中间件记录响应时间
- ADD: 加入`loggin`中间件包装日志输出
- CHANGE: 更新logger版本至v1.3.0
- CHANGE: 改进日志相关
- ADD: 加入日志等级配置项
2025-02-09 23:13:57 +08:00
WJQSERVER
f5c32915b9 2.0.7 (#45)
2.0.7
---
- RELEASE: v2.0.7正式版发布;
- CHANGE: 更新Go版本至1.23.6
- CHANGE: 更新Logger版本至v1.2.0
2025-02-05 19:17:36 +08:00
WJQSERVER
286fa0f311 2.0.6 (#42)
2.0.6
---
- RELEASE: v2.0.6正式版发布;祝各位新春快乐!
- CHANGE: 优化前端的连接转换逻辑
- CHANGE: 优化代码内不必要的函数化, 1.4之后, 函数化疑似有点太多了
- 优化`HTTP Client`参数
- CHANGE: 为api路由组增加no-cache标头
2025-01-28 23:54:51 +08:00
WJQSERVER
5d08993cbc 2.0.5 (#40)
- RELEASE: v2.0.5正式版发布;
- CHANGE: 优化响应体分块复制实现
- ADD: 加入缓存池
- CHANGE: 改进缓存实现
- CHANGE: 部分杂项改进
2025-01-27 15:00:19 +08:00
WJQSERVER
6e787ced6e 2.0.4 (#39)
- RELEASE: v2.0.4正式版发布;
- CHANGE: 优化GitReq的`HTTP Client`参数, 使其更符合本项目使用场景
- CHANGE: 优化Matches
- REMOVE: 移除Caddyfile残留
- REMOVE: 由于v2改进后稳定性增强, 故移除健康检测
2025-01-26 16:24:37 +08:00
WJQSERVER
460b7514a9 2.0.3 (#37)
* Add support for reusing the Go net/http Client.

* Enhance HTTP Client parameters.

* 25w07a

* update deps

* Optimize HTTP Client parameters.

* 25w07b

* 2.0.3

* Update README.md

---------

Co-authored-by: 里見 灯花 <172008506+satomitouka@users.noreply.github.com>
2025-01-24 19:15:52 +08:00
WJQSERVER
c90140a898 2.0.2 (#36)
* 25w06a

* update

* 25w06b

* update

* update changelog

* 2.0.2

---------

Co-authored-by: 里見 灯花 <172008506+satomitouka@users.noreply.github.com>
2025-01-21 22:49:25 +08:00
WJQSERVER
f7e4fe71d7 update 2025-01-21 14:38:22 +08:00
WJQSERVER
33973b786d update config 2025-01-20 11:33:38 +08:00
WJQSERVER
36fe815e35 v2.0.1 (#34) 2025-01-20 10:53:05 +08:00
WJQSERVER
c393191b93 v2 (#33)
- CHANGE: 优化`proxy`核心模块, 使用Chuncked Buffer传输数据, 减少内存占用
- REMOVE: caddy
- REMOVE: nocache
- CHANGE: 优化前端页面
2025-01-19 22:00:10 +08:00
WJQSERVER
beb441f0b0 update go to 1.23.5 (#31)
* update go to 1.23.5

* 1.8.3
2025-01-18 09:47:04 +08:00
WJQSERVER
c45adfb915 fix 2025-01-15 07:33:16 +08:00
WJQSERVER
102dc00b27 dev update 2025-01-15 07:22:25 +08:00
里見 灯花
b0042397c9 fix 2025-01-15 07:00:13 +08:00
里見 灯花
70b46c0fb2 1.8.2 (#30)
- RELEASE: v1.8.2正式版发布; 这或许会是v1的最后一个版本
- FIX: 修复部分日志表述错误
- CHANGE: 关闭gin框架的fmt日志打印, 在高并发场景下提升一定性能(go 打印终端日志性能较差,可能造成性能瓶颈)
2025-01-15 06:40:23 +08:00
WJQSERVER
5258046faa 25w03a 2025-01-12 17:07:34 +08:00
WJQSERVER
eddd37a59c 1.8.1 2025-01-09 16:10:48 +08:00
WJQSERVER
7a57317a8b 25w02a 2025-01-09 00:13:35 +08:00
WJQSERVER
6f95e1c182 25w02a 2025-01-09 00:13:11 +08:00
WJQSERVER
31b0b72450 update deps 2025-01-07 20:15:24 +08:00
WJQSERVER
00ae38753e 1.8.0 2025-01-05 11:59:00 +08:00
WJQSERVER
2aa665d89a update copyright info 2025-01-05 11:58:28 +08:00
WJQSERVER
17a2ba173d update deps 2025-01-05 11:57:47 +08:00
WJQSERVER
a0e5846e11 25w01e 2025-01-03 21:20:10 +08:00
WJQSERVER
972baee564 25w01d 2025-01-03 18:45:25 +08:00
WJQSERVER
a281d4c779 25w01c 2025-01-03 17:25:15 +08:00
WJQSERVER
e4252d0596 update caddy 2025-01-02 10:37:18 +08:00
WJQSERVER
de65889a4d fix 2025-01-01 08:50:28 +08:00
WJQSERVER
90b9c69dad 25w01a 2025-01-01 08:45:53 +08:00
WJQSERVER
acd38f4fe0 update authPassThrough 2025-01-01 08:45:29 +08:00
WJQSERVER
83e6b78a93 add embed.FS and debug 2025-01-01 08:45:21 +08:00
WJQSERVER
8371f9564f add .gitignore 2025-01-01 08:44:27 +08:00
WJQSERVER
546a8ca981 24w29a 2024-12-31 19:41:00 +08:00
WJQSERVER
be6314bd53 1.7.9 2024-12-31 09:21:25 +08:00
WJQSERVER
28331e9ec5 24w28b 2024-12-30 09:08:44 +08:00
WJQSERVER
51f179f9e9 (api, auth, main, proxy, rate): Replace the logger library with an external library and optimize log handling. 2024-12-30 09:07:54 +08:00
WJQSERVER
33eb0e2d34 24w28a 2024-12-29 14:44:11 +08:00
WJQSERVER
9bfca20d11 24w28a 2024-12-29 14:43:53 +08:00
dependabot[bot]
ede418420d Bump github.com/imroc/req/v3 from 3.49.0 to 3.49.1 (#26)
Bumps [github.com/imroc/req/v3](https://github.com/imroc/req) from 3.49.0 to 3.49.1.
- [Release notes](https://github.com/imroc/req/releases)
- [Commits](https://github.com/imroc/req/compare/v3.49.0...v3.49.1)

---
updated-dependencies:
- dependency-name: github.com/imroc/req/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-26 08:09:56 +08:00
WJQSERVER
243172c988 change deps check freq 2024-12-26 08:08:07 +08:00
WJQSERVER
a8d2b0700c remove upx 2024-12-20 20:57:51 +08:00
WJQSERVER
891ce86101 Update CHANGELOG.md 2024-12-20 20:52:51 +08:00
WJQSERVER
1bf4eca13d 1.7.8 2024-12-20 20:49:50 +08:00
WJQSERVER
e01028df08 24w27e 2024-12-20 20:28:20 +08:00
WJQSERVER
708f25c02b 24w27e 2024-12-20 20:28:06 +08:00
WJQSERVER
31678b243c remove upx 2024-12-19 14:38:36 +08:00
WJQSERVER
0944b81dcb 24w27d 2024-12-19 14:22:42 +08:00
WJQSERVER
723b849ee0 24w27d 2024-12-19 14:22:26 +08:00
WJQSERVER
a537c09491 update page to fix something wrong 2024-12-19 00:18:24 +08:00
WJQSERVER
6f050d38ac 24w27c 2024-12-19 00:12:38 +08:00
WJQSERVER
d8c57b7191 24w27c 2024-12-19 00:12:17 +08:00
WJQSERVER
a77f265a17 update deps 2024-12-16 17:57:13 +08:00
里見 灯花
7b9a18225a fix config.toml issue & revert wrong commit (#24)
* fix

* Update config.toml

* fix

* Update auth.go

* Update go.mod

* Update blacklist.go

* Update whitelist.go
2024-12-13 14:28:47 +08:00
里見 灯花
44105fc0cf Update CHANGELOG.md 2024-12-13 14:14:42 +08:00
里見 灯花
3d742960cc Update DEV-VERSION 2024-12-13 14:13:36 +08:00
里見 灯花
0fb7ee3679 Merge branch 'main' into main 2024-12-13 14:10:44 +08:00
dependabot[bot]
efe734d976 Bump golang.org/x/crypto from 0.29.0 to 0.31.0 in the go_modules group (#23)
Bumps the go_modules group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.29.0 to 0.31.0 - [提交](https://github.com/golang/crypto/compare/v0.29.0...v0.31.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: indirect dependency-group: go_modules ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 11:56:42 +08:00
WJQSERVER
890dc067b9 24w27a 2024-12-12 11:48:49 +08:00
WJQSERVER
a43f1f20f6 add api.github.com support 2024-12-12 11:48:23 +08:00
WJQSERVER
e59c118475 1.7.7 2024-12-08 00:12:18 +08:00
WJQSERVER
d1f862e799 24w26a 2024-12-04 22:40:15 +08:00
WJQSERVER
d5e3b7fc23 1.7.6 2024-11-30 17:49:42 +08:00
WJQSERVER
8dea98f795 1.7.6 2024-11-30 17:49:28 +08:00
WJQSERVER
b1a60017e4 24w25b 2024-11-30 16:37:35 +08:00
WJQSERVER
96bd4a2c18 24w25a-fix0 2024-11-30 16:23:33 +08:00
WJQSERVER
43c576bd82 24w25a 2024-11-29 23:29:45 +08:00
WJQSERVER
580865d082 update caddyfile 2024-11-29 23:29:15 +08:00
WJQSERVER
c2af045019 update deps 2024-11-29 23:28:59 +08:00
WJQSERVER
d297f785db H2C-fix2 2024-11-28 08:58:27 +08:00
WJQSERVER
f6f1ff7362 update README.md 2024-11-25 10:43:05 +08:00
WJQSERVER
920b025fda 1.7.5 2024-11-25 10:30:06 +08:00
WJQSERVER
59428e47aa 24w24c 2024-11-25 10:26:48 +08:00
WJQSERVER
e3f9a0d4c6 update deps 2024-11-25 10:25:12 +08:00
WJQSERVER
a14202f75d 24w24b 2024-11-25 10:08:34 +08:00
WJQSERVER
9d59731afe fix H2C 2024-11-25 10:08:10 +08:00
WJQSERVER
784407b2dd update changeLog 2024-11-24 07:49:22 +08:00
WJQSERVER
32fcc1071f 24w24a 2024-11-23 12:45:39 +08:00
WJQSERVER
32584f9516 24w24a 2024-11-23 12:25:17 +08:00
WJQSERVER
0ce5c2c11c update deps 2024-11-22 10:29:13 +08:00
WJQSERVER
1bfb2af51d Update README.md 2024-11-22 02:05:40 +08:00
WJQSERVER
6bd4a84dc1 Update README.md 2024-11-22 02:04:55 +08:00
WJQSERVER
06dc764063 1.7.4 2024-11-17 18:32:33 +08:00
WJQSERVER
c55ae4a5b7 24w23a 2024-11-15 19:04:35 +08:00
WJQSERVER
85896ffbfe Merge branch 'main' of https://github.com/WJQSERVER-STUDIO/ghproxy 2024-11-11 19:35:51 +08:00
WJQSERVER
4397bc2cf9 1.7.3 2024-11-11 19:35:39 +08:00
dependabot[bot]
3ea6d34fb2 Bump golang.org/x/time from 0.7.0 to 0.8.0 (#20)
Bumps [golang.org/x/time](https://github.com/golang/time) from 0.7.0 to 0.8.0. - [提交](https://github.com/golang/time/compare/v0.7.0...v0.8.0) --- updated-dependencies: - dependency-name: golang.org/x/time dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 19:32:32 +08:00
WJQSERVER
391c4b3594 1.7.2 2024-11-10 18:14:45 +08:00
WJQSERVER
0ab01a9b83 1.7.1 2024-11-08 12:14:04 +08:00
WJQSERVER
8787c11e06 24w22b 2024-11-07 13:52:49 +08:00
WJQSERVER
06191e52e5 Add HEAD request to fetch Content-Length 2024-11-07 13:37:40 +08:00
WJQSERVER
2113205123 Update deps 2024-11-07 13:32:41 +08:00
WJQSERVER
67a006df0f Bump Go From 1.23.2 to 1.23.3 2024-11-07 13:31:53 +08:00
WJQSERVER
45b99393e6 24w22a 2024-11-05 11:32:16 +08:00
WJQSERVER
b6c4dbbf9c 24w22a 2024-11-05 11:28:11 +08:00
WJQSERVER
d9cc088dce 24w22a 2024-11-05 09:58:10 +08:00
WJQSERVER
723ab6c3e9 1.7.0 2024-11-04 19:05:36 +08:00
WJQSERVER
11ba7cb0e5 1.7.0 2024-11-04 18:38:47 +08:00
WJQSERVER
f1ff3e9d27 1.7.0 2024-11-04 18:38:12 +08:00
WJQSERVER
b1e3a9c1bd 24w21d 2024-11-04 05:59:39 +08:00
WJQSERVER
267dfafcb9 24w21d 2024-11-04 05:53:53 +08:00
WJQSERVER
e32adadaff 24w21c-hotfix 2024-11-03 07:39:08 +08:00
WJQSERVER
942dda86e7 24w21c-hotfix 2024-11-03 07:37:49 +08:00
WJQSERVER
bbfc7c0d6d 24w21c 2024-11-03 07:33:34 +08:00
WJQSERVER
5af898b3e6 24w21c 2024-11-03 07:32:48 +08:00
WJQSERVER
3c162bf9d7 24w21b 2024-11-03 07:13:54 +08:00
WJQSERVER
1eed79463d 24w21a 2024-11-01 04:12:28 +08:00
WJQSERVER
6f67f6f5b4 24w21a 2024-11-01 03:58:47 +08:00
WJQSERVER
2a5570a447 1.6.2 2024-11-01 00:53:25 +08:00
WJQSERVER
eb45ab8798 1.6.2 2024-11-01 00:46:14 +08:00
WJQSERVER
2b7e949658 24w20b 2024-10-29 03:49:12 +08:00
WJQSERVER
fe652821bd 24w20b 2024-10-29 03:08:03 +08:00
WJQSERVER
92b3e19954 24w20a 2024-10-28 18:02:58 +08:00
WJQSERVER
19f753a515 update 2024-10-27 18:08:12 +08:00
WJQSERVER
157cbf9d3a deploy shell update 2024-10-25 18:12:25 +08:00
WJQSERVER
f80782b803 deploy shell update 2024-10-25 18:00:31 +08:00
WJQSERVER
b1c8658a1c hotfix 2024-10-25 17:01:06 +08:00
WJQSERVER
57cd7e1ce7 1.6.1 2024-10-24 22:30:41 +08:00
WJQSERVER
29046382a5 24w19d 2024-10-24 22:21:12 +08:00
WJQSERVER
2355447f97 24w19d 2024-10-24 22:10:36 +08:00
WJQSERVER
8187464007 24w19d 2024-10-24 21:50:53 +08:00
WJQSERVER
575e36ef90 24w19d 2024-10-24 21:50:38 +08:00
WJQSERVER
c31e887ad3 Merge branch 'main' of https://github.com/WJQSERVER-STUDIO/ghproxy 2024-10-24 14:44:40 +08:00
WJQSERVER
b633bec69b 24w19c 2024-10-24 14:44:38 +08:00
WJQSERVER
052827bca7 Update issue templates 2024-10-24 02:15:10 +08:00
WJQSERVER
0c1eb34e49 24w19b 2024-10-24 01:59:45 +08:00
WJQSERVER
3489e6d744 24w19b 2024-10-24 01:46:03 +08:00
WJQSERVER
a8af0fb687 24w19a 2024-10-24 01:23:01 +08:00
WJQSERVER
c0af779642 24w19a 2024-10-24 01:17:57 +08:00
WJQSERVER
d7949f625a 1.6.0 2024-10-20 20:14:39 +08:00
WJQSERVER
ddd12729a4 24w18f 2024-10-20 18:56:37 +08:00
WJQSERVER
91b7cfe533 24w18e 2024-10-20 18:16:30 +08:00
WJQSERVER
aee81ba4fd 24w18e 2024-10-20 17:48:31 +08:00
WJQSERVER
e5d941414e 24w18e 2024-10-20 17:39:27 +08:00
WJQSERVER
c50f23c399 24w18d 2024-10-19 16:05:18 +08:00
WJQSERVER
47d062a1c4 hotfix 2024-10-17 18:01:15 +08:00
WJQSERVER
57ba06e01e 24w18c 2024-10-17 17:45:29 +08:00
WJQSERVER
52fdaf5f81 24w18b 2024-10-17 10:43:36 +08:00
WJQSERVER
d6b8f2b812 hotfix 2024-10-17 09:57:14 +08:00
WJQSERVER
00d2113904 hotfix 2024-10-17 00:04:29 +08:00
WJQSERVER
670bca31ca 24w18a 2024-10-16 23:52:07 +08:00
WJQSERVER
f77de0d37a 24w18a 2024-10-16 23:32:25 +08:00
WJQSERVER
1c18ccc363 24w18a 2024-10-16 21:51:24 +08:00
WJQSERVER
a386304d42 update 2024-10-16 17:45:51 +08:00
WJQSERVER
a1fdd0f477 1.5.2 2024-10-16 15:19:10 +08:00
WJQSERVER
e94a98668e 24w17b 2024-10-16 13:08:58 +08:00
WJQSERVER
22273f166c 24w17b 2024-10-16 12:54:21 +08:00
WJQSERVER
2187514da7 24w17b 2024-10-16 12:48:02 +08:00
WJQSERVER
ac859c39a6 24w17a 2024-10-16 06:31:36 +08:00
WJQSERVER
518bbf2600 24w17a 2024-10-16 06:27:28 +08:00
WJQSERVER
d383c2d2a6 24w17a 2024-10-16 06:25:59 +08:00
WJQSERVER
6d055e19e3 1.5.1 2024-10-14 16:16:56 +08:00
WJQSERVER
18f78385f3 1.5.1 2024-10-14 16:16:04 +08:00
WJQSERVER
2edb6f6dd7 Merge branch 'main' of https://github.com/WJQSERVER-STUDIO/ghproxy 2024-10-14 13:49:33 +08:00
WJQSERVER
d42ed119d4 24w16a 2024-10-14 13:49:30 +08:00
dependabot[bot]
072f73fdce Bump github.com/imroc/req/v3 from 3.47.0 to 3.48.0 (#6)
Bumps [github.com/imroc/req/v3](https://github.com/imroc/req) from 3.47.0 to 3.48.0. - [Release notes](https://github.com/imroc/req/releases) - [提交](https://github.com/imroc/req/compare/v3.47.0...v3.48.0) --- updated-dependencies: - dependency-name: github.com/imroc/req/v3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-14 13:38:56 +08:00
WJQSERVER
02b4857334 Merge branch 'main' of https://github.com/WJQSERVER-STUDIO/ghproxy 2024-10-13 09:41:20 +08:00
WJQSERVER
a156f1b2e7 update 2024-10-13 09:41:18 +08:00
里見 灯花
1d8c3914d2 Update README.md 2024-10-13 08:50:57 +08:00
WJQSERVER
b05dd37500 1.5.0 2024-10-12 10:20:43 +08:00
WJQSERVER
7fabd3c3e2 1.5.0 2024-10-12 10:20:20 +08:00
WJQSERVER
6dc20398ae 24w15d 2024-10-12 07:03:23 +08:00
WJQSERVER
03118a24a3 24w15d 2024-10-12 06:32:16 +08:00
WJQSERVER
0cb7c9f948 24w15c 2024-10-12 04:30:09 +08:00
WJQSERVER
824656f9d0 24w15c 2024-10-12 03:50:34 +08:00
WJQSERVER
e3d56ae9b7 24w15b 2024-10-11 12:06:16 +08:00
WJQSERVER
794ba22232 24w15b 2024-10-11 11:59:15 +08:00
WJQSERVER
f71c95e381 24w15b 2024-10-11 11:45:19 +08:00
WJQSERVER
997860f3ef 24w15a 2024-10-11 11:38:17 +08:00
WJQSERVER
81c35030e6 24w15a 2024-10-11 10:57:04 +08:00
WJQSERVER
80d2bc0068 fix1 2024-10-11 10:51:15 +08:00
WJQSERVER
4173617e72 fix 2024-10-11 10:48:33 +08:00
WJQSERVER
6a1a1b3c8b 24w15a 2024-10-11 10:45:15 +08:00
WJQSERVER
a21d5659ac 24w15a 2024-10-11 10:39:03 +08:00
WJQSERVER
a0de2f627d 24w15a 2024-10-11 10:34:42 +08:00
WJQSERVER
b47638d066 24w15a 2024-10-11 10:29:43 +08:00
WJQSERVER
69d4d53a51 24w15a 2024-10-11 10:25:33 +08:00
WJQSERVER
6864925dbe 1.4.3 2024-10-11 08:04:39 +08:00
WJQSERVER
2bdc2d0ace 1.4.3 2024-10-11 08:04:08 +08:00
WJQSERVER
1e496fb09e 24w14a 2024-10-11 02:54:16 +08:00
里見 灯花
23bb463490 Update build-dev.yml 2024-10-11 01:31:14 +08:00
里見 灯花
8eb6c09562 Update build-dev.yml 2024-10-11 01:20:16 +08:00
里見 灯花
1277af445c Update build-dev.yml 2024-10-11 01:17:59 +08:00
里見 灯花
62ee8ede1a 24w14a 2024-10-11 00:23:38 +08:00
里見 灯花
87b62e0342 24w14a 2024-10-11 00:21:34 +08:00
里見 灯花
245040a497 24w14a 2024-10-11 00:21:06 +08:00
里見 灯花
1b5b34d265 Update build-dev.yml 2024-10-11 00:14:39 +08:00
WJQSERVER
96fba543a2 1.4.2 2024-10-10 23:00:52 +08:00
WJQSERVER
38430a3a68 1.4.2 2024-10-10 22:55:22 +08:00
WJQSERVER
71c5f25d1d update 2024-10-10 22:38:41 +08:00
WJQSERVER
2a53ac1b72 24w13c 2024-10-10 20:51:54 +08:00
里見 灯花
505c2e559e Merge pull request #7 from WJQSERVER-STUDIO/main
[pull] main from WJQSERVER-STUDIO:main
2024-10-10 20:09:50 +08:00
里見 灯花
dd9a0c8adb Update proxy.go 2024-10-10 20:09:16 +08:00
里見 灯花
f4cb77a72e Update whitelist.go 2024-10-10 20:08:33 +08:00
里見 灯花
4155b9cf4f Update blacklist.go 2024-10-10 20:08:21 +08:00
里見 灯花
00538f3d91 Update auth.go 2024-10-10 20:08:02 +08:00
里見 灯花
1fd1e3bc2a Update main.go 2024-10-10 20:07:44 +08:00
里見 灯花
b83c242416 Update go.mod 2024-10-10 20:06:08 +08:00
WJQSERVER
57146483ec 1.4.2 2024-10-10 09:58:33 +08:00
WJQSERVER
03c33449db 1.4.2 2024-10-10 07:56:42 +08:00
WJQSERVER
6b9b864a4f update 2024-10-10 07:40:21 +08:00
WJQSERVER
db7f8f4bcb 24w13b 2024-10-10 03:22:10 +08:00
WJQSERVER
bfcb835608 24w13b 2024-10-10 03:14:13 +08:00
WJQSERVER
1eeeb56e9b 24w13b 2024-10-10 03:09:01 +08:00
WJQSERVER
d0b20a8828 24w13b 2024-10-10 03:05:48 +08:00
WJQSERVER
b57aa84bda 24w13b 2024-10-10 03:03:13 +08:00
WJQSERVER
4eb9d1a899 24w13a 2024-10-09 22:29:24 +08:00
WJQSERVER
d3d9f78820 1.4.1 2024-10-09 20:53:50 +08:00
WJQSERVER
f3a49b83f2 24w12c 2024-10-09 20:31:02 +08:00
WJQSERVER
fd7491aaa7 24w12c 2024-10-09 20:26:48 +08:00
WJQSERVER
b11a864495 24w12c 2024-10-09 20:14:44 +08:00
WJQSERVER
564364db74 fix 2024-10-09 01:06:13 +08:00
WJQSERVER
a1f0eaafa6 24w12b 2024-10-09 00:59:58 +08:00
WJQSERVER
2bbb539dda 24w12b 2024-10-09 00:59:14 +08:00
WJQSERVER
7ba2b94a00 24w12a 2024-10-09 00:41:45 +08:00
WJQSERVER
5e764a6cd7 24w12a 2024-10-08 21:15:03 +08:00
鬼鬼
abeb6ee8eb Fixed link format error in README.md (#3) 2024-10-08 20:09:34 +08:00
WJQSERVER
df93a90081 1.4.0 2024-10-08 20:04:07 +08:00
WJQSERVER
05032158d4 24w11b 2024-10-08 06:20:35 +08:00
WJQSERVER
b5bfc809a2 24w11a 2024-10-08 05:33:41 +08:00
WJQSERVER
dc8f4a28e9 24w11a 2024-10-08 05:28:56 +08:00
WJQSERVER
f5d7d0994e 24w10a 2024-10-06 19:38:53 +08:00
WJQSERVER
e3cffe70d9 24w10a 2024-10-06 19:33:20 +08:00
67 changed files with 5277 additions and 1292 deletions

40
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,40 @@
---
name: Bug report
about: 报告问题与漏洞
title: "[BUG]"
labels: bug
assignees: ''
---
### 问题描述
请简要描述发现的问题是什么,以及如何重现。
### 复现步骤
1. 打开...
2. 点击...
3. 观察到...
### 预期行为
请描述在正常情况下应该发生什么。
### 实际行为
请描述实际发生了什么。
### 截图
如果适用,请添加截图以帮助解释您的问题。
### 环境信息
- 发行版: [例如 Debian12, Alpine-Edge ]
- 部署方式: [可执行文件/Docker ]
- GHProxy版本: [例如 1.0.0]
### 附加信息
请提供任何其他可能有助于我们解决问题的信息。

View File

@@ -0,0 +1,20 @@
---
name: Features request
about: 提出新功能建议
title: "[Features]"
labels: enhancement
assignees: ''
---
### 功能描述
请简要描述您希望增加的功能。
### 功能原因
请说明您为什么需要这个功能。
### 功能实现
请详细描述您期望的功能实现。

View File

@@ -8,4 +8,4 @@ updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
interval: "daily"

View File

@@ -4,54 +4,96 @@ on:
workflow_dispatch:
push:
branches:
- 'main'
- 'dev'
paths:
- 'DEV-VERSION'
jobs:
build:
prepare:
runs-on: ubuntu-latest
env:
OUTPUT_BINARY: ghproxy
OUTPUT_ARCHIVE: ghproxy.tar.gz
GO_VERSION: 1.23.2
steps:
- uses: actions/checkout@v3
- name: Load VERSION
- name: 加载版本号
run: |
if [ -f DEV-VERSION ]; then
echo "VERSION=$(cat DEV-VERSION)" >> $GITHUB_ENV
else
echo "DEV-VERSION file not found!" && exit 1
fi
- name: Set up Go
- name: 输出版本号
run: |
echo "Version: ${{ env.VERSION }}"
- name: 预先创建Pre-release
id: create_release
uses: ncipollo/release-action@v1
with:
name: ${{ env.VERSION }}
artifacts: ./DEV-VERSION
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.VERSION }}
allowUpdates: true
prerelease: true
body: ${{ env.VERSION }}
env:
export PATH: $PATH:/usr/local/go/bin
build:
runs-on: ubuntu-latest
needs: prepare
strategy:
matrix:
goos: [linux, darwin, freebsd]
goarch: [amd64, arm64]
env:
OUTPUT_BINARY: ghproxy
GO_VERSION: 1.24
steps:
- uses: actions/checkout@v4
with:
ref: dev
- name: 加载版本号
run: |
if [ -f DEV-VERSION ]; then
echo "VERSION=$(cat DEV-VERSION)" >> $GITHUB_ENV
else
echo "DEV-VERSION file not found!" && exit 1
fi
- name: 安装 Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Build
- name: 编译
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
go build -o ${{ env.OUTPUT_BINARY }} ./main.go
- name: Package
CGO_ENABLED=0 go build -ldflags "-X main.version=${{ env.VERSION }} -X main.dev=true" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go
- name: 打包
run: |
tar -czvf ${{ env.OUTPUT_ARCHIVE }} ./${{ env.OUTPUT_BINARY }}
- name: Upload to GitHub Artifacts
uses: actions/upload-artifact@v3
mkdir ghproxyd
cp ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./ghproxyd/
mv ./ghproxyd/${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./ghproxyd/${{ env.OUTPUT_BINARY }}
cp LICENSE ./ghproxyd/
tar -czf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz -C ghproxyd .
ls
- name: 上传Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_BINARY }}
name: ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
path: |
./${{ env.OUTPUT_ARCHIVE }}
./${{ env.OUTPUT_BINARY }}
./${{ env.OUTPUT_BINARY }}*
- name: 上传至Release
id: create_release
uses: ncipollo/release-action@v1
with:
name: ${{ env.VERSION }}
artifacts: ./${{ env.OUTPUT_ARCHIVE }}, ./${{ env.OUTPUT_BINARY }}
artifacts: ./${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.VERSION }}
allowUpdates: true
prerelease: true
body: ${{ env.VERSION }}
env:
export PATH: $PATH:/usr/local/go/bin
@@ -61,10 +103,13 @@ jobs:
env:
IMAGE_NAME: wjqserver/ghproxy
DOCKERFILE: docker/dockerfile/dev/Dockerfile
DOCKERFILE_PATH: docker/dockerfile/dev
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: dev
- name: Load VERSION
run: |
if [ -f DEV-VERSION ]; then
@@ -86,10 +131,10 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 构建镜像
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
file: ./${{ env.DOCKERFILE }}
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ env.VERSION }}

View File

@@ -9,48 +9,86 @@ on:
- 'VERSION'
jobs:
build:
prepare:
runs-on: ubuntu-latest
env:
OUTPUT_BINARY: ghproxy
OUTPUT_ARCHIVE: ghproxy.tar.gz
GO_VERSION: 1.23.2
steps:
- uses: actions/checkout@v3
- name: Load VERSION
- name: 加载版本号
run: |
if [ -f VERSION ]; then
echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
else
echo "VERSION file not found!" && exit 1
fi
- name: Set up Go
- name: 输出版本号
run: |
echo "Version: ${{ env.VERSION }}"
- name: 预先创建release
id: create_release
uses: ncipollo/release-action@v1
with:
name: ${{ env.VERSION }}
artifacts: ./VERSION
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.VERSION }}
allowUpdates: true
body: ${{ env.VERSION }}
env:
export PATH: $PATH:/usr/local/go/bin
build:
runs-on: ubuntu-latest
needs: prepare # 确保这个作业在 prepare 作业完成后运行
strategy:
matrix:
goos: [linux, darwin, freebsd]
goarch: [amd64, arm64]
env:
OUTPUT_BINARY: ghproxy
GO_VERSION: 1.24
steps:
- uses: actions/checkout@v3
- name: 加载版本号
run: |
if [ -f VERSION ]; then
echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
else
echo "VERSION file not found!" && exit 1
fi
- name: 安装 Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Build
- name: 编译
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
go build -o ${{ env.OUTPUT_BINARY }} ./main.go
- name: Package
CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${{ env.VERSION }}" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go
- name: 打包
run: |
tar -czvf ${{ env.OUTPUT_ARCHIVE }} ./${{ env.OUTPUT_BINARY }}
- name: Upload to GitHub Artifacts
uses: actions/upload-artifact@v3
mkdir ghproxyd
cp ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./ghproxyd/
mv ./ghproxyd/${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./ghproxyd/${{ env.OUTPUT_BINARY }}
cp LICENSE ./ghproxyd/
tar -czf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz -C ghproxyd .
- name: 上传Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_BINARY }}
name: ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
path: |
./${{ env.OUTPUT_ARCHIVE }}
./${{ env.OUTPUT_BINARY }}
./${{ env.OUTPUT_BINARY }}*
- name: 上传至Release
id: create_release
uses: ncipollo/release-action@v1
with:
name: ${{ env.VERSION }}
artifacts: ./${{ env.OUTPUT_ARCHIVE }}, ./${{ env.OUTPUT_BINARY }}
artifacts: ./${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.VERSION }}
allowUpdates: true
body: ${{ env.VERSION }}
env:
export PATH: $PATH:/usr/local/go/bin
@@ -59,20 +97,19 @@ jobs:
needs: build # 确保这个作业在 build 作业完成后运行
env:
IMAGE_NAME: wjqserver/ghproxy # 定义镜像名称变量
DOCKERFILE: docker/dockerfile/release/Dockerfile
DOCKERFILE: docker/dockerfile/release/Dockerfile # 定义 Dockerfile 路径变量
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Load VERSION
run: |
if [ -f DEV-VERSION ]; then
echo "VERSION=$(cat DEV-VERSION)" >> $GITHUB_ENV
if [ -f VERSION ]; then
echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
else
echo "DEV-VERSION file not found!" && exit 1
echo "VERSION file not found!" && exit 1
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -86,10 +123,10 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 构建镜像
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
file: ./${{ env.DOCKERFILE }}
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ env.VERSION }}

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
demo
demo.toml
*.log
*.bak
list.json
repos

View File

@@ -1,5 +1,848 @@
# 更新日志
2.4.1 - 2025-03-12
---
- CHANGE: 重构路由匹配
- CHANGE: 更新相关依赖以修复错误
25w17a - 2025-03-12
---
- PRE-RELEASE: 此版本是v2.4.1的预发布版本,请勿在生产环境中使用;
- CHANGE: 重构路由匹配
- CHANGE: 更新相关依赖以修复错误
2.4.0 - 2025-03-12
---
- ADD: 支持通过[Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git)实现Git Clone缓存
- CHANGE: 使用更高性能的Buffer Pool 实现, 调用 github.com/WJQSERVER-STUDIO/go-utils/copyb
- CHANGE: 改进路由匹配
- CHANGE: 更新依赖
- CHANGE: 改进前端
25w16d - 2025-03-12
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 使用更高性能的Buffer Pool 实现
25w16c
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 使用更高性能的Buffer Pool 实现
- CHANGE: 改进路由匹配
25w16b
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 修改路由
- CHANGE: 改进前端
25w16a
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 变更CORS配置
- ADD: 使用GO-GIT实现git smart http服务端和客户端
- CHANGE: 更新依赖
2.3.1
---
- CHANGE: 改进`Pages``External`模式下的路由
- CHANGE: 使用`H2C` bool 代替 `enableH2C` string (2.4.0 弃用 `enableH2C`)
- CHANGE: 使用`Mode` string 代替`Pages`内的 `enable` bool (2.4.0 弃用 `enable`)
25w15a
---
- PRE-RELEASE: 此版本是v2.3.1的预发布版本,请勿在生产环境中使用;
- CHANGE: 改进`Pages``External`模式下的路由
- CHANGE: 使用`H2C` bool 代替 `enableH2C` string (2.4.0 弃用 `enableH2C`)
- CHANGE: 使用`Mode` string 代替`Pages`内的 `enable` bool (2.4.0 弃用 `enable`)
2.3.0
---
- CHANGE: 使用`touka-httpc`封装`HTTP Client`, 更新到`v0.2.0`版本, 参看`touka-httpc`
- CHANGE: 重构前端页面, 见[#49](https://github.com/WJQSERVER-STUDIO/ghproxy/pull/49)
- CHANGE: 重构`blacklist`实现
- CHANGE: 优化404处理
- CHANGE: 重构`whitelist`实现
- CHANGE: 对`proxy`进行结构性调整
- CHANGE: `chunckedreq``gitreq`共用`BufferPool``HTTP Client`
- CHANGE: 新增`HTTP Client`配置块
- CHANGE: 加入内置主题配置, 支持通过配置切换主题
- CHANGE: 将许可证转为WJQserver Studio License 2.0
25w14b
---
- PRE-RELEASE: 此版本是v2.3.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 将许可证转为WJQserver Studio License 2.0
25w14a
---
- PRE-RELEASE: 此版本是v2.3.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 使用`touka-httpc`封装`HTTP Client`, 更新到`v0.2.0`版本, 参看`touka-httpc`
- CHANGE: 重构前端页面, 见[#49](https://github.com/WJQSERVER-STUDIO/ghproxy/pull/49)
- CHANGE: 重构`blacklist`实现
- CHANGE: 优化404处理
- CHANGE: 重构`whitelist`实现
- CHANGE: 对`proxy`进行结构性调整
- CHANGE: `chunckedreq``gitreq`共用`BufferPool``HTTP Client`
- CHANGE: 新增`HTTP Client`配置块
- CHANGE: 加入内置主题配置, 支持通过配置切换主题
25w14t-2
---
- PRE-RELEASE: 此版本是测试验证版本,请勿在生产环境中使用;
- CHANGE: 使用`touka-httpc`封装`HTTP Client`,更新到`v0.1.0`版本, 参看`touka-httpc`
- CHANGE: 重构`whitelist`实现
- CHANGE: 对`proxy`进行结构性调整
- CHANGE: `chunckedreq``gitreq`共用`BufferPool``HTTP Client`
- CHANGE: 新增`HTTP Client`配置块
25w14t-1
---
- PRE-RELEASE: 此版本是测试验证版本,请勿在生产环境中使用;
- CHANGE: 使用`touka-httpc`封装`HTTP Client`
- CHANGE: 重构前端页面, 见[#49](https://github.com/WJQSERVER-STUDIO/ghproxy/pull/49)
- CHANGE: 重构`blacklist`实现
- CHANGE: 优化404处理
2.2.0
---
- RELEASE: v2.2.0正式版发布;
- CHANGE: 更新Go版本至1.24.0
- ADD: 加入`Socks5``HTTP(S)`出站支持
- CHANGE: 配置新增`Outbound`配置块
25w13b
---
- PRE-RELEASE: 此版本是v2.2.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 更新Go版本至1.24.0
25w13a
---
- PRE-RELEASE: 此版本是v2.2.0的预发布版本,请勿在生产环境中使用;
- ADD: 加入`Socks5``HTTP(S)`出站支持
- CHANGE: 配置新增`Outbound`配置块
2.1.0
---
- RELEASE: v2.1.0正式版发布;
- CHANGE: 加入`FreeBSD``Darwin`系统支持
- CHANGE: 更新安全政策, v1和24w版本序列生命周期正式结束
- ADD: 加入`timing`中间件记录响应时间
- ADD: 加入`loggin`中间件包装日志输出
- CHANGE: 更新logger版本至v1.3.0
- CHANGE: 改进日志相关
- ADD: 加入日志等级配置项
25w12d
---
- PRE-RELEASE: 此版本是v2.1.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 处理类型断言相关问题
25w12c
---
- PRE-RELEASE: 此版本是v2.1.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 加入`FreeBSD``Darwin`系统支持
25w12b
---
- PRE-RELEASE: 此版本是v2.0.8/v2.1.0的预发布版本,请勿在生产环境中使用;
- ADD: 加入`timing`中间件记录响应时间
- ADD: 加入`loggin`中间件包装日志输出
- CHANGE: 更新安全政策, v1和24w版本序列生命周期正式结束
25w12a
---
- PRE-RELEASE: 此版本是v2.0.8/v2.1.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 更新logger版本至v1.3.0
- CHANGE: 改进日志相关
- ADD: 加入日志等级配置项
2.0.7
---
- RELEASE: v2.0.7正式版发布;
- CHANGE: 更新Go版本至1.23.6
- CHANGE: 更新Logger版本至v1.2.0
25w11a
---
- PRE-RELEASE: 此版本是v2.0.7的预发布版本,请勿在生产环境中使用;
- CHANGE: 更新Go版本至1.23.6
- CHANGE: 更新Logger版本至v1.2.0
2.0.6
---
- RELEASE: v2.0.6正式版发布;祝各位新春快乐!
- CHANGE: 优化前端的连接转换逻辑
- CHANGE: 优化代码内不必要的函数化, 1.4之后, 函数化疑似有点太多了
- 优化`HTTP Client`参数
- CHANGE: 为api路由组增加no-cache标头
25w10b
---
- PRE-RELEASE: 此版本是v2.0.6的预发布版本,请勿在生产环境中使用;祝各位新春快乐!
- CHANGE: 为api路由组增加no-cache标头
25w10a
---
- PRE-RELEASE: 此版本是v2.0.6的预发布版本,请勿在生产环境中使用;祝各位新春快乐!
- CHANGE: 改进前端的连接转换逻辑
- CHANGE: 优化代码内不必要的函数化, 1.4之后, 函数化疑似有点太多了
- 优化`HTTP Client`参数
2.0.5
---
- RELEASE: v2.0.5正式版发布;
- CHANGE: 优化响应体分块复制实现
- ADD: 加入缓存池
- CHANGE: 改进缓存实现
- CHANGE: 部分杂项改进
25w09b
---
- PRE-RELEASE: 此版本是v2.0.5的预发布版本,请勿在生产环境中使用;
- REMOVE: 移除残留配置
25w09a
---
- PRE-RELEASE: 此版本是v2.0.5的预发布版本,请勿在生产环境中使用;
- CHANGE: 改进缓存实现
- ADD: 加入缓存池
2.0.4
---
- RELEASE: v2.0.4正式版发布;
- CHANGE: 优化GitReq的`HTTP Client`参数, 使其更符合本项目使用场景
- CHANGE: 优化Matches
- REMOVE: 移除Caddyfile残留
- REMOVE: 由于v2改进后稳定性增强, 故移除健康检测
25w08b
---
- PRE-RELEASE: 此版本是v2.0.4的预发布版本,请勿在生产环境中使用;
- REMOVE: 由于v2改进后稳定性增强, 故移除健康检测
25w08a
---
- PRE-RELEASE: 此版本是v2.0.4的预发布版本,请勿在生产环境中使用;
- CHANGE: 优化GitReq的`HTTP Client`参数, 使其更符合本项目使用场景
- CHANGE: 优化Matches
- REMOVE: 移除Caddyfile残留
2.0.3
---
- RELEASE: v2.0.3正式版发布;
- CHANGE: 优化`HTTP Client`参数, 使其更符合本项目使用场景
25w07b
---
- PRE-RELEASE: 此版本是v2.0.3的预发布版本,请勿在生产环境中使用;
- CHANGE: 改进`HTTP Client`参数
25w07a
---
- PRE-RELEASE: 此版本是v2.0.3的预发布版本,请勿在生产环境中使用;
- CHANGE: 为`HTTP Client`增加复用, 对性能有所优化
- CHANGE: 优化`HTTP Client`参数, 使其更符合本项目使用场景
2.0.2
---
- RELEASE: v2.0.2正式版发布; 此版本是v2.0.1改进
- CHANGE: 由于用户使用了不符合`RFC 9113`规范的请求头, 导致`ghproxy`无法正常工作, 在此版本为用户的错误行为提供补丁;
25w06b
---
- PRE-RELEASE: 此版本是改进验证版本,普通用户请勿使用;
- CHANGE: 由于用户使用了不符合`RFC 9113`规范的请求头, 导致`ghproxy`无法正常工作, 在此版本为用户的错误行为提供补丁;
25w06a
---
- PRE-RELEASE: 此版本是改进验证版本,普通用户请勿使用;
- CHANGE: Remove `Conection: Upgrade` header, which is not currently supported by some web server configurations.
v2.0.1
---
- RELEASE: v2.0.1正式版发布; 此版本是v2.0.0的小修复版本, 主要修复了Docker启动脚本存在的一些问题
- FIX: 修复Docker启动脚本存在的一些问题
25w05a
---
- PRE-RELEASE: 此版本是v2.0.1的候选版本,请勿在生产环境中使用;
- FIX: 修复Docker启动脚本存在的一些问题
2.0.0
---
- RELEASE: v2.0.0正式版发布; 此版本圆了几个月前画的饼, 在大文件下载的内存占用方面做出了巨大改进
- CHANGE: 优化`proxy`核心模块, 使用Chuncked Buffer传输数据, 减少内存占用
- REMOVE: caddy
- REMOVE: nocache
- CHANGE: 优化前端页面, 增加更多功能(来自1.8.1版本, 原本也是为v2所设计的)
25w04c
---
- PRE-RELEASE: 此版本是v2的候选版本,请勿在生产环境中使用;
- CHANGE: 大幅优化`proxy`核心模块, 使用Chuncked Buffer传输数据, 减少内存占用
v1.8.3
---
- RELEASE: v1.8.3, 此版本作为v1.8.2的依赖更新版本(在v2发布前, v1仍会进行漏洞修复)
- CHANGE: 更新Go版本至`1.23.5`以解决CVE漏洞
25w04b
---
- PRE-RELEASE: 此版本是v2的候选版本(技术验证版),请勿在生产环境中使用; 我们可能会撤除v2更新计划(若技术验证版顺利通过, 则会发布v2正式版)
- REMOVE: caddy
25w04a
---
- PRE-RELEASE: 此版本是v2的候选版本(技术验证版),请勿在生产环境中使用; 我们可能会撤除v2更新计划(若技术验证版顺利通过, 则会发布v2正式版)
- CHANGE: 大幅修改核心组件
1.8.2
---
- RELEASE: v1.8.2正式版发布; 这或许会是v1的最后一个版本
- FIX: 修复部分日志表述错误
- CHANGE: 关闭`gin`框架的`fmt`日志打印, 在高并发场景下提升一定性能(go 打印终端日志性能较差,可能造成性能瓶颈)
25w03a
---
- PRE-RELEASE: 此版本是v1.8.2的候选预发布版本,请勿在生产环境中使用
- FIX: 修复部分日志表述错误
- CHANGE: 关闭`gin`框架的`fmt`日志打印, 在高并发场景下提升一定性能(go 打印终端日志性能较差,可能造成性能瓶颈)
1.8.1
---
- RELEASE: v1.8.1正式版发布; 此版本发布较为仓促, 用于修复caddy2.9.0导致的问题
- CHANGE: 更新底包至`v2.9.1`
- FIX: 修复caddy2.9.0导致的问题
- CHANGE: 对前端进行重构(非最终决定,各位可将其与原先的版本对比, 若有相关建议, 请与开发团队进行交流)
25w02a
---
- PRE-RELEASE: 此版本是v1.8.1的候选预发布版本,请勿在生产环境中使用
- CHANGE: 更新底包至`v2.9.1`
- CHANGE: 对前端进行重构(非最终决定,各位可将其与原先的版本对比, 若有相关建议, 请与开发团队进行交流)
v1.8.0
---
- RELEASE: v1.8.0正式版发布; 这是2025年的第一个正式版本发版,祝各位新年快乐!
- CHANGE: 更新底包至`v2.9.0`
- CHANGE: 优化`Auth`参数透传至`"Authorization: token {token}"`功能, 增加`dev`参数以便调试
- CHANGE: 优化`config.toml`默认配置, 增加`embed.FS`内嵌前端支持, 并优化相关逻辑
- CHANGE: 更新前端页面版权声明
25w01e
---
- PRE-RELEASE: 此版本是v1.8.0的预发布版本,请勿在生产环境中使用
- FIX: 修复引入token参数透传功能导致的一些问题
25w01d
---
- PRE-RELEASE: 此版本是v1.8.0的预发布版本,请勿在生产环境中使用
- CHANGE: 尝试修复部分问题
25w01c
---
- PRE-RELEASE: 此版本是v1.8.0的预发布版本,请勿在生产环境中使用
- CHANGE: 改进token参数透传功能
25w01b
---
- PRE-RELEASE: 此版本是v1.8.0的预发布版本,请勿在生产环境中使用
- CHANGE: 将底包更新至`v2.9.0`
25w01a
---
- PRE-RELEASE: 此版本是v1.8.0的预发布版本,请勿在生产环境中使用; 同时,这也是2025年的第一个pre-release版本,祝各位新年快乐! (同时,请注意版本号的变化)
- ADD: 加入`dev`参数, 以便pre-release版本调试(实验性)
- ADD: 加入基于`embed.FS`的内嵌前端, config.toml中的`[pages]`配置为false时自动使用内嵌前端
- CHANGE: 完善24w29a版本新加入的`Auth`参数透传至`"Authorization: token {token}"`功能,对相关逻辑进行完善
- FIX: 修正24w29a版本新加入的`Auth`参数透传至`"Authorization: token {token}"`功能的一些问题
24w29a
---
- PRE-RELEASE: 此版本是一个实验性功能测试版本,请勿在生产环境中使用; 同时,这也是2024年的最后一个pre-release版本
- ADD: `Auth` token参数透传至`"Authorization: token {token}"`, 为私有仓库拉取提供一定便利性(需要更多测试)
- CHANGE: 更新相关依赖库
v1.7.9
---
- RELEASE: 安全性及小型修复, 建议用户自行选择是否升级
- CHANGE: 将`logger`库作为外部库引入, 使维护性更好, 同时修正了部分日志问题并提升部分性能
- CHANGE: 更新相关依赖库, 更新`req`库以解决`net`标准库的`CVE-2021-38561`漏洞
- FIX: 修复安装脚本内的错误
24w28b
---
- PRE-RELEASE: 此版本是v1.7.9的预发布版本,请勿在生产环境中使用
- CHANGE: 将`logger`库作为外部库引入, 使维护性更好, 同时修正了部分日志问题并提升部分性能
24w28a
---
- PRE-RELEASE: 此版本是v1.7.9的预发布版本,请勿在生产环境中使用
- CHANGE: 更新相关依赖库, 更新`req`库以解决`net`标准库的`CVE-2021-38561`漏洞
- FIX: 修复安装脚本内的错误
v1.7.8
---
- RELEASE: 我们建议您升级到此版本, 以解决一些依赖库的安全漏洞和与caddy相关的内存问题
- CHANGE: 更新底包至`v24.12.20`可能解决部分与`caddy`相关的内存问题
- CHANGE: 更新相关依赖库,解决`net`标准库的`CVE-2024-45338`
- CHANGE: 小幅更新前端页面
- FIX: 修复`config.toml`默认配置内的错误
- ADD: 新增`api.github.com`反代支持, 强制性要求开启`Header Auth`功能(需要更多测试)
24w27e
---
- PRE-RELEASE: 此版本是v1.7.8的预发布候选版本(若无问题,此版本将会成为v1.7.8正式版本),请勿在生产环境中使用
- CHANGE: 更新底包至`v24.12.20`可能解决部分与`caddy`相关的内存问题
24w27d
---
- PRE-RELEASE: 此版本是v1.7.8的预发布候选版本,请勿在生产环境中使用
- CHANGE: 更新相关依赖库,解决`net`标准库的`CVE-2024-45338`
- CHANGE: 小幅更新前端页面
24w27c
---
- PRE-RELEASE: 此版本做为实验性功能测试版本,请勿在生产环境中使用
- CHANGE: 更新docker底包至`v2.9.0-beta.3` , 可能解决部分内存相关问题
- CHANGE: 更新相关依赖库
24w27b
---
- PRE-RELEASE: 此版本做为实验性功能测试版本,请勿在生产环境中使用
- FIX: 修复`config.toml`默认配置内的错误
24w27a
---
- PRE-RELEASE: 此版本做为实验性功能测试版本,请勿在生产环境中使用
- ADD: 新增`api.github.com`反代支持, 强制性要求开启`Header Auth`功能
v1.7.7
---
- CHANGE: 更新相关依赖库
- CHANGE: 更新Go版本至1.23.4
- CHANGE: 更新release及dev版本底包
24w26a
---
- PRE-RELEASE: 此版本是v1.7.7的预发布版本,请勿在生产环境中使用
- CHANGE: 更新相关依赖库
- CHANGE: 更新Go版本至1.23.4
- CHANGE: 更新release及dev版本底包
v1.7.6
---
- RELEASE: 版本在v1.7.4及以上的用户,我们建议升级到此版本以解决于v1.7.4版本功能更新所引入的问题
- FIX: 进一步修正 H2C相关配置逻辑问题
- CHANGE: 对Caddy配置进行实验性修改,优化H2C配置
- CHANGE: 更新相关依赖库
24w25b
---
- PRE-RELEASE: 此版本是v1.7.6的预发布版本,请勿在生产环境中使用
- 说明: 本版本为24w25a-fix0
- FIX: 进一步修正 H2C相关配置逻辑问题
24w25a
---
- PRE-RELEASE: 此版本是v1.7.6的预发布版本,请勿在生产环境中使用
- 说明: 本版本为v1.7.6的其中一个候选与开发测试版本,相关改动不一定实装
- FIX: 进一步修正 H2C相关配置逻辑问题
- CHANGE: 对Caddy配置进行实验性修改,优化H2C配置
- CHANGE: 更新相关依赖库
v1.7.5
---
- FIX: 修复 v1.7.4 版本 Docker 镜像默认配置导致的 403 问题
- ADD: `Rate`模块加入`IP`速率限制,可限制单个IP的请求速率 (需要更多测试)
- CHANGE: 处理积攒的依赖库更新
24w24c
---
- PRE-RELEASE: 此版本是v1.7.5的预发布版本,请勿在生产环境中使用
- CHANGE: 更新依赖
24w24b
---
- PRE-RELEASE: 此版本是v1.7.5的预发布版本,请勿在生产环境中使用
- FIX: 修复 Docker 默认配置导致的 403 问题
24w24a
---
- PRE-RELEASE: 此版本是v1.7.5的预发布版本,请勿在生产环境中使用
- ADD: `Rate`模块加入`IP`速率限制,可限制单个IP的请求速率 (需要更多测试)
- CHANGE: 处理积攒的依赖库更新,更新如下依赖库:
- **github.com/gabriel-vasile/mimetype**: 从 v1.4.6 升级到 v1.4.7
- **github.com/go-playground/validator/v10**: 从 v10.22.1 升级到 v10.23.0
- **github.com/klauspost/cpuid/v2**: 从 v2.2.8 升级到 v2.2.9
- **github.com/onsi/ginkgo/v2**: 从 v2.21.0 升级到 v2.22.0
- **golang.org/x/arch**: 从 v0.11.0 升级到 v0.12.0
- **golang.org/x/crypto**: 从 v0.28.0 升级到 v0.29.0
- **golang.org/x/exp**: 从 v0.0.0-20241009180824-f66d83c29e7c 升级到 v0.0.0-20241108190413-2d47ceb2692f
- **golang.org/x/mod**: 从 v0.21.0 升级到 v0.22.0
- **golang.org/x/net**: 从 v0.30.0 升级到 v0.31.0
- **golang.org/x/sync**: 从 v0.8.0 升级到 v0.9.0
- **golang.org/x/sys**: 从 v0.26.0 升级到 v0.27.0
- **golang.org/x/text**: 从 v0.19.0 升级到 v0.20.0
- **golang.org/x/tools**: 从 v0.26.0 升级到 v0.27.0
- **google.golang.org/protobuf**: 从 v1.35.1 升级到 v1.35.2
v1.7.4
---
- CHANGE: 对二进制文件部署脚本进行优化
- CHANGE&ADD: 新增H2C相关配置
- ADD: `Auth`模块加入`Header`鉴权,使用`GH-Auth`的值进行鉴权
24w23a
---
- PRE-RELEASE: 此版本是v1.7.4的预发布版本,请勿在生产环境中使用
- ADD: `Auth`模块加入`Header`鉴权,使用`GH-Auth`的值进行鉴权
- CHANGE: 对二进制文件部署脚本进行优化
- CHANGE&ADD: 新增H2C相关配置
v1.7.3
---
- CHANGE: Bump golang.org/x/time from 0.7.0 to 0.8.0
- FIX: 修复故障熔断的相关问题
v1.7.2
---
- CHANGE: 为`nocache`版本加入测试性的故障熔断机制
v1.7.1
---
- CHANGE: 更新Go版本至1.23.3
- CHANGE: 更新相关依赖库
- ADD: 对`Proxy`模块进行优化,增加使用`HEAD`方式预获取`Content-Length`
- CHANGE: 将`release``dev`版本的底包切换至`wjqserver/caddy:2.9.0-rc4-alpine`,将`nocache`版本的底包切换至`alpine:latest`
- CHANGE: 对`nocache`版本的`config.toml``init.sh`进行适配性修改
- CHANGE: 加入测试性的故障熔断机制(Failure Circuit Breaker) (nocache版本暂不支持)
24w22b
---
- PRE-RELEASE: 此版本是v1.7.1的预发布版本,请勿在生产环境中使用
- CHANGE: 更新Go版本至1.23.3
- CHANGE: 更新相关依赖库
- ADD: 对`Proxy`模块进行优化,增加使用`HEAD`方式预获取`Content-Length`
- CHANGE: 将`release``dev`版本的底包切换至`wjqserver/caddy:2.9.0-rc4-alpine`,将`nocache`版本的底包切换至`alpine:latest`
- CHANGE: 对`nocache`版本的`config.toml``init.sh`进行适配性修改
24w22a
---
- PRE-RELEASE: 此版本是v1.7.1的预发布版本,请勿在生产环境中使用
- CHANGE: 更新底包
- CHANGE: 加入测试性的故障熔断机制(Failure Circuit Breaker)
v1.7.0
---
- ADD: 加入`rate`模块,实现内置速率限制
- CHANGE: 优化`blacklist``whitelist`模块的匹配算法,提升性能;由原先的完整匹配改为切片匹配,提升匹配效率
- ADD: 加入`version`相关表示与API接口
- ADD: 加入`rate`相关API接口
- CHANGE: 优化前端界面,优化部分样式
- CHANGE: 更新相关依赖库
- CHANGE: 对编译打包进行改进,此后不再提供独立可执行文件,请改为拉取`tar.gz`压缩包
24w21d
---
- PRE-RELEASE: 此版本是v1.7.0的预发布版本,请勿在生产环境中使用
- ADD: 新增`ratePerMinute` API可供查询
- ADD: 前端新增 version 标识
- ADD: 前端新增 `重定向` 按钮,用于重定向到代理后的链接
- CHANGE: 优化输出代码块,使样式更加美观
- CHANGE: 更新相关依赖库
- CHANGE: 对黑名单模块进行实验性功能优化,提升性能(改进匹配算法,在切片后优先匹配user,减少无效匹配)
24w21c
---
- PRE-RELEASE: 此版本是v1.7.0的预发布版本,请勿在生产环境中使用
- CHANGE: 对编译打包进行改进,此后不再提供独立可执行文件,请改为拉取`tar.gz`压缩包
- CHANGE: 由于上述原因,对Docker打包进行相应改进
24w21b
---
- PRE-RELEASE: 此版本是v1.7.0的预发布版本,请勿在生产环境中使用
- ADD: 加入版本号标识与对应API接口
- ADD: 加入速率限制API接口
- CHANGE: 修改打包部分
24w21a
---
- PRE-RELEASE: 此版本是v1.7.0的预发布版本,请勿在生产环境中使用
- ADD: 尝试加入程序内置速率限制
- CHANGE: 更新相关依赖库
- CHANGE: 更换Dev版本底包,于release版本保持一致
v1.6.2
---
- CHANGE: 优化前端界面,优化部分样式
- ADD: 前端加入黑夜模式
- CHANGE: 优化移动端适配
- CHANGE: 优化一键部署脚本,使其更加易用,并增加更多的功能(已于早些时候hotfix)
- CHANGE: 优化部分代码结构,提升性能
- CHANGE: 优化日志记录,对各个部分的日志记录进行统一格式,并对部分重复日志进行合并
24w20b
---
- PRE-RELEASE: 此版本是v1.6.2的预发布版本,请勿在生产环境中使用
- CHANGE: 优化前端界面,优化部分样式
- ADD: 前端加入黑夜模式
- CHANGE: 优化移动端适配
24w20a
---
- PRE-RELEASE: 此版本是v1.6.2的预发布版本,请勿在生产环境中使用
- CHANGE: 大幅修改日志记录,对各个部分的日志记录进行统一格式,并对部分重复日志进行合并
- CHANGE: 大幅优化一键部署脚本,使其更加易用,并增加更多的功能(已于早些时候hotfix)
- CHANGE: 优化部分代码结构,提升性能
v1.6.1
---
- CHANGE: 根据社区建议,将`sizeLimit`由过去的以`byte`为单位,改为以`MB`为单位,以便于直观理解
- ADD: 新增`nocache`版本,供由用户自行优化缓存策略
- CHANGE: 优化`Proxy`核心模块内部结构,提升性能
- REMOVE: 移除`Proxy`模块内部分无用`logInfo`
- FIX & ADD: 修复前端对gist的匹配问题,添加对`gist.githubusercontent.com`的前端转换支持
- CHANGE: 改变部分前端匹配逻辑
- CHANGE: 更新相关依赖库
24w19d
---
- PRE-RELEASE: 此版本是v1.6.1的预发布版本,请勿在生产环境中使用
- ADD: 新增nocache版本,供由用户自行优化缓存策略
- CHANGE: 优化`Proxy`核心模块内部结构,提升性能
- REMOVE: 移除`Proxy`模块内部分无用`logInfo`
24w19c
---
- PRE-RELEASE: 此版本是v1.6.1的预发布版本,请勿在生产环境中使用
- FIX & ADD: 修复前端对gist的匹配问题,添加对`gist.githubusercontent.com`的前端转换支持
- CHANGE: 改变部分前端匹配逻辑
- CHANGE: 更新相关依赖库
24w19b
---
- PRE-RELEASE: 此版本是v1.6.1的预发布版本,请勿在生产环境中使用
- FIX: 修复`sizeLimit`单位更改导致API返回值错误的问题
- FIX: 修正Gist匹配
24w19a
---
- PRE-RELEASE: 此版本是v1.6.1的预发布版本,请勿在生产环境中使用
- CHANGE: 根据社区建议,将`sizeLimit`由过去的以`byte`为单位,改为以`MB`为单位,以便于直观理解
- CHANGE: 更新相关依赖
- CHANGE: 对`Proxy`模块的核心函数进行模块化,为后续修改和扩展提供空间
v1.6.0
---
- CHANGE: 优化代码结构,提升性能
- CHANGE: 引入H2C支持,支持无加密HTTP/2请求,一定程度上提升传输性能
- ADD: 在核心程序内加入静态页面支持,支持不通过caddy等web server提供前端页面
- CHANGE: 优化日志记录,带来更多的可观测性
- CHANGE: 改进前端界面,优化用户体验; 对原有Alert提示进行优化改为ShowToast提示
- CHANGE: 规范化部分函数命名,提升可读性; 同时对config.toml内的参数命名进行规范化(部分参数名称已过时,请注意更新)
- CHANGE: 修改日志检查周期,降低检查频率,避免不必要的资源浪费
- ADD: 增加CORS状态API
24w18f
---
- PRE-RELEASE: 此版本是v1.6.0的预发布版本,请勿在生产环境中使用
- CHANGE: 修正前端页面的部分样式问题
- FIX: 修正部分问题
24w18e
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用
- CHANGE: 引入H2C协议支持,支持无加密HTTP/2请求
- ADD: 尝试在核心程序内加入静态页面支持
- CHANGE: 优化日志记录
- CHANGE: 去除部分无用/重复配置
- CHANGE: 规范化部分函数命名
24w18d
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用
- CHANGE: 更新相关依赖库
- ADD: 增加CORS状态API
- CHANGE: 优化部分函数执行顺序
- CHANGE: 优化前端界面
24w18c
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用
- CHANGE: 修正配置命名,改为驼峰式命名
- CHANGE: 修正函数命名
24w18b
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用
- CHANGE: 经团队考量,移除 Docker 代理功能,若造成了不便敬请谅解
- CHANGE: 修改日志检查周期
24w18a
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用
- CHANGE: 改进Docker 代理
- CHANGE: 改进前端页面的copy提示,弃用alert提示
v1.5.2
---
- FIX: 修正flag传入问题
- CHANGE: 去除/路径重定向,改为返回403,并记录对应请求日志
- CHANGE: 优化Proxy模块的日志记录,记录请求详细信息
24w17b
---
- PRE-RELEASE: 此版本是v1.5.2的预发布版本,请勿在生产环境中使用
- FIX: 修正flag传入问题
- CHANGE: 去除/路径重定向,改为返回403,并记录对应请求日志
- CHANGE: 优化Proxy模块的日志记录,记录请求详细信息
24w17a
---
- PRE-RELEASE: 此版本是v1.5.2的预发布版本,请勿在生产环境中使用
- FIX: 初步修正flag传入问题,但仍有可能存在其他问题
v1.5.1
---
- CHANGE: 优化代码结构,提升性能
- CHANGE: Bump github.com/imroc/req/v3 from 3.48.0 to 3.49.0 by @dependabot in https://github.com/WJQSERVER-STUDIO/ghproxy/pull/7
- ADD: 新增一键部署脚本,简化二进制文件部署流程
24w16a
---
- PRE-RELEASE: 此版本是v1.5.1的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: Bump github.com/imroc/req/v3 from 3.47.0 to 3.48.0 by @dependabot in https://github.com/WJQSERVER-STUDIO/ghproxy/pull/6
- ADD: 新增一键部署脚本,简化二进制文件部署流程
v1.5.0
---
- CHANGE: 优化代码结构,提升性能
- CHANGE: 改进核心部分,即proxy模块的转发部分,对请求体处理与响应体处理进行优化
- CHANGE: 配置文件格式由yaml切换至toml,使其具备更好的可读性
- ADD: 黑白名单引入通配符支持,支持完全屏蔽或放行某个用户,例如`onwer/*`表示匹配`owner`的所有仓库
- ADD: 新增API模块,新增配置开关状态接口,以在前端指示功能状态
- CHANGE: 由于API变动,对前端进行相应调整
- ADD: 日志模块引入日志级别,排障更加直观
- CHANGE: 改进黑白名单机制,若禁用相关功能,则不对相关模块进行初始化
24w15d
---
- PRE-RELEASE: 此版本是v1.5.0的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- ADD: 新增API模块,新增配置开关状态接口,以在前端指示功能状态
- CHANGE: 由于API变动,对前端进行相应调整
24w15c
---
- PRE-RELEASE: 此版本是v1.5.0的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: 改进核心部分,即proxy模块的转发部分,对请求体处理与响应体处理进行优化
- CHANGE: 改进黑白名单机制,若禁用相关功能,则不对对应模块进行初始化
- ADD: 黑白名单引入通配符支持,支持完全屏蔽或放行某个用户,例如`onwer/*`表示匹配`owner`的所有仓库
- ADD: 日志模块引入日志级别,排障更加直观
24w15b
---
- PRE-RELEASE: 此版本是v1.5.0的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- FIX: 修正24w15a版本的部分问题
24w15a
---
- PRE-RELEASE: 此版本是v1.5.0的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: 将配置文件由yaml切换至toml
v1.4.3
---
- CHANGE: 优化代码结构,提升性能
- ADD: 新增命令行参数 `-cfg string` 用于指定配置文件路径
- CHANGE: 对二进制文件大小进行改进
24w14a
---
- PRE-RELEASE: 此版本是v1.4.3的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- ADD: 新增命令行参数 `-cfg string` 用于指定配置文件路径
v1.4.2
---
- CHANGE: 优化代码结构,提升性能
- CHANGE: 初步引入ARM64架构支持
- CHANGE: 对Docker镜像构建进行优化大幅减少镜像体积,从v1.4.0的`111 MB`,到v1.4.1的`58 MB`,再到v1.4.2的`28 MB`
- CHANGE: 切换至wjqserver/caddy:2.9.0-rc-alpine作为基础镜像
24w13c
---
- PRE-RELEASE: 此版本是v1.4.2的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: 修正交叉编译问题
24w13b
---
- PRE-RELEASE: 此版本是v1.4.2的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: 初步引入ARM64支持但仍处于测试阶段
- CHANGE: 对Dockerfile进行优化大幅减少镜像体积
24w13a
---
- PRE-RELEASE: 此版本是v1.4.2的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: 更新相关依赖库
v1.4.1
---
- CHANGE: 优化代码结构,提升性能
- CHANGE: 引入Alpine Linux作为基础镜像,大幅减少Docker镜像体积
- FIX: 修正部分参数错误
- CHANGE: CGO_ENABLED=0
24w12c
---
- PRE-RELEASE: 此版本是v1.4.1的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: 尝试在DEV版本引入Alpine Linux作为基础镜像,减少镜像体积
24w12b
---
- PRE-RELEASE: 此版本是v1.4.1的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: 尝试引入Alpine Linux作为基础镜像,减少镜像体积
24w12a
---
- PRE-RELEASE: 此版本是v1.4.1的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- FIX: 修正部分参数错误
- CHANGE: CGO_ENABLED=0
v1.4.0
---
- CHANGE: 优化代码结构,提升性能
- ADD: 新增auth子模块whitelist.go,支持白名单功能
- ADD: 新增whitelist.json文件,用于配置白名单
- CHANGE&ADD: 在config.yaml文件中新增白名单配置块
- FIX: 由于临时加入且未在原开发路线上计划的白名单功能,导致函数命名冲突,在此修复blacklist.go的函数命名问题
- FIX: 修复黑/白名单是否生效相关问题
24w11b
---
- PRE-RELEASE: 此版本是v1.4.0的预发布版本,请勿在生产环境中使用
- FIX: 修复黑/白名单是否生效相关问题
24w11a
---
- PRE-RELEASE: 此版本是v1.4.0的预发布版本,请勿在生产环境中使用
- **ANNOUNCE**: 自此版本起,DEV版本号格式进行修改,小版本号不再仅限于a/b,而是采用字母表顺序进行排列,此修改将带来一个重要改变,正式版前的预发布版本的数字版本号将会统一,以便于版本管理与发布管理
- CHANGE: 优化代码结构,提升性能
- ADD: 新增auth子模块whitelist.go,支持白名单功能
- ADD: 新增whitelist.json文件,用于配置白名单
- FIX: 由于临时加入且未在原开发路线上计划的白名单功能,导致函数命名冲突,在此修复blacklist.go的函数命名问题
v1.3.1
---
- CHANGE: 优化代码结构,提升性能
@@ -8,6 +851,7 @@ v1.3.1
24w10a
---
- PRE-RELEASE: 此版本是v1.3.1的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: 剃刀计划,减少多余日志输出
- CHANGE: 调整缓存参数
@@ -24,12 +868,14 @@ v1.3.0
24w09b
---
- PRE-RELEASE: 此版本是v1.3.0的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: 修正配置,提升稳定性
- WARNING: 此版本配置文件重构,此版本不再向前兼容,请注意备份文件并重新部署
24w09a
---
- PRE-RELEASE: 此版本是v1.3.0的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- CHANGE: 优化黑名单功能,提升稳定性
- CHANGE&ADD: 新增auth子模块blacklist.go
@@ -38,6 +884,7 @@ v1.3.0
24w08b
---
- PRE-RELEASE: 此版本是v1.3.0的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,提升性能
- ADD & CHANGE: 新增仓库黑名单功能,改进Auth模块
- ADD: 新增blacklist.yaml文件,用于配置仓库黑名单
@@ -54,6 +901,7 @@ v1.2.0
24w08a
---
- PRE-RELEASE: 此版本是v1.2.0的预发布版本,请勿在生产环境中使用
- CHANGE: 同步更新logger模块与golang-temp项目定义的开发规范保持一致
- ADD: 新增日志翻转功能
@@ -65,12 +913,14 @@ v1.1.1
24w07b
---
- PRE-RELEASE: 此版本是v1.1.1的预发布版本,请勿在生产环境中使用
- CHANGE: 修改部分代码与golang-temp项目定义的开发规范保持一致
- CHANGE: 更新Go版本至v1.23.2
- CHANGE: 跟随Caddy更新,修改Caddyfile配置
24w07a
---
- PRE-RELEASE: 此版本是v1.1.1的预发布版本,请勿在生产环境中使用
- CHANGE: 修改部分代码与golang-temp项目定义的开发规范保持一致
- CHANGE: 更新Go版本至v1.23.2
@@ -85,6 +935,7 @@ v1.1.0
24w06b
---
- PRE-RELEASE: 此版本是v1.1.0的预发布版本,请勿在生产环境中使用
- CHANGE: 优化代码结构,对main函数进行模块化,提升可读性
- CHANGE: Docker代理功能移至DEV版本内,保证稳定性
- ADD&CHANGE: 增加Auth(用户鉴权)模块,并改进其的错误处理与日志记录
@@ -94,6 +945,7 @@ v1.1.0
v1.0.0
---
- **ANNOUNCE**: 项目正式发布, 并迁移至[WJQSERVER-STUDIO/ghproxy](https://github.com/WJQSERVER-STUDIO/ghproxy)由Apache License Version 2.0转为WJQserver Studio License 请注意相关条例变更
- CHANGE: 项目正式发布, 并迁移至[WJQSERVER-STUDIO/ghproxy](https://github.com/WJQSERVER-STUDIO/ghproxy)
- CHANGE: 再次重构代码,优化性能,提升稳定性
- CHANGE: 使用golang-temp项目作为底层构建,标准化日志与配置模块
@@ -101,6 +953,7 @@ v1.0.0
24w06a
---
- PRE-RELEASE: 此版本是v1.0.0的预发布版本,请勿在生产环境中使用
- CHANGE: 与v1.0.0版本同步
v0.2.0
@@ -112,21 +965,25 @@ v0.2.0
24w05b
---
- PRE-RELEASE: 此版本是v0.2.0的预发布版本,请勿在生产环境中使用
- CHANGE: 重命名proxychrome函数
- ADD: 增加多处日志记录,便于审计与排障
24w05a
---
- PRE-RELEASE: 此版本是v0.2.0的预发布版本,请勿在生产环境中使用
- FIX 修正上一版本的req请求未继承请求方式的问题
- CHANGE: 优化代码结构,进一步模块化,同时提升性能
24w04b
---
- PRE-RELEASE: 此版本是v0.2.0的预发布版本,请勿在生产环境中使用
- CHANGE: 更换Docker基础镜像为daily版本
- ADD 新增使用req库实现代理请求,使用chrome TLS指纹发起请求
24w04a
---
- PRE-RELEASE: 此版本是v0.2.0的预发布版本,请勿在生产环境中使用
- CHANGE: 调整程序结构,使用init函数初始化配置,并优化代码结构
v0.1.7
@@ -135,6 +992,7 @@ v0.1.7
24w03b
---
- PRE-RELEASE: 此版本是v0.1.7的预发布版本,请勿在生产环境中使用
- CHANGE: 合入上游(wjqserver/caddy:latest)安全更新, 增强镜像安全性
v0.1.6
@@ -144,6 +1002,7 @@ v0.1.6
24w03a
---
- PRE-RELEASE: 此版本是v0.1.6的预发布版本,请勿在生产环境中使用
- CHANGE: 改进Docker代理相关Caddy配置
- ADD: 新增跨域配置选项
@@ -154,16 +1013,17 @@ v0.1.5
24w02b
---
- PRE-RELEASE: 此版本是v0.1.5的预发布版本,请勿在生产环境中使用
- ADD: 新增Docker代理 (未并入正式版)
24w02a
---
- PRE-RELEASE: 此版本是v0.1.5的预发布版本,请勿在生产环境中使用
- CHANGE: 更新Go版本至v1.23.1
- CHANGE: 优化代码行为
v0.1.4
---
正式版24w01b内容更新
- ADD: 新增外部文件配置功能
- ADD: 新增日志功能
- CHANGE: 优化代码结构,提升性能
@@ -171,7 +1031,7 @@ v0.1.4
24w01b
---
标志着项目正式进入自主开发阶段
- PRE-RELEASE: 此版本是v0.1.4的预发布版本,请勿在生产环境中使用
- ADD: 新增外部文件配置功能
- ADD: 新增日志功能
- CHANGE: 优化代码结构,提升性能
@@ -179,12 +1039,13 @@ v0.1.4
v0.1.3
---
开始自行维护项目,脱离上游更新
- **ANNOUNCE**: 开始自行维护项目,脱离上游更新
- CHANGE: 改进已有实现,增强程序稳定性
24w01a
---
首个DEV版本
- PRE-RELEASE: 此版本是v0.1.3的预发布版本,请勿在生产环境中使用
- **ANNOUNCE**: 首个DEV版本发布
- CHANGE: 同步更新
v0.1.2

View File

@@ -1 +1 @@
24w10a
25w17a

214
LICENSE
View File

@@ -1,107 +1,197 @@
WJQserver Studio 开源许可证
版本 1.2
版本 v2.0
版权所有 © WJQserver Studio 2024
定义
许可:指在本许可证内定义的使用、复制、分发与修改的条款与要求。
授权方:指拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体。
您:指行使本许可授予的权限的个人或法律实体。
开源与自由软件
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
本项目不等同于自由软件,使用权限受到本许可证条款的限制。
强调版权所有,所有权利均由 WJQserver Studio 保留。
许可证条款
1. 使用权限
1.1 您被授予在私人环境中自由使用本软件的权限。
1.2 您可以在不修改关键声明的前提下进行商用
* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求
* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。
* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。
* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。
* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。
* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于:
* 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。
* 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。
* 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。
* 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。
* 内部运营用途 (非营利组织) 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。
开源与自由软件
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。
强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。
许可证条款
1. 使用权限
* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。
* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款:
* 1.2.1 保持声明: 您在进行商业使用时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。
* 1.2.2 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于:
* 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。
* 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。
* 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。
* 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。
您必须选择以下两种方式之一:
* i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。
* ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。
2. 复制与分发
2.1 您可以复制和分发本软件的原始版本,前提是必须保留所有版权声明和本许可证。
* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件:
* 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。
* 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。
* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.2.2 条(开源继承与互惠共享)的约束。
3. 修改权限
3.1 您可以在非商业用途下修改本软件,前提是继承本许可证并保留原版权声明。
3.2 禁止在修改后进行商业用途。
* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。
4. 专利引用
4.1 若项目被专利相关引用,必须保留来源声明。
* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.2.2 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。
4.2 若为商业场景,需按照商用处理
* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可
4. 专利权
* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。
* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。
5. 免责声明
5.1 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。
5.2 在任何情况下,授权方均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害负责,即使已被告知可能发生此类损害
* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性
5.3 用户需根据当地法律对待本项目,确保遵守所有适用法规
* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害
6. 许可证期限
6.1 本许可证自2024年开始生效有效期暂为无限。
* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。
6.2 项目所有方有权修改许可证相关条例而不另行通知。
6. 许可证期限与终止
条款修订
7.1 授权方保留随时修改本许可证条款的权利,以便更好地适应法律和技术的发展。
* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。
7.2 修订后的条款将在发布时生效,继续使用本软件即表示接受修订后的条款
* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止
其他
8.1 本许可证不影响您作为最终用户的法定权利。
* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。
8.2 若本许可证的某些条款被认定为不可执行,其余条款仍然有效。
7. 条款修订
* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。
* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。
8. 其他
* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。
* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。
* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。
WJQserver Studio Open Source License
Version 1.2
Version v2.0
Copyright © WJQserver Studio 2024
Definitions
License: The terms and conditions defined within this license for use, copying, distribution, and modification.
Licensor: The individual or organization holding the copyright, or the entity designated by them.
You: The individual or legal entity exercising the permissions granted by this license.
Open Source vs. Free Software
This project is open source, allowing users to access and use the source code under the terms of this license.
This project is not equivalent to free software; usage rights are restricted by this license.
Copyright is emphasized, with all rights reserved by WJQserver Studio.
* License: Refers to the terms and requirements for use, reproduction, distribution, and modification defined within this license.
* Licensor: Refers to the individual or organization that holds the copyright, or the entity designated by the copyright holder, specifically WJQserver Studio in this license.
* Contributor: Refers to the Licensor and individuals or entities who contribute code or software under this License.
* You: Refers to the individual or legal entity exercising permissions granted by this License.
* Derivative Works: Refers to works modified based on the Software or any part thereof, regardless of the extent of modification. This includes but is not limited to modifications, revisions, adaptations, translations, or other forms of creation based on the Software or any part thereof, as well as collective works containing the Software or parts thereof.
* Non-profit Use: Refers to uses not primarily intended for direct commercial profit, including but not limited to:
* Personal Use: Use by an individual for personal learning, research, experimentation, non-commercial projects, personal website development, graduation projects, home entertainment, and other non-directly commercial purposes.
* Educational Use: Use within educational institutions (such as schools, universities, training organizations) for activities such as teaching, research, and academic exchange.
* Scientific Research Use: Use within scientific research institutions, laboratories, and similar organizations for activities such as scientific research and experimental development.
* Charitable and Public Welfare Use: Use by charitable organizations, public welfare organizations, and similar non-profit entities for their public missions or internal operation of charitable activities, or to provide public services that do not directly generate commercial profit.
* Internal Operational Use (Non-profit Organizations): Use within the internal operations of non-profit organizations, such as for administrative management, membership management, internal communication, project management, and other non-directly profit-generating activities.
Open Source and Free Software
This project is open-source software, allowing users to access and use the source code under the premise of complying with this License.
This project aims to provide users with the broadest possible freedom for non-commercial use while ensuring the common development and healthy ecosystem of the community, and providing a clear path for commercial innovation.
Copyright is emphasized; all rights are jointly reserved by WJQserver Studio and Contributors.
License Terms
1. Usage Rights
1.1 You are granted the right to use this software freely in a private environment.
1.2 You may use it commercially without modifying key statements.
1. Permissions for Use
2. Copying and Distribution
2.1 You may copy and distribute the original version of this software, provided all copyright notices and this license are retained.
* 1.1 Non-profit Use: You are granted permission to freely use the Software for any purpose in non-profit use scenarios. Specific non-profit use scenarios include but are not limited to the various situations listed in the Definition section.
3. Modification Rights
3.1 You may modify this software for non-commercial purposes, provided you inherit this license and retain the original copyright notice.
* 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms:
3.2 Modifications cannot be used commercially.
* 1.2.1 Maintain Statements: When conducting commercial use, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software.
* 1.2.2 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to:
4. Patent References
4.1 If the project is cited in patent-related contexts, the source statement must be retained.
* Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works.
* Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services.
* Embedded Commercial Applications: Embedding the Software or its Derivative Works into commercial products or solutions for sale.
* Internal Commercial Operations: Using modified versions within the internal operations of for-profit organizations to directly support their commercial activities, such as customized internal systems, generating commercial revenue directly or indirectly through means including but not limited to placing advertisements in the software or related services (e.g., Google Ads), in-app purchases, membership subscriptions, and charging for value-added features.
4.2 For commercial scenarios, it must be treated as a commercial use.
You must choose one of the following two options:
5. Disclaimer
5.1 This software is provided "as is", without any express or implied warranties, including but not limited to merchantability, fitness for a particular purpose, and non-infringement.
* i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community.
* ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation.
5.2 In no event shall the licensor be liable for any direct, indirect, incidental, special, punitive, or consequential damages arising out of the use or inability to use this software, even if advised of the possibility of such damages.
2. Reproduction and Distribution
5.3 Users must comply with all applicable laws regarding this project.
* 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met:
6. License Duration
6.1 This license is effective from 2024, with an indefinite duration.
* Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices.
* Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License.
6.2 The project owner reserves the right to modify the license terms without prior notice.
* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing).
Amendments
7.1 The licensor reserves the right to amend this license at any time to better adapt to legal and technological developments.
3. Modification Permissions
7.2 Revised terms become effective upon publication, and continued use of the software indicates acceptance of the revised terms.
* 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use.
Miscellaneous
8.1 This license does not affect your statutory rights as an end user.
* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization.
8.2 If any provision of this license is held to be unenforceable, the remaining provisions shall remain in effect.
* 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License.
4. Patent Rights
* 4.1 No Patent Warranty, Risk Self-Bearing: The software is provided “AS IS”, and the Licensor and Contributors explicitly declare that they do not provide any form of warranty regarding patent infringement issues of this software, nor do they assume any responsibility and consequences arising from patent infringement. Users understand and agree that the patent risk of using this software is entirely borne by the users themselves.
* 4.2 Handling of Patent Disputes: If any patent infringement allegations, lawsuits, or claims arise due to the user's use of this Software, the user shall be solely responsible for handling and bear all legal liabilities. The Licensor and Contributors are under no obligation to participate in any related legal proceedings, nor do they bear any costs or compensation arising therefrom.
5. Disclaimer of Warranty
* 5.1 “AS IS” Provision, No Warranty: The software is provided “AS IS” without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement.
* 5.2 Limitation of Liability: To the maximum extent permitted by applicable law, in no event shall the Licensor or any Contributor be liable for any direct, indirect, incidental, special, punitive, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
* 5.3 User Legal Responsibility: Users shall treat this project in accordance with local laws and regulations to ensure compliance with all applicable laws and regulations.
6. License Term and Termination
* 6.1 License Term: Unless the copyright holder proactively announces the abandonment of the copyright of this software, this License shall be effective indefinitely from the date of your acceptance.
* 6.2 License Termination: If you fail to comply with any terms or conditions of this License, the Licensor has the right to terminate this License. Your License will automatically terminate upon your violation of the terms of this License.
* 6.3 Effect after Termination: Upon termination of the License, all rights granted to you under this License will terminate immediately, but the licenses and rights obtained by recipients of software copies you have legally distributed before the termination of the License will not be affected and will remain valid. The Disclaimer of Warranty (Clause 5) and Limitation of Liability (Clause 5.2) shall remain in effect after the termination of this License.
7. Revision of Terms
* 7.1 Reservation of Revision Rights: The Licensor reserves the right to modify the terms of this License at any time to better adapt to legal, technological developments, and community needs.
* 7.2 Effectiveness and Acceptance of Revisions: Revised terms will take effect upon publication, and unless otherwise stated, continued use, reproduction, distribution, or modification of the Software indicates your acceptance of the revised terms. The Licensor encourages users to periodically review the latest version of this License.
8. Other
* 8.1 Statutory Rights: This License does not affect your statutory rights as an end-user under applicable laws.
* 8.2 Severability of Terms: If certain terms of this License are deemed unenforceable, the remaining terms shall remain in full force and effect.
* 8.3 Version Updates: The Licensor may publish revised versions or new versions of this License. You may choose to continue using the old version of this License or choose to apply the new version.

164
README.md
View File

@@ -1,46 +1,55 @@
# GhProxy
# GHProxy
![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)
[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
[DEMO](ghproxy.1888866.xyz)
[DEMO](https://ghproxy.1888866.xyz)
[TG讨论群组](https://t.me/ghproxy_go)
[版本更新介绍](https://blog.wjqserver.com/categories/my-program/)
## 项目说明
### 项目特点
- 基于Go语言实现,使用[Gin框架](https://github.com/gin-gonic/gin)与[req库](https://github.com/imroc/req)]
- 基于Go语言实现,支持多平台
- 使用[Gin](https://github.com/gin-gonic/gin)作为Web框架
- 使用[Touka-HTTPC](https://github.com/satomitouka/touka-httpc)作为HTTP客户端
- 支持Git clone,raw,realeases等文件拉取
- 支持Docker部署
- 支持速率限制
- 支持用户鉴权
- 支持自定义黑名单
- 符合[RFC 7234](https://httpwg.org/specs/rfc7234.html)的HTTP Cache
- 使用Caddy作为Web Server
- 支持自定义黑名单/白名单
- 基于[WJQSERVER-STUDIO/golang-temp](https://github.com/WJQSERVER-STUDIO/golang-temp)模板构建,具有标准化的日志记录与构建流程
### 项目开发过程
**本项目是[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的重构版本,实现了原项目原定功能的同时,进一步优化了性能**
本项目源于[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)与[WJQSERVER/ghproxy-go-0RTT](https://github.com/WJQSERVER/ghproxy-go-0RTT)两个项目,前者带来了实现框架与资源,后者带来了解决Git clone问题的办法,使得本项目从net/http标准库切换至Gin框架,已解决此困扰已久的问题,在此基础上,本项目进一步优化了性能,并添加了用户鉴权功能,使得部署更加安全可靠。
关于此项目的详细开发过程,请参看Commit记录与[CHANGELOG.md](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/CHANGELOG.md)
- V2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用
- V1.0.0 迁移至本仓库,并再次重构内容实现
- v0.2.0 重构项目实现,Git clone的实现完全自主化
- v0.2.0 重构项目实现
### LICENSE
本项目使用WSL LICENSE Version1.2 (WJQSERVER STUDIO LICENSE Version1.2)
本项目使用WJQserver Studio License 2.0 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)
在v2.3.0之前, 本项目使用WJQserver Studio License 1.2
在v1.0.0版本之前,本项目继承于[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的APACHE2.0 LICENSE VERSION
## 使用示例
```
# 下载文件
https://ghproxy.1888866.xyz/raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh
# 克隆仓库
git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git
```
@@ -51,44 +60,84 @@ git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git
- Docker-cli
```
docker run -p 7210:80 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/caddy:/data/caddy/log --restart always wjqserver/ghproxy
docker run -p 7210:8080 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/caddy:/data/caddy/log -v ./ghproxy/config:/data/ghproxy/config --restart always wjqserver/ghproxy
```
- Docker-Compose
- Docker-Compose (建议使用)
参看[docker-compose.yml](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docker/compose/docker-compose.yml)
### 二进制文件部署(不推荐)
一键部署脚本:
```bash
wget -O install.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/install.sh && chmod +x install.sh &&./install.sh
```
Dev一键部署脚本:
```bash
wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/install-dev.sh && chmod +x install-dev.sh && ./install-dev.sh
```
## 配置说明
### 外部配置文件
本项目采用config.yaml作为外部配置,默认配置如下
使用Docker部署时,慎重修改config.yaml,以免造成不必要的麻烦
本项目采用`config.toml`作为外部配置,默认配置如下
使用Docker部署时,慎重修改`config.toml`,以免造成不必要的麻烦
```yaml
# 核心配置
server:
port: 8080 # 监听端口(小白请勿修改)
host: "127.0.0.1" # 监听地址(小白请勿修改)
sizelimit: 131072000 # 125MB
```toml
[server]
host = "0.0.0.0" # 监听地址
port = 8080 # 监听端口
sizeLimit = 125 # 125MB
H2C = true # 是否开启H2C传输
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; 除以上特殊情况, 会将值直接传入
# 日志配置
logger:
logfilepath: "/data/ghproxy/log/ghproxy.log" # 日志文件路径(小白请勿修改)
maxlogsize: 5 # MB
[httpc]
mode = "auto" # "auto" or "advanced" HTTP客户端模式 自动/高级模式
maxIdleConns = 100 # only for advanced mode 仅用于高级模式
maxIdleConnsPerHost = 60 # only for advanced mode 仅用于高级模式
maxConnsPerHost = 0 # only for advanced mode 仅用于高级模式
# CORS 配置
cors:
enabled: true # 是否开启CORS
[gitclone]
mode = "bypass" # bypass / cache 运行模式, cache模式依赖smart-git
smartGitAddr = "http://127.0.0.1:8080" # smart-git组件地址
# 鉴权配置
auth:
enabled: false # 是否开启鉴权
authtoken: "test" # 鉴权Token
[pages]
mode = "internal" # "internal" or "external" 内部/外部 前端 默认内部
theme = "bootstrap" # "bootstrap" or "nebula" 内置主题
staticPath = "/data/www" # 静态页面文件路径
# 黑名单配置
blacklist:
enabled: true
blacklistfile: "/data/ghproxy/config/blacklist.json"
[log]
logFilePath = "/data/ghproxy/log/ghproxy.log" # 日志文件路径
maxLogSize = 5 # MB 日志文件最大大小
level = "info" # 日志级别 dump, debug, info, warn, error, none
[auth]
authMethod = "parameters" # 鉴权方式,支持parameters,header
authToken = "token" # 用户鉴权Token
enabled = false # 是否开启用户鉴权
[blacklist]
blacklistFile = "/data/ghproxy/config/blacklist.json" # 黑名单文件路径
enabled = false # 是否开启黑名单
[whitelist]
enabled = false # 是否开启白名单
whitelistFile = "/data/ghproxy/config/whitelist.json" # 白名单文件路径
[rateLimit]
enabled = false # 是否开启速率限制
rateMethod = "total" # "ip" or "total" 速率限制方式
ratePerMinute = 180 # 每分钟限制请求数量
burst = 5 # 突发请求数量
[outbound]
enabled = false # 是否使用自定义代理出站
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" 支持Socks5/HTTP(S)出站传输
```
### 黑名单配置
@@ -100,7 +149,23 @@ blacklist:
"blacklist": [
"test/test1",
"example/repo2",
"another/repo3"
"another/*"
"another"
]
}
```
### 白名单配置
白名单配置位于config/whitelist.json,格式如下:
```json
{
"whitelist": [
"test/test1",
"example/repo2",
"another/*"
"another"
]
}
```
@@ -109,26 +174,23 @@ blacklist:
```Caddyfile
example.com {
reverse_proxy {
to 127.0.0.1:7210
header_up X-Real-IP {remote_host}
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
}
encode zstd gzip
reverse_proxy * 127.0.0.1:7210
}
```
## TODO & DEV
### 前端页面
### TODO
![ghproxy-demo.png](https://webp.wjqserver.com/ghproxy/1.8.1-light.png)
![ghproxy-demo-dark.png](https://webp.wjqserver.com/ghproxy/1.8.1-dark.png)
- [x] 允许更多参数通过config结构传入
- [x] 改进程序效率
- [x] 用户鉴权
- [x] 仓库黑名单
## 赞助
### DEV
如果您觉得本项目对您有帮助,欢迎赞助支持,您的赞助将用于Demo服务器开支及开发者时间成本支出,感谢您的支持!
- [x] Docker Pull 代理
为爱发电,开源不易
爱发电: https://afdian.com/a/wjqserver
### 捐赠列表
虚位以待...

View File

@@ -6,22 +6,20 @@
| 版本 | 是否支持 |
| --- | --- |
| v1.x.x | :white_check_mark: |
| **w**a/b | :warning: 这是测试版本,用于开发测试,可能存在未知的安全隐患 |
| v2.x.x | :white_check_mark: 当前最新版本序列, 受支持 |
| v1.x.x | :x: 这些版本已结束生命周期,不再受支持 |
| 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 |
| v0.x.x | :x: 这些版本不再受支持 |
### 版本说明
- **v1.0.0**: 这是正式发布的版本
- **xx W xx A/B***: 这是开发测试版本,可能存在未知的安全隐患。不推荐在生产环境中使用。
- **v0.x.x**: 这些早期版本不再受支持,建议尽快升级到 v1.0.0 或更高版本。
### 用户须知
本项目为开源项目,开发者不对使用本项目造成的任何损失或问题承担责任。用户需自行评估并承担使用本项目的风险。
使用本项目,请遵循 **[WSL (WJQSERVER-STUDIO LICENSE)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议。
本项目所有文件均受到 WSL (WJQSERVER-STUDIO LICENSE) 协议保护,任何人不得在任何情况下以非 WSL (WJQSERVER-STUDIO LICENSE) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。
## 报告漏洞
如果您发现本项目存在安全漏洞,请通过发送ISSUES或尝试联系项目维护者来报告。请在您的报告中包含以下信息:

View File

@@ -1 +1 @@
1.3.1
2.4.1

129
api/api.go Normal file
View File

@@ -0,0 +1,129 @@
package api
import (
"encoding/json"
"ghproxy/config"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin"
)
var (
router *gin.Engine
cfg *config.Config
)
var (
logw = logger.Logw
LogDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
func NoCacheMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 设置禁止缓存的响应头
c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
c.Next() // 继续处理请求
}
}
func InitHandleRouter(cfg *config.Config, router *gin.Engine, version string) {
apiRouter := router.Group("api", NoCacheMiddleware())
{
apiRouter.GET("/size_limit", func(c *gin.Context) {
SizeLimitHandler(cfg, c)
})
apiRouter.GET("/whitelist/status", func(c *gin.Context) {
WhiteListStatusHandler(c, cfg)
})
apiRouter.GET("/blacklist/status", func(c *gin.Context) {
BlackListStatusHandler(c, cfg)
})
apiRouter.GET("/cors/status", func(c *gin.Context) {
CorsStatusHandler(c, cfg)
})
apiRouter.GET("/healthcheck", func(c *gin.Context) {
HealthcheckHandler(c)
})
apiRouter.GET("/version", func(c *gin.Context) {
VersionHandler(c, version)
})
apiRouter.GET("/rate_limit/status", func(c *gin.Context) {
RateLimitStatusHandler(c, cfg)
})
apiRouter.GET("/rate_limit/limit", func(c *gin.Context) {
RateLimitLimitHandler(c, cfg)
})
}
logInfo("API router Init success")
}
func SizeLimitHandler(cfg *config.Config, c *gin.Context) {
sizeLimit := cfg.Server.SizeLimit
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"MaxResponseBodySize": sizeLimit,
})
}
func WhiteListStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Whitelist": cfg.Whitelist.Enabled,
})
}
func BlackListStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Blacklist": cfg.Blacklist.Enabled,
})
}
func CorsStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Cors": cfg.Server.Cors,
})
}
func HealthcheckHandler(c *gin.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Status": "OK",
})
}
func VersionHandler(c *gin.Context, version string) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Version": version,
})
}
func RateLimitStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"RateLimit": cfg.RateLimit.Enabled,
})
}
func RateLimitLimitHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"RatePerMinute": cfg.RateLimit.RatePerMinute,
})
}

29
auth/auth-header.go Normal file
View File

@@ -0,0 +1,29 @@
package auth
import (
"fmt"
"ghproxy/config"
"github.com/gin-gonic/gin"
)
func AuthHeaderHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
if !cfg.Auth.Enabled {
return true, ""
}
// 获取"GH-Auth"的值
authToken := c.GetHeader("GH-Auth")
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.Request.Method, c.Request.Host, c.Request.URL.Path, c.Request.Proto, c.Request.RemoteAddr, authToken)
if authToken == "" {
err := "Auth Header == nil"
return false, err
}
isValid = authToken == cfg.Auth.AuthToken
if !isValid {
err := fmt.Sprintf("Auth token incorrect: %s", authToken)
return false, err
}
return isValid, ""
}

30
auth/auth-parameters.go Normal file
View File

@@ -0,0 +1,30 @@
package auth
import (
"fmt"
"ghproxy/config"
"github.com/gin-gonic/gin"
)
func AuthParametersHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
if !cfg.Auth.Enabled {
return true, ""
}
authToken := c.Query("auth_token")
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto, authToken)
if authToken == "" {
err := "Auth token == nil"
return false, err
}
isValid = authToken == cfg.Auth.AuthToken
if !isValid {
err := fmt.Sprintf("Auth token incorrect: %s", authToken)
return false, err
}
return isValid, ""
}

View File

@@ -2,34 +2,50 @@ package auth
import (
"ghproxy/config"
"ghproxy/logger"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin"
)
var logw = logger.Logw
var (
logw = logger.Logw
LogDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
func AuthHandler(c *gin.Context, cfg *config.Config) bool {
// 如果身份验证未启用,直接返回 true
if !cfg.Auth.Enabled {
return true
func Init(cfg *config.Config) {
if cfg.Blacklist.Enabled {
err := InitBlacklist(cfg)
if err != nil {
logError(err.Error())
return
}
}
// 获取 auth_token 参数
authToken := c.Query("auth_token")
logw("auth_token received: %s", authToken)
// 验证 token
if authToken == "" {
logw("auth FAILED: no auth_token provided")
return false
if cfg.Whitelist.Enabled {
err := InitWhitelist(cfg)
if err != nil {
logError(err.Error())
return
}
}
logDebug("Auth Init")
}
func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
if cfg.Auth.AuthMethod == "parameters" {
isValid, err = AuthParametersHandler(c, cfg)
return isValid, err
} else if cfg.Auth.AuthMethod == "header" {
isValid, err = AuthHeaderHandler(c, cfg)
return isValid, err
} else if cfg.Auth.AuthMethod == "" {
logError("Auth method not set")
return true, ""
} else {
logError("Auth method not supported")
return false, "Auth method not supported"
}
isValid := authToken == cfg.Auth.AuthToken
if !isValid {
logw("auth FAILED: invalid auth_token: %s", authToken)
}
logw("auth SUCCESS: %t", isValid)
return isValid
}

View File

@@ -2,44 +2,89 @@ package auth
import (
"encoding/json"
"fmt"
"ghproxy/config"
"os"
"strings"
"sync"
)
type Config struct {
Blacklist []string `json:"blacklist"`
type Blacklist struct {
userSet map[string]struct{} // 用户级黑名单
repoSet map[string]map[string]struct{} // 仓库级黑名单
initOnce sync.Once // 确保初始化只执行一次
initialized bool // 初始化状态标识
}
var (
cfg *config.Config
blacklistfile = "/data/ghproxy/config/blacklist.json"
blacklist *Config
instance *Blacklist
initErr error
)
func LoadBlacklist(cfg *config.Config) {
blacklistfile = cfg.Blacklist.BlacklistFile
blacklist = &Config{}
data, err := os.ReadFile(blacklistfile)
if err != nil {
logw("Failed to read blacklist file: %v", err)
// InitBlacklist 初始化黑名单(线程安全,仅执行一次)
func InitBlacklist(cfg *config.Config) error {
instance = &Blacklist{
userSet: make(map[string]struct{}),
repoSet: make(map[string]map[string]struct{}),
}
err = json.Unmarshal(data, blacklist)
data, err := os.ReadFile(cfg.Blacklist.BlacklistFile)
if err != nil {
logw("Failed to unmarshal blacklist JSON: %v", err)
return fmt.Errorf("failed to read blacklist: %w", err)
}
}
func CheckBlacklist(fullrepo string) bool {
return forRangeCheck(blacklist.Blacklist, fullrepo)
}
var list struct {
Entries []string `json:"blacklist"`
}
if err := json.Unmarshal(data, &list); err != nil {
return fmt.Errorf("invalid blacklist format: %w", err)
}
func forRangeCheck(blist []string, fullrepo string) bool {
for _, blocked := range blist {
if blocked == fullrepo {
return true
for _, entry := range list.Entries {
user, repo := splitUserRepo(entry)
switch {
case repo == "" || repo == "*":
instance.userSet[user] = struct{}{}
default:
if _, exists := instance.repoSet[user]; !exists {
instance.repoSet[user] = make(map[string]struct{})
}
instance.repoSet[user][repo] = struct{}{}
}
}
instance.initialized = true
return nil
}
// CheckBlacklist 检查用户和仓库是否在黑名单中(无锁设计)
func CheckBlacklist(username, repo string) bool {
if instance == nil || !instance.initialized {
return false
}
// 先检查用户级黑名单
if _, exists := instance.userSet[username]; exists {
return true
}
// 再检查仓库级黑名单
if repos, userExists := instance.repoSet[username]; userExists {
// 允许仓库名为空时的全用户仓库匹配
if repo == "" {
return true
}
_, repoExists := repos[repo]
return repoExists
}
return false
}
// splitUserRepo 优化分割逻辑(仅初始化时使用)
func splitUserRepo(fullRepo string) (user, repo string) {
if idx := strings.Index(fullRepo, "/"); idx > 0 {
return fullRepo[:idx], fullRepo[idx+1:]
}
return fullRepo, ""
}

91
auth/whitelist.go Normal file
View File

@@ -0,0 +1,91 @@
package auth
import (
"encoding/json"
"fmt"
"ghproxy/config"
"os"
"strings"
"sync"
)
// Whitelist 用于存储白名单信息
type Whitelist struct {
userSet map[string]struct{} // 用户级白名单
repoSet map[string]map[string]struct{} // 仓库级白名单
initOnce sync.Once // 确保初始化只执行一次
initialized bool // 初始化状态标识
}
var (
whitelistInstance *Whitelist
whitelistInitErr error
)
// InitWhitelist 初始化白名单(线程安全,仅执行一次)
func InitWhitelist(cfg *config.Config) error {
whitelistInstance = &Whitelist{
userSet: make(map[string]struct{}),
repoSet: make(map[string]map[string]struct{}),
}
data, err := os.ReadFile(cfg.Whitelist.WhitelistFile)
if err != nil {
return fmt.Errorf("failed to read whitelist: %w", err)
}
var list struct {
Entries []string `json:"whitelist"`
}
if err := json.Unmarshal(data, &list); err != nil {
return fmt.Errorf("invalid whitelist format: %w", err)
}
for _, entry := range list.Entries {
user, repo := splitUserRepoWhitelist(entry)
switch {
case repo == "" || repo == "*":
whitelistInstance.userSet[user] = struct{}{}
default:
if _, exists := whitelistInstance.repoSet[user]; !exists {
whitelistInstance.repoSet[user] = make(map[string]struct{})
}
whitelistInstance.repoSet[user][repo] = struct{}{}
}
}
whitelistInstance.initialized = true
return nil
}
// CheckWhitelist 检查用户和仓库是否在白名单中(无锁设计)
func CheckWhitelist(username, repo string) bool {
if whitelistInstance == nil || !whitelistInstance.initialized {
return false
}
// 先检查用户级白名单
if _, exists := whitelistInstance.userSet[username]; exists {
return true
}
// 再检查仓库级白名单
if repos, userExists := whitelistInstance.repoSet[username]; userExists {
// 允许仓库名为空时的全用户仓库匹配
if repo == "" {
return true
}
_, repoExists := repos[repo]
return repoExists
}
return false
}
// splitUserRepoWhitelist 分割用户和仓库信息(仅初始化时使用)
func splitUserRepoWhitelist(fullRepo string) (user, repo string) {
if idx := strings.Index(fullRepo, "/"); idx > 0 {
return fullRepo[:idx], fullRepo[idx+1:]
}
return fullRepo, ""
}

View File

@@ -1,107 +0,0 @@
{
debug
http_port 80
https_port 443
order cache before rewrite
cache {
cache_name GhProxyCache
}
log {
level INFO
output file /data/caddy/log/caddy.log {
roll_size 5MB
roll_keep 10
}
}
}
(log) {
log {
format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
time_format "02/Jan/2006:15:04:05 -0700"
}
output file /data/caddy/log/{args[0]}/access.log {
roll_size 5MB
roll_keep 10
roll_keep_for 24h
}
}
}
(error_page) {
handle_errors {
rewrite * /{err.status_code}.html
root * /data/caddy/pages/errors
file_server
}
}
(encode) {
encode {
zstd best
br 5 v2
gzip 5
minimum_length 512
}
}
(cache) {
cache {
allowed_http_verbs GET
stale {args[0]}
ttl {args[1]}
}
}
(header_realip) {
header_up X-Real-IP {remote_host}
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
}
(rate_limit) {
route /* {
rate_limit {remote.ip} {args[0]}r/m 10000 429
}
}
:80 {
reverse_proxy {
to 127.0.0.1:8080
import header_realip
}
import log ghproxy
import cache 0s 300s
import error_page
import encode
import rate_limit 60
route / {
root /data/www
file_server
import cache 0s 24h
}
route /favicon.ico {
root /data/www
file_server
import cache 0s 24h
}
handle_errors {
@redirects `{err.status_code} in [301, 302, 307]`
reverse_proxy @redirects {
header_up Location {http.response.header.Location}
}
}
route /v2* {
reverse_proxy https://registry-1.docker.io {
header_up Host registry-1.docker.io
header_up X-Real-IP {remote}
header_up X-Forwarded-For {http.request.header.X-Forwarded-For}
header_up X-Forwarded-Proto {scheme}
header_up Authorization {http.request.header.Authorization}
}
}
}
import /data/caddy/config.d/*

View File

@@ -1,93 +0,0 @@
{
debug
http_port 80
https_port 443
order cache before rewrite
cache {
cache_name GhProxyCache
}
log {
level INFO
output file /data/caddy/log/caddy.log {
roll_size 5MB
roll_keep 10
}
}
}
(log) {
log {
format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
time_format "02/Jan/2006:15:04:05 -0700"
}
output file /data/caddy/log/{args[0]}/access.log {
roll_size 5MB
roll_keep 10
roll_keep_for 24h
}
}
}
(error_page) {
handle_errors {
rewrite * /{err.status_code}.html
root * /data/caddy/pages/errors
file_server
}
}
(encode) {
encode {
zstd best
br 5 v2
gzip 5
minimum_length 512
}
}
(cache) {
cache {
allowed_http_verbs GET
stale {args[0]}
ttl {args[1]}
}
}
(header_realip) {
header_up X-Real-IP {remote_host}
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
}
(rate_limit) {
route /* {
rate_limit {remote.ip} {args[0]}r/m 10000 429
}
}
:80 {
reverse_proxy {
to 127.0.0.1:8080
import header_realip
}
import log ghproxy
import cache 0s 300s
import error_page
import encode
route /* {
rate_limit {remote.ip} 60r/m 10000 429
}
route / {
root /data/www
file_server
import cache 0s 24h
}
route /favicon.ico {
root /data/www
file_server
import cache 0s 24h
}
}
import /data/caddy/config.d/*

View File

@@ -1,8 +1,7 @@
{
"blacklist": [
"test/test1",
"example/repo2",
"another/repo3"
]
}
"blacklist": [
"eviluser",
"spamuser/bad-repo",
"malwareuser/*"
]
}

View File

@@ -1,52 +1,124 @@
package config
import (
"os"
"gopkg.in/yaml.v3"
"github.com/BurntSushi/toml"
)
type Config struct {
Server struct {
Port int `yaml:"port"`
Host string `yaml:"host"`
SizeLimit int `yaml:"sizelimit"`
} `yaml:"server"`
Log struct {
LogFilePath string `yaml:"logfilepath"`
MaxLogSize int `yaml:"maxlogsize"`
} `yaml:"logger"`
CORS struct {
Enabled bool `yaml:"enabled"`
} `yaml:"cors"`
Auth struct {
Enabled bool `yaml:"enabled"`
AuthToken string `yaml:"authtoken"`
} `yaml:"auth"`
Blacklist struct {
Enabled bool `yaml:"enabled"`
BlacklistFile string `yaml:"blacklistfile"`
} `yaml:"blacklist"`
Server ServerConfig
Httpc HttpcConfig
GitClone GitCloneConfig
Pages PagesConfig
Log LogConfig
Auth AuthConfig
Blacklist BlacklistConfig
Whitelist WhitelistConfig
RateLimit RateLimitConfig
Outbound OutboundConfig
}
// LoadConfig 从 YAML 配置文件加载配置
/*
[server]
host = "0.0.0.0" # 监听地址
port = 8080 # 监听端口
sizeLimit = 125 # 125MB
H2C = true # 是否开启H2C传输
enableH2C = "on" # 是否开启H2C传输(latest和dev版本请开启) on/off (2.4.0弃用)
*/
type ServerConfig struct {
Port int `toml:"port"`
Host string `toml:"host"`
SizeLimit int `toml:"sizeLimit"`
H2C bool `toml:"H2C"`
Cors string `toml:"cors"`
EnableH2C string `toml:"enableH2C"`
Debug bool `toml:"debug"`
}
/*
[httpc]
mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode
*/
type HttpcConfig struct {
Mode string `toml:"mode"`
MaxIdleConns int `toml:"maxIdleConns"`
MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"`
MaxConnsPerHost int `toml:"maxConnsPerHost"`
}
/*
[gitclone]
mode = "bypass" # bypass / cache
smartGitAddr = ":8080"
*/
type GitCloneConfig struct {
Mode string `toml:"mode"`
SmartGitAddr string `toml:"smartGitAddr"`
}
/*
[pages]
mode = "internal" # "internal" or "external"
enabled = false
theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/data/www"
*/
type PagesConfig struct {
Mode string `toml:"mode"`
Enabled bool `toml:"enabled"`
Theme string `toml:"theme"`
StaticDir string `toml:"staticDir"`
}
type LogConfig struct {
LogFilePath string `toml:"logFilePath"`
MaxLogSize int `toml:"maxLogSize"`
Level string `toml:"level"`
}
type AuthConfig struct {
Enabled bool `toml:"enabled"`
AuthMethod string `toml:"authMethod"`
AuthToken string `toml:"authToken"`
PassThrough bool `toml:"passThrough"`
}
type BlacklistConfig struct {
Enabled bool `toml:"enabled"`
BlacklistFile string `toml:"blacklistFile"`
}
type WhitelistConfig struct {
Enabled bool `toml:"enabled"`
WhitelistFile string `toml:"whitelistFile"`
}
type RateLimitConfig struct {
Enabled bool `toml:"enabled"`
RateMethod string `toml:"rateMethod"`
RatePerMinute int `toml:"ratePerMinute"`
Burst int `toml:"burst"`
}
/*
[outbound]
enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
*/
type OutboundConfig struct {
Enabled bool `toml:"enabled"`
Url string `toml:"url"`
}
// LoadConfig 从 TOML 配置文件加载配置
func LoadConfig(filePath string) (*Config, error) {
var config Config
if err := loadYAML(filePath, &config); err != nil {
if _, err := toml.DecodeFile(filePath, &config); err != nil {
return nil, err
}
return &config, nil
}
// LoadyamlConfig 从 YAML 配置文件加载配置
func loadYAML(filePath string, out interface{}) error {
data, err := os.ReadFile(filePath)
if err != nil {
return err
}
return yaml.Unmarshal(data, out)
}

51
config/config.toml Normal file
View File

@@ -0,0 +1,51 @@
[server]
host = "0.0.0.0"
port = 8080
sizeLimit = 125 # MB
H2C = true
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false
[httpc]
mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode
[gitclone]
mode = "bypass" # bypass / cache
smartGitAddr = "http://127.0.0.1:8080"
[pages]
mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/data/www"
[log]
logFilePath = "/data/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB
level = "info" # dump, debug, info, warn, error, none
[auth]
authMethod = "parameters" # "header" or "parameters"
authToken = "token"
enabled = false
passThrough = false
[blacklist]
blacklistFile = "/data/ghproxy/config/blacklist.json"
enabled = false
[whitelist]
enabled = false
whitelistFile = "/data/ghproxy/config/whitelist.json"
[rateLimit]
enabled = false
rateMethod = "total" # "ip" or "total"
ratePerMinute = 180
burst = 5
[outbound]
enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"

View File

@@ -1,24 +0,0 @@
# Server Configuration
server:
port: 8080
host: "127.0.0.1"
sizelimit: 131072000 # 125MB
# Logging Configuration
logger:
logfilepath: "/data/ghproxy/log/ghproxy.log"
maxlogsize: 5 # MB
# CORS Configuration
cors:
enabled: true
# Authentication Configuration
auth:
enabled: false
authtoken: "test"
# Blacklist Configuration
blacklist:
enabled: true
blacklistfile: "/data/ghproxy/config/blacklist.json"

7
config/whitelist.json Normal file
View File

@@ -0,0 +1,7 @@
{
"whitelist": [
"white/list",
"white/test1",
"example/*"
]
}

51
deploy/config.toml Normal file
View File

@@ -0,0 +1,51 @@
[server]
host = "127.0.0.1"
port = 8080
sizeLimit = 125 # MB
H2C = true
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false
[httpc]
mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode
[gitclone]
mode = "bypass" # bypass / cache
smartGitAddr = "http://127.0.0.1:8080"
[pages]
mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/usr/local/ghproxy/pages"
[log]
logFilePath = "/usr/local/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB
level = "info" # dump, debug, info, warn, error, none
[auth]
authMethod = "parameters" # "header" or "parameters"
authToken = "token"
enabled = false
passThrough = false
[blacklist]
blacklistFile = "/usr/local/ghproxy/config/blacklist.json"
enabled = false
[whitelist]
enabled = false
whitelistFile = "/usr/local/ghproxy/config/whitelist.json"
[rateLimit]
enabled = false
rateMethod = "total" # "ip" or "total"
ratePerMinute = 180
burst = 5
[outbound]
enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"

13
deploy/ghproxy.service Normal file
View File

@@ -0,0 +1,13 @@
[Unit]
Description=Github Proxy Service
After=network.target
[Service]
ExecStart=/bin/bash -c '/usr/local/ghproxy/ghproxy -cfg /usr/local/ghproxy/config/config.toml > /usr/local/ghproxy/log/run.log 2>&1'
WorkingDirectory=/usr/local/ghproxy
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target

144
deploy/install-dev.sh Normal file
View File

@@ -0,0 +1,144 @@
# /bin/bash
# https://github.com/WJQSERVER-STUDIO/ghproxy
ghproxy_dir="/usr/local/ghproxy"
# install packages
install() {
if [ $# -eq 0 ]; then
echo "ARGS NOT FOUND"
return 1
fi
for package in "$@"; do
if ! command -v "$package" &>/dev/null; then
if command -v dnf &>/dev/null; then
dnf -y update && dnf install -y "$package"
elif command -v yum &>/dev/null; then
yum -y update && yum -y install "$package"
elif command -v apt &>/dev/null; then
apt update -y && apt install -y "$package"
elif command -v apk &>/dev/null; then
apk update && apk add "$package"
else
echo "UNKNOWN PACKAGE MANAGER"
return 1
fi
fi
done
return 0
}
# 检查是否为root用户
if [ "$EUID" -ne 0 ]; then
echo "请以root用户运行此脚本"
exit 1
fi
# 安装依赖包
install curl wget sed
# 查看当前架构是否为linux/amd64或linux/arm64
ARCH=$(uname -m)
if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then
echo " $ARCH 架构不被支持"
exit 1
fi
# 重写架构值,改为amd64或arm64
if [ "$ARCH" == "x86_64" ]; then
ARCH="amd64"
elif [ "$ARCH" == "aarch64" ]; then
ARCH="arm64"
fi
# 获取监听端口
read -p "请输入程序监听的端口(默认8080): " PORT
if [ -z "$PORT" ]; then
PORT=8080
fi
# 本机监听/泛监听(127.0.0.1/0.0.0.0)
read -p "请键入程序监听的IP(默认127.0.0.1)(0.0.0.0为泛监听): " IP
if [ -z "$IP" ]; then
IP="127.0.0.1"
fi
# 安装目录
read -p "请输入安装目录(默认/usr/local/ghproxy): " ghproxy_dir
if [ -z "$ghproxy_dir" ]; then
ghproxy_dir="/usr/local/ghproxy"
fi
# 创建目录
mkdir -p ${ghproxy_dir}
mkdir -p ${ghproxy_dir}/config
mkdir -p ${ghproxy_dir}/log
mkdir -p ${ghproxy_dir}/pages
# 获取最新版本号
VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/DEV-VERSION)
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/DEV-VERSION
# 下载ghproxy
wget -q -O ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/$VERSION/ghproxy-linux-$ARCH.tar.gz
install tar
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
chmod +x ${ghproxy_dir}/ghproxy
# 下载pages
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/pages/bootstrap/index.html
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/pages/bootstrap/favicon.ico
# 下载配置文件
if [ -f ${ghproxy_dir}/config/config.toml ]; then
echo "配置文件已存在, 跳过下载"
echo "[WARNING] 请检查配置文件是否正确DEV版本升级时请注意配置文件兼容性"
sleep 2
else
wget -q -O ${ghproxy_dir}/config/config.toml https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/config.toml
fi
# 替换 port = 8080
sed -i "s/port = 8080/port = $PORT/g" ${ghproxy_dir}/config/config.toml
sed -i 's/host = "127.0.0.1"/host = "'"$IP"'"/g' ${ghproxy_dir}/config/config.toml
sed -i "s|staticDir = \"/usr/local/ghproxy/pages\"|staticDir = \"${ghproxy_dir}/pages\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|logFilePath = \"/usr/local/ghproxy/log/ghproxy.log\"|logFilePath = \"${ghproxy_dir}/log/ghproxy.log\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|blacklistFile = \"/usr/local/ghproxy/config/blacklist.json\"|blacklistFile = \"${ghproxy_dir}/config/blacklist.json\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelistFile = \"${ghproxy_dir}/config/whitelist.json\"|g" ${ghproxy_dir}/config/config.toml
# 下载systemd服务文件
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/ghproxy.service
else
cat <<EOF > /etc/systemd/system/ghproxy.service
[Unit]
Description=Github Proxy Service
After=network.target
[Service]
ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -cfg $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1'
WorkingDirectory=$ghproxy_dir
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target
EOF
fi
# 启动ghproxy
systemctl daemon-reload
systemctl enable ghproxy
systemctl start ghproxy
echo "ghproxy 安装成功, 监听端口为 $PORT"

144
deploy/install.sh Normal file
View File

@@ -0,0 +1,144 @@
# /bin/bash
# https://github.com/WJQSERVER-STUDIO/ghproxy
ghproxy_dir="/usr/local/ghproxy"
# install packages
install() {
if [ $# -eq 0 ]; then
echo "ARGS NOT FOUND"
return 1
fi
for package in "$@"; do
if ! command -v "$package" &>/dev/null; then
if command -v dnf &>/dev/null; then
dnf -y update && dnf install -y "$package"
elif command -v yum &>/dev/null; then
yum -y update && yum -y install "$package"
elif command -v apt &>/dev/null; then
apt update -y && apt install -y "$package"
elif command -v apk &>/dev/null; then
apk update && apk add "$package"
else
echo "UNKNOWN PACKAGE MANAGER"
return 1
fi
fi
done
return 0
}
# 检查是否为root用户
if [ "$EUID" -ne 0 ]; then
echo "请以root用户运行此脚本"
exit 1
fi
# 安装依赖包
install curl wget sed
# 查看当前架构是否为linux/amd64或linux/arm64
ARCH=$(uname -m)
if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then
echo " $ARCH 架构不被支持"
exit 1
fi
# 重写架构值,改为amd64或arm64
if [ "$ARCH" == "x86_64" ]; then
ARCH="amd64"
elif [ "$ARCH" == "aarch64" ]; then
ARCH="arm64"
fi
# 获取监听端口
read -p "请输入程序监听的端口(默认8080): " PORT
if [ -z "$PORT" ]; then
PORT=8080
fi
# 本机监听/泛监听(127.0.0.1/0.0.0.0)
read -p "请键入程序监听的IP(默认127.0.0.1)(0.0.0.0为泛监听): " IP
if [ -z "$IP" ]; then
IP="127.0.0.1"
fi
# 安装目录
read -p "请输入安装目录(默认/usr/local/ghproxy): " ghproxy_dir
if [ -z "$ghproxy_dir" ]; then
ghproxy_dir="/usr/local/ghproxy"
fi
# 创建目录
mkdir -p ${ghproxy_dir}
mkdir -p ${ghproxy_dir}/config
mkdir -p ${ghproxy_dir}/log
mkdir -p ${ghproxy_dir}/pages
# 获取最新版本号
VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/VERSION)
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/VERSION
# 下载ghproxy
wget -q -O ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/${VERSION}/ghproxy-linux-${ARCH}.tar.gz
install tar
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
chmod +x ${ghproxy_dir}/ghproxy
# 下载pages
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/bootstrap/index.html
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/bootstrap/favicon.ico
# 下载配置文件
if [ -f ${ghproxy_dir}/config/config.toml ]; then
echo "配置文件已存在, 跳过下载"
echo "[WARNING] 请检查配置文件是否正确DEV版本升级时请注意配置文件兼容性"
sleep 2
else
wget -q -O ${ghproxy_dir}/config/config.toml https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/config.toml
fi
# 替换 port = 8080
sed -i "s/port = 8080/port = $PORT/g" ${ghproxy_dir}/config/config.toml
sed -i 's/host = "127.0.0.1"/host = "'"$IP"'"/g' ${ghproxy_dir}/config/config.toml
sed -i "s|staticDir = \"/usr/local/ghproxy/pages\"|staticDir = \"${ghproxy_dir}/pages\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|logFilePath = \"/usr/local/ghproxy/log/ghproxy.log\"|logFilePath = \"${ghproxy_dir}/log/ghproxy.log\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|blacklistFile = \"/usr/local/ghproxy/config/blacklist.json\"|blacklistFile = \"${ghproxy_dir}/config/blacklist.json\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelistFile = \"${ghproxy_dir}/config/whitelist.json\"|g" ${ghproxy_dir}/config/config.toml
# 下载systemd服务文件
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/ghproxy.service
else
cat <<EOF > /etc/systemd/system/ghproxy.service
[Unit]
Description=Github Proxy Service
After=network.target
[Service]
ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -cfg $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1'
WorkingDirectory=$ghproxy_dir
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target
EOF
fi
# 启动ghproxy
systemctl daemon-reload
systemctl enable ghproxy
systemctl start ghproxy
echo "ghproxy 安装成功, 监听端口为 $PORT"

27
deploy/uninstall.sh Normal file
View File

@@ -0,0 +1,27 @@
# /bin/bash
# 停止 ghproxy 服务
systemctl stop ghproxy
# 删除 ghproxy 服务
systemctl disable ghproxy
rm /etc/systemd/system/ghproxy.service
# 获取安装文件夹
read -p "请输入 ghproxy 安装文件夹路径(默认 /usr/local/ghproxy): " install_path
if [ -z "$install_path" ]; then
install_path="/usr/local/ghproxy"
fi
# 删除 ghproxy 文件夹
# 检查目录是否存在ghproxy文件
if [ -f "$install_path" ]; then
echo "ghproxy 未安装或安装路径错误"
exit 1
else
echo "ghproxy 安装目录已确认,正在卸载..."
rm -r $install_path
fi
echo "ghproxy 已成功卸载"

View File

@@ -8,4 +8,4 @@ services:
- './ghproxy/log/caddy:/data/caddy/log'
- './ghproxy/config:/data/ghproxy/config'
ports:
- '7210:80'
- '7210:8080'

View File

@@ -1,22 +1,47 @@
FROM wjqserver/caddy:daily
FROM alpine:latest AS builder
ARG USER=WJQSERVER-STUDIO
ARG REPO=ghproxy
ARG APPLICATION=ghproxy
ARG TARGETOS
ARG TARGETARCH
ARG TARGETPLATFORM
# 创建文件夹
RUN mkdir -p /data/www
RUN mkdir -p /data/${APPLICATION}/config
RUN mkdir -p /data/${APPLICATION}/log
RUN wget -O /data/www/index.html https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/index.html
RUN wget -O /data/www/favicon.ico https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/favicon.ico
RUN wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/release/Caddyfile
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/main/DEV-VERSION) && \
wget -O /data/${APPLICATION}/${APPLICATION} https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}
RUN wget -O /data/${APPLICATION}/config.yaml https://raw.githubusercontent.com/${USER}/${REPO}/main/config/config.yaml
RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/blacklist.json
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/init.sh
# 安装依赖
RUN apk add --no-cache curl wget tar
# 后端
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/dev/DEV-VERSION) && \
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \
tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \
rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/dev/docker/dockerfile/dev/init.sh
# 拉取配置
#RUN wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/dev/caddyfile/dev/Caddyfile
RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/dev/config/config.toml
RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/dev/config/blacklist.json
RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/dev/config/whitelist.json
# 权限
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh
FROM alpine:latest
RUN apk add --no-cache curl
COPY --from=builder /data/www /data/www
COPY --from=builder /data/${APPLICATION} /data/${APPLICATION}
COPY --from=builder /usr/local/bin/init.sh /usr/local/bin/init.sh
# 权限
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh
CMD ["/usr/local/bin/init.sh"]

View File

@@ -0,0 +1,17 @@
#!/bin/sh
APPLICATION=ghproxy
if [ ! -f /data/${APPLICATION}/config/blacklist.json ]; then
cp /data/${APPLICATION}/blacklist.json /data/${APPLICATION}/config/blacklist.json
fi
if [ ! -f /data/${APPLICATION}/config/whitelist.json ]; then
cp /data/${APPLICATION}/whitelist.json /data/${APPLICATION}/config/whitelist.json
fi
if [ ! -f /data/${APPLICATION}/config/config.toml ]; then
cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml
fi
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1

View File

@@ -1,20 +1,47 @@
FROM wjqserver/caddy:latest
FROM alpine:latest AS builder
ARG USER=WJQSERVER-STUDIO
ARG REPO=ghproxy
ARG APPLICATION=ghproxy
ARG TARGETOS
ARG TARGETARCH
ARG TARGETPLATFORM
# 创建文件夹
RUN mkdir -p /data/www
RUN mkdir -p /data/${APPLICATION}/config
RUN mkdir -p /data/${APPLICATION}/log
RUN wget -O /data/www/index.html https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/index.html
RUN wget -O /data/www/favicon.ico https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/favicon.ico
RUN wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/release/Caddyfile
# 安装依赖
RUN apk add --no-cache curl wget tar
# 后端
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/main/VERSION) && \
wget -O /data/${APPLICATION}/${APPLICATION} https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}
RUN wget -O /data/${APPLICATION}/config.yaml https://raw.githubusercontent.com/${USER}/${REPO}/main/config/config.yaml
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \
tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \
rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/docker/dockerfile/release/init.sh
# 拉取配置
#RUN wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/release/Caddyfile
RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/main/config/config.toml
RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/blacklist.json
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/init.sh
RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/whitelist.json
# 权限
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh
FROM alpine:latest
RUN apk add --no-cache curl
COPY --from=builder /data/www /data/www
#COPY --from=builder /data/caddy /data/caddy
COPY --from=builder /data/${APPLICATION} /data/${APPLICATION}
COPY --from=builder /usr/local/bin/init.sh /usr/local/bin/init.sh
# 权限
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh

View File

@@ -0,0 +1,17 @@
#!/bin/sh
APPLICATION=ghproxy
if [ ! -f /data/${APPLICATION}/config/blacklist.json ]; then
cp /data/${APPLICATION}/blacklist.json /data/${APPLICATION}/config/blacklist.json
fi
if [ ! -f /data/${APPLICATION}/config/whitelist.json ]; then
cp /data/${APPLICATION}/whitelist.json /data/${APPLICATION}/config/whitelist.json
fi
if [ ! -f /data/${APPLICATION}/config/config.toml ]; then
cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml
fi
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1

120
gitclone/git-client.go Normal file
View File

@@ -0,0 +1,120 @@
package gitclone
import (
"archive/tar"
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/pierrec/lz4"
)
func CloneRepo(dir string, repoName string, repoUrl string) error {
repoPath := dir
_, err := git.PlainClone(repoPath, true, &git.CloneOptions{
URL: repoUrl,
Progress: os.Stdout,
Mirror: true,
})
if err != nil && !errors.Is(err, git.ErrRepositoryAlreadyExists) {
fmt.Printf("Fail to clone: %v\n", err)
} else if err != nil && errors.Is(err, git.ErrRepositoryAlreadyExists) {
// 移除文件夹
fmt.Printf("Repository already exists\n")
err = os.RemoveAll(repoPath)
if err != nil {
fmt.Printf("Fail to remove: %v\n", err)
return err
}
_, err = git.PlainClone(repoPath, true, &git.CloneOptions{
URL: repoUrl,
Progress: os.Stdout,
Mirror: true,
})
if err != nil {
fmt.Printf("Fail to clone: %v\n", err)
return err
}
}
// 压缩
err = CompressRepo(repoPath)
if err != nil {
fmt.Printf("Fail to compress: %v\n", err)
return err
}
return nil
}
// CompressRepo 将指定的仓库压缩成 LZ4 格式的压缩包
func CompressRepo(repoPath string) error {
lz4File, err := os.Create(repoPath + ".lz4")
if err != nil {
return fmt.Errorf("failed to create LZ4 file: %w", err)
}
defer lz4File.Close()
// 创建 LZ4 编码器
lz4Writer := lz4.NewWriter(lz4File)
defer lz4Writer.Close()
// 创建 tar.Writer
tarBuffer := new(bytes.Buffer)
tarWriter := tar.NewWriter(tarBuffer)
// 遍历仓库目录并打包
err = filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 创建 tar 文件头
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
header.Name, err = filepath.Rel(repoPath, path)
if err != nil {
return err
}
// 写入 tar 文件头
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
// 如果是文件,写入文件内容
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(tarWriter, file)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return fmt.Errorf("failed to walk through repo directory: %w", err)
}
// 关闭 tar.Writer
if err := tarWriter.Close(); err != nil {
return fmt.Errorf("failed to close tar writer: %w", err)
}
// 将 tar 数据写入 LZ4 压缩包
if _, err := lz4Writer.Write(tarBuffer.Bytes()); err != nil {
return fmt.Errorf("failed to write to LZ4 file: %w", err)
}
return nil
}

14
gitclone/gitclone.go Normal file
View File

@@ -0,0 +1,14 @@
package gitclone
import (
"github.com/WJQSERVER-STUDIO/go-utils/logger"
)
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)

164
gitclone/smart-http.go Normal file
View File

@@ -0,0 +1,164 @@
package gitclone
/*
package gitclone
import (
"compress/gzip"
"ghproxy/config"
"io"
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5/plumbing/format/pktline"
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/server"
)
// MIT https://github.com/erred/gitreposerver
// httpInfoRefs 函数处理 /info/refs 请求,用于 Git 客户端获取仓库的引用信息。
// 返回一个 gin.HandlerFunc 类型的处理函数。
func HttpInfoRefs(cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
repo := c.Param("repo") // 从 Gin 上下文中获取路由参数 "repo",即仓库名
username := c.Param("username")
repoName := repo
dir := cfg.GitClone.Dir + "/" + username + "/" + repo
url := "https://github.com/" + username + "/" + repo
// 输出 repo user dir url
logInfo("Repo: %s, User: %s, Dir: %s, Url: %s\n", repoName, username, dir, url)
_, err := os.Stat(dir) // 检查目录是否存在
if os.IsNotExist(err) {
CloneRepo(dir, repoName, url)
}
// 检查请求参数 "service" 是否为 "git-upload-pack"。
// 这是为了确保只处理 smart git 的 upload-pack 服务请求。
if c.Query("service") != "git-upload-pack" {
c.String(http.StatusForbidden, "only smart git") // 如果 service 参数不正确,返回 403 Forbidden 状态码和错误信息
log.Printf("Request to /info/refs with invalid service: %s, repo: %s\n", c.Query("service"), repoName) // 记录无效 service 参数的日志
return // 结束处理
}
c.Header("content-type", "application/x-git-upload-pack-advertisement") // 设置 HTTP 响应头的 Content-Type 为 advertisement 类型。
// 这种类型用于告知客户端服务器支持的 Git 服务。
ep, err := transport.NewEndpoint("/") // 创建一个新的传输端点 (Endpoint)。这里使用根路径 "/" 作为端点,表示本地文件系统。
if err != nil { // 检查创建端点是否出错
log.Printf("Error creating endpoint: %v, repo: %s\n", err, repoName) // 记录创建端点错误日志
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
return // 结束处理
}
bfs := osfs.New(dir) // 创建一个基于本地文件系统的 billy Filesystem (bfs)。dir 变量指定了仓库的根目录。
ld := server.NewFilesystemLoader(bfs) // 创建一个基于文件系统的仓库加载器 (Loader)。Loader 负责从文件系统中加载仓库。
svr := server.NewServer(ld) // 创建一个新的 Git 服务器 (Server)。Server 负责处理 Git 服务请求。
sess, err := svr.NewUploadPackSession(ep, nil) // 创建一个新的 upload-pack 会话 (Session)。Session 用于处理客户端的 upload-pack 请求。
if err != nil { // 检查创建会话是否出错
log.Printf("Error creating upload pack session: %v, repo: %s\n", err, repoName) // 记录创建会话错误日志
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
return // 结束处理
}
ar, err := sess.AdvertisedReferencesContext(c.Request.Context()) // 获取已通告的引用 (Advertised References)。Advertised References 包含了仓库的分支、标签等信息。
if err != nil { // 检查获取 Advertised References 是否出错
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
log.Printf("Error getting advertised references: %v, repo: %s\n", err, repoName) // 记录获取 Advertised References 错误日志
return // 结束处理
}
// 设置 Advertised References 的前缀 (Prefix)。
// Prefix 通常包含 # service=git-upload-pack 和 pktline.Flush。
// # service=git-upload-pack 用于告知客户端服务器提供的是 upload-pack 服务。
// pktline.Flush 用于在 pkt-line 格式中发送 flush-pkt。
ar.Prefix = [][]byte{
[]byte("# service=git-upload-pack"), // 服务类型声明
pktline.Flush, // pkt-line flush 信号
}
err = ar.Encode(c.Writer) // 将 Advertised References 编码并写入 HTTP 响应。使用 pkt-line 格式进行编码。
if err != nil { // 检查编码和写入是否出错
log.Printf("Error encoding advertised references: %v, repo: %s\n", err, repoName) // 记录编码错误日志
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
return // 结束处理
}
}
}
// httpGitUploadPack 函数处理 /git-upload-pack 请求,用于处理 Git 客户端的推送 (push) 操作。
// 返回一个 gin.HandlerFunc 类型的处理函数。
func HttpGitUploadPack(cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
repo := c.Param("repo") // 从 Gin 上下文中获取路由参数 "repo",即仓库名
username := c.Param("username")
repoName := repo
dir := cfg.GitClone.Dir + "/" + username + "/" + repo
c.Header("content-type", "application/x-git-upload-pack-result") // 设置 HTTP 响应头的 Content-Type 为 result 类型。
// 这种类型用于返回 upload-pack 操作的结果。
var bodyReader io.Reader = c.Request.Body // 初始化 bodyReader 为 HTTP 请求的 body。用于读取客户端发送的数据。
// 检查请求头 "Content-Encoding" 是否为 "gzip"。
// 如果是 gzip则需要使用 gzip 解压缩请求 body。
if c.GetHeader("Content-Encoding") == "gzip" {
gzipReader, err := gzip.NewReader(c.Request.Body) // 创建一个新的 gzip Reader用于解压缩请求 body。
if err != nil { // 检查创建 gzip Reader 是否出错
log.Printf("Error creating gzip reader: %v, repo: %s\n", err, repoName) // 记录创建 gzip Reader 错误日志
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
return // 结束处理
}
defer gzipReader.Close() // 延迟关闭 gzip Reader确保资源释放
bodyReader = gzipReader // 将 bodyReader 替换为 gzip Reader后续从 gzip Reader 中读取数据
}
upr := packp.NewUploadPackRequest() // 创建一个新的 UploadPackRequest 对象。UploadPackRequest 用于解码客户端发送的 upload-pack 请求数据。
err := upr.Decode(bodyReader) // 解码请求 body 中的数据到 UploadPackRequest 对象中。使用 packp 协议格式进行解码。
if err != nil { // 检查解码是否出错
log.Printf("Error decoding upload pack request: %v, repo: %s\n", err, repoName) // 记录解码错误日志
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
return // 结束处理
}
ep, err := transport.NewEndpoint("/") // 创建一个新的传输端点 (Endpoint)。这里使用根路径 "/" 作为端点,表示本地文件系统。
if err != nil { // 检查创建端点是否出错
log.Printf("Error creating endpoint: %v, repo: %s\n", err, repoName) // 记录创建端点错误日志
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
return // 结束处理
}
bfs := osfs.New(dir) // 创建一个基于本地文件系统的 billy Filesystem (bfs)。dir 变量指定了仓库的根目录。
ld := server.NewFilesystemLoader(bfs) // 创建一个基于文件系统的仓库加载器 (Loader)。Loader 负责从文件系统中加载仓库。
svr := server.NewServer(ld) // 创建一个新的 Git 服务器 (Server)。Server 负责处理 Git 服务请求。
sess, err := svr.NewUploadPackSession(ep, nil) // 创建一个新的 upload-pack 会话 (Session)。Session 用于处理客户端的 upload-pack 请求。
if err != nil { // 检查创建会话是否出错
log.Printf("Error creating upload pack session: %v, repo: %s\n", err, repoName) // 记录创建会话错误日志
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
return // 结束处理
}
res, err := sess.UploadPack(c.Request.Context(), upr) // 处理 upload-pack 请求,执行实际的仓库推送操作。
// sess.UploadPack 函数接收 context 和 UploadPackRequest 对象作为参数,返回 UploadPackResult 和 error。
if err != nil { // 检查 UploadPack 操作是否出错
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
log.Printf("Error during upload pack: %v, repo: %s\n", err, repoName) // 记录 UploadPack 操作错误日志
return // 结束处理
}
err = res.Encode(c.Writer) // 将 UploadPackResult 编码并写入 HTTP 响应。使用 pkt-line 格式进行编码。
if err != nil { // 检查编码和写入是否出错
log.Printf("Error encoding upload pack result: %v, repo: %s\n", err, repoName) // 记录编码错误日志
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
return // 结束处理
}
}
}
*/

81
go.mod
View File

@@ -1,50 +1,61 @@
module ghproxy
go 1.23.2
go 1.24.1
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudflare/circl v1.4.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/BurntSushi/toml v1.4.0
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0
github.com/gin-gonic/gin v1.10.0
github.com/go-git/go-git/v5 v5.14.0
github.com/pierrec/lz4 v2.6.1+incompatible
github.com/satomitouka/touka-httpc v0.3.3
golang.org/x/net v0.37.0
golang.org/x/time v0.11.0
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 // indirect
github.com/bytedance/sonic v1.13.1 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/frankban/quicktest v1.14.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/imroc/req/v3 v3.46.1 // indirect
github.com/go-playground/validator/v10 v10.25.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.47.0 // indirect
github.com/refraction-networking/utls v1.6.7 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/tools v0.25.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

218
go.sum
View File

@@ -1,51 +1,93 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg=
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 h1:gJEQspQPB527Vp2FPcdOrynQEj3YYtrg1ixVSB/JvZM=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 h1:Uk4N7Sh4OPth3am3xVv17JlAm7tsna97ZLQRpQj7r5c=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/imroc/req/v3 v3.46.1 h1:oahr2hBTb3AaFI4P6jkN0Elj2ZVKJcdQ/IjWqeIKjvc=
github.com/imroc/req/v3 v3.46.1/go.mod h1:weam9gmyb00QnOtu6HXSnk44dNFkIUQb5QdMx13FeUU=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -55,68 +97,90 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y=
github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E=
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/satomitouka/touka-httpc v0.3.3 h1:Th0uJ5do3oqqZgdUDtqD1SH11x8TcJmrwHMJQlEIKCg=
github.com/satomitouka/touka-httpc v0.3.3/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/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-20190902080502-41f04d3bba15/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/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

16
init.sh
View File

@@ -2,23 +2,17 @@
APPLICATON=ghproxy
if [ ! -f /data/caddy/config/Caddyfile ]; then
cp /data/caddy/Caddyfile /data/caddy/config/Caddyfile
fi
if [ ! -f /data/${APPLICATON}/config/blacklist.json ]; then
cp /data/${APPLICATON}/blacklist.json /data/${APPLICATON}/config/blacklist.json
fi
if [ ! -f /data/${APPLICATON}/config/whitelist.json ]; then
cp /data/${APPLICATON}/whitelist.json /data/${APPLICATON}/config/whitelist.json
fi
if [ ! -f /data/${APPLICATON}/config/config.yaml ]; then
cp /data/${APPLICATON}/config.yaml /data/${APPLICATON}/config/config.yaml
fi
/data/caddy/caddy run --config /data/caddy/config/Caddyfile > /data/${APPLICATON}/log/caddy.log 2>&1 &
/data/${APPLICATON}/${APPLICATON} > /data/ghproxy/log/run.log 2>&1 &
while [[ true ]]; do
sleep 1
done
/data/${APPLICATON}/${APPLICATON} > /data/${APPLICATON}/log/run.log 2>&1

View File

@@ -1,159 +0,0 @@
package logger
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sync"
"time"
)
var (
logw = Logw
logFile *os.File
logger *log.Logger
logChannel = make(chan string, 100)
quitChannel = make(chan struct{})
logFileMutex sync.Mutex // 保护 logFile 的互斥锁
)
// Init 初始化日志记录器,接受日志文件路径作为参数
func Init(logFilePath string, maxLogsize int) error {
logFileMutex.Lock()
defer logFileMutex.Unlock()
var err error
logFile, err = os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return err
}
logger = log.New(logFile, "", 0)
go logWorker()
go monitorLogSize(logFilePath, maxLogsize)
return nil
}
// logWorker 处理日志记录
func logWorker() {
for {
select {
case msg := <-logChannel:
timestamp := time.Now().Format("02/Jan/2006:15:04:05 -0700")
logger.Println(timestamp + " - " + msg)
case <-quitChannel:
return
}
}
}
// Log 直接记录日志的函数
func Log(customMessage string) {
logChannel <- customMessage
}
// Logw 用于格式化日志记录
func Logw(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
Log(message)
}
// Close 关闭日志文件
func Close() {
logFileMutex.Lock()
defer logFileMutex.Unlock()
if logFile != nil {
quitChannel <- struct{}{}
if err := logFile.Close(); err != nil {
fmt.Printf("Error closing log file: %v", err)
}
}
}
func monitorLogSize(logFilePath string, maxLogsize int) {
var maxLogsizeBytes int64 = int64(maxLogsize) * 1024 * 1024 // 最大日志文件大小单位为MB
for {
time.Sleep(600 * time.Second) // 每10分钟检查一次
logFileMutex.Lock()
info, err := logFile.Stat()
logFileMutex.Unlock()
if err == nil && info.Size() > maxLogsizeBytes {
if err := rotateLogFile(logFilePath); err != nil {
logw("Log Rotation Failed: %s", err)
}
}
}
}
func rotateLogFile(logFilePath string) error {
logFileMutex.Lock()
defer logFileMutex.Unlock()
if logFile != nil {
if err := logFile.Close(); err != nil {
logw("Error closing log file for rotation: %v", err)
}
}
// 打开当前日志文件
logFile, err := os.Open(logFilePath)
if err != nil {
return fmt.Errorf("failed to open log file: %s, error: %w", logFilePath, err)
}
defer logFile.Close()
newLogFilePath := logFilePath + "-" + time.Now().Format("20060102-150405") + ".tar.gz"
outFile, err := os.Create(newLogFilePath)
if err != nil {
return fmt.Errorf("failed to create gz file: %s, error: %w", newLogFilePath, err)
}
defer outFile.Close()
gzWriter, err := gzip.NewWriterLevel(outFile, gzip.BestCompression)
if err != nil {
return fmt.Errorf("failed to create gz writer: %w", err)
}
defer gzWriter.Close()
tarWriter := tar.NewWriter(gzWriter)
defer tarWriter.Close()
logFileStat, err := logFile.Stat()
if err != nil {
return fmt.Errorf("failed to stat log file: %s, error: %w", logFilePath, err)
}
logFileHeader := &tar.Header{
Name: filepath.Base(logFilePath),
Size: logFileStat.Size(),
Mode: 0644,
ModTime: logFileStat.ModTime(),
}
if err := tarWriter.WriteHeader(logFileHeader); err != nil {
return fmt.Errorf("failed to write log file header: %s, error: %w", logFilePath, err)
}
if _, err := io.Copy(tarWriter, logFile); err != nil {
return fmt.Errorf("failed to copy log file: %s, error: %w", logFilePath, err)
}
if err := os.Truncate(logFilePath, 0); err != nil {
return fmt.Errorf("failed to truncate log file: %s, error: %w", logFilePath, err)
}
// 重新打开日志文件
logFile, err = os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return fmt.Errorf("failed to reopen log file: %s, error: %w", logFilePath, err)
}
logger.SetOutput(logFile)
return nil
}

34
loggin/loggin.go Normal file
View File

@@ -0,0 +1,34 @@
package loggin
import (
"ghproxy/timing"
"time"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin"
)
var (
logw = logger.Logw
LogDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
// 日志中间件
func Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 处理请求
c.Next()
var timingResults time.Duration
// 获取计时结果
timingResults, _ = timing.Get(c)
// 记录日志 IP METHOD URL USERAGENT PROTOCOL STATUS TIMING
logInfo("%s %s %s %s %d %s ", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Writer.Status(), timingResults)
}
}

285
main.go
View File

@@ -1,105 +1,284 @@
package main
import (
"encoding/json"
"embed"
"flag"
"fmt"
"log"
"io"
"io/fs"
"net/http"
"regexp"
"time"
"ghproxy/api"
"ghproxy/auth"
"ghproxy/config"
"ghproxy/logger"
"ghproxy/middleware/loggin"
"ghproxy/middleware/timing"
"ghproxy/proxy"
"ghproxy/rate"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin"
)
var (
cfg *config.Config
logw = logger.Logw
router *gin.Engine
configfile = "/data/ghproxy/config/config.yaml"
configfile = "/data/ghproxy/config/config.toml"
cfgfile string
version string
dev string
runMode string
limiter *rate.RateLimiter
iplimiter *rate.IPRateLimiter
)
var (
exps = []*regexp.Regexp{
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:releases|archive)/.*`),
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/.*`),
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:info|git-).*`),
regexp.MustCompile(`^(?:https?://)?raw\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.+?/.+`),
regexp.MustCompile(`^(?:https?://)?gist\.github\.com/([^/]+)/.+?/.+`),
}
//go:embed pages/bootstrap/*
pagesFS embed.FS
//go:embed pages/nebula/*
NebulaPagesFS embed.FS
)
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
func readFlag() {
flag.StringVar(&cfgfile, "cfg", configfile, "config file path")
}
func loadConfig() {
var err error
// 初始化配置
cfg, err = config.LoadConfig(configfile)
cfg, err = config.LoadConfig(cfgfile)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
fmt.Printf("Failed to load config: %v\n", err)
}
if cfg.Server.Debug {
fmt.Println("Config File Path: ", cfgfile)
fmt.Printf("Loaded config: %v\n", cfg)
}
fmt.Printf("Loaded config: %v\n", cfg)
}
func setupLogger() {
// 初始化日志模块
func setupLogger(cfg *config.Config) {
var err error
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize) // 传递日志文件路径
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize)
if err != nil {
log.Fatalf("Failed to initialize logger: %v", err)
fmt.Printf("Failed to initialize logger: %v\n", err)
}
logw("Logger initialized")
logw("Init Completed")
err = logger.SetLogLevel(cfg.Log.Level)
if err != nil {
fmt.Printf("Logger Level Error: %v\n", err)
}
fmt.Printf("Log Level: %s\n", cfg.Log.Level)
logDebug("Config File Path: ", cfgfile)
logDebug("Loaded config: %v\n", cfg)
logInfo("Init Completed")
}
func LoadBlacklist(cfg *config.Config) {
auth.LoadBlacklist(cfg)
func loadlist(cfg *config.Config) {
auth.Init(cfg)
}
func setupApi(cfg *config.Config, router *gin.Engine, version string) {
api.InitHandleRouter(cfg, router, version)
}
func setupRateLimit(cfg *config.Config) {
if cfg.RateLimit.Enabled {
if cfg.RateLimit.RateMethod == "ip" {
iplimiter = rate.NewIPRateLimiter(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute)
} else if cfg.RateLimit.RateMethod == "total" {
limiter = rate.New(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute)
} else {
logError("Invalid RateLimit Method: %s", cfg.RateLimit.RateMethod)
}
}
}
func InitReq(cfg *config.Config) {
proxy.InitReq(cfg)
}
// loadEmbeddedPages 加载嵌入式页面资源
func loadEmbeddedPages(cfg *config.Config) (fs.FS, error) {
var pages fs.FS
var err error
switch cfg.Pages.Theme {
case "bootstrap":
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
case "nebula":
pages, err = fs.Sub(NebulaPagesFS, "pages/nebula")
default:
pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题
logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme)
}
if err != nil {
return nil, fmt.Errorf("failed to load embedded pages: %w", err)
}
return pages, nil
}
// setupPages 设置页面路由
func setupPages(cfg *config.Config, router *gin.Engine) {
switch cfg.Pages.Mode {
case "internal":
// 加载嵌入式资源
pages, err := loadEmbeddedPages(cfg)
if err != nil {
logError("Failed when processing internal pages: %s", err)
return
}
// 设置嵌入式资源路由
router.GET("/", gin.WrapH(http.FileServer(http.FS(pages))))
router.GET("/favicon.ico", gin.WrapH(http.FileServer(http.FS(pages))))
router.GET("/script.js", gin.WrapH(http.FileServer(http.FS(pages))))
router.GET("/style.css", gin.WrapH(http.FileServer(http.FS(pages))))
//router.GET("/bootstrap.min.css", gin.WrapH(http.FileServer(http.FS(pages))))
case "external":
// 设置外部资源路径
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
javascriptsPath := fmt.Sprintf("%s/script.js", cfg.Pages.StaticDir)
stylesheetsPath := fmt.Sprintf("%s/style.css", cfg.Pages.StaticDir)
//bootstrapPath := fmt.Sprintf("%s/bootstrap.min.css", cfg.Pages.StaticDir)
// 设置外部资源路由
router.GET("/", func(c *gin.Context) {
c.File(indexPagePath)
logInfo("IP:%s UA:%s METHOD:%s HTTPv:%s", c.ClientIP(), c.Request.UserAgent(), c.Request.Method, c.Request.Proto)
})
router.StaticFile("/favicon.ico", faviconPath)
router.StaticFile("/script.js", javascriptsPath)
router.StaticFile("/style.css", stylesheetsPath)
//router.StaticFile("/bootstrap.min.css", bootstrapPath)
default:
// 处理无效的Pages Mode
logWarning("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode)
// 加载嵌入式资源
pages, err := loadEmbeddedPages(cfg)
if err != nil {
logError("Failed when processing pages: %s", err)
return
}
// 设置嵌入式资源路由
router.GET("/", gin.WrapH(http.FileServer(http.FS(pages))))
router.GET("/favicon.ico", gin.WrapH(http.FileServer(http.FS(pages))))
router.GET("/script.js", gin.WrapH(http.FileServer(http.FS(pages))))
router.GET("/style.css", gin.WrapH(http.FileServer(http.FS(pages))))
}
}
func init() {
readFlag()
flag.Parse()
loadConfig()
setupLogger()
LoadBlacklist(cfg)
setupLogger(cfg)
InitReq(cfg)
loadlist(cfg)
setupRateLimit(cfg)
// 设置 Gin 模式
gin.SetMode(gin.ReleaseMode)
if cfg.Server.Debug {
dev = "true"
version = "dev"
}
if dev == "true" {
gin.SetMode(gin.DebugMode)
runMode = "dev"
} else {
gin.SetMode(gin.ReleaseMode)
runMode = "release"
}
// 初始化路由
router = gin.Default()
logDebug("Run Mode: %s", runMode)
// 定义路由
router.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://ghproxy0rtt.1888866.xyz/")
gin.LoggerWithWriter(io.Discard)
router = gin.New()
// 添加recovery中间件
router.Use(gin.Recovery())
// 添加log中间件
router.Use(loggin.Middleware())
// 添加计时中间件
router.Use(timing.Middleware())
if cfg.Server.H2C {
router.UseH2C = true
}
setupApi(cfg, router, version)
setupPages(cfg, router)
// 1. GitHub Releases/Archive - Use distinct path segments for type
router.GET("/github.com/:username/:repo/releases/*filepath", func(c *gin.Context) { // Distinct path for releases
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
router.GET("/api", api)
// 健康检查
router.GET("/api/healthcheck", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
router.GET("/github.com/:username/:repo/archive/*filepath", func(c *gin.Context) { // Distinct path for archive
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
// 2. GitHub Blob/Raw - Use distinct path segments for type
router.GET("/github.com/:username/:repo/blob/*filepath", func(c *gin.Context) { // Distinct path for blob
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
router.GET("/github.com/:username/:repo/raw/*filepath", func(c *gin.Context) { // Distinct path for raw
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
router.GET("/github.com/:username/:repo/info/*filepath", func(c *gin.Context) { // Distinct path for info
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
router.GET("/github.com/:username/:repo/git-upload-pack", func(c *gin.Context) {
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
// 4. Raw GitHubusercontent - Keep as is (assuming it's distinct enough)
router.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(c *gin.Context) {
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
// 5. Gist GitHubusercontent - Keep as is (assuming it's distinct enough)
router.GET("/gist.githubusercontent.com/:username/*filepath", func(c *gin.Context) {
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
// 6. GitHub API Repos - Keep as is (assuming it's distinct enough)
router.GET("/api.github.com/repos/:username/:repo/*filepath", func(c *gin.Context) {
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
// 未匹配路由处理
router.NoRoute(func(c *gin.Context) {
proxy.NoRouteHandler(cfg)(c)
logInfo(c.Request.URL.Path)
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
fmt.Printf("GHProxy Version: %s\n", version)
fmt.Printf("A Go Based High-Performance Github Proxy \n")
fmt.Printf("Made by WJQSERVER-STUDIO\n")
}
func main() {
// 启动服务器
err := router.Run(fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port))
if err != nil {
log.Fatalf("Error starting server: %v\n", err)
logError("Failed to start server: %v\n", err)
}
fmt.Println("Program finished")
}
func api(c *gin.Context) {
// 设置响应头
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"MaxResponseBodySize": cfg.Server.SizeLimit,
})
defer logger.Close()
fmt.Println("Program Exit")
}

View File

@@ -0,0 +1,34 @@
package loggin
import (
"ghproxy/middleware/timing"
"time"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin"
)
var (
logw = logger.Logw
LogDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
// 日志中间件
func Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 处理请求
c.Next()
var timingResults time.Duration
// 获取计时结果
timingResults, _ = timing.Get(c)
// 记录日志 IP METHOD URL USERAGENT PROTOCOL STATUS TIMING
logInfo("%s %s %s %s %d %s ", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Writer.Status(), timingResults)
}
}

View File

@@ -0,0 +1,86 @@
package timing
import (
"sync"
"time"
"github.com/gin-gonic/gin"
)
// 阶段计时结构(固定数组优化)
type timingData struct {
phases [8]struct { // 预分配8个阶段存储
name string
dur time.Duration
}
count int
start time.Time
}
// 对象池(内存重用优化)
var pool = sync.Pool{
New: func() interface{} {
return new(timingData)
},
}
// 中间件入口
func Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 从池中获取计时器
td := pool.Get().(*timingData)
td.start = time.Now()
td.count = 0
// 存储到上下文
c.Set("timing", td)
// 请求完成后回收对象
defer func() {
pool.Put(td)
}()
c.Next()
}
}
// 记录阶段耗时
func Record(c *gin.Context, name string) {
if val, exists := c.Get("timing"); exists {
//td := val.(*timingData)
td, ok := val.(*timingData)
if !ok {
return
}
if td.count < len(td.phases) {
td.phases[td.count].name = name
td.phases[td.count].dur = time.Since(td.start) // 直接记录当前时间
td.count++
}
}
}
// 获取计时结果(日志输出用)
func Get(c *gin.Context) (total time.Duration, phases []struct {
Name string
Dur time.Duration
}) {
if val, exists := c.Get("timing"); exists {
//td := val.(*timingData)
td, ok := val.(*timingData)
if !ok {
return
}
for i := 0; i < td.count; i++ {
phases = append(phases, struct {
Name string
Dur time.Duration
}{
Name: td.phases[i].name,
Dur: td.phases[i].dur,
})
}
total = time.Since(td.start)
}
return
}

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

104
pages/bootstrap/index.html Normal file
View File

@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Github文件加速</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
onerror="this.onerror=null; this.href='https://static.wjqserver.com/bootstrap.min.css';">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container py-4 py-md-5">
<main>
<div class="card mb-4">
<div class="card-body">
<h1 class="text-center mb-4">Github文件加速</h1>
<p class="lead text-center mb-4">为访问Github文件进行加速</p>
<form id="github-form">
<div class="mb-3">
<input type="text" class="form-control form-control-lg" id="githubLinkInput"
placeholder="请键入需要代理的 Github 链接">
</div>
<button type="submit" class="btn btn-primary btn-lg w-100">获取文件链接</button>
</form>
<div id="output" class="mt-3 bg-light p-3 rounded position-relative" style="display: none;">
<pre id="formattedLinkOutput" class="mb-0"></pre>
<button id="copyButton"
class="btn btn-outline-secondary btn-sm position-absolute top-0 end-0 m-2" title="复制链接">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-clipboard" viewBox="0 0 16 16">
<path
d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" />
<path
d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z" />
</svg>
</button>
<button id="openButton"
class="btn btn-outline-secondary btn-sm position-absolute top-0 end-0 m-2 me-5"
title="在新标签页中打开">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-box-arrow-up-right" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z" />
<path fill-rule="evenodd"
d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z" />
</svg>
</button>
</div>
<p class="text-muted small mt-3 mb-0">GitHub 链接带不带协议头均可,支持 release、archive 以及文件,转换后链接均可使用。</p>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">文件大小限制</h5>
<p class="card-text" id="sizeLimitDisplay">...</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">白名单状态</h5>
<p class="card-text" id="whiteListStatus">...</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">黑名单状态</h5>
<p class="card-text" id="blackListStatus">...</p>
</div>
</div>
</div>
</div>
</main>
<footer class="text-center mt-4">
<p class="text-muted">
Copyright &copy; 2024-2025 WJQSERVER-STUDIO<br>
<a href="https://github.com/WJQSERVER-STUDIO/ghproxy" class="text-decoration-none">GitHub 仓库</a> |
<a href="https://t.me/ghproxy_go" class="text-decoration-none">Telegram 交流群</a>
</p>
</footer>
</div>
<div class="toast-container position-fixed top-0 end-0 p-3">
<div id="toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body"></div>
</div>
</div>
<div id="versionBadge" class="version-badge"></div>
<script src="https://static.wjqserver.com/bootstrap.bundle.min.js"></script>
<script src="script.js"></script>
</body>
</html>

84
pages/bootstrap/script.js Normal file
View File

@@ -0,0 +1,84 @@
const githubForm = document.getElementById('github-form');
const githubLinkInput = document.getElementById('githubLinkInput');
const formattedLinkOutput = document.getElementById('formattedLinkOutput');
const output = document.getElementById('output');
const copyButton = document.getElementById('copyButton');
const openButton = document.getElementById('openButton');
const toast = new bootstrap.Toast(document.getElementById('toast'));
function showToast(message) {
const toastBody = document.querySelector('.toast-body');
toastBody.textContent = message;
toast.show();
}
function formatGithubLink(githubLink) {
const currentHost = window.location.host;
let formattedLink = "";
if (githubLink.startsWith("https://github.com/") || githubLink.startsWith("http://github.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + "/github.com" + githubLink.substring(githubLink.indexOf("/", 8));
} else if (githubLink.startsWith("github.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
} else if (githubLink.startsWith("https://raw.githubusercontent.com/") || githubLink.startsWith("http://raw.githubusercontent.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + githubLink.substring(githubLink.indexOf("/", 7));
} else if (githubLink.startsWith("raw.githubusercontent.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
} else if (githubLink.startsWith("https://gist.githubusercontent.com/") || githubLink.startsWith("http://gist.githubusercontent.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + "/gist.github.com" + githubLink.substring(githubLink.indexOf("/", 18));
} else if (githubLink.startsWith("gist.githubusercontent.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
} else {
showToast('请输入有效的GitHub链接');
return null;
}
return formattedLink;
}
githubForm.addEventListener('submit', function (e) {
e.preventDefault();
const formattedLink = formatGithubLink(githubLinkInput.value);
if (formattedLink) {
formattedLinkOutput.textContent = formattedLink;
output.style.display = 'block';
}
});
copyButton.addEventListener('click', function () {
navigator.clipboard.writeText(formattedLinkOutput.textContent).then(() => {
showToast('链接已复制到剪贴板');
});
});
openButton.addEventListener('click', function () {
window.open(formattedLinkOutput.textContent, '_blank');
});
function fetchAPI() {
fetch('/api/size_limit')
.then(response => response.json())
.then(data => {
document.getElementById('sizeLimitDisplay').textContent = `${data.MaxResponseBodySize} MB`;
});
fetch('/api/whitelist/status')
.then(response => response.json())
.then(data => {
document.getElementById('whiteListStatus').textContent = data.Whitelist ? '已开启' : '已关闭';
});
fetch('/api/blacklist/status')
.then(response => response.json())
.then(data => {
document.getElementById('blackListStatus').textContent = data.Blacklist ? '已开启' : '已关闭';
});
fetch('/api/version')
.then(response => response.json())
.then(data => {
document.getElementById('versionBadge').textContent = data.Version;
});
}
document.addEventListener('DOMContentLoaded', fetchAPI);

259
pages/bootstrap/style.css Normal file
View File

@@ -0,0 +1,259 @@
/* 通用样式 */
:root {
--primary-color: #007aff;
/* 主要按钮颜色 */
--secondary-color: #34c759;
/* 次要按钮颜色 */
--background-color: #f9f9f9;
/* 亮色模式背景 */
--card-background: #ffffff;
/* 卡片背景 */
--text-color: #333333;
/* 亮色模式文本颜色 */
--border-color: #e0e0e0;
/* 边框颜色 */
--input-background: #ffffff;
/* 输入框背景 */
--input-border: #d1d1d6;
/* 输入框边框 */
--pre-background: #f1f3f4;
/* 代码块背景 */
--pre-text-color: #333333;
/* 代码块文本颜色 */
--version-badge-hover: #39c5bb;
/* 说明文字颜色 */
--muted-text-color: #6c757d;
}
body {
background-color: var(--background-color);
color: var(--text-color);
font-family: sans-serif;
line-height: 1.8;
font-size: 15px;
margin: 0;
padding: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--text-color);
font-weight: 800;
letter-spacing: 0.5px;
margin: 1rem 0;
}
p,
span,
a,
li {
color: var(--text-color);
margin-bottom: 1rem;
}
a {
text-decoration: none;
color: var(--primary-color);
}
a:hover {
color: #0056b3;
}
.card {
background-color: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
margin: 16px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
transform: translateY(-4px);
}
.btn-outline-secondary {
border-radius: 50%;
padding: 6px;
transition: #e9e9e9 0.3s ease-in-out, color 0.3s ease-in-out;
}
.btn-outline-secondary:hover {
background-color: var(--primary-color);
color: white;
}
.form-control {
background-color: var(--input-background);
border: 1px solid var(--input-border);
color: var(--text-color);
padding: 10px;
border-radius: 4px;
font-size: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3);
}
.text-muted {
color: var(--muted-text-color) !important;
}
.bg-light {
background-color: var(--card-background) !important;
}
pre {
background-color: var(--pre-background);
color: var(--pre-text-color);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
font-size: 14px;
line-height: 1.6;
}
.version-badge {
position: fixed;
bottom: 20px;
right: 20px;
background-color: var(--secondary-color);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease-in-out;
}
.version-badge:hover {
background-color: var(--version-badge-hover);
}
footer {
padding: 16px;
text-align: center;
color: var(--text-color);
font-size: 0.9rem;
background-color: var(--card-background);
}
footer a {
color: var(--primary-color);
}
footer a:hover {
color: #0056b3;
}
/* 暗色模式 */
@media (prefers-color-scheme: dark) {
:root {
--background-color: #121212;
/* 深灰色背景 */
--card-background: #1e1e1e;
/* 卡片背景稍浅 */
--text-color: #ffffff;
/* 纯白文本 */
--primary-color: #0a84ff;
/* 按钮蓝色 */
--secondary-color: #30d158;
/* 次要按钮绿色 */
--border-color: #3a3a3a;
/* 边框颜色 */
--input-background: #2c2c2c;
/* 输入框背景 */
--input-border: #4a4a4a;
/* 输入框边框 */
--pre-background: #3b3636;
/* 代码块背景 */
--pre-text-color: #ffffff;
/* 代码块文本颜色 */
--version-badge-hover: #39c5bc9a;
/* 说明文字颜色 */
--muted-text-color: #a0a0a0;
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
h1,
h2,
h3,
h4,
h5,
h6,
p,
span,
a,
li {
color: var(--text-color);
}
.card {
background-color: var(--card-background);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.btn-outline-secondary {
border-radius: 50%;
padding: 6px;
transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out;
}
.btn-outline-secondary:hover {
background-color: var(--primary-color);
color: white;
}
.toast {
background-color: var(--card-background);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.toast-body {
padding: 12px;
}
.form-control {
background-color: var(--input-background);
border: 1px solid var(--input-border);
color: var(--text-color);
}
.bg-light {
background-color: var(--card-background) !important;
}
pre {
background-color: var(--pre-background);
color: var(--pre-text-color);
}
footer {
background-color: var(--card-background);
color: var(--text-color);
}
footer a {
color: var(--primary-color);
}
}

View File

@@ -1,226 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Github文件加速">
<title>Github文件加速</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://font.sec.miui.com/font/css?family=MiSans:400,700:MiSans">
<style>
body {
background-color: #f8f9fac5;
font-family: 'Misans', Arial, sans-serif;
padding: 30px;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
min-height: 100vh;
margin: 0;
position: relative;
}
.container {
max-width: 800px;
text-align: center;
min-height: 45vh;
}
h1 {
font-weight: bold;
margin-bottom: 75px;
}
.rounded-button {
border-radius: 6px;
transition: background-color 0.3s, transform 0.2s;
padding: 10px 30px;
background-color: #39c5bb;
color: white;
border: none;
margin-bottom: 10px;
}
.rounded-button:hover {
background-color: #39c5bcda;
transform: scale(1.05);
}
.tips>p:first-child::before {
position: sticky;
color: #7b7b7b;
margin-bottom: 5px;
}
footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
}
pre {
background: #2d2d2d;
color: #f8f8f2;
padding: 20px 20px;
margin: 10px 0;
border-radius: 8px;
overflow-x: auto;
position: relative;
}
pre::before {
content: " ";
display: block;
position: absolute;
top: 10px;
left: 10px;
width: 12px;
height: 12px;
background: #ff5f56;
border-radius: 50%;
box-shadow: 20px 0 0 #ffbd2e, 40px 0 0 #27c93f;
}
code {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.875em;
}
.code {
position: relative;
padding-right: 0px;
}
.copy-button {
position: absolute;
top: 5px;
right: 10px;
background: rgba(118, 119, 121, 0.7);
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
transition: opacity 0.3s;
z-index: 1;
}
pre:hover .copy-button {
opacity: 1;
}
#visitor-info {
margin-top: 10px;
text-align: center;
line-height: 0.5;
}
</style>
</head>
<body>
<div class="container">
<h1>Github文件加速</h1>
<div class="form-group">
<input type="text" class="form-control" id="githubLinkInput" placeholder="键入Github链接">
</div>
<button class="btn rounded-button" id="formatButton">获取文件链接</button>
<div class="code" id="outputBlock">
<button class="copy-button" id="copyButton" onclick="copyCode(this)">Copy</button>
<pre id="formattedLinkOutput"></pre>
</div>
<div class="tips">
<p>GitHub链接带不带协议头均可支持release、archive以及文件转换后链接均可使用</a></p>
<p id="sizeLimitDisplay">文件大小限制: ...</p>
</div>
</div>
<script>
function formatGithubLink() {
var githubLinkInput = document.getElementById('githubLinkInput');
var currentHost = window.location.host;
var formattedLink = "";
if (githubLinkInput.value.startsWith("https://github.com/") || githubLinkInput.value.startsWith("http://github.com/")) {
formattedLink = "https://" + currentHost + "/github.com" + githubLinkInput.value.substring(githubLinkInput.value.indexOf("/", 8));
} else if (githubLinkInput.value.startsWith("github.com/")) {
formattedLink = "https://" + currentHost + "/" + githubLinkInput.value;
} else if (githubLinkInput.value.startsWith("https://raw.githubusercontent.com/") || githubLinkInput.value.startsWith("http://raw.githubusercontent.com/")) {
formattedLink = "https://" + currentHost + githubLinkInput.value.substring(githubLinkInput.value.indexOf("/", 7));
} else if (githubLinkInput.value.startsWith("raw.githubusercontent.com/")) {
formattedLink = "https://" + currentHost + "/" + githubLinkInput.value;
} else if (!githubLinkInput.value.trim()) {
alert('请输入有效的GitHub链接');
}
var formattedLinkOutput = document.getElementById('formattedLinkOutput');
formattedLinkOutput.textContent = formattedLink;
}
document.getElementById('formatButton').addEventListener('click', formatGithubLink);
document.getElementById('copyButton').addEventListener('click', function () {
const output = document.getElementById('formattedLinkOutput');
const range = document.createRange();
range.selectNode(output);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
document.execCommand('copy');
window.getSelection().removeAllRanges();
alert('链接已复制到剪贴板');
});
function fetchAPI() {
fetch(window.location.origin + '/api')
.then(response => response.json())
.then(data => {
const sizeLimitDisplay = document.getElementById('sizeLimitDisplay');
const sizeInMB = (data.MaxResponseBodySize / (1024 * 1024)).toFixed(0);
sizeLimitDisplay.textContent = `文件大小限制: ${sizeInMB} MB`;
})
.catch(error => {
console.error('Error fetching API:', error);
});
}
document.addEventListener('DOMContentLoaded', fetchAPI);
</script>
</body>
<footer>
<p>
Copyright &copy; 2024 WJQSERVER-STUDIO
</p>
<p>
GitHub仓库地址<a
href="https://github.com/WJQSERVER-STUDIO/ghproxy">https://github.com/WJQSERVER-STUDIO/ghproxy</a>
</p>
<div id="visitor-info" style="text-align: center; margin-top: 15px;">
<p>您的IP地址: <span id="visitor-ip"></span></p>
<p>当前位置: <span id="visitor-country"></span> <img id="visitor-flag" src="" alt="" width="24" height="16"></p>
</div>
<script>
fetch('https://ip.1888866.xyz/ip-lookup')
.then(response => {
if (!response.ok) {
throw new Error('网络响应失败');
}
return response.json();
})
.then(data => {
document.getElementById('visitor-ip').textContent = data.ip;
document.getElementById('visitor-country').textContent = data.country_name;
document.getElementById('visitor-flag').src = `https://flagcdn.com/w20/${data.country_code.toLowerCase()}.png`;
})
.catch(error => {
console.error('获取地理位置信息失败:', error);
const visitorInfo = document.getElementById('visitor-info');
visitorInfo.innerHTML = '<p>无法获取您的地理位置信息,请稍后再试。</p>';
});
</script>
</footer>
</html>

BIN
pages/nebula/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

169
pages/nebula/index.html Normal file
View File

@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="zh" data-bs-theme="auto">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub加速服务</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
onerror="this.onerror=null; this.href='https://static.wjqserver.com/bootstrap.min.css';">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container py-4 py-lg-5">
<header class="text-center mb-5">
<h1 class="display-5 fw-bold mb-3">GitHub加速服务</h1>
<p class="lead text-muted">高速稳定的 GitHub 资源访问解决方案</p>
</header>
<div class="main-card p-4 mb-4">
<form id="mainForm" class="mb-4">
<div class="mb-3">
<label for="inputUrl" class="form-label fw-semibold">GitHub 链接</label>
<input type="url" class="form-control form-control-lg" id="inputUrl" placeholder="输入 GitHub 文件/仓库链接"
required>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100 py-2">
🚀 生成加速链接
</button>
</form>
<!-- 结果输出 -->
<div id="output" class="mt-4" hidden>
<div class="code-block">
<code id="outputLink" class="d-block text-break"></code>
</div>
<div class="d-flex gap-2 mt-3">
<button id="copyBtn" class="btn btn-outline-secondary">
⎘ 复制链接
</button>
<button id="openBtn" class="btn btn-primary">
↗ 立即打开
</button>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="main-card p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">文件大小限制</h6>
<small class="text-muted">最大支持文件尺寸</small>
</div>
<span id="sizeLimit" class="status-badge bg-primary">...</span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="main-card p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">白名单状态</h6>
<small class="text-muted">访问控制列表</small>
</div>
<span id="whitelistStatus" class="status-badge bg-success">...</span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="main-card p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">黑名单状态</h6>
<small class="text-muted">屏蔽列表状态</small>
</div>
<span id="blacklistStatus" class="status-badge bg-danger">...</span>
</div>
</div>
</div>
</div>
<div class="main-card p-4">
<h2 class="h4 fw-bold mb-4">📚 详细使用指南</h2>
<div class="guide-section">
<h3 class="h5 fw-semibold mb-3">支持的工具</h3>
<ul class="list-unstyled">
<li class="mb-2">✅ 支持域名github.com</li>
<li class="mb-2">✅ 支持域名raw.githubusercontent.com</li>
<li class="mb-2">✅ 支持域名gist.githubusercontent.com</li>
<li class="mb-2">✅ 支持HTTPS Git Clone</li>
<li class="text-muted">❌ 不支持 SSH Git Clone</li>
</ul>
</div>
<div class="guide-section">
<h3 class="h5 fw-semibold mb-3">基础用法示例</h3>
<div class="command-example">
<h4 class="h6 fw-medium mb-2">Git 克隆</h4>
<code
class="d-block mb-3">git clone <span class="protocol">https</span>://<span class="host">example.com</span>/https://github.com/user/project.git</code>
<h4 class="h6 fw-medium mb-2">私有仓库克隆</h4>
<code
class="d-block">git clone <span class="protocol">https</span>://user:your_token@<span class="host">example.com</span>/https://github.com/user/project.git</code>
</div>
<div class="command-example">
<h4 class="h6 fw-medium mb-2">文件下载</h4>
<code
class="d-block mb-3">wget <span class="protocol">https</span>://<span class="host">example.com</span>/https://raw.githubusercontent.com/user/project/main/README.md</code>
<h4 class="h6 fw-medium mb-2">版本发布</h4>
<code
class="d-block">curl -LO <span class="protocol">https</span>://<span class="host">example.com</span>/https://github.com/user/project/releases/download/v1.0.0/project_1.0.0.amd64.tar.gz</code>
</div>
</div>
<div class="guide-section">
<h3 class="h5 fw-semibold mb-3">支持的文件类型</h3>
<div class="row g-3">
<div class="col-md-6">
<div class="main-card p-3">
<h4 class="h6 fw-medium mb-2">原始文件</h4>
<code
class="d-block text-muted fs-sm">https://raw.githubusercontent.com/user/repo/main/file.txt</code>
</div>
</div>
<div class="col-md-6">
<div class="main-card p-3">
<h4 class="h6 fw-medium mb-2">分支源码</h4>
<code class="d-block text-muted fs-sm">https://github.com/user/repo/archive/main.zip</code>
</div>
</div>
<div class="col-md-6">
<div class="main-card p-3">
<h4 class="h6 fw-medium mb-2">版本发布</h4>
<code
class="d-block text-muted fs-sm">https://github.com/user/repo/releases/download/v1.0.0/app.zip</code>
</div>
</div>
<div class="col-md-6">
<div class="main-card p-3">
<h4 class="h6 fw-medium mb-2">Gist 文件</h4>
<code
class="d-block text-muted fs-sm">https://gist.githubusercontent.com/user/gist_id/raw/file.txt</code>
</div>
</div>
<div class="col-md-6">
<div class="main-card p-3">
<h4 class="h6 fw-medium mb-2">HTTPS Git Clone</h4>
<code class="d-block text-muted fs-sm">git clone https://github.com/user/repo.git</code>
</div>
</div>
</div>
</div>
</div>
<footer class="text-center mt-5 text-muted">
<p class="mb-0 small" id="version">v </p>
<p class="mb-0 small">Copyright © 2024 - <span id="currentYear"></span> WJQSERVER-STUDIO<br>
<a href="https://github.com/WJQSERVER-STUDIO/ghproxy" class="text-decoration-none">GitHub 仓库</a> |
<a href="https://t.me/ghproxy_go" class="text-decoration-none">Telegram 交流群</a>
</p>
</footer>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body"></div>
</div>
</div>
<script src="https://static.wjqserver.com/bootstrap.bundle.min.js"></script>
</body>
</html>

131
pages/nebula/script.js Normal file
View File

@@ -0,0 +1,131 @@
(() => {
'use strict';
// 初始化基础配置
const currentYear = new Date().getFullYear();
document.getElementById('currentYear').textContent = currentYear;
const toast = new bootstrap.Toast('#liveToast');
// DOM 元素
const form = document.getElementById('mainForm');
const input = document.getElementById('inputUrl');
const output = document.getElementById('output');
const outputLink = document.getElementById('outputLink');
// 获取当前域名
const CURRENT_PROTOCOL = window.location.protocol.replace(':', '');
const CURRENT_HOST = window.location.host;
// 替换协议部分
document.querySelectorAll('code .protocol').forEach(span => {
span.textContent = CURRENT_PROTOCOL;
});
// 替换域名部分
document.querySelectorAll('code .host').forEach(span => {
span.textContent = CURRENT_HOST;
});
// URL 转换规则
const URL_RULES = [
{
regex: /^(?:https?:\/\/)?(?:www\.)?(github\.com\/.*)/i,
build: path => `${location.protocol}//${location.host}/${path}`
},
{
regex: /^(?:https?:\/\/)?(raw\.githubusercontent\.com\/.*)/i,
build: path => `${location.protocol}//${location.host}/${path}`
},
{
regex: /^(?:https?:\/\/)?(gist\.(?:githubusercontent|github)\.com\/.*)/i,
build: path => `${location.protocol}//${location.host}/${path}`
}
];
// 核心功能:链接转换
function transformGitHubURL(url) {
const cleanURL = url.trim().replace(/^https?:\/\//i, '');
for (const rule of URL_RULES) {
const match = cleanURL.match(rule.regex);
if (match) return rule.build(match[1]);
}
return null;
}
// 事件处理
form.addEventListener('submit', e => {
e.preventDefault();
if (!input.checkValidity()) {
input.classList.add('is-invalid');
showToast('⚠️ 请输入有效的 GitHub 链接');
return;
}
const result = transformGitHubURL(input.value);
if (!result) {
showToast('❌ 不支持的链接格式');
return;
}
outputLink.textContent = result;
output.hidden = false;
window.scrollTo({ top: output.offsetTop - 100, behavior: 'smooth' });
});
document.getElementById('copyBtn').addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(outputLink.textContent);
showToast('✅ 链接已复制');
} catch {
showToast('❌ 复制失败');
}
});
document.getElementById('openBtn').addEventListener('click', () => {
window.open(outputLink.textContent, '_blank', 'noopener,noreferrer');
});
// 服务状态监控
async function loadServiceStatus() {
try {
const [size, whitelist, blacklist, version] = await Promise.all([
fetchJSON('/api/size_limit'),
fetchJSON('/api/whitelist/status'),
fetchJSON('/api/blacklist/status'),
fetchJSON('/api/version')
]);
updateStatus('sizeLimit', `${size.MaxResponseBodySize}MB`);
updateStatus('whitelistStatus', whitelist.Whitelist ? '已开启' : '已关闭');
updateStatus('blacklistStatus', blacklist.Blacklist ? '已开启' : '已关闭');
updateStatus('version', `Version ${version.Version}`);
} catch {
showToast('⚠️ 服务状态获取失败');
}
}
async function fetchJSON(url) {
const response = await fetch(url);
if (!response.ok) throw new Error('API Error');
return response.json();
}
function updateStatus(elementId, text) {
const element = document.getElementById(elementId);
if (element) element.textContent = text;
}
// 工具函数
function showToast(message) {
const toastBody = document.querySelector('.toast-body');
toastBody.textContent = message;
toast.show();
}
// 初始化
input.addEventListener('input', () => {
input.classList.remove('is-invalid');
if (output.hidden === false) output.hidden = true;
});
loadServiceStatus();
})();

157
pages/nebula/style.css Normal file
View File

@@ -0,0 +1,157 @@
:root {
--primary: #007aff;
--secondary: #007aff;
--background: #f9f9f9;
--card-bg: #ffffff;
--text: #333333;
--border: #d1d1d6;
--muted: #64748b;
--success: #00a83ed2;
--danger: #ce0000dd;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #121212;
--card-bg: #1e1e1e;
--text: #ffffff;
--border: #334155;
--muted: #94a3b8;
}
.form-control::placeholder {
color: var(--muted);
}
.text-muted {
color: var(--muted) !important;
}
.btn-outline-secondary {
color: var(--muted);
border-color: var(--border);
}
.status-badge {
color: var(--text) !important;
}
.code-block {
background: rgba(255, 255, 255, 0.05);
}
.command-example {
background: rgba(255, 255, 255, 0.03);
}
.btn-primary {
--bs-btn-bg: var(--primary);
--bs-btn-border-color: var(--primary);
--bs-btn-hover-bg: var(--secondary);
--bs-btn-hover-border-color: var(--secondary);
}
.form-control {
background-color: rgba(255, 255, 255, 0.05);
border-color: var(--border);
color: var(--text);
}
a {
color: var(--secondary);
}
}
body {
background: var(--background);
color: var(--text);
font-family: 'Inter', system-ui, sans-serif;
line-height: 1.6;
}
.main-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.main-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
}
.code-block {
background: rgba(37, 99, 235, 0.05);
border-left: 3px solid var(--primary);
border-radius: 6px;
padding: 1rem;
position: relative;
overflow-x: auto;
}
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
}
.guide-section {
border-left: 3px solid var(--primary);
padding-left: 1rem;
margin: 2rem 0;
}
.command-example {
position: relative;
padding: 1.25rem;
background: rgba(37, 99, 235, 0.03);
border-radius: 8px;
margin: 1rem 0;
}
.command-example::before {
content: "➜";
position: absolute;
left: -1.5rem;
color: var(--muted);
}
.toast {
position: fixed;
top: 1%;
right: 1%;
background-color: var(--card-background);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.toast-body {
padding: 12px;
}
#version {
text-align: center;
font-size: 13px;
line-height: 26px;
color: #747474;
}
.bg-primary {
--bs-bg-opacity: 1;
background-color: #2c82de !important;
}
.bg-success {
--bs-bg-opacity: 1;
background-color: #2c82de !important;
}
.bg-danger {
--bs-bg-opacity: 1;
background-color: #2c82de !important;
}

37
proxy/authpass.go Normal file
View File

@@ -0,0 +1,37 @@
package proxy
import (
"ghproxy/config"
"net/http"
"github.com/gin-gonic/gin"
)
func AuthPassThrough(c *gin.Context, cfg *config.Config, req *http.Request) {
if cfg.Auth.PassThrough {
token := c.Query("token")
if token != "" {
logDebug("%s %s %s %s %s Auth-PassThrough: token %s", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, token)
switch cfg.Auth.AuthMethod {
case "parameters":
if !cfg.Auth.Enabled {
req.Header.Set("Authorization", "token "+token)
} else {
logWarning("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto)
// 500 Internal Server Error
c.JSON(http.StatusInternalServerError, gin.H{"error": "Conflict Auth Method"})
return
}
case "header":
if cfg.Auth.Enabled {
req.Header.Set("Authorization", "token "+token)
}
default:
logWarning("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto)
// 500 Internal Server Error
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid Auth Method / Auth Method is not be set"})
return
}
}
}
}

138
proxy/chunkreq.go Normal file
View File

@@ -0,0 +1,138 @@
package proxy
import (
"bytes"
"fmt"
"ghproxy/config"
"io"
"net/http"
"strconv"
"github.com/WJQSERVER-STUDIO/go-utils/copyb"
"github.com/gin-gonic/gin"
)
func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) {
method := c.Request.Method
// 发送HEAD请求, 预获取Content-Length
headReq, err := client.NewRequest("HEAD", u, nil)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, headReq)
removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
AuthPassThrough(c, cfg, headReq)
headResp, err := client.Do(headReq)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
//defer headResp.Body.Close()
defer func(Body io.ReadCloser) {
if err := Body.Close(); err != nil {
logError("Failed to close response body: %v", err)
}
}(headResp.Body)
contentLength := headResp.Header.Get("Content-Length")
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
if contentLength != "" {
size, err := strconv.Atoi(contentLength)
if err == nil && size > sizelimit {
finalURL := headResp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
return
}
}
body, err := readRequestBody(c)
if err != nil {
HandleError(c, err.Error())
return
}
bodyReader := bytes.NewBuffer(body)
req, err := client.NewRequest(method, u, bodyReader)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req)
removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
AuthPassThrough(c, cfg, req)
resp, err := client.Do(req)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
defer resp.Body.Close()
// 错误处理(404)
if resp.StatusCode == 404 {
c.String(http.StatusNotFound, "File Not Found")
return
}
contentLength = resp.Header.Get("Content-Length")
if contentLength != "" {
size, err := strconv.Atoi(contentLength)
if err == nil && size > sizelimit {
finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
return
}
}
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
}
}
headersToRemove := map[string]struct{}{
"Content-Security-Policy": {},
"Referrer-Policy": {},
"Strict-Transport-Security": {},
}
for header := range headersToRemove {
resp.Header.Del(header)
}
/*
if cfg.CORS.Enabled {
c.Header("Access-Control-Allow-Origin", "*")
} else {
c.Header("Access-Control-Allow-Origin", "")
}
*/
switch cfg.Server.Cors {
case "*":
c.Header("Access-Control-Allow-Origin", "*")
case "":
c.Header("Access-Control-Allow-Origin", "*")
case "nil":
c.Header("Access-Control-Allow-Origin", "")
default:
c.Header("Access-Control-Allow-Origin", cfg.Server.Cors)
}
c.Status(resp.StatusCode)
//_, err = io.CopyBuffer(c.Writer, resp.Body, nil)
_, err = copyb.CopyBuffer(c.Writer, resp.Body, nil)
if err != nil {
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
} else {
c.Writer.Flush() // 确保刷入
}
}

135
proxy/dial.go Normal file
View File

@@ -0,0 +1,135 @@
/*
made&PR by @lfhy
https://github.com/WJQSERVER-STUDIO/ghproxy/pull/46
*/
package proxy
import (
"ghproxy/config"
"net/http"
"net/url"
"strings"
"golang.org/x/net/proxy"
)
// initTransport 初始化 HTTP 传输层的代理设置
func initTransport(cfg *config.Config, transport *http.Transport) {
// 如果代理功能未启用,直接返回
if !cfg.Outbound.Enabled {
return
}
// 如果代理 URL 未设置,使用环境变量中的代理配置
if cfg.Outbound.Url == "" {
transport.Proxy = http.ProxyFromEnvironment
logWarning("Outbound proxy is not set, using environment variables")
return
}
// 尝试解析代理 URL
proxyInfo, err := url.Parse(cfg.Outbound.Url)
if err != nil {
// 如果解析失败,记录错误日志并使用环境变量中的代理配置
logError("Failed to parse outbound proxy URL %v", err)
transport.Proxy = http.ProxyFromEnvironment
return
}
// 根据代理 URL 的 scheme协议类型选择代理类型
switch strings.ToLower(proxyInfo.Scheme) {
case "http", "https": // 如果是 HTTP/HTTPS 代理
transport.Proxy = http.ProxyURL(proxyInfo) // 设置 HTTP(S) 代理
logInfo("Using HTTP(S) proxy: %s", proxyInfo.Redacted())
case "socks5": // 如果是 SOCKS5 代理
// 调用 newProxyDial 创建 SOCKS5 代理拨号器
proxyDialer := newProxyDial(cfg.Outbound.Url)
transport.Proxy = nil // 禁用 HTTP Proxy 设置,因为 SOCKS5 不需要 HTTP Proxy
// 尝试将 Dialer 转换为支持上下文的 ContextDialer
if contextDialer, ok := proxyDialer.(proxy.ContextDialer); ok {
transport.DialContext = contextDialer.DialContext
} else {
// 如果不支持 ContextDialer则回退到传统的 Dial 方法
transport.Dial = proxyDialer.Dial
logWarning("SOCKS5 dialer does not support ContextDialer, using legacy Dial")
}
logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url)
default: // 如果代理协议不支持
logError("Unsupported proxy scheme: %s", proxyInfo.Scheme)
transport.Proxy = http.ProxyFromEnvironment // 回退到环境变量代理
}
}
// newProxyDial 创建一个 SOCKS5 代理拨号器
func newProxyDial(proxyUrls string) proxy.Dialer {
var proxyDialer proxy.Dialer = proxy.Direct // 初始为直接连接,不使用代理
// 支持多个代理 URL以逗号分隔
for _, proxyUrl := range strings.Split(proxyUrls, ",") {
proxyUrl = strings.TrimSpace(proxyUrl) // 去除首尾空格
if proxyUrl == "" { // 跳过空的代理 URL
continue
}
// 解析代理 URL
urlInfo, err := url.Parse(proxyUrl)
if err != nil {
// 如果 URL 解析失败,记录错误日志并跳过
logError("Failed to parse proxy URL %q: %v", proxyUrl, err)
continue
}
// 检查代理协议是否为 SOCKS5
if urlInfo.Scheme != "socks5" {
logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme)
continue
}
// 解析代理认证信息(用户名和密码)
auth := parseAuth(urlInfo)
// 创建 SOCKS5 代理拨号器
dialer, err := createSocksDialer(urlInfo.Host, auth, proxyDialer)
if err != nil {
// 如果创建失败,记录错误日志并跳过
logError("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err)
continue
}
// 更新代理拨号器,支持代理链
proxyDialer = dialer
}
return proxyDialer
}
// parseAuth 解析代理 URL 中的认证信息(用户名和密码)
func parseAuth(urlInfo *url.URL) *proxy.Auth {
// 如果 URL 中没有用户信息,返回 nil
if urlInfo.User == nil {
return nil
}
// 获取用户名
username := urlInfo.User.Username()
// 获取密码注意Password() 返回两个值,需要显式处理第二个值)
password, passwordSet := urlInfo.User.Password()
if !passwordSet {
password = "" // 如果密码未设置,使用空字符串
}
// 返回包含用户名和密码的认证信息
return &proxy.Auth{
User: username,
Password: password, // 允许空密码
}
}
// createSocksDialer 创建 SOCKS5 拨号器
func createSocksDialer(host string, auth *proxy.Auth, previous proxy.Dialer) (proxy.Dialer, error) {
// 调用 golang.org/x/net/proxy 提供的 SOCKS5 方法创建拨号器
return proxy.SOCKS5("tcp", host, auth, previous)
}

153
proxy/gitreq.go Normal file
View File

@@ -0,0 +1,153 @@
package proxy
import (
"bytes"
"fmt"
"ghproxy/config"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/WJQSERVER-STUDIO/go-utils/copyb"
"github.com/gin-gonic/gin"
)
func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) {
method := c.Request.Method
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logInfo("U:%s", u)
if cfg.GitClone.Mode == "cache" {
userPath, repoPath, remainingPath, err := extractParts(u)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to extract parts from URL: %v", err))
return
}
// 构建新url
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath
}
body, err := readRequestBody(c)
if err != nil {
HandleError(c, err.Error())
return
}
bodyReader := bytes.NewBuffer(body)
// 创建请求
req, err := client.NewRequest(method, u, bodyReader)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req)
AuthPassThrough(c, cfg, req)
resp, err := client.Do(req)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
//defer resp.Body.Close()
defer func(Body io.ReadCloser) {
if err := Body.Close(); err != nil {
logError("Failed to close response body: %v", err)
}
}(resp.Body)
contentLength := resp.Header.Get("Content-Length")
if contentLength != "" {
size, err := strconv.Atoi(contentLength)
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
if err == nil && size > sizelimit {
finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
return
}
}
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
}
}
headersToRemove := map[string]struct{}{
"Content-Security-Policy": {},
"Referrer-Policy": {},
"Strict-Transport-Security": {},
}
for header := range headersToRemove {
resp.Header.Del(header)
}
switch cfg.Server.Cors {
case "*":
c.Header("Access-Control-Allow-Origin", "*")
case "":
c.Header("Access-Control-Allow-Origin", "*")
case "nil":
c.Header("Access-Control-Allow-Origin", "")
default:
c.Header("Access-Control-Allow-Origin", cfg.Server.Cors)
}
c.Status(resp.StatusCode)
/*
// 使用固定32KB缓冲池
buffer := BufferPool.Get().([]byte)
defer BufferPool.Put(buffer)
_, err = io.CopyBuffer(c.Writer, resp.Body, buffer)
if err != nil {
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
} else {
c.Writer.Flush() // 确保刷入
}
*/
_, err = copyb.CopyBuffer(c.Writer, resp.Body, nil)
if err != nil {
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
} else {
c.Writer.Flush() // 确保刷入
}
}
// extractParts 从给定的 URL 中提取所需的部分
func extractParts(rawURL string) (string, string, string, error) {
// 解析 URL
parsedURL, err := url.Parse(rawURL)
if err != nil {
return "", "", "", err
}
// 获取路径部分并分割
pathParts := strings.Split(parsedURL.Path, "/")
// 提取所需的部分
if len(pathParts) < 3 {
return "", "", "", fmt.Errorf("URL path is too short")
}
// 提取 /WJQSERVER-STUDIO 和 /go-utils.git
repoOwner := "/" + pathParts[1]
repoName := "/" + pathParts[2]
// 剩余部分
remainingPath := strings.Join(pathParts[3:], "/")
if remainingPath != "" {
remainingPath = "/" + remainingPath
}
return repoOwner, repoName, remainingPath, nil
}

234
proxy/handler.go Normal file
View File

@@ -0,0 +1,234 @@
package proxy
import (
"fmt"
"ghproxy/auth"
"ghproxy/config"
"ghproxy/rate"
"net/http"
"regexp"
"strings"
"github.com/gin-gonic/gin"
)
var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
/*
var exps = []*regexp.Regexp{
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:releases|archive)/.*`), // 匹配 GitHub Releases 或 Archive 链接
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/.*`), // 匹配 GitHub Blob 或 Raw 链接
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:info|git-).*`), // 匹配 GitHub Info 或 Git 相关链接 (例如 .gitattributes, .gitignore)
regexp.MustCompile(`^(?:https?://)?raw\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.+?/.+`), // 匹配 raw.githubusercontent.com 链接
regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/.+?/.+`), // 匹配 gist.githubusercontent.com 链接
regexp.MustCompile(`^(?:https?://)?api\.github\.com/repos/([^/]+)/([^/]+)/.*`), // 匹配 api.github.com/repos 链接 (GitHub API)
}
*/
func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter, runMode string) gin.HandlerFunc {
return func(c *gin.Context) {
// 限制访问频率
if cfg.RateLimit.Enabled {
var allowed bool
switch cfg.RateLimit.RateMethod {
case "ip":
allowed = iplimiter.Allow(c.ClientIP())
case "total":
allowed = limiter.Allow()
default:
logWarning("Invalid RateLimit Method")
return
}
if !allowed {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "Too Many Requests"})
logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Request.Method, c.Request.URL.RequestURI(), c.Request.Header.Get("User-Agent"), c.Request.Proto)
return
}
}
//rawPath := strings.TrimPrefix(c.Request.URL.Path, "/") // 去掉前缀/
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/") // 去掉前缀/
matches := re.FindStringSubmatch(rawPath) // 匹配路径
logInfo("Matches: %v", matches)
// 匹配路径错误处理
if len(matches) < 3 {
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logWarning(errMsg)
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
return
}
// 制作url
rawPath = "https://" + matches[2]
var (
user string
repo string
matcher string
)
// 匹配 "https://github.com"开头的链接
if strings.HasPrefix(rawPath, "https://github.com") {
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
if strings.HasPrefix(remainingPath, "/") {
remainingPath = strings.TrimPrefix(remainingPath, "/")
}
// 预期格式/user/repo/more...
// 取出user和repo和最后部分
parts := strings.Split(remainingPath, "/")
if len(parts) <= 2 {
logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
return
}
user = parts[0]
repo = parts[1]
// 匹配 "https://github.com"开头的链接
if len(parts) >= 3 {
switch parts[2] {
case "releases", "archive":
matcher = "releases"
case "blob", "raw":
matcher = "blob"
case "info", "git-upload-pack":
matcher = "clone"
default:
fmt.Println("Invalid URL: Unknown type")
}
}
}
// 匹配 "https://raw"开头的链接
if strings.HasPrefix(rawPath, "https://raw") {
remainingPath := strings.TrimPrefix(rawPath, "https://")
parts := strings.Split(remainingPath, "/")
if len(parts) <= 3 {
logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
return
}
user = parts[1]
repo = parts[2]
matcher = "raw"
}
// 匹配 "https://gist"开头的链接
if strings.HasPrefix(rawPath, "https://gist") {
remainingPath := strings.TrimPrefix(rawPath, "https://")
parts := strings.Split(remainingPath, "/")
if len(parts) <= 3 {
logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
return
}
user = parts[1]
matcher = "gist"
}
// 匹配 "https://api.github.com/"开头的链接
if strings.HasPrefix(rawPath, "https://api.github.com/") {
matcher = "api"
remainingPath := strings.TrimPrefix(rawPath, "https://api.github.com/")
parts := strings.Split(remainingPath, "/")
if parts[0] == "repos" {
user = parts[1]
repo = parts[2]
}
if parts[0] == "users" {
user = parts[1]
}
if cfg.Auth.AuthMethod != "header" || !cfg.Auth.Enabled {
c.JSON(http.StatusForbidden, gin.H{"error": "HeaderAuth is not enabled."})
logError("%s %s %s %s %s HeaderAuth-Error: HeaderAuth is not enabled.", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
return
}
}
username := user
//username, repo := MatchUserRepo(rawPath, cfg, c, matches) // 匹配用户名和仓库名
logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, username, repo)
// dump log 记录详细信息 c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, full Header
LogDump("%s %s %s %s %s %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, c.Request.Header)
repouser := fmt.Sprintf("%s/%s", username, repo)
// 白名单检查
if cfg.Whitelist.Enabled {
whitelist := auth.CheckWhitelist(username, repo)
if !whitelist {
logErrMsg := fmt.Sprintf("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, repouser)
errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", repouser)
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
logWarning(logErrMsg)
return
}
}
// 黑名单检查
if cfg.Blacklist.Enabled {
blacklist := auth.CheckBlacklist(username, repo)
if blacklist {
logErrMsg := fmt.Sprintf("%s %s %s %s %s Blacklist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, repouser)
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", repouser)
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
logWarning(logErrMsg)
return
}
}
/*
matches = CheckURL(rawPath, c)
if matches == nil {
c.AbortWithStatus(http.StatusNotFound)
logWarning("%s %s %s %s %s 404-NOMATCH", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
return
}
*/
// 若匹配api.github.com/repos/用户名/仓库名/路径, 则检查是否开启HeaderAuth
// 处理blob/raw路径
if matcher == "blob" {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
}
// 鉴权
authcheck, err := auth.AuthHandler(c, cfg)
if !authcheck {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
}
// IP METHOD URL USERAGENT PROTO MATCHES
logDebug("%s %s %s %s %s Matches: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, matches)
switch matcher {
case "releases", "blob", "raw", "gist", "api":
//ProxyRequest(c, rawPath, cfg, "chrome", runMode)
ChunkedProxyRequest(c, rawPath, cfg, "chrome", runMode) // dev test chunk
case "clone":
//ProxyRequest(c, rawPath, cfg, "git", runMode)
GitReq(c, rawPath, cfg, "git", runMode)
default:
c.String(http.StatusForbidden, "Invalid input.")
fmt.Println("Invalid input.")
return
}
}
}
/*
func CheckURL(u string, c *gin.Context) []string {
for _, exp := range exps {
if matches := exp.FindStringSubmatch(u); matches != nil {
return matches[1:]
}
}
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logError(errMsg)
return nil
}
*/

95
proxy/httpc.go Normal file
View File

@@ -0,0 +1,95 @@
package proxy
import (
"fmt"
"ghproxy/config"
"net/http"
"sync"
"time"
httpc "github.com/satomitouka/touka-httpc"
)
var BufferSize int = 32 * 1024 // 32KB
var (
tr *http.Transport
BufferPool *sync.Pool
client *httpc.Client
)
func InitReq(cfg *config.Config) {
initHTTPClient(cfg)
// 初始化固定大小的缓存池
BufferPool = &sync.Pool{
New: func() interface{} {
return make([]byte, BufferSize)
},
}
}
func initHTTPClient(cfg *config.Config) {
/*
ctr = &http.Transport{
MaxIdleConns: 100,
MaxConnsPerHost: 60,
IdleConnTimeout: 20 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
}
*/
var proTolcols = new(http.Protocols)
proTolcols.SetHTTP1(true)
proTolcols.SetHTTP2(true)
proTolcols.SetUnencryptedHTTP2(true)
if cfg.Httpc.Mode == "auto" {
tr = &http.Transport{
//MaxIdleConns: 160,
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
Protocols: proTolcols,
}
} else if cfg.Httpc.Mode == "advanced" {
tr = &http.Transport{
MaxIdleConns: cfg.Httpc.MaxIdleConns,
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
Protocols: proTolcols,
}
} else {
// 错误的模式
logError("unknown httpc mode: %s", cfg.Httpc.Mode)
fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
logWarning("use Auto to Run HTTP Client")
fmt.Println("use Auto to Run HTTP Client")
tr = &http.Transport{
//MaxIdleConns: 160,
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
}
}
if cfg.Outbound.Enabled {
initTransport(cfg, tr)
}
if cfg.Server.Debug {
client = httpc.New(
httpc.WithTransport(tr),
httpc.WithDumpLog(),
)
} else {
client = httpc.New(
httpc.WithTransport(tr),
)
}
}

34
proxy/matchrepo.go Normal file
View File

@@ -0,0 +1,34 @@
package proxy
import (
"fmt"
"ghproxy/config"
"net/http"
"regexp"
"github.com/gin-gonic/gin"
)
// 预定义regex
var (
pathRegex = regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`) // 匹配路径
gistRegex = regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.*`) // 匹配gist路径
)
// 提取用户名和仓库名
func MatchUserRepo(rawPath string, cfg *config.Config, c *gin.Context, matches []string) (string, string) {
if gistMatches := gistRegex.FindStringSubmatch(rawPath); len(gistMatches) == 3 {
LogDump("%s %s %s %s %s Matched-Username: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, gistMatches[1])
return gistMatches[1], ""
}
// 定义路径
if pathMatches := pathRegex.FindStringSubmatch(matches[2]); len(pathMatches) >= 4 {
return pathMatches[2], pathMatches[3]
}
// 返回错误信息
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logWarning(errMsg)
c.String(http.StatusForbidden, "Invalid path; expected username/repo, Path: %s", rawPath)
return "", ""
}

View File

@@ -1,216 +1,36 @@
// proxy/proxy.go 实验性
package proxy
import (
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"ghproxy/auth"
"ghproxy/config"
"ghproxy/logger"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin"
"github.com/imroc/req/v3"
)
var logw = logger.Logw
var exps = []*regexp.Regexp{
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:releases|archive)/.*`),
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/.*`),
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:info|git-).*`),
regexp.MustCompile(`^(?:https?://)?raw\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.+?/.+`),
regexp.MustCompile(`^(?:https?://)?gist\.github\.com/([^/]+)/.+?/.+`),
}
func NoRouteHandler(cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/")
re := regexp.MustCompile(`^(http:|https:)?/?/?(.*)`)
matches := re.FindStringSubmatch(rawPath)
if len(matches) < 3 {
logw("Invalid URL: %s", rawPath)
c.String(http.StatusForbidden, "Invalid URL.")
return
}
rawPath = "https://" + matches[2]
// 提取用户名和仓库名,格式为 handle/<username>/<repo>/*
pathmatches := regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`)
pathParts := pathmatches.FindStringSubmatch(matches[2])
if len(pathParts) < 4 {
logw("Invalid path: %s", rawPath)
c.String(http.StatusForbidden, "Invalid path; expected username/repo.")
return
}
username := pathParts[2]
repo := pathParts[3]
logw("Blacklist Check > Username: %s, Repo: %s", username, repo)
fullrepo := fmt.Sprintf("%s/%s", username, repo)
// 黑名单检查
blacklistpass := auth.CheckBlacklist(fullrepo)
if blacklistpass {
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", fullrepo)
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
logw(errMsg)
return
}
matches = CheckURL(rawPath)
if matches == nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
if exps[1].MatchString(rawPath) {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
}
if !auth.AuthHandler(c, cfg) {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
logw("Unauthorized request: %s", rawPath)
return
}
logw("Matches: %v", matches)
switch {
case exps[0].MatchString(rawPath), exps[1].MatchString(rawPath), exps[3].MatchString(rawPath), exps[4].MatchString(rawPath):
logw("%s Matched - USE proxy-chrome", rawPath)
ProxyRequest(c, rawPath, cfg, "chrome")
case exps[2].MatchString(rawPath):
logw("%s Matched - USE proxy-git", rawPath)
ProxyRequest(c, rawPath, cfg, "git")
default:
c.String(http.StatusForbidden, "Invalid input.")
return
}
}
}
func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string) {
method := c.Request.Method
logw("%s %s", method, u)
client := req.C()
switch mode {
case "chrome":
client.SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36").
SetTLSFingerprintChrome().
ImpersonateChrome()
case "git":
client.SetUserAgent("git/2.33.1")
}
// 日志模块
var (
logw = logger.Logw
LogDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
// 读取请求体
func readRequestBody(c *gin.Context) ([]byte, error) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to read request body: %v", err))
return
logError("failed to read request body: %v", err)
return nil, fmt.Errorf("failed to read request body: %v", err)
}
defer c.Request.Body.Close()
req := client.R().SetBody(body)
for key, values := range c.Request.Header {
for _, value := range values {
req.SetHeader(key, value)
}
}
resp, err := SendRequest(req, method, u)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
defer resp.Body.Close()
if err := HandleResponseSize(resp, cfg, c); err != nil {
logw("Error handling response size: %v", err)
return
}
CopyResponseHeaders(resp, c, cfg)
c.Status(resp.StatusCode)
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
logw("Failed to copy response body: %v", err)
}
}
func SendRequest(req *req.Request, method, url string) (*req.Response, error) {
switch method {
case "GET":
return req.Get(url)
case "POST":
return req.Post(url)
case "PUT":
return req.Put(url)
case "DELETE":
return req.Delete(url)
default:
logw("Unsupported method: %s", method)
return nil, fmt.Errorf("unsupported method: %s", method)
}
}
func HandleResponseSize(resp *req.Response, cfg *config.Config, c *gin.Context) error {
contentLength := resp.Header.Get("Content-Length")
if contentLength != "" {
size, err := strconv.Atoi(contentLength)
if err == nil && size > cfg.Server.SizeLimit {
finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL)
logw("Redirecting to %s due to size limit (%d bytes)", finalURL, size)
return fmt.Errorf("response size exceeds limit")
}
}
return nil
}
func CopyResponseHeaders(resp *req.Response, c *gin.Context, cfg *config.Config) {
headersToRemove := map[string]struct{}{
"Content-Security-Policy": {},
"Referrer-Policy": {},
"Strict-Transport-Security": {},
}
for header := range headersToRemove {
resp.Header.Del(header)
}
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
}
}
c.Header("Access-Control-Allow-Origin", "")
if cfg.CORS.Enabled {
c.Header("Access-Control-Allow-Origin", "*")
}
return body, nil
}
func HandleError(c *gin.Context, message string) {
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message))
logw(message)
}
func CheckURL(u string) []string {
for _, exp := range exps {
if matches := exp.FindStringSubmatch(u); matches != nil {
logw("URL matched: %s, Matches: %v", u, matches[1:])
return matches[1:]
}
}
errMsg := fmt.Sprintf("Invalid URL: %s", u)
logw(errMsg)
return nil
logError(message)
}

79
proxy/proxyreq.go Normal file
View File

@@ -0,0 +1,79 @@
package proxy
/*
func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) {
method := c.Request.Method
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
client := createHTTPClient(mode)
if runMode == "dev" {
client.DevMode()
}
// 发送HEAD请求, 预获取Content-Length
headReq := client.R()
setRequestHeaders(c, headReq)
AuthPassThrough(c, cfg, headReq)
headResp, err := headReq.Head(u)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
defer headResp.Body.Close()
if err := HandleResponseSize(headResp, cfg, c); err != nil {
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
}
body, err := readRequestBody(c)
if err != nil {
HandleError(c, err.Error())
return
}
req := client.R().SetBody(body)
setRequestHeaders(c, req)
AuthPassThrough(c, cfg, req)
resp, err := SendRequest(c, req, method, u)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
defer resp.Body.Close()
if err := HandleResponseSize(resp, cfg, c); err != nil {
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
}
CopyResponseHeaders(resp, c, cfg)
c.Status(resp.StatusCode)
if err := copyResponseBody(c, resp.Body); err != nil {
logError("%s %s %s %s %s Response-Copy-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
}
}
// 复制响应体
func copyResponseBody(c *gin.Context, respBody io.Reader) error {
_, err := io.Copy(c.Writer, respBody)
return err
}
// 判断并选择TLS指纹
func createHTTPClient(mode string) *req.Client {
client := req.C()
switch mode {
case "chrome":
client.SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36").
SetTLSFingerprintChrome().
ImpersonateChrome()
case "git":
client.SetUserAgent("git/2.33.1")
}
return client
}
*/

21
proxy/reqheader.go Normal file
View File

@@ -0,0 +1,21 @@
package proxy
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 设置请求头
func setRequestHeaders(c *gin.Context, req *http.Request) {
for key, values := range c.Request.Header {
for _, value := range values {
req.Header.Set(key, value)
}
}
}
func removeWSHeader(req *http.Request) {
req.Header.Del("Upgrade")
req.Header.Del("Connection")
}

65
rate/rate.go Normal file
View File

@@ -0,0 +1,65 @@
package rate
import (
"time"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"golang.org/x/time/rate"
)
// 日志输出
var (
logw = logger.Logw
LogDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
// 总体限流器
type RateLimiter struct {
limiter *rate.Limiter
}
// 基于IP的限流器
type IPRateLimiter struct {
limiters map[string]*RateLimiter
limit int
burst int
duration time.Duration
}
func New(limit int, burst int, duration time.Duration) *RateLimiter {
return &RateLimiter{
limiter: rate.NewLimiter(rate.Limit(float64(limit)/duration.Seconds()), burst),
}
}
func (rl *RateLimiter) Allow() bool {
return rl.limiter.Allow()
}
func NewIPRateLimiter(limit int, burst int, duration time.Duration) *IPRateLimiter {
return &IPRateLimiter{
limiters: make(map[string]*RateLimiter),
limit: limit,
burst: burst,
duration: duration,
}
}
func (rl *IPRateLimiter) Allow(ip string) bool {
if ip == "" {
logWarning("empty ip")
return false
}
limiter, ok := rl.limiters[ip]
if !ok {
// 创建新的 RateLimiter 并存储
limiter = New(rl.limit, rl.burst, rl.duration)
rl.limiters[ip] = limiter
}
return limiter.Allow()
}

86
timing/timing.go Normal file
View File

@@ -0,0 +1,86 @@
package timing
import (
"sync"
"time"
"github.com/gin-gonic/gin"
)
// 阶段计时结构(固定数组优化)
type timingData struct {
phases [8]struct { // 预分配8个阶段存储
name string
dur time.Duration
}
count int
start time.Time
}
// 对象池(内存重用优化)
var pool = sync.Pool{
New: func() interface{} {
return new(timingData)
},
}
// 中间件入口
func Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 从池中获取计时器
td := pool.Get().(*timingData)
td.start = time.Now()
td.count = 0
// 存储到上下文
c.Set("timing", td)
// 请求完成后回收对象
defer func() {
pool.Put(td)
}()
c.Next()
}
}
// 记录阶段耗时
func Record(c *gin.Context, name string) {
if val, exists := c.Get("timing"); exists {
//td := val.(*timingData)
td, ok := val.(*timingData)
if !ok {
return
}
if td.count < len(td.phases) {
td.phases[td.count].name = name
td.phases[td.count].dur = time.Since(td.start) // 直接记录当前时间
td.count++
}
}
}
// 获取计时结果(日志输出用)
func Get(c *gin.Context) (total time.Duration, phases []struct {
Name string
Dur time.Duration
}) {
if val, exists := c.Get("timing"); exists {
//td := val.(*timingData)
td, ok := val.(*timingData)
if !ok {
return
}
for i := 0; i < td.count; i++ {
phases = append(phases, struct {
Name string
Dur time.Duration
}{
Name: td.phases[i].name,
Dur: td.phases[i].dur,
})
}
total = time.Since(td.start)
}
return
}