Compare commits

...

58 Commits

Author SHA1 Message Date
WJQSERVER
bf75e62eb2 Merge pull request #121 from WJQSERVER-STUDIO/dev
3.5.3
2025-06-13 15:30:54 +08:00
wjqserver
a5bf7686bd 3.5.3 2025-06-13 15:23:46 +08:00
WJQSERVER
a1991367c3 Merge pull request #120 from WJQSERVER-STUDIO/dev
3.5.2
2025-06-11 11:55:13 +08:00
wjqserver
b86e58cddf 3.5.2 2025-06-11 11:40:00 +08:00
wjqserver
44673b9a3f 3.5.2 2025-06-11 11:39:10 +08:00
wjqserver
5b05588375 update deps && LICENSE 2025-06-10 22:11:52 +08:00
WJQSERVER
65769975b6 Merge pull request #119 from WJQSERVER-STUDIO/dev
optimize matcher performance
2025-06-09 23:32:08 +08:00
wjqserver
5731418822 3.5.1 2025-06-09 23:31:37 +08:00
wjqserver
8d5b764ec7 optimize matcher 2025-06-09 23:30:39 +08:00
wjqserver
5dde21a403 optimize matcher performance 2025-06-09 06:58:21 +08:00
wjqserver
f706615d87 update deps 2025-06-07 23:44:47 +08:00
WJQSERVER
b29940df21 Merge pull request #118 from WJQSERVER-STUDIO/dev
3.5.0
2025-06-05 22:55:42 +08:00
wjqserver
185522133b 3.5.0 2025-06-05 22:55:24 +08:00
wjqserver
6be6e1ba2c 25w44a 2025-06-05 20:14:58 +08:00
WJQSERVER
1ba100c28d Merge pull request #116 from WJQSERVER-STUDIO/dev
3.4.3
2025-06-05 17:30:01 +08:00
wjqserver
1370617f5b 3.4.3 2025-06-05 17:29:18 +08:00
wjqserver
e829c2baff 25w43a 2025-06-05 00:24:21 +08:00
WJQSERVER
75d909ef16 Merge pull request #113 from WJQSERVER-STUDIO/dev
revert github.com/nyaruka/phonenumbers to v1.6.1
2025-06-03 16:48:03 +08:00
wjqserver
2bab0a9774 3.4.2 2025-06-03 16:47:00 +08:00
wjqserver
171fe61342 25w42a 2025-06-02 21:00:49 +08:00
WJQSERVER
362ad96fbe Merge pull request #111 from WJQSERVER-STUDIO/dev
update docs link
2025-05-30 17:32:00 +08:00
wjqserver
5b17e1f0b6 update docs link 2025-05-30 17:31:35 +08:00
WJQSERVER
e40e1aadee Merge 3.4.1 #110 2025-05-29 15:26:44 +08:00
wjqserver
82943428d3 optimize cache & remove panic change to fallback error json 2025-05-29 15:13:28 +08:00
wjqserver
b7ce929db8 3.4.1 2025-05-29 15:01:44 +08:00
wjqserver
68bf51aaed 25w41b 2025-05-28 21:35:52 +08:00
wjqserver
16b6b05fb8 25w41a 2025-05-28 20:17:04 +08:00
WJQSERVER
d2b2d823b8 3.4.0
Add support for multi-target docker image(oci) proxy (3.4.0)
Add Hub theme
2025-05-21 16:24:08 +08:00
wjqserver
4598257faa update readme.md 2025-05-21 16:07:07 +08:00
wjqserver
1afb352194 3.4.0 2025-05-21 15:24:05 +08:00
wjqserver
430e313d47 avoid nil *ptr & fix path 2025-05-21 12:08:17 +08:00
wjqserver
31d435bfa0 add oci proxy & nest shell api 2025-05-21 11:55:04 +08:00
wjqserver
6ff23f639e add hub theme & add more check for wcache close 2025-05-21 11:54:43 +08:00
wjqserver
c7954ae91a 25w40a 2025-05-21 09:03:14 +08:00
wjqserver
11099176bf add support for multi-target docker image(oci) proxy 2025-05-21 09:03:00 +08:00
WJQSERVER
f3eb92ea51 Merge pull request #107 from WJQSERVER-STUDIO/dev
3.3.3
2025-05-20 10:10:19 +08:00
wjqserver
5ddbf1d2a0 3.3.3 2025-05-20 10:05:55 +08:00
wjqserver
d38ca3969f revert route handle for 3.3.x 2025-05-20 10:03:48 +08:00
wjqserver
146b0d7748 update nest 2025-05-19 12:16:20 +08:00
wjqserver
d92424cb94 25w39a 2025-05-19 12:00:36 +08:00
WJQSERVER
0f437dc891 Merge pull request #106 from WJQSERVER-STUDIO/dev
3.3.2
2025-05-18 07:05:19 +08:00
wjqserver
816b35654a update readme.md 2025-05-18 06:20:55 +08:00
wjqserver
a4fae95526 3.3.2 2025-05-18 06:13:00 +08:00
wjqserver
ea0e4e9801 change the default theme to design 2025-05-18 06:11:44 +08:00
wjqserver
5facc36947 update docs 2025-05-18 06:09:04 +08:00
WJQSERVER
5c25bc012f Merge pull request #105 from WJQSERVER-STUDIO/dev
3.3.1
2025-05-16 19:54:20 +08:00
wjqserver
b2712f8184 3.3.1 2025-05-16 19:53:48 +08:00
wjqserver
566a0ea26a 25w37a 2025-05-16 19:28:08 +08:00
wjqserver
7d4aae1668 merge customTarget into target 2025-05-16 00:24:57 +08:00
wjqserver
052243b095 add customTarget 2025-05-16 00:15:04 +08:00
wjqserver
4ded2186d8 update deps 2025-05-15 18:50:36 +08:00
WJQSERVER
aa95daf8c0 Merge pull request #103 from WJQSERVER-STUDIO/dev
3.3.0
2025-05-15 18:46:29 +08:00
wjqserver
89b850c1ec 3.3.0 2025-05-15 18:45:52 +08:00
wjqserver
ce814875e1 25w36d 2025-05-14 17:55:37 +08:00
wjqserver
47c03763a7 25w36c 2025-05-14 01:34:05 +08:00
wjqserver
71bc2aaed7 add bandwidth limiter 2025-05-14 01:33:54 +08:00
wjqserver
3f8d16511e 25w36b 2025-05-13 19:04:21 +08:00
wjqserver
43469532d4 25w36a 2025-05-13 14:51:34 +08:00
32 changed files with 2628 additions and 597 deletions

View File

@@ -61,7 +61,7 @@ jobs:
fi
- name: 拉取前端
run: |
sudo git clone https://github.com/WJQSERVER-STUDIO/GHPrxoy-Frontend.git pages
sudo git clone https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend.git pages
sudo rm -rf pages/.git/
- name: 安装 Go

View File

@@ -62,7 +62,7 @@ jobs:
fi
- name: 拉取前端
run: |
sudo git clone https://github.com/WJQSERVER-STUDIO/GHPrxoy-Frontend.git pages
sudo git clone https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend.git pages
sudo rm -rf pages/.git/
- name: 安装 Go

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ demo.toml
*.bak
list.json
repos
pages
pages
*_test

View File

@@ -1,5 +1,148 @@
# 更新日志
3.5.3 - 2025-06-13
---
- CHANGE: 显式配置`WithStreamBody(true)`
3.5.2 - 2025-06-11
---
- CHANGE: 加入MPL 2.0许可证, 项目转为双重许可
3.5.1 - 2025-06-09
---
- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op`
25w45a - 2025-06-09
---
- PRE-RELEASE: 此版本是v3.5.1预发布版本,请勿在生产环境中使用;
- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op`
3.5.0 - 2025-06-05
---
- CHANGE: 更新许可证 v2.0 => v2.1
- CHANGE: 修正工作流的一些问题
- ADD: 增加`ForceAllowApiPassList`, 实现 #114
25w44a - 2025-06-05
---
- PRE-RELEASE: 此版本是v3.5.0预发布版本,请勿在生产环境中使用;
- CHANGE: 更新许可证 v2.0 => v2.1
- CHANGE: 修正工作流的一些问题
- ADD: 增加`ForceAllowApiPassList`, 实现 #114
3.4.3 - 2025-06-05
---
- CHANGE: 弃用`adaptor.GetCompatRequest`, 切换到`adaptor.HertzHandler`
- CHANGE: 为`embedFS`使用包装器, 使其支持`Last-Modified`
- CHANGE: 为静态资源增加`Cache-Control: public, max-age=3600, must-revalidate`
25w43a - 2025-06-05
---
- PRE-RELEASE: 此版本是v3.4.3预发布版本,请勿在生产环境中使用;
- CHANGE: 弃用`adaptor.GetCompatRequest`, 切换到`adaptor.HertzHandler`
- CHANGE: 为`embedFS`使用包装器, 使其支持`Last-Modified`
- CHANGE: 为静态资源增加`Cache-Control: public, max-age=3600, must-revalidate`
3.4.2 - 2025-06-03
---
- DEP: 回滚 github.com/nyaruka/phonenumbers 版本到 v1.6.1, v1.6.3观测到了一些反射造成的内存占用异常
25w42a - 2025-06-02
---
- PRE-RELEASE: 此版本是v3.4.2预发布版本,请勿在生产环境中使用;
- DEP: 回滚 github.com/nyaruka/phonenumbers 版本到 v1.6.1, v1.6.3观测到了一些反射造成的内存占用异常
3.4.1 - 2025-05-29
---
- ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染
- CHANGE: 把json库替换到[sonic](github.com/bytedance/sonic)
25w41b - 2025-05-28
---
- PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用;
- CHANGE: 把json库替换到[sonic](github.com/bytedance/sonic)
25w41a - 2025-05-28
---
- PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用;
- ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染
- CHANGE: 替换到实验性的`encoding/json/v2`
3.4.0 - 2025-05-21
---
- ADD: 初步实现多`target` Docker代理
- ADD: 加入`weakcache`用于处理短期令牌
- ADD: 新增`hub`主题
- ADD: 新增`/api/shell_nest/status``/api/oci_proxy/status` API
25w40b - 2025-05-21
---
- PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用;
- ADD: 新增`hub`主题
- ADD: 新增`/api/shell_nest/status``/api/oci_proxy/status` API
- CHANGE: 对细节进行优化
25w40a - 2025-05-21
---
- PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用;
- ADD: 初步实现多`target` Docker代理
- ADD: 加入`weakcache`用于处理短期令牌
3.3.3 - 2025-05-20
---
- CHANGE: 加入`senseClientDisconnection``async`配置项
25w39a - 2025-05-19
---
- PRE-RELEASE: 此版本是v3.3.3预发布版本,请勿在生产环境中使用;
- CHANGE: 加入`senseClientDisconnection``async`配置项
3.3.2 - 2025-05-18
---
- CHANGE: 默认主题改为`design`
25w38a - 2025-05-18
---
- PRE-RELEASE: 此版本是v3.3.2预发布版本,请勿在生产环境中使用;
- CHANGE: 默认主题改为`design`
3.3.1 - 2025-05-16
- CHANGE: 为`target`放宽限制, 支持自定义
- CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0`
25w37a - 2025-05-16
---
- PRE-RELEASE: 此版本是v3.3.1预发布版本,请勿在生产环境中使用;
- CHANGE: 为`target`放宽限制, 支持自定义
- CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0`
3.3.0 - 2025-05-15
---
- CHANGE: 为`httpc`加入`request builder``withcontext`选项
- ADD: 加入带宽限制功能
- ADD: 为`netpoll`模式开启探测客户端是否断开功能
25w36d - 2025-05-14
---
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
- ADD: 为`netpoll`模式开启探测客户端是否断开功能
25w36c - 2025-05-14
---
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
- ADD: 加入带宽限制功能
- CHANGE: 将`httpc`切换回主分支, `25w36b`测试的部分已被合入`httpc`主线
25w36b - 2025-05-13
---
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
- CHANGE: `httpc`切换到`dev`, 测试在retry前检查ctx状态
25w36a - 2025-05-13
---
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
- CHANGE: 为`httpc`加入`request builder``withcontext`选项
3.2.4 - 2025-05-13
---
- CHANGE: 移除未使用的变量与相关计算

View File

@@ -1 +1 @@
25w35a
25w45a

514
LICENSE
View File

@@ -1,197 +1,373 @@
WJQserver Studio 开源许可证
版本 v2.0
Mozilla Public License Version 2.0
==================================
版权所有 © WJQserver Studio 2024
1. Definitions
--------------
定义
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。
* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。
* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。
* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。
* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。
* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于:
* 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。
* 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。
* 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。
* 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。
* 内部运营用途 (非营利组织) 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
开源与自由软件
1.3. "Contribution"
means Covered Software of a particular Contributor.
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。
强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
许可证条款
1.5. "Incompatible With Secondary Licenses"
means
1. 使用权限
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款:
1.6. "Executable Form"
means any form of the work other than Source Code Form.
* 1.2.1 保持声明: 您在进行商业使用时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。
* 1.2.2 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于:
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
* 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。
* 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。
* 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。
* 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。
1.8. "License"
means this document.
您必须选择以下两种方式之一:
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
* i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。
* ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。
1.10. "Modifications"
means any of the following:
2. 复制与分发
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件:
(b) any new file in Source Code Form that contains any Covered
Software.
* 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。
* 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.2.2 条(开源继承与互惠共享)的约束。
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
3. 修改权限
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.2.2 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。
2. License Grants and Conditions
--------------------------------
* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。
2.1. Grants
4. 专利权
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
5. 免责声明
2.2. Effective Date
* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害。
2.3. Limitations on Grant Scope
* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。
6. 许可证期限与终止
* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。
* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。
* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。
7. 条款修订
* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。
* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。
8. 其他
* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。
* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。
* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。
WJQserver Studio Open Source License
Version v2.0
Copyright © WJQserver Studio 2024
Definitions
* 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. Permissions for Use
* 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.
* 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:
* 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:
* 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.
You must choose one of the following two options:
* 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.
2. Reproduction and Distribution
* 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:
* 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.
* 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).
3. Modification Permissions
* 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.
* 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.
* 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.
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

199
LICENSE-WSL Normal file
View File

@@ -0,0 +1,199 @@
WJQserver Studio 开源许可证
版本 v2.1
版权所有 © WJQserver Studio 2024
定义
* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。
* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。
* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。
* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。
* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。
* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于:
* 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。
* 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。
* 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。
* 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。
* 内部运营用途 (非营利组织) 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。
开源与自由软件
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。
强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。
许可证条款
1. 使用权限
* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。
* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款:
* 1.2.1 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于:
* 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。
* 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。
* 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。
* 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。
您必须选择以下两种方式之一:
* i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。
* ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。
* 1.3 保持声明: 公开发布服务时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。
2. 复制与分发
* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件:
* 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。
* 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。
* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.3 条(开源继承与互惠共享)的约束。
3. 修改权限
* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。
* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.3 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。
* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。
4. 专利权
* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。
* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。
5. 免责声明
* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。
* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害。
* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。
6. 许可证期限与终止
* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。
* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。
* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。
7. 条款修订
* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。
* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。
8. 其他
* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。
* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。
* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。
WJQserver Studio Open Source License
Version v2.1
Copyright © WJQserver Studio 2024
Definitions
* 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. Permissions for Use
* 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.
* 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:
* 1.2.1 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:
* 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.
You must choose one of the following two options:
* 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.
* 1.3 Maintain Statements: When publish services to public, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software.
2. Reproduction and Distribution
* 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:
* 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.
* 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.3 of this License (Open Source Inheritance and Reciprocal Sharing).
3. Modification Permissions
* 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.
* 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.3 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.
* 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.

View File

@@ -17,12 +17,13 @@
- 🌐 **使用字节旗下的 [HertZ](https://github.com/cloudwego/hertz) 作为 Web 框架**
- 📡 **使用 [Touka-HTTPC](https://github.com/satomitouka/touka-httpc) 作为 HTTP 客户端**
- 📥 **支持 Git clone、raw、releases 等文件拉取**
- 🐳 **支持反代Docker, GHCR等镜像仓库**
- 🎨 **支持多个前端主题**
- 🚫 **支持自定义黑名单/白名单**
- 🗄️ **支持 Git Clone 缓存(配合 [Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git)**
- 🐳 **支持 Docker 部署**
- 🐳 **支持自托管**
- 🐳 **支持自托管与Docker容器化部署**
-**支持速率限制**
-**支持带宽速率限制**
- 🔒 **支持用户鉴权**
- 🐚 **支持 shell 脚本多层嵌套加速**
@@ -34,11 +35,11 @@
[相关文章](https://blog.wjqserver.com/categories/my-program/)
[项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md)
[GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护
### 使用示例
```
```bash
# 下载文件
https://ghproxy.1888866.xyz/raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh
https://ghproxy.1888866.xyz/https://raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh
@@ -46,6 +47,15 @@ https://ghproxy.1888866.xyz/https://raw.githubusercontent.com/WJQSERVER-STUDIO/t
# 克隆仓库
git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git
git clone https://ghproxy.1888866.xyz/https://github.com/WJQSERVER-STUDIO/ghproxy.git
# Docker(OCI) 代理
docker pull gh.example.com/wjqserver/ghproxy
docker pull gh.example.com/adguard/adguardhome
docker pull gh.example.com/docker.io/wjqserver/ghproxy
docker pull gh.example.com/docker.io/adguard/adguardhome
docker pull gh.example.com/ghcr.io/openfaas/queue-worker
```
## 部署说明
@@ -99,7 +109,9 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox
## LICENSE
本项目使用WJQserver Studio License 2.0 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)
v3.5.2开始, 本项目使用 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) 和 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 双重许可, 您可从中选择一个使用
前端位于单独仓库中, 且各个主题均存在各自的许可证, 本项目许可证并不包括前端
在v2.3.0之前, 本项目使用WJQserver Studio License 1.2

View File

@@ -1 +1 @@
3.2.4
3.5.3

View File

@@ -49,14 +49,18 @@ func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) {
apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) {
SmartGitStatusHandler(cfg, c, ctx)
})
apiRouter.GET("/shell_nest/status", func(ctx context.Context, c *app.RequestContext) {
shellNestStatusHandler(cfg, c, ctx)
})
apiRouter.GET("/oci_proxy/status", func(ctx context.Context, c *app.RequestContext) {
ociProxyStatusHandler(cfg, c, ctx)
})
}
logInfo("API router Init success")
}
func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
sizeLimit := cfg.Server.SizeLimit
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"MaxResponseBodySize": sizeLimit,
@@ -64,7 +68,6 @@ func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Con
}
func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"Whitelist": cfg.Whitelist.Enabled,
@@ -72,7 +75,6 @@ func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte
}
func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"Blacklist": cfg.Blacklist.Enabled,
@@ -80,7 +82,6 @@ func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte
}
func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"Cors": cfg.Server.Cors,
@@ -88,23 +89,24 @@ func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Co
}
func HealthcheckHandler(c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"Status": "OK",
"Repo": "WJQSERVER-STUDIO/GHProxy",
"Author": "WJQSERVER-STUDIO",
}))
}
func VersionHandler(c *app.RequestContext, ctx context.Context, version string) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"Version": version,
"Repo": "WJQSERVER-STUDIO/GHProxy",
"Author": "WJQSERVER-STUDIO",
}))
}
func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"RateLimit": cfg.RateLimit.Enabled,
@@ -112,7 +114,6 @@ func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte
}
func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"RatePerMinute": cfg.RateLimit.RatePerMinute,
@@ -120,9 +121,23 @@ func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx contex
}
func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"enabled": cfg.GitClone.Mode == "cache",
}))
}
func shellNestStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"enabled": cfg.Shell.Editor,
}))
}
func ociProxyStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"enabled": cfg.Docker.Enabled,
"target": cfg.Docker.Target,
}))
}

View File

@@ -1,12 +1,13 @@
package auth
import (
"encoding/json"
"fmt"
"ghproxy/config"
"os"
"strings"
"sync"
json "github.com/bytedance/sonic"
)
type Blacklist struct {

View File

@@ -1,12 +1,13 @@
package auth
import (
"encoding/json"
"fmt"
"ghproxy/config"
"os"
"strings"
"sync"
json "github.com/bytedance/sonic"
)
// Whitelist 用于存储白名单信息

View File

@@ -34,14 +34,15 @@ debug = false
*/
type ServerConfig struct {
Port int `toml:"port"`
Host string `toml:"host"`
NetLib string `toml:"netlib"`
SizeLimit int `toml:"sizeLimit"`
MemLimit int64 `toml:"memLimit"`
H2C bool `toml:"H2C"`
Cors string `toml:"cors"`
Debug bool `toml:"debug"`
Port int `toml:"port"`
Host string `toml:"host"`
NetLib string `toml:"netlib"`
SenseClientDisconnection bool `toml:"senseClientDisconnection"`
SizeLimit int `toml:"sizeLimit"`
MemLimit int64 `toml:"memLimit"`
H2C bool `toml:"H2C"`
Cors string `toml:"cors"`
Debug bool `toml:"debug"`
}
/*
@@ -98,6 +99,7 @@ type LogConfig struct {
LogFilePath string `toml:"logFilePath"`
MaxLogSize int `toml:"maxLogSize"`
Level string `toml:"level"`
Async bool `toml:"async"`
HertZLogPath string `toml:"hertzLogPath"`
}
@@ -108,15 +110,17 @@ Key = ""
Token = "token"
enabled = false
passThrough = false
ForceAllowApi = true
ForceAllowApi = false
ForceAllowApiPassList = false
*/
type AuthConfig struct {
Enabled bool `toml:"enabled"`
Method string `toml:"method"`
Key string `toml:"key"`
Token string `toml:"token"`
PassThrough bool `toml:"passThrough"`
ForceAllowApi bool `toml:"ForceAllowApi"`
Enabled bool `toml:"enabled"`
Method string `toml:"method"`
Key string `toml:"key"`
Token string `toml:"token"`
PassThrough bool `toml:"passThrough"`
ForceAllowApi bool `toml:"ForceAllowApi"`
ForceAllowApiPassList bool `toml:"ForceAllowApiPassList"`
}
type BlacklistConfig struct {
@@ -129,11 +133,35 @@ type WhitelistConfig struct {
WhitelistFile string `toml:"whitelistFile"`
}
/*
[rateLimit]
enabled = false
rateMethod = "total" # "total" or "ip"
ratePerMinute = 100
burst = 10
[rateLimit.bandwidthLimit]
enabled = false
totalLimit = "100mbps"
totalBurst = "100mbps"
singleLimit = "10mbps"
singleBurst = "10mbps"
*/
type RateLimitConfig struct {
Enabled bool `toml:"enabled"`
RateMethod string `toml:"rateMethod"`
RatePerMinute int `toml:"ratePerMinute"`
Burst int `toml:"burst"`
Enabled bool `toml:"enabled"`
RateMethod string `toml:"rateMethod"`
RatePerMinute int `toml:"ratePerMinute"`
Burst int `toml:"burst"`
BandwidthLimit BandwidthLimitConfig
}
type BandwidthLimitConfig struct {
Enabled bool `toml:"enabled"`
TotalLimit string `toml:"totalLimit"`
TotalBurst string `toml:"totalBurst"`
SingleLimit string `toml:"singleLimit"`
SingleBurst string `toml:"singleBurst"`
}
/*
@@ -232,12 +260,13 @@ func DefaultConfig() *Config {
HertZLogPath: "/data/ghproxy/log/hertz.log",
},
Auth: AuthConfig{
Enabled: false,
Method: "parameters",
Key: "",
Token: "token",
PassThrough: false,
ForceAllowApi: false,
Enabled: false,
Method: "parameters",
Key: "",
Token: "token",
PassThrough: false,
ForceAllowApi: false,
ForceAllowApiPassList: false,
},
Blacklist: BlacklistConfig{
Enabled: false,
@@ -252,6 +281,13 @@ func DefaultConfig() *Config {
RateMethod: "total",
RatePerMinute: 100,
Burst: 10,
BandwidthLimit: BandwidthLimitConfig{
Enabled: false,
TotalLimit: "100mbps",
TotalBurst: "100mbps",
SingleLimit: "10mbps",
SingleBurst: "10mbps",
},
},
Outbound: OutboundConfig{
Enabled: false,

View File

@@ -2,6 +2,7 @@
host = "0.0.0.0"
port = 8080
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
senseClientDisconnection = false
sizeLimit = 125 # MB
memLimit = 0 # MB
H2C = true
@@ -33,6 +34,7 @@ staticDir = "/data/www"
logFilePath = "/data/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB
level = "info" # dump, debug, info, warn, error, none
async = false
hertzLogPath = "/data/ghproxy/log/hertz.log"
[auth]
@@ -42,6 +44,7 @@ key = ""
enabled = false
passThrough = false
ForceAllowApi = false
ForceAllowApiPassList = false
[blacklist]
blacklistFile = "/data/ghproxy/config/blacklist.json"
@@ -57,6 +60,13 @@ rateMethod = "total" # "ip" or "total"
ratePerMinute = 180
burst = 5
[rateLimit.bandwidthLimit]
enabled = false
totalLimit = "100mbps"
totalBurst = "100mbps"
singleLimit = "10mbps"
singleBurst = "10mbps"
[outbound]
enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"

View File

@@ -1,5 +1,7 @@
# ghproxy 用户配置文档
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
`ghproxy` 的配置主要通过修改 `config` 目录下的 `config.toml``blacklist.json``whitelist.json` 文件来实现。本文档将详细介绍这些配置文件的作用以及用户可以自定义的配置选项。
## `config.toml` - 主配置文件
@@ -68,13 +70,20 @@ rateMethod = "total" # "ip" or "total"
ratePerMinute = 180
burst = 5
[rateLimit.bandwidthLimit]
enabled = false
totalLimit = "100mbps"
totalBurst = "100mbps"
singleLimit = "10mbps"
singleBurst = "10mbps"
[outbound]
enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
[docker]
enabled = false
target = "ghcr" # ghcr/dockerhub
target = "ghcr" # ghcr/dockerhub or "xx.example.com"
```
### 配置项详细说明
@@ -291,6 +300,27 @@ target = "ghcr" # ghcr/dockerhub
* 类型: 整数 (`int`)
* 默认值: `5`
* 说明: 允许在短时间内超过 `ratePerMinute` 的突发请求数。
* **`[rateLimit.bandwidthLimit]` 带宽速率限制**
* `enabled`: 是否启用带宽速率限制。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 将根据配置的策略限制带宽使用,防止服务被滥用。
* `totalLimit`: 全局带宽限制。
* 类型: 字符串 (`string`)
* 默认值: `"100mbps"`
* 说明: 设置全局最大带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。
* `totalBurst`: 全局突发带宽。
* 类型: 字符串 (`string`)
* 默认值: `"100mbps"`
* 说明: 设置全局突发带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。
* `singleLimit`: 单个连接带宽限制。
* 类型: 字符串 (`string`)
* 默认值: `"10mbps"`
* 说明: 设置单个连接的最大带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。
* `singleBurst`: 单个连接突发带宽。
* 类型: 字符串 (`string`)
* 默认值: `"10mbps"`
* 说明: 设置单个连接的突发带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。
* **`[outbound]` - 出站代理配置**
@@ -318,6 +348,7 @@ target = "ghcr" # ghcr/dockerhub
* 说明: 指定要代理的 Docker 注册表。
* `"ghcr"`: 代理 GitHub Container Registry (ghcr.io)。
* `"dockerhub"`: 代理 Docker Hub (docker.io)。
* 自定义, 支持传入自定义target, 例如`"docker.example.com"`
## `blacklist.json` - 黑名单配置

View File

@@ -1,5 +1,7 @@
# Flag
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
GHProxy接受以下flag传入
```bash

View File

@@ -1,5 +1,7 @@
## GHProxy 文档
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
### 配置文件
https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md

33
go.mod
View File

@@ -4,19 +4,25 @@ go 1.24.3
require (
github.com/BurntSushi/toml v1.5.0
github.com/WJQSERVER-STUDIO/httpc v0.5.0
github.com/WJQSERVER-STUDIO/logger v1.6.0
github.com/cloudwego/hertz v0.9.7
github.com/WJQSERVER-STUDIO/httpc v0.7.0
github.com/WJQSERVER-STUDIO/logger v1.8.0
github.com/cloudwego/hertz v0.10.0
github.com/hertz-contrib/http2 v0.1.8
golang.org/x/net v0.40.0
golang.org/x/time v0.11.0
golang.org/x/net v0.41.0
golang.org/x/time v0.12.0
)
require (
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2
github.com/bytedance/sonic v1.13.3
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/wjqserver/modembed v0.0.1
)
require (
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2 // indirect
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 // indirect
github.com/bytedance/gopkg v0.1.2 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/gopkg v0.1.4 // indirect
@@ -24,15 +30,20 @@ require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/nyaruka/phonenumbers v1.6.1 // indirect
github.com/nyaruka/phonenumbers v1.6.3 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
replace github.com/nyaruka/phonenumbers => github.com/nyaruka/phonenumbers v1.6.1 // 1.6.3 has reflect leaking
//replace github.com/WJQSERVER-STUDIO/httpc v0.5.1 => /data/github/WJQSERVER-STUDIO/httpc
//replace github.com/WJQSERVER-STUDIO/logger v1.6.0 => /data/github/WJQSERVER-STUDIO/logger

46
go.sum
View File

@@ -2,19 +2,21 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
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.2 h1:9CSf+V0ZQPl2ijC/g6v/ObemmhpKcikKVIodsaLExTA=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE=
github.com/WJQSERVER-STUDIO/httpc v0.5.0 h1:0yJA+dOgbnO3R/mAWPjlbUq5lIqaxRV38XfiX3jt6pg=
github.com/WJQSERVER-STUDIO/httpc v0.5.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE=
github.com/WJQSERVER-STUDIO/logger v1.6.0 h1:xK2xV7hlkMXaWzvj4+cNoNWA+JfnJaHX6VU+RrPnr7Q=
github.com/WJQSERVER-STUDIO/logger v1.6.0/go.mod h1:TICMsR7geROHBg6rxwkqUNGydo34XVsX93yeoxyfuyY=
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc5f0DK8N9agIJCpM1Cqw2NE=
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2/go.mod h1:yPX8xuZH+py7eLJwOYj3VVI/4/Yuy5+x8Mhq8qezcPg=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSjSpQtVKqEDpiZ5Q=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE=
github.com/WJQSERVER-STUDIO/httpc v0.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc=
github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE=
github.com/WJQSERVER-STUDIO/logger v1.8.0 h1:AQ3Qe2kxiqpuOoDlRzseGP6u4LAaJc+ng4l8P+CK7Co=
github.com/WJQSERVER-STUDIO/logger v1.8.0/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM=
github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ=
github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4=
github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
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=
@@ -22,8 +24,8 @@ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCy
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50=
github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI=
github.com/cloudwego/hertz v0.9.7 h1:tAVaiO+vTf+ZkQhvNhKbDJ0hmC4oJ7bzwDi1KhvhHy4=
github.com/cloudwego/hertz v0.9.7/go.mod h1:t6d7NcoQxPmETvzPMMIVPHMn5C5QzpqIiFsaavoLJYQ=
github.com/cloudwego/hertz v0.10.0 h1:V0vmBaLdQPlgL6w2TA6PZL1g6SGgQznFx6vqxWdCcKw=
github.com/cloudwego/hertz v0.10.0/go.mod h1:lRBohmcDkGx5TLK6QKFGdzJ6n3IXqGueHsOiXcYgXA4=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4=
github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU=
@@ -37,6 +39,8 @@ 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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hertz-contrib/http2 v0.1.8 h1:kjfCGkUxJZHgfPsnRjx1FLJBG55KvtvSQD214guBQLw=
github.com/hertz-contrib/http2 v0.1.8/go.mod h1:m42hrl8fiTwE4p8c7JdRUZpkePEthvV89q3elL2GeD0=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@@ -80,15 +84,17 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
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/wjqserver/modembed v0.0.1 h1:8ZDz7t9M5DLrUFlYgBUUmrMzxWsZPmHvOazkr/T2jEs=
github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g9yyCztTXOQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -98,8 +104,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -127,10 +133,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

216
main.go
View File

@@ -17,9 +17,11 @@ import (
"ghproxy/middleware/loggin"
"ghproxy/proxy"
"ghproxy/rate"
"ghproxy/weakcache"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/hertz-contrib/http2/factory"
"github.com/wjqserver/modembed"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
@@ -50,6 +52,10 @@ var (
pagesFS embed.FS
)
var (
wcache *weakcache.Cache[string] // docker token缓存
)
var (
logw = logger.Logw
logDump = logger.LogDump
@@ -121,6 +127,7 @@ func loadConfig() {
func setupLogger(cfg *config.Config) {
var err error
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize)
if err != nil {
fmt.Printf("Failed to initialize logger: %v\n", err)
@@ -131,6 +138,8 @@ func setupLogger(cfg *config.Config) {
fmt.Printf("Logger Level Error: %v\n", err)
os.Exit(1)
}
logger.SetAsync(cfg.Log.Async)
fmt.Printf("Log Level: %s\n", cfg.Log.Level)
logDebug("Config File Path: ", cfgfile)
logDebug("Loaded config: %v\n", cfg)
@@ -181,29 +190,36 @@ func setupRateLimit(cfg *config.Config) {
}
func InitReq(cfg *config.Config) {
proxy.InitReq(cfg)
err := proxy.InitReq(cfg)
if err != nil {
fmt.Printf("Failed to initialize request: %v\n", err)
os.Exit(1)
}
}
// loadEmbeddedPages 加载嵌入式页面资源
func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
pageFS := modembed.NewModTimeFS(pagesFS, time.Now())
var pages fs.FS
var err error
switch cfg.Pages.Theme {
case "bootstrap":
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
pages, err = fs.Sub(pageFS, "pages/bootstrap")
case "nebula":
pages, err = fs.Sub(pagesFS, "pages/nebula")
pages, err = fs.Sub(pageFS, "pages/nebula")
case "design":
pages, err = fs.Sub(pagesFS, "pages/design")
pages, err = fs.Sub(pageFS, "pages/design")
case "metro":
pages, err = fs.Sub(pagesFS, "pages/metro")
pages, err = fs.Sub(pageFS, "pages/metro")
case "classic":
pages, err = fs.Sub(pagesFS, "pages/classic")
pages, err = fs.Sub(pageFS, "pages/classic")
case "mino":
pages, err = fs.Sub(pagesFS, "pages/mino")
pages, err = fs.Sub(pageFS, "pages/mino")
case "hub":
pages, err = fs.Sub(pageFS, "pages/hub")
default:
pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题
logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme)
pages, err = fs.Sub(pageFS, "pages/design") // 默认主题
logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme)
}
if err != nil {
@@ -211,13 +227,16 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
}
// 初始化errPagesFs
errPagesInitErr := proxy.InitErrPagesFS(pagesFS)
errPagesInitErr := proxy.InitErrPagesFS(pageFS)
if errPagesInitErr != nil {
logWarning("errPagesInitErr: %s", errPagesInitErr)
}
var assets fs.FS
assets, err = fs.Sub(pagesFS, "pages/assets")
assets, err = fs.Sub(pageFS, "pages/assets")
if err != nil {
return nil, nil, fmt.Errorf("failed to load embedded assets: %w", err)
}
return pages, assets, nil
}
@@ -263,6 +282,12 @@ func setupPages(cfg *config.Config, r *server.Hertz) {
}
}
func pageCacheHeader() func(ctx context.Context, c *app.RequestContext) {
return func(ctx context.Context, c *app.RequestContext) {
c.Header("Cache-Control", "public, max-age=3600, must-revalidate")
}
}
func setInternalRoute(cfg *config.Config, r *server.Hertz) error {
// 加载嵌入式资源
@@ -271,61 +296,69 @@ func setInternalRoute(cfg *config.Config, r *server.Hertz) error {
logError("Failed when processing pages: %s", err)
return err
}
// 设置嵌入式资源路由
r.GET("/", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/bootstrap.min.css", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(assets))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/bootstrap.bundle.min.js", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(assets))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
/*
// 设置嵌入式资源路由
r.GET("/", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(assets))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/bootstrap.min.css", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(assets))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/bootstrap.bundle.min.js", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(assets))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
*/
r.GET("/", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages))))
r.GET("/favicon.ico", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets))))
r.GET("/script.js", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages))))
r.GET("/style.css", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages))))
r.GET("/bootstrap.min.css", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets))))
r.GET("/bootstrap.bundle.min.js", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets))))
return nil
}
@@ -353,6 +386,9 @@ func init() {
setMemLimit(cfg)
loadlist(cfg)
setupRateLimit(cfg)
if cfg.Docker.Enabled {
wcache = proxy.InitWeakCache()
}
if cfg.Server.Debug {
runMode = "dev"
@@ -366,6 +402,16 @@ func init() {
}
}
var viaString string = "WJQSERVER-STUDIO/GHProxy"
func viaHeader() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
protoVersion := "1.1"
c.Header("Via", protoVersion+" "+viaString)
c.Next(ctx)
}
}
func main() {
if showVersion || showHelp {
return
@@ -384,12 +430,14 @@ func main() {
server.WithH2C(true),
server.WithHostPorts(addr),
server.WithTransport(standard.NewTransporter),
server.WithStreamBody(true),
)
r.AddProtocol("h2", factory.NewServerFactory())
} else {
r = server.New(
server.WithHostPorts(addr),
server.WithTransport(standard.NewTransporter),
server.WithStreamBody(true),
)
}
} else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" {
@@ -397,11 +445,15 @@ func main() {
r = server.New(
server.WithH2C(true),
server.WithHostPorts(addr),
server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection),
server.WithStreamBody(true),
)
r.AddProtocol("h2", factory.NewServerFactory())
} else {
r = server.New(
server.WithHostPorts(addr),
server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection),
server.WithStreamBody(true),
)
}
} else {
@@ -412,6 +464,7 @@ func main() {
r.Use(recovery.Recovery()) // Recovery中间件
r.Use(loggin.Middleware()) // log中间件
r.Use(viaHeader())
setupApi(cfg, r, version)
setupPages(cfg, r)
@@ -459,10 +512,27 @@ func main() {
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) {
proxy.GhcrRouting(cfg)(ctx, c)
r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) {
emptyJSON := "{}"
c.Header("Content-Type", "application/json")
c.Header("Content-Length", fmt.Sprint(len(emptyJSON)))
c.Header("Docker-Distribution-API-Version", "registry/2.0")
c.Status(200)
c.Write([]byte(emptyJSON))
})
r.Any("/v2/:target/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
proxy.GhcrWithImageRouting(cfg)(ctx, c)
})
/*
r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) {
proxy.GhcrRouting(cfg)(ctx, c)
})
*/
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
})
@@ -476,17 +546,21 @@ func main() {
http.ListenAndServe("localhost:6060", nil)
}()
}
if wcache != nil {
defer wcache.StopCleanup()
}
r.Spin()
defer logger.Close()
defer func() {
if hertZfile != nil {
var err error
err = hertZfile.Close()
err := hertZfile.Close()
if err != nil {
logError("Failed to close hertz log file: %v", err)
}
}
}()
r.Spin()
fmt.Println("Program Exit")
}

62
proxy/authparse.go Normal file
View File

@@ -0,0 +1,62 @@
package proxy
import (
"fmt"
"strings"
)
// BearerAuthParams 用于存放解析出的 Bearer 认证参数
type BearerAuthParams struct {
Realm string
Service string
Scope string
}
// parseBearerWWWAuthenticateHeader 解析 Bearer 方案的 Www-Authenticate Header。
// 它期望格式为 'Bearer key1="value1",key2="value2",...'
// 并尝试将已知参数解析到 BearerAuthParams struct 中。
func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, error) {
if headerValue == "" {
return nil, fmt.Errorf("header value is empty")
}
// 检查 Scheme 是否是 "Bearer"
parts := strings.SplitN(headerValue, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
return nil, fmt.Errorf("invalid or non-bearer header format: got '%s'", headerValue)
}
paramsStr := parts[1]
paramPairs := strings.Split(paramsStr, ",")
tempMap := make(map[string]string)
for _, pair := range paramPairs {
trimmedPair := strings.TrimSpace(pair)
keyValue := strings.SplitN(trimmedPair, "=", 2)
if len(keyValue) != 2 {
logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue)
continue
}
key := strings.TrimSpace(keyValue[0])
value := strings.TrimSpace(keyValue[1])
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
value = value[1 : len(value)-1]
}
tempMap[key] = value
}
//从 map 中提取值并填充到 struct
authParams := &BearerAuthParams{}
if realm, ok := tempMap["realm"]; ok {
authParams.Realm = realm
}
if service, ok := tempMap["service"]; ok {
authParams.Service = service
}
if scope, ok := tempMap["scope"]; ok {
authParams.Scope = scope
}
return authParams, nil
}

64
proxy/bandwidth.go Normal file
View File

@@ -0,0 +1,64 @@
package proxy
import (
"errors"
"ghproxy/config"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"golang.org/x/time/rate"
)
var (
bandwidthLimit rate.Limit
bandwidthBurst rate.Limit
)
func UnDefiendRateStringErrHandle(err error) error {
if errors.Is(err, &limitreader.UnDefiendRateStringErr{}) {
logWarning("UnDefiendRateStringErr: %s", err)
return nil
}
return err
}
func SetGlobalRateLimit(cfg *config.Config) error {
if cfg.RateLimit.BandwidthLimit.Enabled {
var err error
var totalLimit rate.Limit
var totalBurst rate.Limit
totalLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalLimit)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse total bandwidth limit: %v", err)
return err
}
totalBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalBurst)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse total bandwidth burst: %v", err)
return err
}
limitreader.SetGlobalRateLimit(totalLimit, int(totalBurst))
err = SetBandwidthLimit(cfg)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to set bandwidth limit: %v", err)
return err
}
} else {
limitreader.SetGlobalRateLimit(rate.Inf, 0)
}
return nil
}
func SetBandwidthLimit(cfg *config.Config) error {
var err error
bandwidthLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleLimit)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse bandwidth limit: %v", err)
return err
}
bandwidthBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleBurst)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse bandwidth burst: %v", err)
return err
}
return nil
}

View File

@@ -8,21 +8,34 @@ import (
"net/http"
"strconv"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app"
)
func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
var (
method []byte
req *http.Request
resp *http.Response
err error
req *http.Request
resp *http.Response
err error
)
method = c.Request.Method()
go func() {
<-ctx.Done()
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
if req != nil {
req.Body.Close()
}
}()
req, err = client.NewRequest(string(method), u, c.Request.BodyStream())
rb := client.NewRequestBuilder(string(c.Request.Method()), u)
rb.NoDefaultHeaders()
rb.SetBody(c.Request.BodyStream())
rb.WithContext(ctx)
req, err = rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
@@ -58,8 +71,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
bodySize = -1
}
if err == nil && bodySize > sizelimit {
var finalURL string
finalURL = resp.Request.URL.String()
finalURL := resp.Request.URL.String()
err = resp.Body.Close()
if err != nil {
logError("Failed to close response body: %v", err)
@@ -92,31 +104,40 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
c.Status(resp.StatusCode)
if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor {
bodyReader := resp.Body
if cfg.RateLimit.BandwidthLimit.Enabled {
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
}
defer bodyReader.Close()
if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor {
// 判断body是不是gzip
var compress string
if resp.Header.Get("Content-Encoding") == "gzip" {
compress = "gzip"
}
logDebug("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol())
logDebug("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol())
c.Header("Content-Length", "")
var reader io.Reader
reader, _, err = processLinks(resp.Body, compress, string(c.Request.Host()), cfg)
reader, _, err = processLinks(bodyReader, compress, string(c.Request.Host()), cfg)
c.SetBodyStream(reader, -1)
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.Header.GetProtocol(), err)
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), err)
ErrorPage(c, NewErrorWithStatusLookup(500, fmt.Sprintf("Failed to copy response body: %v", err)))
return
}
} else {
if contentLength != "" {
c.SetBodyStream(resp.Body, bodySize)
c.SetBodyStream(bodyReader, bodySize)
return
}
c.SetBodyStream(resp.Body, -1)
c.SetBodyStream(bodyReader, -1)
}
}

View File

@@ -3,32 +3,102 @@ package proxy
import (
"context"
"fmt"
json "github.com/bytedance/sonic"
"ghproxy/config"
"ghproxy/weakcache"
"io"
"net/http"
"strconv"
"strings"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app"
)
func GhcrRouting(cfg *config.Config) app.HandlerFunc {
var (
dockerhubTarget = "registry-1.docker.io"
ghcrTarget = "ghcr.io"
)
var cache *weakcache.Cache[string]
type imageInfo struct {
User string
Repo string
Image string
}
func InitWeakCache() *weakcache.Cache[string] {
cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
return cache
}
func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
if cfg.Docker.Enabled {
if cfg.Docker.Target == "ghcr" {
GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr")
} else if cfg.Docker.Target == "dockerhub" {
GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub")
charToFind := '.'
reqTarget := c.Param("target")
reqImageUser := c.Param("user")
reqImageName := c.Param("repo")
reqFilePath := c.Param("filepath")
path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
target := ""
if strings.ContainsRune(reqTarget, charToFind) {
if reqTarget == "docker.io" {
target = dockerhubTarget
} else if reqTarget == "ghcr.io" {
target = ghcrTarget
} else {
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not Allowed"))
return
target = reqTarget
}
} else {
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
return
path = string(c.Request.RequestURI())
reqImageUser = c.Param("target")
reqImageName = c.Param("user")
}
image := &imageInfo{
User: reqImageUser,
Repo: reqImageName,
Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
}
GhcrToTarget(ctx, c, cfg, target, path, image)
}
}
func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) {
if cfg.Docker.Enabled {
if target != "" {
GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+string(c.Request.QueryString()), image, cfg, target)
} else {
if cfg.Docker.Target == "ghcr" {
GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget)
} else if cfg.Docker.Target == "dockerhub" {
GhcrRequest(ctx, c, "https://"+dockerhubTarget+string(c.Request.RequestURI()), image, cfg, dockerhubTarget)
} else if cfg.Docker.Target != "" {
// 自定义taget
GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), image, cfg, cfg.Docker.Target)
} else {
// 配置为空
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set"))
return
}
}
} else {
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
return
}
}
func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *imageInfo, cfg *config.Config, target string) {
var (
method []byte
@@ -37,13 +107,23 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
err error
)
go func() {
<-ctx.Done()
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
if req != nil {
req.Body.Close()
}
}()
method = c.Request.Method()
rb := client.NewRequestBuilder(string(method), u)
rb := ghcrclient.NewRequestBuilder(string(method), u)
rb.NoDefaultHeaders()
rb.SetBody(c.Request.BodyStream())
rb.WithContext(ctx)
//req, err = client.NewRequest(string(method), u, c.Request.BodyStream())
req, err = rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
@@ -56,14 +136,68 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
req.Header.Add(headerKey, headerValue)
})
resp, err = client.Do(req)
req.Header.Set("Host", target)
if image != nil {
token, exist := cache.Get(image.Image)
if exist {
logDebug("Use Cache Token: %s", token)
req.Header.Set("Authorization", "Bearer "+token)
}
}
resp, err = ghcrclient.Do(req)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
// 错误处理(404)
if resp.StatusCode == 404 {
// 处理状态码
if resp.StatusCode == 401 {
// 请求target /v2/路径
if string(c.Request.URI().Path()) != "/v2/" {
resp.Body.Close()
if image == nil {
ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized"))
return
}
token := ChallengeReq(target, image, ctx, c)
// 更新kv
if token != "" {
logDump("Update Cache Token: %s", token)
cache.Put(image.Image, token)
}
rb := ghcrclient.NewRequestBuilder(string(method), u)
rb.NoDefaultHeaders()
rb.SetBody(c.Request.BodyStream())
rb.WithContext(ctx)
req, err = rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
c.Request.Header.VisitAll(func(key, value []byte) {
headerKey := string(key)
headerValue := string(value)
req.Header.Add(headerKey, headerValue)
})
req.Header.Set("Host", target)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err = ghcrclient.Do(req)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
}
} else if resp.StatusCode == 404 { // 错误处理(404)
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
return
}
@@ -84,8 +218,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
bodySize = -1
}
if err == nil && bodySize > sizelimit {
var finalURL string
finalURL = resp.Request.URL.String()
finalURL := resp.Request.URL.String()
err = resp.Body.Close()
if err != nil {
logError("Failed to close response body: %v", err)
@@ -99,17 +232,97 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
// 复制响应头,排除需要移除的 header
for key, values := range resp.Header {
for _, value := range values {
//c.Header(key, value)
c.Response.Header.Add(key, value)
}
}
c.Status(resp.StatusCode)
bodyReader := resp.Body
if cfg.RateLimit.BandwidthLimit.Enabled {
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
}
if contentLength != "" {
c.SetBodyStream(resp.Body, bodySize)
c.SetBodyStream(bodyReader, bodySize)
return
}
c.SetBodyStream(resp.Body, -1)
c.SetBodyStream(bodyReader, -1)
}
type AuthToken struct {
Token string `json:"token"`
}
func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) {
var resp401 *http.Response
var req401 *http.Request
var err error
rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
rb401.NoDefaultHeaders()
rb401.WithContext(ctx)
rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ")
req401, err = rb401.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
req401.Header.Set("Host", target)
resp401, err = ghcrclient.Do(req401)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
defer resp401.Body.Close()
bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
if err != nil {
logError("Failed to parse Www-Authenticate header: %v", err)
return
}
scope := fmt.Sprintf("repository:%s:pull", image.Image)
getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm).
NoDefaultHeaders().
WithContext(ctx).
AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ").
SetHeader("Host", bearer.Service).
AddQueryParam("service", bearer.Service).
AddQueryParam("scope", scope)
getAuthReq, err := getAuthRB.Build()
if err != nil {
logError("Failed to create request: %v", err)
return
}
authResp, err := ghcrclient.Do(getAuthReq)
if err != nil {
logError("Failed to send request: %v", err)
return
}
defer authResp.Body.Close()
bodyBytes, err := io.ReadAll(authResp.Body)
if err != nil {
logError("Failed to read auth response body: %v", err)
return
}
// 解码json
var authToken AuthToken
err = json.Unmarshal(bodyBytes, &authToken)
if err != nil {
logError("Failed to decode auth response body: %v", err)
return
}
token = authToken.Token
return token
}

View File

@@ -2,12 +2,18 @@ package proxy
import (
"bytes"
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"sync"
"fmt"
"html/template"
"io/fs"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/cloudwego/hertz/pkg/app"
lru "github.com/hashicorp/golang-lru/v2"
)
// 日志模块
@@ -22,7 +28,7 @@ var (
func HandleError(c *app.RequestContext, message string) {
ErrorPage(c, NewErrorWithStatusLookup(500, message))
logError(message)
logError("Error handled: %s", message)
}
type GHProxyErrors struct {
@@ -123,6 +129,22 @@ type ErrorPageData struct {
ErrorMessage string
}
// ToCacheKey 为 ErrorPageData 生成一个唯一的 SHA256 字符串键。
// 使用 gob 序列化来确保结构体内容到字节序列的顺序一致性,然后计算哈希。
func (d ErrorPageData) ToCacheKey() string {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(d)
if err != nil {
logError("Failed to gob encode ErrorPageData for cache key: %v", err)
return ""
}
hasher := sha256.New()
hasher.Write(buf.Bytes())
return hex.EncodeToString(hasher.Sum(nil))
}
func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData {
return ErrorPageData{
StatusCode: errInfo.StatusCode,
@@ -133,25 +155,130 @@ func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData {
}
}
func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) {
pageData, err := htmlTemplateRender(errPagesFs, ErrPageUnwarper(errInfo))
if err != nil {
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
logDebug("Error reading page.tmpl: %v", err)
return
}
c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData)
return
// SizedLRUCache 实现了基于字节大小限制的 LRU 缓存。
// 它包装了 hashicorp/golang-lru/v2.Cache并额外管理缓存的总字节大小。
type SizedLRUCache struct {
cache *lru.Cache[string, []byte]
mu sync.Mutex // 保护 currentBytes 字段
maxBytes int64 // 缓存的最大字节容量
currentBytes int64 // 缓存当前占用的字节数
}
func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) {
tmplPath := "page.tmpl"
tmpl, err := template.ParseFS(fsys, tmplPath)
// NewSizedLRUCache 创建一个新的 SizedLRUCache 实例。
// 内部的 lru.Cache 的条目容量被设置为一个较大的值 (例如 10000)
// 因为主要的逐出逻辑将由字节大小限制来控制。
func NewSizedLRUCache(maxBytes int64) (*SizedLRUCache, error) {
if maxBytes <= 0 {
return nil, fmt.Errorf("maxBytes must be positive")
}
c := &SizedLRUCache{
maxBytes: maxBytes,
}
// 创建内部 LRU 缓存,并提供一个 OnEvictedFunc 回调函数。
// 当内部 LRU 缓存因其自身的条目容量限制或 RemoveOldest 方法被调用而逐出条目时,
// 此回调函数会被执行,从而更新 currentBytes。
var err error
c.cache, err = lru.NewWithEvict[string, []byte](10000, func(key string, value []byte) {
c.mu.Lock()
defer c.mu.Unlock()
c.currentBytes -= int64(len(value))
logDebug("LRU evicted key: %s, size: %d, current total: %d", key, len(value), c.currentBytes)
})
if err != nil {
return nil, fmt.Errorf("error parsing template: %w", err)
return nil, err
}
return c, nil
}
// Get 从缓存中检索值。
func (c *SizedLRUCache) Get(key string) ([]byte, bool) {
return c.cache.Get(key)
}
// Add 向缓存中添加或更新一个键值对,并在必要时执行逐出以满足字节限制。
func (c *SizedLRUCache) Add(key string, value []byte) {
c.mu.Lock() // 保护 currentBytes 和逐出逻辑
defer c.mu.Unlock()
itemSize := int64(len(value))
// 如果待添加的条目本身就大于缓存的最大容量,则不进行缓存。
if itemSize > c.maxBytes {
logWarning("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes)
return
}
// 如果键已存在,则首先从 currentBytes 中减去旧值的大小,并从内部 LRU 中移除旧条目。
if oldVal, ok := c.cache.Get(key); ok {
c.currentBytes -= int64(len(oldVal))
c.cache.Remove(key)
logDebug("Key %s exists, removed old size %d. Current total: %d", key, len(oldVal), c.currentBytes)
}
// 主动逐出最旧的条目,直到有足够的空间容纳新条目。
for c.currentBytes+itemSize > c.maxBytes && c.cache.Len() > 0 {
_, oldVal, existed := c.cache.RemoveOldest()
if !existed {
logWarning("Attempted to remove oldest, but item not found.")
break
}
logDebug("Proactively evicted item (size %d) to free space. Current total: %d", len(oldVal), c.currentBytes)
}
// 添加新条目到内部 LRU 缓存。
c.cache.Add(key, value)
c.currentBytes += itemSize // 手动增加新条目的大小到 currentBytes。
logDebug("Item added: key %s, size: %d, current total: %d", key, itemSize, c.currentBytes)
}
const maxErrorPageCacheBytes = 512 * 1024 // 错误页面缓存的最大容量512KB
var errorPageCache *SizedLRUCache
func init() {
// 初始化 SizedLRUCache。
var err error
errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes)
if err != nil {
logError("Failed to initialize error page LRU cache: %v", err)
panic(err)
}
}
// parsedTemplateOnce 用于确保 HTML 模板只被解析一次。
var (
parsedTemplateOnce sync.Once
parsedTemplate *template.Template
parsedTemplateErr error
)
// getParsedTemplate 用于获取缓存的解析后的 HTML 模板。
func getParsedTemplate() (*template.Template, error) {
parsedTemplateOnce.Do(func() {
tmplPath := "page.tmpl"
// 确保 errPagesFs 已初始化。这要求在任何 ErrorPage 调用之前调用 InitErrPagesFS。
if errPagesFs == nil {
parsedTemplateErr = fmt.Errorf("errPagesFs not initialized. Call InitErrPagesFS first")
return
}
parsedTemplate, parsedTemplateErr = template.ParseFS(errPagesFs, tmplPath)
if parsedTemplateErr != nil {
parsedTemplate = nil
}
})
return parsedTemplate, parsedTemplateErr
}
// htmlTemplateRender 修改为使用缓存的模板。
func htmlTemplateRender(data interface{}) ([]byte, error) {
tmpl, err := getParsedTemplate()
if err != nil {
return nil, fmt.Errorf("failed to get parsed template: %w", err)
}
if tmpl == nil {
return nil, fmt.Errorf("template is nil")
return nil, fmt.Errorf("template is nil after parsing")
}
// 创建一个 bytes.Buffer 用于存储渲染结果
@@ -159,9 +286,44 @@ func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) {
err = tmpl.Execute(&buf, data)
if err != nil {
return nil, fmt.Errorf("error executing template: %w", err)
return nil, fmt.Errorf("failed to execute template: %w", err)
}
// 返回 buffer 的内容作为 []byte
return buf.Bytes(), nil
}
func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) {
// 将 errInfo 转换为 ErrorPageData 结构体
pageDataStruct := ErrPageUnwarper(errInfo)
// 使用 ErrorPageData 生成一个唯一的 SHA256 缓存键
cacheKey := pageDataStruct.ToCacheKey()
if cacheKey == "" {
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
logWarning("Failed to generate cache key for error page: %v", errInfo)
return
}
var pageData []byte
var err error
// 尝试从缓存中获取页面数据
if cachedPage, found := errorPageCache.Get(cacheKey); found {
pageData = cachedPage
logDebug("Serving error page from cache (Key: %s)", cacheKey)
} else {
// 如果不在缓存中,则渲染页面
pageData, err = htmlTemplateRender(pageDataStruct)
if err != nil {
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
logWarning("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err)
return
}
// 将渲染结果存入缓存
errorPageCache.Add(cacheKey, pageData)
logDebug("Cached error page (Key: %s, Size: %d bytes)", cacheKey, len(pageData))
}
c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData)
}

View File

@@ -8,15 +8,32 @@ import (
"net/http"
"strconv"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app"
)
func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) {
var (
req *http.Request
resp *http.Response
)
go func() {
<-ctx.Done()
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
if req != nil {
req.Body.Close()
}
}()
method := string(c.Request.Method())
bodyReader := bytes.NewBuffer(c.Request.Body())
reqBodyReader := bytes.NewBuffer(c.Request.Body())
//bodyReader := c.Request.BodyStream()
//bodyReader := c.Request.BodyStream() // 不可替换为此实现
if cfg.GitClone.Mode == "cache" {
userPath, repoPath, remainingPath, queryParams, err := extractParts(u)
@@ -28,14 +45,11 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode()
}
var (
resp *http.Response
)
if cfg.GitClone.Mode == "cache" {
rb := gitclient.NewRequestBuilder(method, u)
rb.NoDefaultHeaders()
rb.SetBody(bodyReader)
rb.SetBody(reqBodyReader)
rb.WithContext(ctx)
req, err := rb.Build()
if err != nil {
@@ -54,7 +68,8 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
} else {
rb := client.NewRequestBuilder(string(c.Request.Method()), u)
rb.NoDefaultHeaders()
rb.SetBody(bodyReader)
rb.SetBody(reqBodyReader)
rb.WithContext(ctx)
req, err := rb.Build()
if err != nil {
@@ -89,7 +104,6 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
for key, values := range resp.Header {
for _, value := range values {
//c.Header(key, value)
c.Response.Header.Add(key, value)
}
}
@@ -122,5 +136,11 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
c.Response.Header.Set("Expires", "0")
}
c.SetBodyStream(resp.Body, -1)
bodyReader := resp.Body
if cfg.RateLimit.BandwidthLimit.Enabled {
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
}
c.SetBodyStream(bodyReader, -1)
}

View File

@@ -12,17 +12,26 @@ import (
var BufferSize int = 32 * 1024 // 32KB
var (
tr *http.Transport
gittr *http.Transport
client *httpc.Client
gitclient *httpc.Client
tr *http.Transport
gittr *http.Transport
client *httpc.Client
gitclient *httpc.Client
ghcrtr *http.Transport
ghcrclient *httpc.Client
)
func InitReq(cfg *config.Config) {
func InitReq(cfg *config.Config) error {
initHTTPClient(cfg)
if cfg.GitClone.Mode == "cache" {
initGitHTTPClient(cfg)
}
initGhcrHTTPClient(cfg)
err := SetGlobalRateLimit(cfg)
if err != nil {
return err
}
return nil
}
func initHTTPClient(cfg *config.Config) {
@@ -72,6 +81,7 @@ func initHTTPClient(cfg *config.Config) {
httpc.WithTransport(tr),
)
}
}
func initGitHTTPClient(cfg *config.Config) {
@@ -142,3 +152,51 @@ func initGitHTTPClient(cfg *config.Config) {
)
}
}
func initGhcrHTTPClient(cfg *config.Config) {
var proTolcols = new(http.Protocols)
proTolcols.SetHTTP1(true)
proTolcols.SetHTTP2(true)
if cfg.Httpc.Mode == "auto" {
ghcrtr = &http.Transport{
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
Protocols: proTolcols,
}
} else if cfg.Httpc.Mode == "advanced" {
ghcrtr = &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")
ghcrtr = &http.Transport{
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
}
}
if cfg.Outbound.Enabled {
initTransport(cfg, ghcrtr)
}
if cfg.Server.Debug {
ghcrclient = httpc.New(
httpc.WithTransport(ghcrtr),
httpc.WithDumpLog(),
)
} else {
ghcrclient = httpc.New(
httpc.WithTransport(ghcrtr),
)
}
}

View File

@@ -1,16 +1,146 @@
package proxy
import (
"bufio"
"compress/gzip"
"fmt"
"ghproxy/config"
"io"
"net/url"
"regexp"
"strings"
"sync"
)
var (
githubPrefix = "https://github.com/"
rawPrefix = "https://raw.githubusercontent.com/"
gistPrefix = "https://gist.github.com/"
apiPrefix = "https://api.github.com/"
githubPrefixLen int
rawPrefixLen int
gistPrefixLen int
apiPrefixLen int
)
func init() {
githubPrefixLen = len(githubPrefix)
rawPrefixLen = len(rawPrefix)
gistPrefixLen = len(gistPrefix)
apiPrefixLen = len(apiPrefix)
//log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen)
}
// Matcher 从原始URL路径中高效地解析并匹配代理规则.
func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) {
if len(rawPath) < 18 {
return "", "", "", NewErrorWithStatusLookup(404, "path too short")
}
// 匹配 "https://github.com/"
if strings.HasPrefix(rawPath, githubPrefix) {
remaining := rawPath[githubPrefixLen:]
i := strings.IndexByte(remaining, '/')
if i <= 0 {
return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing user")
}
user := remaining[:i]
remaining = remaining[i+1:]
i = strings.IndexByte(remaining, '/')
if i <= 0 {
return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing repo")
}
repo := remaining[:i]
remaining = remaining[i+1:]
if len(remaining) == 0 {
return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing action")
}
i = strings.IndexByte(remaining, '/')
action := remaining
if i != -1 {
action = remaining[:i]
}
var matcher string
switch action {
case "releases", "archive":
matcher = "releases"
case "blob":
matcher = "blob"
case "raw":
matcher = "raw"
case "info", "git-upload-pack":
matcher = "clone"
default:
return "", "", "", NewErrorWithStatusLookup(400, fmt.Sprintf("unsupported github action: %s", action))
}
return user, repo, matcher, nil
}
// 匹配 "https://raw.githubusercontent.com/"
if strings.HasPrefix(rawPath, rawPrefix) {
remaining := rawPath[rawPrefixLen:]
// 这里的逻辑与 github.com 的类似, 需要提取 user, repo, branch, file...
// 我们只需要 user 和 repo
i := strings.IndexByte(remaining, '/')
if i <= 0 {
return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing user")
}
user := remaining[:i]
remaining = remaining[i+1:]
i = strings.IndexByte(remaining, '/')
if i <= 0 {
return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing repo")
}
repo := remaining[:i]
// raw 链接至少需要 user/repo/branch 三部分
remaining = remaining[i+1:]
if len(remaining) == 0 {
return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing branch/commit")
}
return user, repo, "raw", nil
}
// 匹配 "https://gist.github.com/"
if strings.HasPrefix(rawPath, gistPrefix) {
remaining := rawPath[gistPrefixLen:]
i := strings.IndexByte(remaining, '/')
if i <= 0 {
// case: https://gist.github.com/user
// 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的
if len(remaining) > 0 {
return remaining, "", "gist", nil
}
return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user")
}
// case: https://gist.github.com/user/gist_id...
user := remaining[:i]
return user, "", "gist", nil
}
// 匹配 "https://api.github.com/"
if strings.HasPrefix(rawPath, apiPrefix) {
if !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) {
return "", "", "", NewErrorWithStatusLookup(403, "API proxy requires header authentication")
}
remaining := rawPath[apiPrefixLen:]
var user, repo string
if strings.HasPrefix(remaining, "repos/") {
parts := strings.SplitN(remaining[6:], "/", 3)
if len(parts) >= 2 {
user = parts[0]
repo = parts[1]
}
} else if strings.HasPrefix(remaining, "users/") {
parts := strings.SplitN(remaining[6:], "/", 2)
if len(parts) >= 1 {
user = parts[0]
}
}
return user, repo, "api", nil
}
return "", "", "", NewErrorWithStatusLookup(404, "no matcher found for the given path")
}
// 原实现
/*
func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) {
var (
user string
@@ -20,9 +150,12 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro
// 匹配 "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, "/")
}
//if strings.HasPrefix(remainingPath, "/") {
// remainingPath = strings.TrimPrefix(remainingPath, "/")
//}
remainingPath = strings.TrimPrefix(remainingPath, "/")
// 预期格式/user/repo/more...
// 取出user和repo和最后部分
parts := strings.Split(remainingPath, "/")
@@ -103,226 +236,56 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro
errMsg := "Didn't match any matcher"
return "", "", "", NewErrorWithStatusLookup(404, errMsg)
}
func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) {
// 匹配 "https://github.com"开头的链接
if strings.HasPrefix(rawPath, "https://github.com") {
return true, nil
}
// 匹配 "https://raw.githubusercontent.com"开头的链接
if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") {
return true, nil
}
// 匹配 "https://raw.github.com"开头的链接
if strings.HasPrefix(rawPath, "https://raw.github.com") {
return true, nil
}
// 匹配 "https://gist.githubusercontent.com"开头的链接
if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") {
return true, nil
}
// 匹配 "https://gist.github.com"开头的链接
if strings.HasPrefix(rawPath, "https://gist.github.com") {
return true, nil
}
if cfg.Shell.RewriteAPI {
// 匹配 "https://api.github.com/"开头的链接
if strings.HasPrefix(rawPath, "https://api.github.com") {
return true, nil
}
}
return false, nil
}
// 匹配文件扩展名是sh的rawPath
func MatcherShell(rawPath string) bool {
return strings.HasSuffix(rawPath, ".sh")
}
// LinkProcessor 是一个函数类型,用于处理提取到的链接。
type LinkProcessor func(string) string
// 自定义 URL 修改函数
func modifyURL(url string, host string, cfg *config.Config) string {
// 去除url内的https://或http://
matched, err := EditorMatcher(url, cfg)
if err != nil {
logDump("Invalid URL: %s", url)
return url
}
if matched {
var u = url
u = strings.TrimPrefix(u, "https://")
u = strings.TrimPrefix(u, "http://")
logDump("Modified URL: %s", "https://"+host+"/"+u)
return "https://" + host + "/" + u
}
return url
}
*/
var (
matchedMatchers = []string{
"blob",
"raw",
"gist",
}
proxyableMatchersMap map[string]struct{}
initMatchersOnce sync.Once
)
// matchString 检查目标字符串是否在给定的字符串集合中
func matchString(target string, stringsToMatch []string) bool {
matchMap := make(map[string]struct{}, len(stringsToMatch))
for _, str := range stringsToMatch {
matchMap[str] = struct{}{}
}
_, exists := matchMap[target]
func initMatchers() {
initMatchersOnce.Do(func() {
matchers := []string{"blob", "raw", "gist"}
proxyableMatchersMap = make(map[string]struct{}, len(matchers))
for _, m := range matchers {
proxyableMatchersMap[m] = struct{}{}
}
})
}
// matchString 与原始版本签名兼容
func matchString(target string) bool {
initMatchers()
_, exists := proxyableMatchersMap[target]
return exists
}
// extractParts 从给定的 URL 中提取所需的部分
// extractParts 与原始版本签名兼容
func extractParts(rawURL string) (string, string, string, url.Values, error) {
// 解析 URL
parsedURL, err := url.Parse(rawURL)
if err != nil {
return "", "", "", nil, err
}
// 获取路径部分并分割
pathParts := strings.Split(parsedURL.Path, "/")
path := parsedURL.Path
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
// 提取所需的部分
if len(pathParts) < 3 {
parts := strings.SplitN(path, "/", 3)
if len(parts) < 2 {
return "", "", "", nil, 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
repoOwner := "/" + parts[0]
repoName := "/" + parts[1]
var remainingPath string
if len(parts) > 2 {
remainingPath = "/" + parts[2]
}
// 查询参数
queryParams := parsedURL.Query()
return repoOwner, repoName, remainingPath, queryParams, nil
return repoOwner, repoName, remainingPath, parsedURL.Query(), nil
}
var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`)
// processLinks 处理链接,返回包含处理后数据的 io.Reader
func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) {
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
readerOut = pipeReader
go func() { // 在 Goroutine 中执行写入操作
defer func() {
if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误
if err != nil {
if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader
logError("pipeWriter close with error failed: %v, original error: %v", closeErr, err)
}
} else {
if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭
logError("pipeWriter close failed: %v", closeErr)
if err == nil { // 如果之前没有错误,记录关闭错误
err = closeErr
}
}
}
}
}()
defer func() {
if err := input.Close(); err != nil {
logError("input close failed: %v", err)
}
}()
var bufReader *bufio.Reader
if compress == "gzip" {
// 解压gzip
gzipReader, gzipErr := gzip.NewReader(input)
if gzipErr != nil {
err = fmt.Errorf("gzip解压错误: %v", gzipErr)
return // Goroutine 中使用 return 返回错误
}
defer gzipReader.Close()
bufReader = bufio.NewReader(gzipReader)
} else {
bufReader = bufio.NewReader(input)
}
var bufWriter *bufio.Writer
var gzipWriter *gzip.Writer
// 根据是否gzip确定 writer 的创建
if compress == "gzip" {
gzipWriter = gzip.NewWriter(pipeWriter) // 使用 pipeWriter
bufWriter = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小
} else {
bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter
}
//确保writer关闭
defer func() {
var closeErr error // 局部变量用于保存defer中可能发生的错误
if gzipWriter != nil {
if closeErr = gzipWriter.Close(); closeErr != nil {
logError("gzipWriter close failed %v", closeErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = closeErr
}
}
}
if flushErr := bufWriter.Flush(); flushErr != nil {
logError("writer flush failed %v", flushErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = flushErr
}
}
}()
// 使用正则表达式匹配 http 和 https 链接
for {
line, readErr := bufReader.ReadString('\n')
if readErr != nil {
if readErr == io.EOF {
break // 文件结束
}
err = fmt.Errorf("读取行错误: %v", readErr) // 传递错误
return // Goroutine 中使用 return 返回错误
}
// 替换所有匹配的 URL
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
logDump("originalURL: %s", originalURL)
return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义
})
n, writeErr := bufWriter.WriteString(modifiedLine)
written += int64(n) // 更新写入的字节数
if writeErr != nil {
err = fmt.Errorf("写入文件错误: %v", writeErr) // 传递错误
return // Goroutine 中使用 return 返回错误
}
}
// 在返回之前,再刷新一次 (虽然 defer 中已经有 flush但这里再加一次确保及时刷新)
if flushErr := bufWriter.Flush(); flushErr != nil {
if err == nil { // 避免覆盖之前的错误
err = flushErr
}
return // Goroutine 中使用 return 返回错误
}
}()
return readerOut, written, nil // 返回 reader 和 writtenerror 由 Goroutine 通过 pipeWriter.CloseWithError 传递
}

309
proxy/matcher_test.go Normal file
View File

@@ -0,0 +1,309 @@
package proxy
import (
"ghproxy/config"
"net/url"
"reflect"
"testing"
)
func TestMatcher_Compatibility(t *testing.T) {
// --- 准备各种配置用于测试 ---
cfgWithAuth := &config.Config{
Auth: config.AuthConfig{Enabled: true, Method: "header", ForceAllowApi: false},
}
cfgNoAuth := &config.Config{
Auth: config.AuthConfig{Enabled: false},
}
cfgApiForceAllowed := &config.Config{
Auth: config.AuthConfig{ForceAllowApi: true},
}
cfgWrongAuthMethod := &config.Config{
Auth: config.AuthConfig{Enabled: true, Method: "none"},
}
testCases := []struct {
name string
rawPath string
config *config.Config
expectedUser string
expectedRepo string
expectedMatcher string
expectError bool
expectedErrCode int
}{
{
name: "GH Releases Path",
rawPath: "https://github.com/owner/repo/releases/download/v1.0/asset.zip",
config: cfgWithAuth,
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "releases",
},
{
name: "GH Archive Path",
rawPath: "https://github.com/owner/repo.git/archive/main.zip",
config: cfgWithAuth,
expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "releases",
},
{
name: "GH Blob Path",
rawPath: "https://github.com/owner/repo/blob/main/path/to/file.go",
config: cfgWithAuth,
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "blob",
},
{
name: "GH Raw Path",
rawPath: "https://github.com/owner/repo/raw/main/image.png",
config: cfgWithAuth,
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw",
},
{
name: "GH Clone Info Refs",
rawPath: "https://github.com/owner/repo.git/info/refs?service=git-upload-pack",
config: cfgWithAuth,
expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "clone",
},
{
name: "GH Clone Git Upload Pack",
rawPath: "https://github.com/owner/repo/git-upload-pack",
config: cfgWithAuth,
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "clone",
},
{
name: "Girhub Broken Path",
rawPath: "https://github.com/owner",
config: cfgWithAuth,
expectError: true, expectedErrCode: 400,
},
{
name: "RawGHUserContent Path",
rawPath: "https://raw.githubusercontent.com/owner/repo/branch/file.sh",
config: cfgWithAuth,
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw",
},
{
name: "Gist Path",
rawPath: "https://gist.github.com/user/abcdef1234567890",
config: cfgWithAuth,
expectedUser: "user", expectedRepo: "", expectedMatcher: "gist",
},
{
name: "API Repos Path (with Auth)",
rawPath: "https://api.github.com/repos/owner/repo/pulls",
config: cfgWithAuth,
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api",
},
{
name: "API Users Path (with Auth)",
rawPath: "https://api.github.com/users/someuser/repos",
config: cfgWithAuth,
expectedUser: "someuser", expectedRepo: "", expectedMatcher: "api",
},
{
name: "API Other Path (with Auth)",
rawPath: "https://api.github.com/octocat",
config: cfgWithAuth,
expectedUser: "", expectedRepo: "", expectedMatcher: "api",
},
{
name: "API Path (Force Allowed)",
rawPath: "https://api.github.com/repos/owner/repo",
config: cfgApiForceAllowed, // Auth disabled, but force allowed
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api",
},
{
name: "Malformed GH Path (no repo)",
rawPath: "https://github.com/owner/",
config: cfgWithAuth,
expectError: true, expectedErrCode: 400,
},
{
name: "Malformed GH Path (no action)",
rawPath: "https://github.com/owner/repo",
config: cfgWithAuth,
expectError: true, expectedErrCode: 400,
},
{
name: "Malformed GH Path (empty user)",
rawPath: "https://github.com//repo/blob/main/file.go",
config: cfgWithAuth,
expectError: true, expectedErrCode: 400,
},
{
name: "Malformed Raw Path (no repo)",
rawPath: "https://raw.githubusercontent.com/owner/",
config: cfgWithAuth,
expectError: true, expectedErrCode: 400,
},
{
name: "Malformed Gist Path (no user)",
rawPath: "https://gist.github.com/",
config: cfgWithAuth,
expectError: true, expectedErrCode: 400,
},
{
name: "Unsupported GH Action",
rawPath: "https://github.com/owner/repo/issues/123",
config: cfgWithAuth,
expectError: true, expectedErrCode: 400,
},
{
name: "API Path (No Auth)",
rawPath: "https://api.github.com/user",
config: cfgNoAuth,
expectError: true, expectedErrCode: 403,
},
{
name: "API Path (Wrong Auth Method)",
rawPath: "https://api.github.com/user",
config: cfgWrongAuthMethod,
expectError: true, expectedErrCode: 403,
},
{
name: "No Matcher Found (other domain)",
rawPath: "https://bitbucket.org/owner/repo",
config: cfgWithAuth,
expectError: true, expectedErrCode: 404,
},
{
name: "No Matcher Found (path too short)",
rawPath: "https://a.co",
config: cfgWithAuth,
expectError: true, expectedErrCode: 404,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
user, repo, matcher, ghErr := Matcher(tc.rawPath, tc.config)
if tc.expectError {
if ghErr == nil {
t.Fatalf("Expected a GHProxyErrors error, but got nil")
}
if ghErr.StatusCode != tc.expectedErrCode {
t.Errorf("Expected error code %d, but got %d (msg: %s)",
tc.expectedErrCode, ghErr.StatusCode, ghErr.ErrorMessage)
}
} else {
if ghErr != nil {
t.Fatalf("Expected no error, but got: %s", ghErr.ErrorMessage)
}
if user != tc.expectedUser {
t.Errorf("user: got %q, want %q", user, tc.expectedUser)
}
if repo != tc.expectedRepo {
t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo)
}
if matcher != tc.expectedMatcher {
t.Errorf("matcher: got %q, want %q", matcher, tc.expectedMatcher)
}
}
})
}
}
func TestExtractParts_Compatibility(t *testing.T) {
testCases := []struct {
name string
rawURL string
expectedOwner string
expectedRepo string
expectedRem string
expectedQuery url.Values
expectError bool
}{
{
name: "Standard git clone URL",
rawURL: "https://github.com/WJQSERVER-STUDIO/go-utils.git/info/refs?service=git-upload-pack",
expectedOwner: "/WJQSERVER-STUDIO",
expectedRepo: "/go-utils.git",
expectedRem: "/info/refs",
expectedQuery: url.Values{"service": []string{"git-upload-pack"}},
},
{
name: "No remaining path",
rawURL: "https://example.com/owner/repo",
expectedOwner: "/owner",
expectedRepo: "/repo",
expectedRem: "",
expectedQuery: url.Values{},
},
{
name: "Root path only",
rawURL: "https://example.com/",
expectError: true, // Path is too short
},
{
name: "One level path",
rawURL: "https://example.com/owner",
expectError: true, // Path is too short
},
{
name: "Empty path segments",
rawURL: "https://example.com//repo/a", // Will be treated as /repo/a
expectedOwner: "", // First part is empty
expectedRepo: "/repo",
expectedRem: "/a",
},
{
name: "Invalid URL format",
rawURL: "://invalid",
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
owner, repo, rem, query, err := extractParts(tc.rawURL)
if (err != nil) != tc.expectError {
t.Fatalf("extractParts() error = %v, expectError %v", err, tc.expectError)
}
if !tc.expectError {
if owner != tc.expectedOwner {
t.Errorf("owner: got %q, want %q", owner, tc.expectedOwner)
}
if repo != tc.expectedRepo {
t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo)
}
if rem != tc.expectedRem {
t.Errorf("remaining path: got %q, want %q", rem, tc.expectedRem)
}
if !reflect.DeepEqual(query, tc.expectedQuery) {
t.Errorf("query: got %v, want %v", query, tc.expectedQuery)
}
}
})
}
}
func TestMatchString_Compatibility(t *testing.T) {
testCases := []struct {
target string
expected bool
}{
{"blob", true}, {"raw", true}, {"gist", true},
{"clone", false}, {"releases", false},
}
for _, tc := range testCases {
t.Run(tc.target, func(t *testing.T) {
if got := matchString(tc.target); got != tc.expected {
t.Errorf("matchString('%s') = %v; want %v", tc.target, got, tc.expected)
}
})
}
}
func BenchmarkMatcher(b *testing.B) {
cfg := &config.Config{}
path := "https://github.com/WJQSERVER/speedtest-ex/releases/download/v1.2.0/speedtest-linux-amd64.tar.gz"
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _, _ = Matcher(path, cfg)
}
}

181
proxy/nest.go Normal file
View File

@@ -0,0 +1,181 @@
package proxy
import (
"bufio"
"compress/gzip"
"fmt"
"ghproxy/config"
"io"
"strings"
)
func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) {
// 匹配 "https://github.com"开头的链接
if strings.HasPrefix(rawPath, "https://github.com") {
return true, nil
}
// 匹配 "https://raw.githubusercontent.com"开头的链接
if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") {
return true, nil
}
// 匹配 "https://raw.github.com"开头的链接
if strings.HasPrefix(rawPath, "https://raw.github.com") {
return true, nil
}
// 匹配 "https://gist.githubusercontent.com"开头的链接
if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") {
return true, nil
}
// 匹配 "https://gist.github.com"开头的链接
if strings.HasPrefix(rawPath, "https://gist.github.com") {
return true, nil
}
if cfg.Shell.RewriteAPI {
// 匹配 "https://api.github.com/"开头的链接
if strings.HasPrefix(rawPath, "https://api.github.com") {
return true, nil
}
}
return false, nil
}
// 匹配文件扩展名是sh的rawPath
func MatcherShell(rawPath string) bool {
return strings.HasSuffix(rawPath, ".sh")
}
// LinkProcessor 是一个函数类型,用于处理提取到的链接。
type LinkProcessor func(string) string
// 自定义 URL 修改函数
func modifyURL(url string, host string, cfg *config.Config) string {
// 去除url内的https://或http://
matched, err := EditorMatcher(url, cfg)
if err != nil {
logDump("Invalid URL: %s", url)
return url
}
if matched {
var u = url
u = strings.TrimPrefix(u, "https://")
u = strings.TrimPrefix(u, "http://")
logDump("Modified URL: %s", "https://"+host+"/"+u)
return "https://" + host + "/" + u
}
return url
}
// processLinks 处理链接,返回包含处理后数据的 io.Reader
func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) {
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
readerOut = pipeReader
go func() { // 在 Goroutine 中执行写入操作
defer func() {
if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误
if err != nil {
if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader
logError("pipeWriter close with error failed: %v, original error: %v", closeErr, err)
}
} else {
if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭
logError("pipeWriter close failed: %v", closeErr)
if err == nil { // 如果之前没有错误,记录关闭错误
err = closeErr
}
}
}
}
}()
defer func() {
if err := input.Close(); err != nil {
logError("input close failed: %v", err)
}
}()
var bufReader *bufio.Reader
if compress == "gzip" {
// 解压gzip
gzipReader, gzipErr := gzip.NewReader(input)
if gzipErr != nil {
err = fmt.Errorf("gzip解压错误: %v", gzipErr)
return // Goroutine 中使用 return 返回错误
}
defer gzipReader.Close()
bufReader = bufio.NewReader(gzipReader)
} else {
bufReader = bufio.NewReader(input)
}
var bufWriter *bufio.Writer
var gzipWriter *gzip.Writer
// 根据是否gzip确定 writer 的创建
if compress == "gzip" {
gzipWriter = gzip.NewWriter(pipeWriter) // 使用 pipeWriter
bufWriter = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小
} else {
bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter
}
//确保writer关闭
defer func() {
var closeErr error // 局部变量用于保存defer中可能发生的错误
if gzipWriter != nil {
if closeErr = gzipWriter.Close(); closeErr != nil {
logError("gzipWriter close failed %v", closeErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = closeErr
}
}
}
if flushErr := bufWriter.Flush(); flushErr != nil {
logError("writer flush failed %v", flushErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = flushErr
}
}
}()
// 使用正则表达式匹配 http 和 https 链接
for {
line, readErr := bufReader.ReadString('\n')
if readErr != nil {
if readErr == io.EOF {
break // 文件结束
}
err = fmt.Errorf("读取行错误: %v", readErr) // 传递错误
return // Goroutine 中使用 return 返回错误
}
// 替换所有匹配的 URL
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
logDump("originalURL: %s", originalURL)
return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义
})
n, writeErr := bufWriter.WriteString(modifiedLine)
written += int64(n) // 更新写入的字节数
if writeErr != nil {
err = fmt.Errorf("写入文件错误: %v", writeErr) // 传递错误
return // Goroutine 中使用 return 返回错误
}
}
// 在返回之前,再刷新一次 (虽然 defer 中已经有 flush但这里再加一次确保及时刷新)
if flushErr := bufWriter.Flush(); flushErr != nil {
if err == nil { // 避免覆盖之前的错误
err = flushErr
}
return // Goroutine 中使用 return 返回错误
}
}()
return readerOut, written, nil // 返回 reader 和 writtenerror 由 Goroutine 通过 pipeWriter.CloseWithError 传递
}

View File

@@ -10,11 +10,12 @@ import (
)
func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) bool {
if cfg.Auth.ForceAllowApi && cfg.Auth.ForceAllowApiPassList {
return false
}
// 白名单检查
if cfg.Whitelist.Enabled {
var whitelist bool
whitelist = auth.CheckWhitelist(user, repo)
whitelist := auth.CheckWhitelist(user, repo)
if !whitelist {
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo)))
logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
@@ -24,8 +25,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
// 黑名单检查
if cfg.Blacklist.Enabled {
var blacklist bool
blacklist = auth.CheckBlacklist(user, repo)
blacklist := auth.CheckBlacklist(user, repo)
if blacklist {
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo)))
logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)

258
weakcache/weakcache.go Normal file
View File

@@ -0,0 +1,258 @@
package weakcache
import (
"container/list"
"sync"
"time"
"weak" // Go 1.24 引入的 weak 包
)
// DefaultExpiration 默认过期时间,这里设置为 15 分钟。
// 这是一个导出的常量,方便用户使用包时引用默认值。
const DefaultExpiration = 5 * time.Minute
// cleanupInterval 是后台清理 Go routine 的扫描间隔,这里设置为 5 分钟。
// 这是一个内部常量,不导出。
const cleanupInterval = 2 * time.Minute
// cacheEntry 缓存项的内部结构。不导出。
type cacheEntry[T any] struct {
Value T
Expiration time.Time
key string // 存储key方便在list.Element中引用
}
// Cache 是一个基于 weak.Pointer, 带有过期和大小上限 (FIFO) 的泛型缓存。
// 这是一个导出的类型。
type Cache[T any] struct {
mu sync.RWMutex
// 修正缓存存储key -> weak.Pointer 到 cacheEntry 结构体 (而不是指向结构体的指针)
// weak.Make(*cacheEntry[T]) 返回 weak.Pointer[cacheEntry[T]]
data map[string]weak.Pointer[cacheEntry[T]]
// FIFO 链表:存储 key 的 list.Element
// 链表头部是最近放入的,尾部是最早放入的(最老的)
fifoList *list.List
// FIFO 元素的映射key -> *list.Element
fifoMap map[string]*list.Element
defaultExpiration time.Duration
maxSize int // 缓存最大容量0 表示无限制
stopCleanup chan struct{}
wg sync.WaitGroup // 用于等待清理 Go routine 退出
}
// NewCache 创建一个新的缓存实例。
// expiration: 新添加项的默认过期时间。如果为 0则使用 DefaultExpiration。
// maxSize: 缓存的最大容量0 表示无限制。当达到上限时,采用 FIFO 策略淘汰。
// 这是一个导出的构造函数。
func NewCache[T any](expiration time.Duration, maxSize int) *Cache[T] {
if expiration <= 0 {
expiration = DefaultExpiration
}
c := &Cache[T]{
// 修正:初始化 map值类型已修正
data: make(map[string]weak.Pointer[cacheEntry[T]]),
fifoList: list.New(),
fifoMap: make(map[string]*list.Element),
defaultExpiration: expiration,
maxSize: maxSize,
stopCleanup: make(chan struct{}),
}
// 启动后台清理 Go routine
c.wg.Add(1)
go c.cleanupLoop()
return c
}
// Put 将值放入缓存。如果 key 已存在,会更新其值和过期时间。
// 这是导出的方法。
func (c *Cache[T]) Put(key string, value T) {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
expiration := now.Add(c.defaultExpiration)
// 如果 key 已经存在,更新其值和过期时间。
// 在 FIFO 策略中, Put 更新不改变其在链表中的位置,除非旧的 entry 已经被 GC。
if elem, ok := c.fifoMap[key]; ok {
// 从 data map 中获取弱引用wp 的类型现在是 weak.Pointer[cacheEntry[T]]
if wp, dataOk := c.data[key]; dataOk {
// wp.Value() 返回 *cacheEntry[T] entry 的类型现在是 *cacheEntry[T]
entry := wp.Value()
if entry != nil {
// 旧的 cacheEntry 仍在内存中,直接更新
entry.Value = value
entry.Expiration = expiration
// 在严格 FIFO 中,更新不移动位置
return
}
// 如果 weak.Pointer.Value() 为 nil说明之前的 cacheEntry 已经被 GC 了
// 此时需要创建一个新的 entry并将其从旧位置移除再重新添加
c.fifoList.Remove(elem)
delete(c.fifoMap, key)
} else {
c.fifoList.Remove(elem)
delete(c.fifoMap, key)
}
}
// 新建缓存项 (注意这里是结构体值,而不是指针)
// weak.Make 接收的是指针 *T
entry := &cacheEntry[T]{ // 创建结构体指针
Value: value,
Expiration: expiration,
key: key, // 存储 key
}
// 将新的 *cacheEntry[T] 包装成 weak.Pointer[cacheEntry[T]] 存入 data map
// weak.Make(entry) 现在返回 weak.Pointer[cacheEntry[T]],类型匹配 data map 的值类型
c.data[key] = weak.Make(entry)
// 添加到 FIFO 链表头部 (最近放入/更新的在头部)
// PushFront 返回新的 list.Element
c.fifoMap[key] = c.fifoList.PushFront(key)
// 检查大小上限并进行淘汰 (淘汰尾部的最老项)
c.evictIfNeeded()
}
// Get 从缓存中获取值。返回获取到的值和是否存在/是否有效。
// 这是导出的方法。
func (c *Cache[T]) Get(key string) (T, bool) {
c.mu.RLock() // 先读锁
// 从 data map 中获取弱引用wp 的类型现在是 weak.Pointer[cacheEntry[T]]
wp, ok := c.data[key]
c.mu.RUnlock() // 立即释放读锁如果需要写操作removeEntry可以获得锁
var zero T // 零值
if !ok {
return zero, false
}
// 尝试获取实际的 cacheEntry 指针
// wp.Value() 返回 *cacheEntry[T] entry 的类型现在是 *cacheEntry[T]
entry := wp.Value()
if entry == nil {
// 对象已被GC回收需要清理此弱引用
c.removeEntry(key) // 内部会加写锁
return zero, false
}
// 检查过期时间 (通过 entry 指针访问字段)
if time.Now().After(entry.Expiration) {
// 逻辑上已过期
c.removeEntry(key) // 内部会加写锁
return zero, false
}
// 在 FIFO 缓存中Get 操作不改变项在链表中的位置
return entry.Value, true // 通过 entry 指针访问值字段
}
// removeEntry 从缓存中移除项。
// 这个方法是内部使用的,不导出。需要被调用者确保持有写锁,或者内部自己加锁。
// 考虑到 Get 和 cleanupLoop 可能会调用,让其内部自己加锁更安全。
func (c *Cache[T]) removeEntry(key string) {
c.mu.Lock()
defer c.mu.Unlock()
// 从 data map 中删除
delete(c.data, key)
// 从 FIFO 链表和 fifoMap 中删除
if elem, ok := c.fifoMap[key]; ok {
c.fifoList.Remove(elem)
delete(c.fifoMap, key)
}
}
// evictIfNeeded 检查是否需要淘汰最老FIFO 链表尾部)的项。
// 这个方法是内部使用的,不导出。必须在持有写锁的情况下调用。
func (c *Cache[T]) evictIfNeeded() {
if c.maxSize > 0 && c.fifoList.Len() > c.maxSize {
// 淘汰 FIFO 链表尾部的元素 (最老的)
oldest := c.fifoList.Back()
if oldest != nil {
keyToEvict := oldest.Value.(string) // 链表元素存储的是 key
c.fifoList.Remove(oldest)
delete(c.fifoMap, keyToEvict)
delete(c.data, keyToEvict) // 移除弱引用
}
}
}
// Size 返回当前缓存中的弱引用项数量。
// 注意:这个数量可能包含已被 GC 回收但尚未清理的项。
// 这是一个导出的方法。
func (c *Cache[T]) Size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.data)
}
// cleanupLoop 后台清理 Go routine。不导出。
func (c *Cache[T]) cleanupLoop() {
defer c.wg.Done()
// 使用内部常量 cleanupInterval
ticker := time.NewTicker(cleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.cleanupExpiredAndGCed()
case <-c.stopCleanup:
return
}
}
}
// cleanupExpiredAndGCed 扫描并清理已过期或已被 GC 回收的项。不导出。
func (c *Cache[T]) cleanupExpiredAndGCed() {
c.mu.Lock() // 清理时需要写锁
defer c.mu.Unlock()
now := time.Now()
keysToRemove := make([]string, 0, len(c.data)) // 预估容量
// 遍历 data map 查找需要清理的键
for key, wp := range c.data {
// wp 的类型是 weak.Pointer[cacheEntry[T]]
// wp.Value() 返回 *cacheEntry[T] entry 的类型是 *cacheEntry[T]
entry := wp.Value() // 尝试获取强引用
if entry == nil {
// 已被 GC 回收
keysToRemove = append(keysToRemove, key)
} else if now.After(entry.Expiration) {
// 逻辑过期 (通过 entry 指针访问字段)
keysToRemove = append(keysToRemove, key)
}
}
// 执行删除操作
for _, key := range keysToRemove {
// 从 data map 中删除
delete(c.data, key)
// 从 FIFO 链表和 fifoMap 中删除
// 需要再次检查 fifoMap因为在持有锁期间evictIfNeeded 可能已经移除了这个 key
if elem, ok := c.fifoMap[key]; ok {
c.fifoList.Remove(elem)
delete(c.fifoMap, key)
}
}
}
// StopCleanup 停止后台清理 Go routine。
// 这是一个导出的方法。
func (c *Cache[T]) StopCleanup() {
close(c.stopCleanup)
c.wg.Wait() // 等待 Go routine 退出
}