Compare commits

...

19 Commits
3.5.1 ... v4

Author SHA1 Message Date
wjqserver
ceda8220fd fix resp header setting 2025-06-16 08:50:05 +08:00
wjqserver
1636bf1548 update auth init 2025-06-16 08:45:47 +08:00
wjqserver
a4d324a361 4.0.0-beta.0 2025-06-16 08:28:02 +08:00
wjqserver
91c3ad7fd8 3.5.6 2025-06-15 16:42:42 +08:00
wjqserver
97b1f69f99 25w48c 2025-06-15 15:51:50 +08:00
wjqserver
fd7e270db4 25w48b 2025-06-15 15:14:15 +08:00
wjqserver
cf5ae0d184 fix blob rewrite 2025-06-15 11:02:16 +08:00
wjqserver
0008366e07 25w48a 2025-06-14 23:06:11 +08:00
wjqserver
e0cbfed1e7 3.5.5 2025-06-14 22:17:13 +08:00
wjqserver
1b06260a14 25w47a 2025-06-14 22:11:31 +08:00
wjqserver
8ab622d149 update matcher for gist usercontent 2025-06-14 22:05:45 +08:00
wjqserver
bbb108689a 3.5.4 2025-06-14 12:50:26 +08:00
wjqserver
41395b1d72 25w46c 2025-06-14 07:16:43 +08:00
wjqserver
bd8412f157 25w46b 2025-06-14 06:51:41 +08:00
wjqserver
d2a0177015 25w46a 2025-06-14 06:47:47 +08:00
wjqserver
a5bf7686bd 3.5.3 2025-06-13 15:23:46 +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
41 changed files with 1181 additions and 1594 deletions

View File

@@ -141,4 +141,7 @@ jobs:
push: true push: true
tags: | tags: |
${{ env.IMAGE_NAME }}:${{ env.VERSION }} ${{ env.IMAGE_NAME }}:${{ env.VERSION }}
${{ env.IMAGE_NAME }}:v3 ${{ env.IMAGE_NAME }}:v4
${{ env.IMAGE_NAME }}:latest
wjqserver/ghproxy-touka:latest
wjqserver/ghproxy-touka:${{ env.VERSION }}

View File

@@ -1,5 +1,69 @@
# 更新日志 # 更新日志
4.0.0-beta.0 - 2025-06-15
---
- BETA-TEST: 此版本是v4.0.0的测试版本,请勿在生产环境中使用;
- CHANGE: 移交到Touka框架
- REMOVE: 移除req rate limit的total方式
- CHANGE: 使用[reco](https://github.com/fenthope/reco)日志库, 异步使能
3.5.6 - 2025-06-15
---
- FIX: 修正blob重写的生成问题
- CHANGE: 改进302重定向逻辑
25w48c - 2025-06-15
---
- PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用;
- CHANGE: 加入内部301处理
25w48b - 2025-06-15
---
- PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用;
- FIX: 修正blob重写的生成问题
- CHANGE: 验证与连接释放相关的修正
25w48a - 2025-06-14
---
- PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用;
- CHANGE: 测试302重定向逻辑
3.5.5 - 2025-06-14
---
- CHANGE: 修正新匹配器的覆盖问题, 同时增加test的覆盖
25w47a - 2025-06-14
---
- PRE-RELEASE: 此版本是v3.5.5预发布版本,请勿在生产环境中使用;
- CHANGE: 修正新匹配器的覆盖问题, 同时增加test的覆盖
3.5.4 - 2025-06-14
---
- CHANGE: 移植来自于[GHProxy-Touka](https://github.com/WJQSERVER-STUDIO/ghproxy-touka)的blob处理逻辑与302处理逻辑
25w46c - 2025-06-14
---
- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用;
- CHANGE: 移植来自于[GHProxy-Touka](https://github.com/WJQSERVER-STUDIO/ghproxy-touka)的blob处理逻辑与302处理逻辑
25w46b - 2025-06-14
---
- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用;
- CHANGE: 修改关闭行为以测试问题
25w46a - 2025-06-14
---
- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用;
- CHANGE: 修改payload行为以测试问题
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 3.5.1 - 2025-06-09
--- ---
- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op` - CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op`

View File

@@ -1 +1 @@
25w45a 4.0.0-beta.0

516
LICENSE
View File

@@ -1,199 +1,373 @@
WJQserver Studio 开源许可证 Mozilla Public License Version 2.0
版本 v2.1 ==================================
版权所有 © 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): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。 1.2. "Contributor Version"
* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。 means the combination of the Contributions of others (if any) used
* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。 by a Contributor and that particular Contributor's Contribution.
* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。
* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。
* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于:
* 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。
* 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。
* 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。
* 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。
* 内部运营用途 (非营利组织) 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。
开源与自由软件 1.3. "Contribution"
means Covered Software of a particular Contributor.
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。 1.4. "Covered Software"
本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。 means Source Code Form to which the initial Contributor has attached
强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。 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 开源继承 (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.
* 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。 1.8. "License"
* 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。 means this document.
* 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。
* 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。
您必须选择以下两种方式之一: 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) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。 1.10. "Modifications"
* ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。 means any of the following:
* 1.3 保持声明: 公开发布服务时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。 (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. 复制与分发 (b) any new file in Source Code Form that contains any Covered
Software.
* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件: 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.
* 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。 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.
* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.3 条(开源继承与互惠共享)的约束。 1.13. "Source Code Form"
means the form of the work preferred for making modifications.
3. 修改权限 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.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。 2. License Grants and Conditions
--------------------------------
* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.3 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。 2.1. Grants
* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。 Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
4. 专利权 (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.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。 (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.
* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。 2.2. Effective Date
5. 免责声明 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.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。 2.3. Limitations on Grant Scope
* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害。 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
* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。 distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
6. 许可证期限与终止 Contributor:
* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。 (a) for any code that a Contributor has removed from Covered Software;
or
* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。
(b) for infringements caused by: (i) Your and any other third party's
* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。 modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
7. 条款修订 Version); or
* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。 (c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。
This License does not grant any rights in the trademarks, service marks,
8. 其他 or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。
2.4. Subsequent Licenses
* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。
No Contributor makes additional grants as a result of Your choice to
* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。 distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
WJQserver Studio Open Source License permitted under the terms of Section 3.3).
Version v2.0
2.5. Representation
Copyright © WJQserver Studio 2024
Each Contributor represents that the Contributor believes its
Definitions Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
* 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. 2.6. Fair Use
* 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. This License is not intended to limit any rights You have under
* 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. applicable copyright doctrines of fair use, fair dealing, or other
* Non-profit Use: Refers to uses not primarily intended for direct commercial profit, including but not limited to: equivalents.
* 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. 2.7. Conditions
* 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. Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
* 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. in Section 2.1.
Open Source and Free Software 3. Responsibilities
-------------------
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. 3.1. Distribution of Source Form
Copyright is emphasized; all rights are jointly reserved by WJQserver Studio and Contributors.
All distribution of Covered Software in Source Code Form, including any
License Terms Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
1. Permissions for Use 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
* 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. attempt to alter or restrict the recipients' rights in the Source Code
Form.
* 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms:
3.2. Distribution of Executable Form
* 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:
If You distribute Covered Software in Executable Form then:
* 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. (a) such Covered Software must also be made available in Source Code
* Embedded Commercial Applications: Embedding the Software or its Derivative Works into commercial products or solutions for sale. Form, as described in Section 3.1, and You must inform recipients of
* 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. 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
You must choose one of the following two options: than the cost of distribution to the recipient; and
* 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. (b) You may distribute such Executable Form under the terms of this
* 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. License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
* 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. the recipients' rights in the Source Code Form under this License.
2. Reproduction and Distribution 3.3. Distribution of a Larger Work
* 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: 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
* Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices. the Covered Software. If the Larger Work is a combination of Covered
* 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. Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
* 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). License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
3. Modification Permissions the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
* 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. License(s).
* 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.4. Notices
* 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. You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
4. Patent Rights or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
* 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. the extent required to remedy known factual inaccuracies.
* 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. 3.5. Application of Additional Terms
5. Disclaimer of Warranty You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
* 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. 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
* 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. such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
* 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. liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
6. License Term and Termination disclaimers of warranty and limitations of liability specific to any
jurisdiction.
* 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.
4. Inability to Comply Due to Statute or Regulation
* 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. 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
7. Revision of Terms statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
* 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. describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
* 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. Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
8. Other recipient of ordinary skill to be able to understand it.
* 8.1 Statutory Rights: This License does not affect your statutory rights as an end-user under applicable laws. 5. Termination
--------------
* 8.2 Severability of Terms: If certain terms of this License are deemed unenforceable, the remaining terms shall remain in full force and effect.
5.1. The rights granted under this License will terminate automatically
* 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. 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

@@ -6,16 +6,15 @@
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/WJQSERVER-STUDIO/ghproxy) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/WJQSERVER-STUDIO/ghproxy)
[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy) [![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
GHProxy是一个基于Go的支持代理Github仓库资源和API的项目, 同时支持Docker镜像代理与脚本嵌套加速等多种功能
支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能
## 项目说明 ## 项目说明
### 项目特点 ### 项目特点
-**基于 Go 语言实现,跨平台的同时提供高并发性能** -**基于 Go 语言实现,跨平台的同时提供高并发性能**
- 🌐 **使用字节旗下的 [HertZ](https://github.com/cloudwego/hertz) 作为 Web 框架** - 🌐 **使用自有[Touka框架](https://github.com/infinite-iroha/touka)作为 HTTP服务端框架**
- 📡 **使用 [Touka-HTTPC](https://github.com/satomitouka/touka-httpc) 作为 HTTP 客户端** - 📡 **使用 [Touka-HTTPC](https://github.com/WJQSERVER-STUDIO/httpc) 作为 HTTP 客户端**
- 📥 **支持 Git clone、raw、releases 等文件拉取** - 📥 **支持 Git clone、raw、releases 等文件拉取**
- 🐳 **支持反代Docker, GHCR等镜像仓库** - 🐳 **支持反代Docker, GHCR等镜像仓库**
- 🎨 **支持多个前端主题** - 🎨 **支持多个前端主题**
@@ -98,9 +97,9 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox
## 项目简史 ## 项目简史
**本项目是[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的重构版本,实现了原项目原定功能的同时,进一步优化了性能** 本项目旨在于构建一个高效且功能多样的GHProxy
关于此项目的详细开发过程,请参看Commit记录与[CHANGELOG.md](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/CHANGELOG.md)
- v4.0.0 迁移到[Touka框架](https://github.com/infinite-iroha/touka)
- v3.0.0 迁移到HertZ框架, 进一步提升效率 - v3.0.0 迁移到HertZ框架, 进一步提升效率
- v2.4.1 对路径匹配进行优化 - v2.4.1 对路径匹配进行优化
- v2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用 - v2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用
@@ -109,7 +108,9 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox
## LICENSE ## 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.1](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 在v2.3.0之前, 本项目使用WJQserver Studio License 1.2
@@ -119,10 +120,6 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox
如果您觉得本项目对您有帮助,欢迎赞助支持,您的赞助将用于Demo服务器开支及开发者时间成本支出,感谢您的支持! 如果您觉得本项目对您有帮助,欢迎赞助支持,您的赞助将用于Demo服务器开支及开发者时间成本支出,感谢您的支持!
为爱发电,开源不易
爱发电: https://afdian.com/a/wjqserver
USDT(TRC20): `TNfSYG6F2vkiibd6J6mhhHNWDgWgNdF5hN` USDT(TRC20): `TNfSYG6F2vkiibd6J6mhhHNWDgWgNdF5hN`
### 捐赠列表 ### 捐赠列表

View File

@@ -6,10 +6,13 @@
| 版本 | 是否支持 | | 版本 | 是否支持 |
| --- | --- | | --- | --- |
| v3.x.x | :white_check_mark: 当前最新版本序列 | | v4.x.x | :white_check_mark: 当前最新版本序列 |
| v3.x.x | :x: 这些版本已结束生命周期,不受支持 |
| v2.x.x | :x: 这些版本已结束生命周期,不受支持 | | v2.x.x | :x: 这些版本已结束生命周期,不受支持 |
| v1.x.x | :x: 这些版本已结束生命周期,不受支持 | | v1.x.x | :x: 这些版本已结束生命周期,不受支持 |
| 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 | | *-rc.x | :warning: 此为PRE-RELEASE预发布版本,用于测试问题 |
| *-beta.x | :warning: 此为Beta测试版本,用于开发与测试,可能存在未知的问题 |
| 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 |
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 | | 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 |
| v0.x.x | :x: 这些版本不再受支持 | | v0.x.x | :x: 这些版本不再受支持 |
@@ -17,9 +20,15 @@
本项目为开源项目,开发者不对使用本项目造成的任何损失或问题承担责任。用户需自行评估并承担使用本项目的风险。 本项目为开源项目,开发者不对使用本项目造成的任何损失或问题承担责任。用户需自行评估并承担使用本项目的风险。
使用本项目,请遵循 **[WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议。 使用本项目,请遵循 **[WSL 2.1 (WJQSERVER-STUDIO LICENSE 2.1)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议 或 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/)
本项目所有文件均受到 WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0) 协议保护,任何人不得在任何情况下以非 WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。 #### 选择WSL 2.1时
本项目所有文件均受到 WSL 2.1 (WJQSERVER-STUDIO LICENSE 2.1) 协议保护,任何人不得在任何情况下以非 WSL 2.1 (WJQSERVER-STUDIO LICENSE 2.1) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。
#### 选择MPL 2.0时
本项目内文件除特别版权标注声明外, 均受到 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 授权保护, 具体条款参看 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/)
## 报告漏洞 ## 报告漏洞

View File

@@ -1 +1 @@
3.5.1 4.0.0

View File

@@ -1,137 +1,131 @@
package api package api
import ( import (
"context"
"ghproxy/config" "ghproxy/config"
"ghproxy/middleware/nocache" "ghproxy/middleware/nocache"
"github.com/WJQSERVER-STUDIO/logger" "github.com/infinite-iroha/touka"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
) )
var ( func InitHandleRouter(cfg *config.Config, r *touka.Engine, version string) {
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) {
apiRouter := r.Group("/api", nocache.NoCacheMiddleware()) apiRouter := r.Group("/api", nocache.NoCacheMiddleware())
{ {
apiRouter.GET("/size_limit", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/size_limit", func(c *touka.Context) {
SizeLimitHandler(cfg, c, ctx) SizeLimitHandler(cfg, c)
}) })
apiRouter.GET("/whitelist/status", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/whitelist/status", func(c *touka.Context) {
WhiteListStatusHandler(cfg, c, ctx) WhiteListStatusHandler(cfg, c)
}) })
apiRouter.GET("/blacklist/status", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/blacklist/status", func(c *touka.Context) {
BlackListStatusHandler(cfg, c, ctx) BlackListStatusHandler(cfg, c)
}) })
apiRouter.GET("/cors/status", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/cors/status", func(c *touka.Context) {
CorsStatusHandler(cfg, c, ctx) CorsStatusHandler(cfg, c)
}) })
apiRouter.GET("/healthcheck", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/healthcheck", func(c *touka.Context) {
HealthcheckHandler(c, ctx) HealthcheckHandler(c)
}) })
apiRouter.GET("/version", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/ok", func(c *touka.Context) {
VersionHandler(c, ctx, version) HealthcheckHandler(c)
}) })
apiRouter.GET("/rate_limit/status", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/version", func(c *touka.Context) {
RateLimitStatusHandler(cfg, c, ctx) VersionHandler(c, version)
}) })
apiRouter.GET("/rate_limit/limit", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/rate_limit/status", func(c *touka.Context) {
RateLimitLimitHandler(cfg, c, ctx) RateLimitStatusHandler(cfg, c)
}) })
apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/rate_limit/limit", func(c *touka.Context) {
SmartGitStatusHandler(cfg, c, ctx) RateLimitLimitHandler(cfg, c)
}) })
apiRouter.GET("/shell_nest/status", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/smartgit/status", func(c *touka.Context) {
shellNestStatusHandler(cfg, c, ctx) SmartGitStatusHandler(cfg, c)
}) })
apiRouter.GET("/oci_proxy/status", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/shell_nest/status", func(c *touka.Context) {
ociProxyStatusHandler(cfg, c, ctx) shellNestStatusHandler(cfg, c)
})
apiRouter.GET("/oci_proxy/status", func(c *touka.Context) {
ociProxyStatusHandler(cfg, c)
}) })
} }
logInfo("API router Init success")
} }
func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func SizeLimitHandler(cfg *config.Config, c *touka.Context) {
sizeLimit := cfg.Server.SizeLimit sizeLimit := cfg.Server.SizeLimit
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"MaxResponseBodySize": sizeLimit, "MaxResponseBodySize": sizeLimit,
})) }))
} }
func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func WhiteListStatusHandler(cfg *config.Config, c *touka.Context) {
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Whitelist": cfg.Whitelist.Enabled, "Whitelist": cfg.Whitelist.Enabled,
})) }))
} }
func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func BlackListStatusHandler(cfg *config.Config, c *touka.Context) {
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Blacklist": cfg.Blacklist.Enabled, "Blacklist": cfg.Blacklist.Enabled,
})) }))
} }
func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func CorsStatusHandler(cfg *config.Config, c *touka.Context) {
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Cors": cfg.Server.Cors, "Cors": cfg.Server.Cors,
})) }))
} }
func HealthcheckHandler(c *app.RequestContext, ctx context.Context) { func HealthcheckHandler(c *touka.Context) {
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Status": "OK", "Status": "OK",
"Repo": "WJQSERVER-STUDIO/GHProxy",
"Author": "WJQSERVER-STUDIO",
})) }))
} }
func VersionHandler(c *app.RequestContext, ctx context.Context, version string) { func VersionHandler(c *touka.Context, version string) {
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Version": version, "Version": version,
"Repo": "WJQSERVER-STUDIO/GHProxy",
"Author": "WJQSERVER-STUDIO",
})) }))
} }
func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func RateLimitStatusHandler(cfg *config.Config, c *touka.Context) {
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"RateLimit": cfg.RateLimit.Enabled, "RateLimit": cfg.RateLimit.Enabled,
})) }))
} }
func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func RateLimitLimitHandler(cfg *config.Config, c *touka.Context) {
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"RatePerMinute": cfg.RateLimit.RatePerMinute, "RatePerMinute": cfg.RateLimit.RatePerMinute,
})) }))
} }
func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func SmartGitStatusHandler(cfg *config.Config, c *touka.Context) {
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"enabled": cfg.GitClone.Mode == "cache", "enabled": cfg.GitClone.Mode == "cache",
})) }))
} }
func shellNestStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func shellNestStatusHandler(cfg *config.Config, c *touka.Context) {
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"enabled": cfg.Shell.Editor, "enabled": cfg.Shell.Editor,
})) }))
} }
func ociProxyStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func ociProxyStatusHandler(cfg *config.Config, c *touka.Context) {
c.Response.Header.Set("Content-Type", "application/json") c.SetHeader("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"enabled": cfg.Docker.Enabled, "enabled": cfg.Docker.Enabled,
"target": cfg.Docker.Target, "target": cfg.Docker.Target,

View File

@@ -4,22 +4,21 @@ import (
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"github.com/cloudwego/hertz/pkg/app" "github.com/infinite-iroha/touka"
) )
func AuthHeaderHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err error) { func AuthHeaderHandler(c *touka.Context, cfg *config.Config) (isValid bool, err error) {
if !cfg.Auth.Enabled { if !cfg.Auth.Enabled {
return true, nil return true, nil
} }
// 获取"GH-Auth"的值 // 获取"GH-Auth"的值
var authToken string var authToken string
if cfg.Auth.Key != "" { if cfg.Auth.Key != "" {
authToken = string(c.GetHeader(cfg.Auth.Key)) authToken = string(c.Request.Header.Get(cfg.Auth.Key))
} else { } else {
authToken = string(c.GetHeader("GH-Auth")) authToken = string(c.Request.Header.Get("GH-Auth"))
} }
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), authToken)
if authToken == "" { if authToken == "" {
return false, fmt.Errorf("Auth token not found") return false, fmt.Errorf("Auth token not found")
} }

View File

@@ -4,10 +4,10 @@ import (
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"github.com/cloudwego/hertz/pkg/app" "github.com/infinite-iroha/touka"
) )
func AuthParametersHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err error) { func AuthParametersHandler(c *touka.Context, cfg *config.Config) (isValid bool, err error) {
if !cfg.Auth.Enabled { if !cfg.Auth.Enabled {
return true, nil return true, nil
} }
@@ -19,8 +19,6 @@ func AuthParametersHandler(c *app.RequestContext, cfg *config.Config) (isValid b
authToken = c.Query("auth_token") authToken = c.Query("auth_token")
} }
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), authToken)
if authToken == "" { if authToken == "" {
return false, fmt.Errorf("Auth token not found") return false, fmt.Errorf("Auth token not found")
} }

View File

@@ -4,38 +4,26 @@ import (
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"github.com/WJQSERVER-STUDIO/logger" "github.com/infinite-iroha/touka"
"github.com/cloudwego/hertz/pkg/app"
) )
var ( func ListInit(cfg *config.Config) error {
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
func Init(cfg *config.Config) {
if cfg.Blacklist.Enabled { if cfg.Blacklist.Enabled {
err := InitBlacklist(cfg) err := InitBlacklist(cfg)
if err != nil { if err != nil {
logError(err.Error()) return err
return
} }
} }
if cfg.Whitelist.Enabled { if cfg.Whitelist.Enabled {
err := InitWhitelist(cfg) err := InitWhitelist(cfg)
if err != nil { if err != nil {
logError(err.Error()) return err
return
} }
} }
logDebug("Auth Init") return nil
} }
func AuthHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err error) { func AuthHandler(c *touka.Context, cfg *config.Config) (isValid bool, err error) {
if cfg.Auth.Method == "parameters" { if cfg.Auth.Method == "parameters" {
isValid, err = AuthParametersHandler(c, cfg) isValid, err = AuthParametersHandler(c, cfg)
return isValid, err return isValid, err
@@ -43,10 +31,10 @@ func AuthHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err e
isValid, err = AuthHeaderHandler(c, cfg) isValid, err = AuthHeaderHandler(c, cfg)
return isValid, err return isValid, err
} else if cfg.Auth.Method == "" { } else if cfg.Auth.Method == "" {
logError("Auth method not set") c.Errorf("Auth method not set")
return true, nil return true, nil
} else { } else {
logError("Auth method not supported %s", cfg.Auth.Method) c.Errorf("Auth method not supported %s", cfg.Auth.Method)
return false, fmt.Errorf("%s", fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method)) return false, fmt.Errorf("%s", fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method))
} }
} }

View File

@@ -7,7 +7,7 @@ import (
"strings" "strings"
"sync" "sync"
json "github.com/bytedance/sonic" "encoding/json"
) )
type Blacklist struct { type Blacklist struct {

View File

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

View File

@@ -25,24 +25,19 @@ type Config struct {
[server] [server]
host = "0.0.0.0" host = "0.0.0.0"
port = 8080 port = 8080
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
sizeLimit = 125 # MB sizeLimit = 125 # MB
memLimit = 0 # MB memLimit = 0 # MB
H2C = true
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false debug = false
*/ */
type ServerConfig struct { type ServerConfig struct {
Port int `toml:"port"` Port int `toml:"port"`
Host string `toml:"host"` Host string `toml:"host"`
NetLib string `toml:"netlib"` SizeLimit int `toml:"sizeLimit"`
SenseClientDisconnection bool `toml:"senseClientDisconnection"` MemLimit int64 `toml:"memLimit"`
SizeLimit int `toml:"sizeLimit"` Cors string `toml:"cors"`
MemLimit int64 `toml:"memLimit"` Debug bool `toml:"debug"`
H2C bool `toml:"H2C"`
Cors string `toml:"cors"`
Debug bool `toml:"debug"`
} }
/* /*
@@ -96,11 +91,9 @@ type PagesConfig struct {
} }
type LogConfig struct { type LogConfig struct {
LogFilePath string `toml:"logFilePath"` LogFilePath string `toml:"logFilePath"`
MaxLogSize int `toml:"maxLogSize"` MaxLogSize int64 `toml:"maxLogSize"`
Level string `toml:"level"` Level string `toml:"level"`
Async bool `toml:"async"`
HertZLogPath string `toml:"hertzLogPath"`
} }
/* /*
@@ -136,7 +129,6 @@ type WhitelistConfig struct {
/* /*
[rateLimit] [rateLimit]
enabled = false enabled = false
rateMethod = "total" # "total" or "ip"
ratePerMinute = 100 ratePerMinute = 100
burst = 10 burst = 10
@@ -149,10 +141,9 @@ burst = 10
*/ */
type RateLimitConfig struct { type RateLimitConfig struct {
Enabled bool `toml:"enabled"` Enabled bool `toml:"enabled"`
RateMethod string `toml:"rateMethod"` RatePerMinute int `toml:"ratePerMinute"`
RatePerMinute int `toml:"ratePerMinute"` Burst int `toml:"burst"`
Burst int `toml:"burst"`
BandwidthLimit BandwidthLimitConfig BandwidthLimit BandwidthLimitConfig
} }
@@ -226,10 +217,8 @@ func DefaultConfig() *Config {
Server: ServerConfig{ Server: ServerConfig{
Port: 8080, Port: 8080,
Host: "0.0.0.0", Host: "0.0.0.0",
NetLib: "netpoll",
SizeLimit: 125, SizeLimit: 125,
MemLimit: 0, MemLimit: 0,
H2C: true,
Cors: "*", Cors: "*",
Debug: false, Debug: false,
}, },
@@ -254,10 +243,9 @@ func DefaultConfig() *Config {
StaticDir: "/data/www", StaticDir: "/data/www",
}, },
Log: LogConfig{ Log: LogConfig{
LogFilePath: "/data/ghproxy/log/ghproxy.log", LogFilePath: "/data/ghproxy/log/ghproxy.log",
MaxLogSize: 10, MaxLogSize: 10,
Level: "info", Level: "info",
HertZLogPath: "/data/ghproxy/log/hertz.log",
}, },
Auth: AuthConfig{ Auth: AuthConfig{
Enabled: false, Enabled: false,
@@ -277,8 +265,8 @@ func DefaultConfig() *Config {
WhitelistFile: "/data/ghproxy/config/whitelist.json", WhitelistFile: "/data/ghproxy/config/whitelist.json",
}, },
RateLimit: RateLimitConfig{ RateLimit: RateLimitConfig{
Enabled: false, Enabled: false,
RateMethod: "total", //RateMethod: "total",
RatePerMinute: 100, RatePerMinute: 100,
Burst: 10, Burst: 10,
BandwidthLimit: BandwidthLimitConfig{ BandwidthLimit: BandwidthLimitConfig{

View File

@@ -1,11 +1,8 @@
[server] [server]
host = "0.0.0.0" host = "0.0.0.0"
port = 8080 port = 8080
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
senseClientDisconnection = false
sizeLimit = 125 # MB sizeLimit = 125 # MB
memLimit = 0 # MB memLimit = 0 # MB
H2C = true
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false debug = false
@@ -33,9 +30,7 @@ staticDir = "/data/www"
[log] [log]
logFilePath = "/data/ghproxy/log/ghproxy.log" logFilePath = "/data/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB maxLogSize = 5 # MB
level = "info" # dump, debug, info, warn, error, none level = "info" # debug, info, warn, error, none
async = false
hertzLogPath = "/data/ghproxy/log/hertz.log"
[auth] [auth]
method = "parameters" # "header" or "parameters" method = "parameters" # "header" or "parameters"
@@ -56,7 +51,6 @@ whitelistFile = "/data/ghproxy/config/whitelist.json"
[rateLimit] [rateLimit]
enabled = false enabled = false
rateMethod = "total" # "ip" or "total"
ratePerMinute = 180 ratePerMinute = 180
burst = 5 burst = 5
@@ -73,4 +67,4 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
[docker] [docker]
enabled = false enabled = false
target = "ghcr" # ghcr/dockerhub target = "dockerhub" # ghcr/dockerhub/ custom

View File

@@ -1,398 +0,0 @@
# ghproxy 用户配置文档
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
`ghproxy` 的配置主要通过修改 `config` 目录下的 `config.toml``blacklist.json``whitelist.json` 文件来实现。本文档将详细介绍这些配置文件的作用以及用户可以自定义的配置选项。
## `config.toml` - 主配置文件
`config.toml``ghproxy` 的主配置文件,采用 TOML 格式。您可以通过修改此文件来定制 `ghproxy` 的各项功能例如服务器端口、连接设置、Git 克隆模式、日志级别、认证方式、黑白名单以及限速策略等。
以下是 `config.toml` 文件的详细配置项说明:
```toml name=config/config.toml
[server]
host = "0.0.0.0"
port = 8080
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
sizeLimit = 125 # MB
memLimit = 0 # MB
H2C = true
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false
[httpc]
mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode
useCustomRawHeaders = false
[gitclone]
mode = "bypass" # bypass / cache
smartGitAddr = "http://127.0.0.1:8080"
ForceH2C = false
[shell]
editor = false
rewriteAPI = false
[pages]
mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/data/www"
[log]
logFilePath = "/data/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB
level = "info" # dump, debug, info, warn, error, none
hertzLogPath = "/data/ghproxy/log/hertz.log"
[auth]
method = "parameters" # "header" or "parameters"
token = "token"
key = ""
enabled = false
passThrough = false
ForceAllowApi = false
[blacklist]
blacklistFile = "/data/ghproxy/config/blacklist.json"
enabled = false
[whitelist]
enabled = false
whitelistFile = "/data/ghproxy/config/whitelist.json"
[rateLimit]
enabled = false
rateMethod = "total" # "ip" or "total"
ratePerMinute = 180
burst = 5
[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 or "xx.example.com"
```
### 配置项详细说明
* **`[server]` - 服务器配置**
* `host`: 监听地址。
* 类型: 字符串 (`string`)
* 默认值: `"0.0.0.0"` (监听所有)
* 说明: 设置 `ghproxy` 监听的网络地址。通常设置为 `"0.0.0.0"` 以监听所有可用的网络接口。
* `port`: 监听端口。
* 类型: 整数 (`int`)
* 默认值: `8080`
* 说明: 设置 `ghproxy` 监听的端口号。
* `netlib`: 底层网络库。
* 类型: 字符串 (`string`)
* 默认值: `""` (HertZ默认处置)
* 说明: `"std"` `"standard"` `"net/http"` `"net"` 均会被设置为go标准库`net/http`, 设置为`"netpoll"`或`""`会由`HertZ`默认逻辑处理
* `sizeLimit`: 请求体大小限制。
* 类型: 整数 (`int`)
* 默认值: `125` (MB)
* 说明: 限制允许接收的请求体最大大小,单位为 MB。用于防止过大的请求导致服务压力过大。
* `memLimit`: `runtime`内存限制
* 类型: 整数 (`int64`)
* 默认值: `0` (不传入)
* 说明: 给`runtime`的指标, 让gc行为更高效
* `H2C`: 是否启用 H2C (HTTP/2 Cleartext) 传输。
* 类型: 布尔值 (`bool`)
* 默认值: `true` (启用)
* 说明: 启用后,允许客户端使用 HTTP/2 协议进行无加密传输,提升性能。
* `cors`: CORS (跨域资源共享) 设置。
* 类型: 字符串 (`string`)
* 默认值: `"*"` (允许所有来源)
* 可选值:
* `""` 或`"*"`: 允许所有来源跨域访问。
* `"nil"`: 禁用 CORS。
* 具体的域名: 例如 `"https://example.com"`,只允许来自指定域名的跨域请求。
* 说明: 配置 CORS 策略,用于控制哪些域名可以跨域访问 `ghproxy` 服务。
* `debug`: 是否启用调试模式。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 会输出更详细的日志信息,用于开发和调试。
* **`[httpc]` - HTTP 客户端配置**
* `mode`: HTTP 客户端模式。
* 类型: 字符串 (`string`)
* 默认值: `"auto"` (自动模式)
* 可选值:
* `"auto"`: 自动模式,使用默认的 HTTP 客户端配置,适用于大多数场景。
* `"advanced"`: 高级模式,允许自定义连接池参数,可以更精细地控制 HTTP 客户端的行为。
* 说明: 选择 HTTP 客户端的运行模式。
* `maxIdleConns`: 最大空闲连接数 (仅在高级模式下生效)。
* 类型: 整数 (`int`)
* 默认值: `100`
* 说明: 设置 HTTP 客户端连接池中保持的最大空闲连接数。
* `maxIdleConnsPerHost`: 每个主机最大空闲连接数 (仅在高级模式下生效)。
* 类型: 整数 (`int`)
* 默认值: `60`
* 说明: 设置 HTTP 客户端连接池中,每个主机允许保持的最大空闲连接数。
* `maxConnsPerHost`: 每个主机最大连接数 (仅在高级模式下生效)。
* 类型: 整数 (`int`)
* 默认值: `0` (不限制)
* 说明: 设置 HTTP 客户端连接池中,每个主机允许建立的最大连接数。设置为 `0` 表示不限制。
* `useCustomRawHeaders`: 使用预定义header避免github waf对应zh-CN的封锁
* 类型: 布尔值(`bool`)
* 默认值: `false`(停用)
* 说明: 启用后, 拉取raw文件会使用程序预定义的固定headers, 而不是原先的复制行为
* **`[gitclone]` - Git 克隆配置**
* `mode`: Git 克隆模式。
* 类型: 字符串 (`string`)
* 默认值: `"bypass"` (绕过模式)
* 可选值:
* `"bypass"`: 绕过模式,直接克隆 GitHub 仓库,不使用任何缓存加速。
* `"cache"`: 缓存模式,使用智能 Git 服务加速克隆,需要配置 `smartGitAddr`。
* 说明: 选择 Git 克隆的模式。
* `smartGitAddr`: 智能 Git 服务地址 (仅在缓存模式下生效)。
* 类型: 字符串 (`string`)
* 默认值: `"http://127.0.0.1:8080"`
* 说明: 当 `mode` 设置为 `"cache"` 时,需要配置智能 Git 服务的地址,用于加速 Git 克隆。
* `ForceH2C`: 是否强制使用 H2C 连接到智能 Git 服务。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (不强制)
* 说明: 如果智能 Git 服务支持 H2C可以设置为 `true` 以强制使用 H2C 连接,提升性能。
* **`[shell]` - Shell 嵌套加速功能配置**
* `editor`: 是否启用编辑(嵌套加速)功能。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后, 会修改`.sh`文件内容以实现嵌套加速
* `rewriteAPI`: 是否重写 API 地址。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 会重写脚本内的Github API地址。
* **`[pages]` - Pages 服务配置**
* `mode`: Pages 服务模式。
* 类型: 字符串 (`string`)
* 默认值: `"internal"` (内置 Pages 服务)
* 可选值:
* `"internal"`: 使用 `ghproxy` 内置的 Pages 服务。
* `"external"`: 使用外部 Pages 位置。
* 说明: 选择 Pages 服务的运行模式。
* `theme`: Pages 主题。
* 类型: 字符串 (`string`)
* 默认值: `"bootstrap"`
* 可选值: 参看[GHProxy项目前端仓库](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend)
* 说明: 设置内置 Pages 服务使用的主题。
* `staticDir`: 静态文件目录。
* 类型: 字符串 (`string`)
* 默认值: `"/data/www"`
* 说明: 指定外置 Pages 服务使用的静态文件目录。
* **`[log]` - 日志配置**
* `logFilePath`: 日志文件路径。
* 类型: 字符串 (`string`)
* 默认值: `"/data/ghproxy/log/ghproxy.log"`
* 说明: 设置 `ghproxy` 日志文件的存储路径。
* `maxLogSize`: 最大日志文件大小。
* 类型: 整数 (`int`)
* 默认值: `5` (MB)
* 说明: 设置单个日志文件的最大大小,单位为 MB。当日志文件大小超过此限制时会进行日志轮转。
* `level`: 日志级别。
* 类型: 字符串 (`string`)
* 默认值: `"info"`
* 可选值: `"dump"`, `"debug"`, `"info"`, `"warn"`, `"error"`, `"none"`
* 说明: 设置日志输出的级别。级别越高,输出的日志信息越少。
* `"dump"`: 输出所有日志,包括最详细的调试信息。
* `"debug"`: 输出调试信息、信息、警告和错误日志。
* `"info"`: 输出信息、警告和错误日志。
* `"warn"`: 输出警告和错误日志。
* `"error"`: 仅输出错误日志。
* `"none"`: 禁用所有日志输出。
* `hertzLogPath`: `HertZ`日志文件路径。
* 类型: 字符串 (`string`)
* 默认值: `"/data/ghproxy/log/hertz.log"`
* 说明: 设置 `HertZ` 日志文件的存储路径。
* **`[auth]` - 认证配置**
* `enabled`: 是否启用认证。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,需要提供正确的认证信息才能访问 `ghproxy` 服务。
* `method`: 认证方法。
* 类型: 字符串 (`string`)
* 默认值: `"parameters"` (URL 参数)
* 可选值: `"header"` 或 `"parameters"`
* `"header"`: 通过请求头 `GH-Auth` 或自定义请求头 (通过 `key` 配置) 传递认证 Token。
* `"parameters"`: 通过 URL 参数 `auth_token` 或自定义 URL 参数名 (通过 `Key` 配置) 传递认证 Token。
* 说明: 选择认证信息传递的方式。
* `key`: 自定义认证 Key。
* 类型: 字符串 (`string`)
* 默认值: `""` (空字符串,使用默认的 `GH-Auth` 请求头或 `auth_token` URL 参数名)
* 说明: 可以自定义认证时使用的请求头名称或 URL 参数名。如果为空,则使用默认的 `GH-Auth` 请求头或 `auth_token` URL 参数名。
* `token`: 认证 Token。
* 类型: 字符串 (`string`)
* 默认值: `"token"`
* 说明: 设置认证时需要提供的 Token 值。
* `passThrough`: 是否认证参数透穿到Github。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (不允许)
* 说明: 如果设置为 `true`相关参数会被透穿到Github。
* `ForceAllowApi`: 是否强制允许 API 访问。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (不强制允许)
* 说明: 如果设置为 `true`,则强制允许对 GitHub API 的访问,即使未启用认证或认证失败。
* **`[blacklist]` - 黑名单配置**
* `enabled`: 是否启用黑名单。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 将根据 `blacklist.json` 文件中的规则阻止对特定用户或仓库的访问。
* `blacklistFile`: 黑名单文件路径。
* 类型: 字符串 (`string`)
* 默认值: `"/data/ghproxy/config/blacklist.json"`
* 说明: 指定黑名单配置文件的路径。
* **`[whitelist]` - 白名单配置**
* `enabled`: 是否启用白名单。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 将只允许访问 `whitelist.json` 文件中规则指定的用户或仓库。白名单的优先级高于黑名单。
* `whitelistFile`: 白名单文件路径。
* 类型: 字符串 (`string`)
* 默认值: `"/data/ghproxy/config/whitelist.json"`
* 说明: 指定白名单配置文件的路径。
* **`[rateLimit]` - 限速配置**
* `enabled`: 是否启用限速。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 将根据配置的策略限制请求速率,防止服务被滥用。
* `rateMethod`: 限速方法。
* 类型: 字符串 (`string`)
* 默认值: `"total"` (全局限速)
* 可选值: `"ip"` 或 `"total"`
* `"ip"`: 基于客户端 IP 地址进行限速,每个 IP 地址都有独立的速率限制。
* `"total"`: 全局限速,所有客户端共享同一个速率限制。
* 说明: 选择限速的策略。
* `ratePerMinute`: 每分钟允许的请求数。
* 类型: 整数 (`int`)
* 默认值: `180`
* 说明: 设置每分钟允许通过的最大请求数。
* `burst`: 突发请求数。
* 类型: 整数 (`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]` - 出站代理配置**
* `enabled`: 是否启用出站代理。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 将通过配置的代理服务器转发所有出站请求。
* `url`: 出站代理 URL。
* 类型: 字符串 (`string`)
* 默认值: `"socks5://127.0.0.1:1080"`
* 支持协议: `socks5://` 和 `http://`
* 说明: 设置出站代理服务器的 URL。支持 SOCKS5 和 HTTP 代理协议。
* **`[docker]` - Docker 镜像代理配置**
* `enabled`: 是否启用 Docker 镜像代理功能。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 当设置为 `true` 时,`ghproxy` 将尝试代理 Docker 镜像的下载请求,以加速从 GitHub Container Registry (GHCR) 或 Docker Hub 下载镜像。
* `target`: 代理的目标 Docker 注册表。
* 类型: 字符串 (`string`)
* 默认值: `"ghcr"` (代理 GHCR)
* 可选值: `"ghcr"` 或 `"dockerhub"`
* 说明: 指定要代理的 Docker 注册表。
* `"ghcr"`: 代理 GitHub Container Registry (ghcr.io)。
* `"dockerhub"`: 代理 Docker Hub (docker.io)。
* 自定义, 支持传入自定义target, 例如`"docker.example.com"`
## `blacklist.json` - 黑名单配置
`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。
```json name=config/blacklist.json
{
"blacklist": [
"eviluser",
"spamuser/bad-repo",
"malwareuser/*"
]
}
```
### 黑名单规则说明
* `blacklist`: 一个 JSON 数组,包含黑名单规则,每条规则为一个字符串。
* **用户名**: 例如 `"eviluser"`,阻止所有名为 `eviluser` 的用户的访问。
* **仓库名**: 例如 `"spamuser/bad-repo"`,阻止访问 `spamuser` 用户下的 `bad-repo` 仓库。
* **通配符**: 例如 `"malwareuser/*"`,使用 `*` 通配符,阻止访问 `malwareuser` 用户下的所有仓库。
* **缩略写法**: 例如 `"example"`, 等同于 `"example/*"` 允许访问 `example` 用户下的所有仓库。
## `whitelist.json` - 白名单配置
`whitelist.json` 文件用于配置白名单规则,只允许访问白名单中指定的用户或仓库。白名单的优先级高于黑名单,如果一个请求同时匹配黑名单和白名单,则白名单生效,请求将被允许。
```json name=config/whitelist.json
{
"whitelist": [
"white/list",
"white/test1",
"example/*",
"example"
]
}
```
### 白名单规则说明
* `whitelist`: 一个 JSON 数组,包含白名单规则,每条规则为一个字符串。
* **仓库名**: 例如 `"white/list"`,允许访问 `white` 用户下的 `list` 仓库。
* **仓库名**: 例如 `"white/test1"`,允许访问 `white` 用户下的 `test1` 仓库。
* **通配符**: 例如 `"example/*"`,使用 `*` 通配符,允许访问 `example` 用户下的所有仓库。
* **缩略写法**: 例如 `"example"`, 等同于 `"example/*"` 允许访问 `example` 用户下的所有仓库。
---

View File

@@ -1,26 +0,0 @@
# Flag
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
GHProxy接受以下flag传入
```bash
root@root:/data/ghproxy$ ghproxy -h
-c string
config file path (default "/data/ghproxy/config/config.toml")
-cfg value
exit
-h show help message and exit
-v show version and exit
```
- `-c`
类型: `string`
默认值: `/data/ghproxy/config/config.toml`
示例: `ghproxy -c /data/ghproxy/demo.toml`
- `-cfg`
已弃用, 被`-c`替代
- `-h`
显示帮助信息
- `-v`
显示版本号

View File

@@ -1,19 +0,0 @@
## GHProxy 文档
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
### 配置文件
https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md
### Flag
https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/flag.md
### 部署
参看 https://blog.wjqserver.com/post/ghproxy-deploy-with-smart-git/
### 前端
https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend

37
go.mod
View File

@@ -1,49 +1,26 @@
module ghproxy module ghproxy
go 1.24.3 go 1.24.4
require ( require (
github.com/BurntSushi/toml v1.5.0 github.com/BurntSushi/toml v1.5.0
github.com/WJQSERVER-STUDIO/httpc v0.5.1 github.com/WJQSERVER-STUDIO/httpc v0.7.0
github.com/WJQSERVER-STUDIO/logger v1.7.3
github.com/cloudwego/hertz v0.10.0
github.com/hertz-contrib/http2 v0.1.8
golang.org/x/net v0.41.0 golang.org/x/net v0.41.0
golang.org/x/time v0.12.0 golang.org/x/time v0.12.0
) )
require ( require (
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2
github.com/bytedance/sonic v1.13.3 github.com/fenthope/ikumi v0.0.2
github.com/fenthope/reco v0.0.3
github.com/fenthope/record v0.0.3
github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/infinite-iroha/touka v0.2.4
github.com/wjqserver/modembed v0.0.1 github.com/wjqserver/modembed v0.0.1
) )
require ( require (
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 // indirect github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 // indirect
github.com/bytedance/gopkg v0.1.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
github.com/cloudwego/netpoll v0.7.0 // indirect
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.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 github.com/valyala/bytebufferpool v1.0.0 // 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.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

158
go.sum
View File

@@ -4,159 +4,25 @@ github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKU
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc=
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc5f0DK8N9agIJCpM1Cqw2NE= 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/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/httpc v0.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE=
github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= github.com/fenthope/ikumi v0.0.2 h1:5oaSTf/Msp7M2O3o/X20omKWEQbFhX4KV0CVF21oCdk=
github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= github.com/fenthope/ikumi v0.0.2/go.mod h1:IYbxzOGndZv/yRrbVMyV6dxh06X2wXCbfxrTRM1IruU=
github.com/WJQSERVER-STUDIO/logger v1.7.3 h1:XoFJ1nBcZKyMvP4v0MZv5jL2q7IkAF7yfXgwyB3MLP4= github.com/fenthope/reco v0.0.3 h1:RmnQ0D9a8PWtwOODawitTe4BztTnS9wYwrDbipISNq4=
github.com/WJQSERVER-STUDIO/logger v1.7.3/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= github.com/fenthope/reco v0.0.3/go.mod h1:mDkGLHte5udWTIcjQTxrABRcf56SSdxBOCLgrRDwI/Y=
github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/fenthope/record v0.0.3 h1:v5urgs5LAkLMlljAT/MjW8fWuRHXPnAraTem5ui7rm4=
github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= github.com/fenthope/record v0.0.3/go.mod h1:KFEkSc4TDZ3QIhP/wglD32uYVA6X1OUcripiao1DEE4=
github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk=
github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4= github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk=
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=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
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.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=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
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 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 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/infinite-iroha/touka v0.2.4 h1:P1nmQYv4VEiTIahCw356VcFvpTFB9i11c31LeLh6WbM=
github.com/hertz-contrib/http2 v0.1.8/go.mod h1:m42hrl8fiTwE4p8c7JdRUZpkePEthvV89q3elL2GeD0= github.com/infinite-iroha/touka v0.2.4/go.mod h1:2MBPtsM+5ClIZ/E1mPEKx1Rb+KIndTwZlIa2CwRPV7A=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX+K940=
github.com/nyaruka/phonenumbers v1.6.1/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/wjqserver/modembed v0.0.1 h1:8ZDz7t9M5DLrUFlYgBUUmrMzxWsZPmHvOazkr/T2jEs= github.com/wjqserver/modembed v0.0.1 h1:8ZDz7t9M5DLrUFlYgBUUmrMzxWsZPmHvOazkr/T2jEs=
github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g9yyCztTXOQ= 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-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA=
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/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=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.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 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 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=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.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/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.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 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/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=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

354
main.go
View File

@@ -1,7 +1,6 @@
package main package main
import ( import (
"context"
"embed" "embed"
"flag" "flag"
"fmt" "fmt"
@@ -14,35 +13,28 @@ import (
"ghproxy/api" "ghproxy/api"
"ghproxy/auth" "ghproxy/auth"
"ghproxy/config" "ghproxy/config"
"ghproxy/middleware/loggin"
"ghproxy/proxy" "ghproxy/proxy"
"ghproxy/rate"
"ghproxy/weakcache" "ghproxy/weakcache"
"github.com/WJQSERVER-STUDIO/logger" "github.com/fenthope/ikumi"
"github.com/hertz-contrib/http2/factory" "github.com/fenthope/reco"
"github.com/fenthope/record"
"github.com/infinite-iroha/touka"
"github.com/wjqserver/modembed" "github.com/wjqserver/modembed"
"golang.org/x/time/rate"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/common/adaptor"
"github.com/cloudwego/hertz/pkg/common/hlog"
"github.com/cloudwego/hertz/pkg/network/standard"
_ "net/http/pprof" _ "net/http/pprof"
) )
var ( var (
cfg *config.Config cfg *config.Config
r *server.Hertz r *touka.Engine
configfile = "/data/ghproxy/config/config.toml" configfile = "/data/ghproxy/config/config.toml"
hertZfile *os.File hertZfile *os.File
cfgfile string cfgfile string
version string version string
runMode string runMode string
limiter *rate.RateLimiter
iplimiter *rate.IPRateLimiter
showVersion bool showVersion bool
showHelp bool showHelp bool
) )
@@ -57,12 +49,12 @@ var (
) )
var ( var (
logw = logger.Logw logger *reco.Logger
logDump = logger.LogDump logDump = logger.Debugf
logDebug = logger.LogDebug logDebug = logger.Debugf
logInfo = logger.LogInfo logInfo = logger.Infof
logWarning = logger.LogWarning logWarning = logger.Warnf
logError = logger.LogError logError = logger.Errorf
) )
func readFlag() { func readFlag() {
@@ -127,39 +119,28 @@ func loadConfig() {
func setupLogger(cfg *config.Config) { func setupLogger(cfg *config.Config) {
var err error var err error
if cfg.Log.Level == "" {
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize) cfg.Log.Level = "info"
}
recoLevel := reco.ParseLevel(cfg.Log.Level)
logger, err = reco.New(reco.Config{
Level: recoLevel,
Mode: reco.ModeText,
FilePath: cfg.Log.LogFilePath,
MaxFileSizeMB: cfg.Log.MaxLogSize,
EnableRotation: true,
Async: true,
})
if err != nil { if err != nil {
fmt.Printf("Failed to initialize logger: %v\n", err) fmt.Printf("Failed to initialize logger: %v\n", err)
os.Exit(1) os.Exit(1)
} }
err = logger.SetLogLevel(cfg.Log.Level) logger.SetLevel(recoLevel)
if err != nil {
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) fmt.Printf("Log Level: %s\n", cfg.Log.Level)
logDebug("Config File Path: ", cfgfile) logger.Debugf("Config File Path: %s", cfgfile)
logDebug("Loaded config: %v\n", cfg) logger.Debugf("Loaded config: %v", cfg)
logInfo("Logger Initialized Successfully") logger.Infof("Logger Initialized Successfully")
}
func setupHertZLogger(cfg *config.Config) {
var err error
if cfg.Log.HertZLogPath != "" {
hertZfile, err = os.OpenFile(cfg.Log.HertZLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
hlog.SetOutput(os.Stdout)
logWarning("Failed to open hertz log file: %v", err)
} else {
hlog.SetOutput(hertZfile)
}
hlog.SetLevel(hlog.LevelInfo)
}
} }
func setMemLimit(cfg *config.Config) { func setMemLimit(cfg *config.Config) {
@@ -170,23 +151,15 @@ func setMemLimit(cfg *config.Config) {
} }
func loadlist(cfg *config.Config) { func loadlist(cfg *config.Config) {
auth.Init(cfg) err := auth.ListInit(cfg)
} if err != nil {
logger.Errorf("Failed to initialize list: %v", err)
func setupApi(cfg *config.Config, r *server.Hertz, version string) {
api.InitHandleRouter(cfg, r, version)
}
func setupRateLimit(cfg *config.Config) {
if cfg.RateLimit.Enabled {
if cfg.RateLimit.RateMethod == "ip" {
iplimiter = rate.NewIPRateLimiter(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute)
} else if cfg.RateLimit.RateMethod == "total" {
limiter = rate.New(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute)
} else {
logError("Invalid RateLimit Method: %s", cfg.RateLimit.RateMethod)
}
} }
}
func setupApi(cfg *config.Config, r *touka.Engine, version string) {
api.InitHandleRouter(cfg, r, version)
} }
func InitReq(cfg *config.Config) { func InitReq(cfg *config.Config) {
@@ -241,7 +214,7 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
} }
// setupPages 设置页面路由 // setupPages 设置页面路由
func setupPages(cfg *config.Config, r *server.Hertz) { func setupPages(cfg *config.Config, r *touka.Engine) {
switch cfg.Pages.Mode { switch cfg.Pages.Mode {
case "internal": case "internal":
err := setInternalRoute(cfg, r) err := setInternalRoute(cfg, r)
@@ -252,21 +225,7 @@ func setupPages(cfg *config.Config, r *server.Hertz) {
} }
case "external": case "external":
// 设置外部资源路径 r.SetUnMatchFS(http.Dir(cfg.Pages.StaticDir))
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
javascriptsPath := fmt.Sprintf("%s/script.js", cfg.Pages.StaticDir)
stylesheetsPath := fmt.Sprintf("%s/style.css", cfg.Pages.StaticDir)
bootstrapPath := fmt.Sprintf("%s/bootstrap.min.css", cfg.Pages.StaticDir)
bootstrapBundlePath := fmt.Sprintf("%s/bootstrap.bundle.min.js", cfg.Pages.StaticDir)
// 设置外部资源路由
r.StaticFile("/", indexPagePath)
r.StaticFile("/favicon.ico", faviconPath)
r.StaticFile("/script.js", javascriptsPath)
r.StaticFile("/style.css", stylesheetsPath)
r.StaticFile("/bootstrap.min.css", bootstrapPath)
r.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath)
default: default:
// 处理无效的Pages Mode // 处理无效的Pages Mode
@@ -282,13 +241,24 @@ func setupPages(cfg *config.Config, r *server.Hertz) {
} }
} }
func pageCacheHeader() func(ctx context.Context, c *app.RequestContext) { var viaString string = "WJQSERVER-STUDIO/GHProxy"
return func(ctx context.Context, c *app.RequestContext) {
c.Header("Cache-Control", "public, max-age=3600, must-revalidate") func pageCacheHeader() func(c *touka.Context) {
return func(c *touka.Context) {
c.AddHeader("Cache-Control", "public, max-age=3600, must-revalidate")
c.Next()
} }
} }
func setInternalRoute(cfg *config.Config, r *server.Hertz) error { func viaHeader() func(c *touka.Context) {
return func(c *touka.Context) {
protoVersion := fmt.Sprintf("%d.%d", c.Request.ProtoMajor, c.Request.ProtoMinor)
c.AddHeader("Via", protoVersion+" "+viaString)
c.Next()
}
}
func setInternalRoute(cfg *config.Config, r *touka.Engine) error {
// 加载嵌入式资源 // 加载嵌入式资源
pages, assets, err := loadEmbeddedPages(cfg) pages, assets, err := loadEmbeddedPages(cfg)
@@ -296,69 +266,14 @@ func setInternalRoute(cfg *config.Config, r *server.Hertz) error {
logError("Failed when processing pages: %s", err) logError("Failed when processing pages: %s", err)
return err return err
} }
/*
// 设置嵌入式资源路由 r.HandleFunc([]string{"GET"}, "/favicon.ico", pageCacheHeader(), touka.FileServer(http.FS(assets)))
r.GET("/", func(ctx context.Context, c *app.RequestContext) { r.HandleFunc([]string{"GET"}, "/", pageCacheHeader(), touka.FileServer(http.FS(pages)))
staticServer := http.FileServer(http.FS(pages)) r.HandleFunc([]string{"GET"}, "/script.js", pageCacheHeader(), touka.FileServer(http.FS(pages)))
req, err := adaptor.GetCompatRequest(&c.Request) r.HandleFunc([]string{"GET"}, "/style.css", pageCacheHeader(), touka.FileServer(http.FS(pages)))
if err != nil { r.HandleFunc([]string{"GET"}, "/bootstrap.min.css", pageCacheHeader(), touka.FileServer(http.FS(assets)))
logError("%s", err) r.HandleFunc([]string{"GET"}, "/bootstrap.bundle.min.js", pageCacheHeader(), touka.FileServer(http.FS(assets)))
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 return nil
} }
@@ -381,11 +296,9 @@ func init() {
loadConfig() loadConfig()
if cfg != nil { // 在setupLogger前添加空值检查 if cfg != nil { // 在setupLogger前添加空值检查
setupLogger(cfg) setupLogger(cfg)
setupHertZLogger(cfg)
InitReq(cfg) InitReq(cfg)
setMemLimit(cfg) setMemLimit(cfg)
loadlist(cfg) loadlist(cfg)
setupRateLimit(cfg)
if cfg.Docker.Enabled { if cfg.Docker.Enabled {
wcache = proxy.InitWeakCache() wcache = proxy.InitWeakCache()
} }
@@ -406,98 +319,99 @@ func main() {
if showVersion || showHelp { if showVersion || showHelp {
return return
} }
logDebug("Run Mode: %s Netlib: %s", runMode, cfg.Server.NetLib)
if cfg == nil { if cfg == nil {
fmt.Println("Config not loaded, exiting.") fmt.Println("Config not loaded, exiting.")
return return
} }
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) r := touka.Default()
if cfg.Server.NetLib == "std" || cfg.Server.NetLib == "standard" || cfg.Server.NetLib == "net" || cfg.Server.NetLib == "net/http" { r.SetProtocols(&touka.ProtocolsConfig{
if cfg.Server.H2C { Http1: true,
r = server.New( Http2_Cleartext: true,
server.WithH2C(true), })
server.WithHostPorts(addr),
server.WithTransport(standard.NewTransporter),
)
r.AddProtocol("h2", factory.NewServerFactory())
} else {
r = server.New(
server.WithHostPorts(addr),
server.WithTransport(standard.NewTransporter),
)
}
} else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" {
if cfg.Server.H2C {
r = server.New(
server.WithH2C(true),
server.WithHostPorts(addr),
server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection),
)
r.AddProtocol("h2", factory.NewServerFactory())
} else {
r = server.New(
server.WithHostPorts(addr),
server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection),
)
}
} else {
logError("Invalid NetLib: %s", cfg.Server.NetLib)
fmt.Printf("Invalid NetLib: %s\n", cfg.Server.NetLib)
os.Exit(1)
}
r.Use(recovery.Recovery()) // Recovery中间件 r.Use(touka.Recovery()) // Recovery中间件
r.Use(loggin.Middleware()) // log中间件 r.SetLogger(logger)
r.Use(record.Middleware()) // log中间件
r.Use(viaHeader())
/*
r.Use(compress.Compression(compress.CompressOptions{
Algorithms: map[string]compress.AlgorithmConfig{
compress.EncodingGzip: {
Level: gzip.BestCompression, // Gzip最高压缩比
PoolEnabled: true, // 启用Gzip压缩器的对象池
},
compress.EncodingDeflate: {
Level: flate.DefaultCompression, // Deflate默认压缩比
PoolEnabled: false, // Deflate不启用对象池
},
compress.EncodingZstd: {
Level: int(zstd.SpeedBestCompression), // Zstandard最佳压缩比
PoolEnabled: true, // 启用Zstandard压缩器的对象池
},
},
}))
*/
if cfg.RateLimit.Enabled {
r.Use(ikumi.TokenRateLimit(ikumi.TokenRateLimiterOptions{
Limit: rate.Limit(cfg.RateLimit.RatePerMinute),
Burst: cfg.RateLimit.Burst,
}))
}
setupApi(cfg, r, version) setupApi(cfg, r, version)
setupPages(cfg, r) setupPages(cfg, r)
r.GET("/github.com/:user/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { r.GET("/github.com/:user/:repo/releases/*filepath", func(c *touka.Context) {
c.Set("matcher", "releases") c.Set("matcher", "releases")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) proxy.RoutingHandler(cfg)(c)
}) })
r.GET("/github.com/:user/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { r.GET("/github.com/:user/:repo/archive/*filepath", func(c *touka.Context) {
c.Set("matcher", "releases") c.Set("matcher", "releases")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) proxy.RoutingHandler(cfg)(c)
}) })
r.GET("/github.com/:user/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { r.GET("/github.com/:user/:repo/blob/*filepath", func(c *touka.Context) {
c.Set("matcher", "blob") c.Set("matcher", "blob")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) proxy.RoutingHandler(cfg)(c)
}) })
r.GET("/github.com/:user/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { r.GET("/github.com/:user/:repo/raw/*filepath", func(c *touka.Context) {
c.Set("matcher", "raw") c.Set("matcher", "raw")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) proxy.RoutingHandler(cfg)(c)
}) })
r.GET("/github.com/:user/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { r.GET("/github.com/:user/:repo/info/*filepath", func(c *touka.Context) {
c.Set("matcher", "clone") c.Set("matcher", "clone")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) proxy.RoutingHandler(cfg)(c)
}) })
r.GET("/github.com/:user/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { r.GET("/github.com/:user/:repo/git-upload-pack", func(c *touka.Context) {
c.Set("matcher", "clone") c.Set("matcher", "clone")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) proxy.RoutingHandler(cfg)(c)
})
r.POST("/github.com/:user/:repo/git-upload-pack", func(c *touka.Context) {
c.Set("matcher", "clone")
proxy.RoutingHandler(cfg)(c)
}) })
r.GET("/raw.githubusercontent.com/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { r.GET("/raw.githubusercontent.com/:user/:repo/*filepath", func(c *touka.Context) {
c.Set("matcher", "raw") c.Set("matcher", "raw")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) proxy.RoutingHandler(cfg)(c)
}) })
r.GET("/gist.githubusercontent.com/:user/*filepath", func(ctx context.Context, c *app.RequestContext) { r.GET("/gist.githubusercontent.com/:user/*filepath", func(c *touka.Context) {
c.Set("matcher", "gist") c.Set("matcher", "gist")
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) proxy.NoRouteHandler(cfg)(c)
}) })
r.GET("/api.github.com/repos/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { r.ANY("/api.github.com/repos/:user/:repo/*filepath", func(c *touka.Context) {
c.Set("matcher", "api") c.Set("matcher", "api")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) proxy.RoutingHandler(cfg)(c)
}) })
r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) { r.GET("/v2/", func(c *touka.Context) {
emptyJSON := "{}" emptyJSON := "{}"
c.Header("Content-Type", "application/json") c.Header("Content-Type", "application/json")
c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) c.Header("Content-Length", fmt.Sprint(len(emptyJSON)))
@@ -505,26 +419,27 @@ func main() {
c.Header("Docker-Distribution-API-Version", "registry/2.0") c.Header("Docker-Distribution-API-Version", "registry/2.0")
c.Status(200) c.Status(200)
c.Write([]byte(emptyJSON)) c.Writer.Write([]byte(emptyJSON))
}) })
r.Any("/v2/:target/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { r.ANY("/v2/:target/:user/:repo/*filepath", func(c *touka.Context) {
proxy.GhcrWithImageRouting(cfg)(ctx, c) proxy.GhcrWithImageRouting(cfg)(c)
}) })
/* /*
r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) { r.Any("/v2/:target/*filepath", func( c *touka.Context) {
proxy.GhcrRouting(cfg)(ctx, c) proxy.GhcrRouting(cfg)(c)
}) })
*/ */
r.NoRoute(func(ctx context.Context, c *app.RequestContext) { r.NoRoute(func(c *touka.Context) {
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) proxy.NoRouteHandler(cfg)(c)
}) })
fmt.Printf("GHProxy Version: %s\n", version) fmt.Printf("GHProxy Version: %s\n", version)
fmt.Printf("A Go Based High-Performance Github Proxy \n") fmt.Printf("A Go Based High-Performance Github Proxy \n")
fmt.Printf("Made by WJQSERVER-STUDIO\n") fmt.Printf("Made by WJQSERVER-STUDIO\n")
fmt.Printf("Power by Touka\n")
if cfg.Server.Debug { if cfg.Server.Debug {
go func() { go func() {
@@ -536,16 +451,13 @@ func main() {
} }
defer logger.Close() defer logger.Close()
defer func() {
if hertZfile != nil {
err := hertZfile.Close()
if err != nil {
logError("Failed to close hertz log file: %v", err)
}
}
}()
r.Spin() addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
err := r.RunShutdown(addr)
if err != nil {
logError("Server Run Error: %v", err)
fmt.Printf("Server Run Error: %v\n", err)
}
fmt.Println("Program Exit") fmt.Println("Program Exit")
} }

View File

@@ -1,32 +0,0 @@
package loggin
import (
"context"
"time"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/cloudwego/hertz/pkg/app"
)
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
// 日志中间件
func Middleware() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
startTime := time.Now()
c.Next(ctx)
endTime := time.Now()
timingResults := endTime.Sub(startTime)
logInfo("%s %s %s %s %s %d %v ", c.ClientIP(), c.Method(), c.Request.Header.GetProtocol(), string(c.Path()), c.Request.Header.UserAgent(), c.Response.StatusCode(), timingResults)
}
}

View File

@@ -1,17 +1,15 @@
package nocache package nocache
import ( import (
"context" "github.com/infinite-iroha/touka"
"github.com/cloudwego/hertz/pkg/app"
) )
func NoCacheMiddleware() app.HandlerFunc { func NoCacheMiddleware() touka.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) { return func(c *touka.Context) {
// 设置禁止缓存的响应头 // 设置禁止缓存的响应头
c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate") c.SetHeader("Cache-Control", "no-store, no-cache, must-revalidate")
c.Response.Header.Set("Pragma", "no-cache") c.SetHeader("Pragma", "no-cache")
c.Response.Header.Set("Expires", "0") c.SetHeader("Expires", "0")
c.Next(ctx) // 继续处理请求 c.Next() // 继续处理请求
} }
} }

View File

@@ -34,7 +34,7 @@ func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, er
trimmedPair := strings.TrimSpace(pair) trimmedPair := strings.TrimSpace(pair)
keyValue := strings.SplitN(trimmedPair, "=", 2) keyValue := strings.SplitN(trimmedPair, "=", 2)
if len(keyValue) != 2 { if len(keyValue) != 2 {
logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue) //logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue)
continue continue
} }
key := strings.TrimSpace(keyValue[0]) key := strings.TrimSpace(keyValue[0])

View File

@@ -4,20 +4,19 @@ import (
"ghproxy/config" "ghproxy/config"
"net/http" "net/http"
"github.com/cloudwego/hertz/pkg/app" "github.com/infinite-iroha/touka"
) )
func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Request) { func AuthPassThrough(c *touka.Context, cfg *config.Config, req *http.Request) {
if cfg.Auth.PassThrough { if cfg.Auth.PassThrough {
token := c.Query("token") token := c.Query("token")
if token != "" { if token != "" {
logDebug("%s %s %s %s %s Auth-PassThrough: token %s", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol(), token)
switch cfg.Auth.Method { switch cfg.Auth.Method {
case "parameters": case "parameters":
if !cfg.Auth.Enabled { if !cfg.Auth.Enabled {
req.Header.Set("Authorization", "token "+token) req.Header.Set("Authorization", "token "+token)
} else { } else {
logWarning("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol()) c.Warnf("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto)
ErrorPage(c, NewErrorWithStatusLookup(500, "Conflict Auth Method")) ErrorPage(c, NewErrorWithStatusLookup(500, "Conflict Auth Method"))
return return
} }
@@ -26,7 +25,7 @@ func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Reques
req.Header.Set("Authorization", "token "+token) req.Header.Set("Authorization", "token "+token)
} }
default: default:
logWarning("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol()) c.Warnf("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto)
ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid Auth Method / Auth Method is not be set")) ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid Auth Method / Auth Method is not be set"))
return return
} }

View File

@@ -15,7 +15,6 @@ var (
func UnDefiendRateStringErrHandle(err error) error { func UnDefiendRateStringErrHandle(err error) error {
if errors.Is(err, &limitreader.UnDefiendRateStringErr{}) { if errors.Is(err, &limitreader.UnDefiendRateStringErr{}) {
logWarning("UnDefiendRateStringErr: %s", err)
return nil return nil
} }
return err return err
@@ -28,18 +27,15 @@ func SetGlobalRateLimit(cfg *config.Config) error {
var totalBurst rate.Limit var totalBurst rate.Limit
totalLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalLimit) totalLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalLimit)
if UnDefiendRateStringErrHandle(err) != nil { if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse total bandwidth limit: %v", err)
return err return err
} }
totalBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalBurst) totalBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalBurst)
if UnDefiendRateStringErrHandle(err) != nil { if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse total bandwidth burst: %v", err)
return err return err
} }
limitreader.SetGlobalRateLimit(totalLimit, int(totalBurst)) limitreader.SetGlobalRateLimit(totalLimit, int(totalBurst))
err = SetBandwidthLimit(cfg) err = SetBandwidthLimit(cfg)
if UnDefiendRateStringErrHandle(err) != nil { if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to set bandwidth limit: %v", err)
return err return err
} }
} else { } else {
@@ -52,12 +48,10 @@ func SetBandwidthLimit(cfg *config.Config) error {
var err error var err error
bandwidthLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleLimit) bandwidthLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleLimit)
if UnDefiendRateStringErrHandle(err) != nil { if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse bandwidth limit: %v", err)
return err return err
} }
bandwidthBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleBurst) bandwidthBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleBurst)
if UnDefiendRateStringErrHandle(err) != nil { if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse bandwidth burst: %v", err)
return err return err
} }
return nil return nil

View File

@@ -9,10 +9,10 @@ import (
"strconv" "strconv"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader" "github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app" "github.com/infinite-iroha/touka"
) )
func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { func ChunkedProxyRequest(ctx context.Context, c *touka.Context, u string, cfg *config.Config, matcher string) {
var ( var (
req *http.Request req *http.Request
@@ -25,14 +25,14 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
if resp != nil && resp.Body != nil { if resp != nil && resp.Body != nil {
resp.Body.Close() resp.Body.Close()
} }
if req != nil { if req != nil && req.Body != nil {
req.Body.Close() req.Body.Close()
} }
}() }()
rb := client.NewRequestBuilder(string(c.Request.Method()), u) rb := client.NewRequestBuilder(c.Request.Method, u)
rb.NoDefaultHeaders() rb.NoDefaultHeaders()
rb.SetBody(c.Request.BodyStream()) rb.SetBody(c.Request.Body)
rb.WithContext(ctx) rb.WithContext(ctx)
req, err = rb.Build() req, err = rb.Build()
@@ -56,6 +56,23 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
return return
} }
// 处理302情况
if resp.StatusCode == 302 || resp.StatusCode == 301 {
//c.Debugf("resp header %s", resp.Header)
finalURL := resp.Header.Get("Location")
if finalURL != "" {
err = resp.Body.Close()
if err != nil {
c.Errorf("Failed to close response body: %v", err)
}
c.Infof("Internal Redirecting to %s", finalURL)
ChunkedProxyRequest(ctx, c, finalURL, cfg, matcher)
return
}
}
// 处理响应体大小限制
var ( var (
bodySize int bodySize int
contentLength string contentLength string
@@ -67,28 +84,25 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
var err error var err error
bodySize, err = strconv.Atoi(contentLength) bodySize, err = strconv.Atoi(contentLength)
if err != nil { if err != nil {
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err) c.Warnf("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, err)
bodySize = -1 bodySize = -1
} }
if err == nil && bodySize > sizelimit { if err == nil && bodySize > sizelimit {
finalURL := resp.Request.URL.String() finalURL := resp.Request.URL.String()
err = resp.Body.Close() err = resp.Body.Close()
if err != nil { if err != nil {
logError("Failed to close response body: %v", err) c.Errorf("Failed to close response body: %v", err)
} }
c.Redirect(301, []byte(finalURL)) c.Redirect(301, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize) c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, bodySize)
return return
} }
} }
// 复制响应头,排除需要移除的 header // 复制响应头,排除需要移除的 header
for key, values := range resp.Header { c.SetHeaders(resp.Header)
if _, shouldRemove := respHeadersToRemove[key]; !shouldRemove { for key := range respHeadersToRemove {
for _, value := range values { c.DelHeader(key)
c.Header(key, value)
}
}
} }
switch cfg.Server.Cors { switch cfg.Server.Cors {
@@ -110,6 +124,8 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
} }
defer bodyReader.Close()
if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor {
// 判断body是不是gzip // 判断body是不是gzip
var compress string var compress string
@@ -117,25 +133,26 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
compress = "gzip" compress = "gzip"
} }
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.Debugf("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), c.Request.Method, u, c.UserAgent(), c.Request.Proto)
c.Header("Content-Length", "") c.Header("Content-Length", "")
var reader io.Reader var reader io.Reader
reader, _, err = processLinks(bodyReader, compress, string(c.Request.Host()), cfg) reader, _, err = processLinks(bodyReader, compress, c.Request.Host, cfg, c)
c.SetBodyStream(reader, -1) c.WriteStream(reader)
if err != nil { if err != nil {
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) c.Errorf("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method, u, c.UserAgent(), c.Request.Proto, err)
ErrorPage(c, NewErrorWithStatusLookup(500, fmt.Sprintf("Failed to copy response body: %v", err))) ErrorPage(c, NewErrorWithStatusLookup(500, fmt.Sprintf("Failed to copy response body: %v", err)))
return return
} }
} else { } else {
if contentLength != "" { if contentLength != "" {
c.SetBodyStream(bodyReader, bodySize) c.SetHeader("Content-Length", contentLength)
c.WriteStream(bodyReader)
return return
} }
c.SetBodyStream(bodyReader, -1) c.WriteStream(bodyReader)
} }
} }

View File

@@ -7,6 +7,7 @@ package proxy
import ( import (
"ghproxy/config" "ghproxy/config"
"log"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@@ -24,7 +25,8 @@ func initTransport(cfg *config.Config, transport *http.Transport) {
// 如果代理 URL 未设置,使用环境变量中的代理配置 // 如果代理 URL 未设置,使用环境变量中的代理配置
if cfg.Outbound.Url == "" { if cfg.Outbound.Url == "" {
transport.Proxy = http.ProxyFromEnvironment transport.Proxy = http.ProxyFromEnvironment
logWarning("Outbound proxy is not set, using environment variables") //logWarning("Outbound proxy is not set, using environment variables")
log.Printf("Outbound proxy is not set, using environment variables")
return return
} }
@@ -32,7 +34,7 @@ func initTransport(cfg *config.Config, transport *http.Transport) {
proxyInfo, err := url.Parse(cfg.Outbound.Url) proxyInfo, err := url.Parse(cfg.Outbound.Url)
if err != nil { if err != nil {
// 如果解析失败,记录错误日志并使用环境变量中的代理配置 // 如果解析失败,记录错误日志并使用环境变量中的代理配置
logError("Failed to parse outbound proxy URL %v", err) log.Printf("Failed to parse outbound proxy URL %v", err)
transport.Proxy = http.ProxyFromEnvironment transport.Proxy = http.ProxyFromEnvironment
return return
} }
@@ -41,7 +43,7 @@ func initTransport(cfg *config.Config, transport *http.Transport) {
switch strings.ToLower(proxyInfo.Scheme) { switch strings.ToLower(proxyInfo.Scheme) {
case "http", "https": // 如果是 HTTP/HTTPS 代理 case "http", "https": // 如果是 HTTP/HTTPS 代理
transport.Proxy = http.ProxyURL(proxyInfo) // 设置 HTTP(S) 代理 transport.Proxy = http.ProxyURL(proxyInfo) // 设置 HTTP(S) 代理
logInfo("Using HTTP(S) proxy: %s", proxyInfo.Redacted()) log.Printf("Using HTTP(S) proxy: %s", cfg.Outbound.Url)
case "socks5": // 如果是 SOCKS5 代理 case "socks5": // 如果是 SOCKS5 代理
// 调用 newProxyDial 创建 SOCKS5 代理拨号器 // 调用 newProxyDial 创建 SOCKS5 代理拨号器
proxyDialer := newProxyDial(cfg.Outbound.Url) proxyDialer := newProxyDial(cfg.Outbound.Url)
@@ -53,11 +55,14 @@ func initTransport(cfg *config.Config, transport *http.Transport) {
} else { } else {
// 如果不支持 ContextDialer则回退到传统的 Dial 方法 // 如果不支持 ContextDialer则回退到传统的 Dial 方法
transport.Dial = proxyDialer.Dial transport.Dial = proxyDialer.Dial
logWarning("SOCKS5 dialer does not support ContextDialer, using legacy Dial") //logWarning("SOCKS5 dialer does not support ContextDialer, using legacy Dial")
log.Printf("SOCKS5 dialer does not support ContextDialer, using legacy Dial")
} }
logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url) //logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url)
log.Printf("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url)
default: // 如果代理协议不支持 default: // 如果代理协议不支持
logError("Unsupported proxy scheme: %s", proxyInfo.Scheme) //logError("Unsupported proxy scheme: %s", proxyInfo.Scheme)
log.Printf("Unsupported proxy scheme: %s", proxyInfo.Scheme)
transport.Proxy = http.ProxyFromEnvironment // 回退到环境变量代理 transport.Proxy = http.ProxyFromEnvironment // 回退到环境变量代理
} }
} }
@@ -77,13 +82,15 @@ func newProxyDial(proxyUrls string) proxy.Dialer {
urlInfo, err := url.Parse(proxyUrl) urlInfo, err := url.Parse(proxyUrl)
if err != nil { if err != nil {
// 如果 URL 解析失败,记录错误日志并跳过 // 如果 URL 解析失败,记录错误日志并跳过
logError("Failed to parse proxy URL %q: %v", proxyUrl, err) //logError("Failed to parse proxy URL %q: %v", proxyUrl, err)
log.Printf("Failed to parse proxy URL %q: %v", proxyUrl, err)
continue continue
} }
// 检查代理协议是否为 SOCKS5 // 检查代理协议是否为 SOCKS5
if urlInfo.Scheme != "socks5" { if urlInfo.Scheme != "socks5" {
logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme) // logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme)
log.Printf("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme)
continue continue
} }
@@ -94,7 +101,8 @@ func newProxyDial(proxyUrls string) proxy.Dialer {
dialer, err := createSocksDialer(urlInfo.Host, auth, proxyDialer) dialer, err := createSocksDialer(urlInfo.Host, auth, proxyDialer)
if err != nil { if err != nil {
// 如果创建失败,记录错误日志并跳过 // 如果创建失败,记录错误日志并跳过
logError("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err) //logError("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err)
log.Printf("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err)
continue continue
} }

View File

@@ -2,9 +2,10 @@ package proxy
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
json "github.com/bytedance/sonic" "github.com/infinite-iroha/touka"
"ghproxy/config" "ghproxy/config"
"ghproxy/weakcache" "ghproxy/weakcache"
@@ -14,7 +15,6 @@ import (
"strings" "strings"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader" "github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app"
) )
var ( var (
@@ -35,8 +35,8 @@ func InitWeakCache() *weakcache.Cache[string] {
return cache return cache
} }
func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc { func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) { return func(c *touka.Context) {
charToFind := '.' charToFind := '.'
reqTarget := c.Param("target") reqTarget := c.Param("target")
@@ -57,7 +57,7 @@ func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
target = reqTarget target = reqTarget
} }
} else { } else {
path = string(c.Request.RequestURI()) path = c.GetRequestURI()
reqImageUser = c.Param("target") reqImageUser = c.Param("target")
reqImageName = c.Param("user") reqImageName = c.Param("user")
} }
@@ -67,24 +67,25 @@ func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName), Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
} }
GhcrToTarget(ctx, c, cfg, target, path, image) GhcrToTarget(c, cfg, target, path, image)
} }
} }
func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) { func GhcrToTarget(c *touka.Context, cfg *config.Config, target string, path string, image *imageInfo) {
if cfg.Docker.Enabled { if cfg.Docker.Enabled {
var ctx = c.Request.Context()
if target != "" { if target != "" {
GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+string(c.Request.QueryString()), image, cfg, target) GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+c.GetReqQueryString(), image, cfg, target)
} else { } else {
if cfg.Docker.Target == "ghcr" { if cfg.Docker.Target == "ghcr" {
GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget) GhcrRequest(ctx, c, "https://"+ghcrTarget+c.GetRequestURI(), image, cfg, ghcrTarget)
} else if cfg.Docker.Target == "dockerhub" { } else if cfg.Docker.Target == "dockerhub" {
GhcrRequest(ctx, c, "https://"+dockerhubTarget+string(c.Request.RequestURI()), image, cfg, dockerhubTarget) GhcrRequest(ctx, c, "https://"+dockerhubTarget+c.GetRequestURI(), image, cfg, dockerhubTarget)
} else if cfg.Docker.Target != "" { } else if cfg.Docker.Target != "" {
// 自定义taget // 自定义taget
GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), image, cfg, cfg.Docker.Target) GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+c.GetRequestURI(), image, cfg, cfg.Docker.Target)
} else { } else {
// 配置为空 // 配置为空
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set")) ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set"))
@@ -98,10 +99,10 @@ func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config
} }
} }
func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *imageInfo, cfg *config.Config, target string) { func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageInfo, cfg *config.Config, target string) {
var ( var (
method []byte method string
req *http.Request req *http.Request
resp *http.Response resp *http.Response
err error err error
@@ -117,11 +118,11 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
} }
}() }()
method = c.Request.Method() method = c.Request.Method
rb := ghcrclient.NewRequestBuilder(string(method), u) rb := ghcrclient.NewRequestBuilder(method, u)
rb.NoDefaultHeaders() rb.NoDefaultHeaders()
rb.SetBody(c.Request.BodyStream()) rb.SetBody(c.Request.Body)
rb.WithContext(ctx) rb.WithContext(ctx)
req, err = rb.Build() req, err = rb.Build()
@@ -130,17 +131,18 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
return return
} }
c.Request.Header.VisitAll(func(key, value []byte) { //c.Request.Header.VisitAll(func(key, value []byte) {
headerKey := string(key) // headerKey := string(key)
headerValue := string(value) // headerValue := string(value)
req.Header.Add(headerKey, headerValue) // req.Header.Add(headerKey, headerValue)
}) //})
copyHeader(c.Request.Header, req.Header)
req.Header.Set("Host", target) req.Header.Set("Host", target)
if image != nil { if image != nil {
token, exist := cache.Get(image.Image) token, exist := cache.Get(image.Image)
if exist { if exist {
logDebug("Use Cache Token: %s", token) c.Debugf("Use Cache Token: %s", token)
req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Authorization", "Bearer "+token)
} }
} }
@@ -154,7 +156,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
// 处理状态码 // 处理状态码
if resp.StatusCode == 401 { if resp.StatusCode == 401 {
// 请求target /v2/路径 // 请求target /v2/路径
if string(c.Request.URI().Path()) != "/v2/" { if string(c.GetRequestURIPath()) != "/v2/" {
resp.Body.Close() resp.Body.Close()
if image == nil { if image == nil {
ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized")) ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized"))
@@ -164,13 +166,13 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
// 更新kv // 更新kv
if token != "" { if token != "" {
logDump("Update Cache Token: %s", token) c.Debugf("Update Cache Token: %s", token)
cache.Put(image.Image, token) cache.Put(image.Image, token)
} }
rb := ghcrclient.NewRequestBuilder(string(method), u) rb := ghcrclient.NewRequestBuilder(string(method), u)
rb.NoDefaultHeaders() rb.NoDefaultHeaders()
rb.SetBody(c.Request.BodyStream()) rb.SetBody(c.Request.Body)
rb.WithContext(ctx) rb.WithContext(ctx)
req, err = rb.Build() req, err = rb.Build()
@@ -178,12 +180,14 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return return
} }
/*
c.Request.Header.VisitAll(func(key, value []byte) { c.Request.Header.VisitAll(func(key, value []byte) {
headerKey := string(key) headerKey := string(key)
headerValue := string(value) headerValue := string(value)
req.Header.Add(headerKey, headerValue) req.Header.Add(headerKey, headerValue)
}) })
*/
copyHeader(c.Request.Header, req.Header)
req.Header.Set("Host", target) req.Header.Set("Host", target)
if token != "" { if token != "" {
@@ -214,27 +218,30 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
var err error var err error
bodySize, err = strconv.Atoi(contentLength) bodySize, err = strconv.Atoi(contentLength)
if err != nil { if err != nil {
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err) c.Warnf("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, err)
bodySize = -1 bodySize = -1
} }
if err == nil && bodySize > sizelimit { if err == nil && bodySize > sizelimit {
finalURL := resp.Request.URL.String() finalURL := resp.Request.URL.String()
err = resp.Body.Close() err = resp.Body.Close()
if err != nil { if err != nil {
logError("Failed to close response body: %v", err) c.Errorf("Failed to close response body: %v", err)
} }
c.Redirect(301, []byte(finalURL)) c.Redirect(301, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize) c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, bodySize)
return return
} }
} }
// 复制响应头,排除需要移除的 header // 复制响应头,排除需要移除的 header
for key, values := range resp.Header { /*
for _, value := range values { for key, values := range resp.Header {
c.Response.Header.Add(key, value) for _, value := range values {
c.Response.Header.Add(key, value)
}
} }
} */
c.SetHeaders(resp.Header)
c.Status(resp.StatusCode) c.Status(resp.StatusCode)
@@ -256,7 +263,7 @@ type AuthToken struct {
Token string `json:"token"` Token string `json:"token"`
} }
func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) { func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka.Context) (token string) {
var resp401 *http.Response var resp401 *http.Response
var req401 *http.Request var req401 *http.Request
var err error var err error
@@ -280,7 +287,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R
defer resp401.Body.Close() defer resp401.Body.Close()
bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate")) bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
if err != nil { if err != nil {
logError("Failed to parse Www-Authenticate header: %v", err) c.Errorf("Failed to parse Www-Authenticate header: %v", err)
return return
} }
@@ -296,13 +303,13 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R
getAuthReq, err := getAuthRB.Build() getAuthReq, err := getAuthRB.Build()
if err != nil { if err != nil {
logError("Failed to create request: %v", err) c.Errorf("Failed to create request: %v", err)
return return
} }
authResp, err := ghcrclient.Do(getAuthReq) authResp, err := ghcrclient.Do(getAuthReq)
if err != nil { if err != nil {
logError("Failed to send request: %v", err) c.Errorf("Failed to send request: %v", err)
return return
} }
@@ -310,7 +317,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R
bodyBytes, err := io.ReadAll(authResp.Body) bodyBytes, err := io.ReadAll(authResp.Body)
if err != nil { if err != nil {
logError("Failed to read auth response body: %v", err) c.Errorf("Failed to read auth response body: %v", err)
return return
} }
@@ -318,7 +325,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R
var authToken AuthToken var authToken AuthToken
err = json.Unmarshal(bodyBytes, &authToken) err = json.Unmarshal(bodyBytes, &authToken)
if err != nil { if err != nil {
logError("Failed to decode auth response body: %v", err) c.Errorf("Failed to decode auth response body: %v", err)
return return
} }
token = authToken.Token token = authToken.Token

View File

@@ -11,24 +11,13 @@ import (
"html/template" "html/template"
"io/fs" "io/fs"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/cloudwego/hertz/pkg/app"
lru "github.com/hashicorp/golang-lru/v2" lru "github.com/hashicorp/golang-lru/v2"
"github.com/infinite-iroha/touka"
) )
// 日志模块 func HandleError(c *touka.Context, message string) {
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
func HandleError(c *app.RequestContext, message string) {
ErrorPage(c, NewErrorWithStatusLookup(500, message)) ErrorPage(c, NewErrorWithStatusLookup(500, message))
logError("Error handled: %s", message) c.Errorf("%s %s %s %s %s Error: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, message)
} }
type GHProxyErrors struct { type GHProxyErrors struct {
@@ -131,18 +120,18 @@ type ErrorPageData struct {
// ToCacheKey 为 ErrorPageData 生成一个唯一的 SHA256 字符串键。 // ToCacheKey 为 ErrorPageData 生成一个唯一的 SHA256 字符串键。
// 使用 gob 序列化来确保结构体内容到字节序列的顺序一致性,然后计算哈希。 // 使用 gob 序列化来确保结构体内容到字节序列的顺序一致性,然后计算哈希。
func (d ErrorPageData) ToCacheKey() string { func (d ErrorPageData) ToCacheKey() (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
enc := gob.NewEncoder(&buf) enc := gob.NewEncoder(&buf)
err := enc.Encode(d) err := enc.Encode(d)
if err != nil { if err != nil {
logError("Failed to gob encode ErrorPageData for cache key: %v", err) //logError("Failed to gob encode ErrorPageData for cache key: %v", err)
return "" return "", fmt.Errorf("failed to gob encode ErrorPageData for cache key: %w", err)
} }
hasher := sha256.New() hasher := sha256.New()
hasher.Write(buf.Bytes()) hasher.Write(buf.Bytes())
return hex.EncodeToString(hasher.Sum(nil)) return hex.EncodeToString(hasher.Sum(nil)), nil
} }
func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData { func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData {
@@ -184,7 +173,7 @@ func NewSizedLRUCache(maxBytes int64) (*SizedLRUCache, error) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.currentBytes -= int64(len(value)) c.currentBytes -= int64(len(value))
logDebug("LRU evicted key: %s, size: %d, current total: %d", key, len(value), c.currentBytes) //logDebug("LRU evicted key: %s, size: %d, current total: %d", key, len(value), c.currentBytes)
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -206,7 +195,7 @@ func (c *SizedLRUCache) Add(key string, value []byte) {
// 如果待添加的条目本身就大于缓存的最大容量,则不进行缓存。 // 如果待添加的条目本身就大于缓存的最大容量,则不进行缓存。
if itemSize > c.maxBytes { if itemSize > c.maxBytes {
logWarning("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes) //c.Warnf("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes)
return return
} }
@@ -214,23 +203,23 @@ func (c *SizedLRUCache) Add(key string, value []byte) {
if oldVal, ok := c.cache.Get(key); ok { if oldVal, ok := c.cache.Get(key); ok {
c.currentBytes -= int64(len(oldVal)) c.currentBytes -= int64(len(oldVal))
c.cache.Remove(key) c.cache.Remove(key)
logDebug("Key %s exists, removed old size %d. Current total: %d", key, len(oldVal), c.currentBytes) //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 { for c.currentBytes+itemSize > c.maxBytes && c.cache.Len() > 0 {
_, oldVal, existed := c.cache.RemoveOldest() _, _, existed := c.cache.RemoveOldest()
if !existed { if !existed {
logWarning("Attempted to remove oldest, but item not found.") //c.Warnf("Attempted to remove oldest, but item not found.")
break break
} }
logDebug("Proactively evicted item (size %d) to free space. Current total: %d", len(oldVal), c.currentBytes) //logDebug("Proactively evicted item (size %d) to free space. Current total: %d", len(oldVal), c.currentBytes)
} }
// 添加新条目到内部 LRU 缓存。 // 添加新条目到内部 LRU 缓存。
c.cache.Add(key, value) c.cache.Add(key, value)
c.currentBytes += itemSize // 手动增加新条目的大小到 currentBytes。 c.currentBytes += itemSize // 手动增加新条目的大小到 currentBytes。
logDebug("Item added: key %s, size: %d, current total: %d", key, itemSize, c.currentBytes) //logDebug("Item added: key %s, size: %d, current total: %d", key, itemSize, c.currentBytes)
} }
const maxErrorPageCacheBytes = 512 * 1024 // 错误页面缓存的最大容量512KB const maxErrorPageCacheBytes = 512 * 1024 // 错误页面缓存的最大容量512KB
@@ -242,7 +231,7 @@ func init() {
var err error var err error
errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes) errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes)
if err != nil { if err != nil {
logError("Failed to initialize error page LRU cache: %v", err) // logError("Failed to initialize error page LRU cache: %v", err)
panic(err) panic(err)
} }
} }
@@ -293,37 +282,50 @@ func htmlTemplateRender(data interface{}) ([]byte, error) {
return buf.Bytes(), nil return buf.Bytes(), nil
} }
func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { func ErrorPage(c *touka.Context, errInfo *GHProxyErrors) {
// 将 errInfo 转换为 ErrorPageData 结构体 // 将 errInfo 转换为 ErrorPageData 结构体
var err error
var cacheKey string
pageDataStruct := ErrPageUnwarper(errInfo) pageDataStruct := ErrPageUnwarper(errInfo)
// 使用 ErrorPageData 生成一个唯一的 SHA256 缓存键 // 使用 ErrorPageData 生成一个唯一的 SHA256 缓存键
cacheKey := pageDataStruct.ToCacheKey() cacheKey, err = pageDataStruct.ToCacheKey()
if err != nil {
c.Warnf("Failed to generate cache key for error page: %v", err)
fallbackErrorJson(c, errInfo)
return
}
// 检查生成的缓存键是否为空,这可能表示序列化或哈希计算失败
if cacheKey == "" { if cacheKey == "" {
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
logWarning("Failed to generate cache key for error page: %v", errInfo) c.Warnf("Failed to generate cache key for error page: %v", errInfo)
return return
} }
var pageData []byte var pageData []byte
var err error
// 尝试从缓存中获取页面数据 // 尝试从缓存中获取页面数据
if cachedPage, found := errorPageCache.Get(cacheKey); found { if cachedPage, found := errorPageCache.Get(cacheKey); found {
pageData = cachedPage pageData = cachedPage
logDebug("Serving error page from cache (Key: %s)", cacheKey) c.Debugf("Serving error page from cache (Key: %s)", cacheKey)
} else { } else {
// 如果不在缓存中,则渲染页面 // 如果不在缓存中,则渲染页面
pageData, err = htmlTemplateRender(pageDataStruct) pageData, err = htmlTemplateRender(pageDataStruct)
if err != nil { if err != nil {
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) 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) c.Warnf("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err)
return return
} }
// 将渲染结果存入缓存 // 将渲染结果存入缓存
errorPageCache.Add(cacheKey, pageData) errorPageCache.Add(cacheKey, pageData)
logDebug("Cached error page (Key: %s, Size: %d bytes)", cacheKey, len(pageData)) c.Debugf("Cached error page (Key: %s, Size: %d bytes)", cacheKey, len(pageData))
} }
c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData) c.Raw(errInfo.StatusCode, "text/html; charset=utf-8", pageData)
}
func fallbackErrorJson(c *touka.Context, errInfo *GHProxyErrors) {
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
} }

View File

@@ -1,7 +1,6 @@
package proxy package proxy
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
@@ -9,13 +8,12 @@ import (
"strconv" "strconv"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader" "github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app" "github.com/infinite-iroha/touka"
) )
func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) { func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, mode string) {
var ( var (
req *http.Request
resp *http.Response resp *http.Response
) )
@@ -24,14 +22,22 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
if resp != nil && resp.Body != nil { if resp != nil && resp.Body != nil {
resp.Body.Close() resp.Body.Close()
} }
if req != nil {
req.Body.Close()
}
}() }()
method := string(c.Request.Method()) /*
fullBody, err := c.GetReqBodyFull()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to read request body: %v", err))
return
}
reqBodyReader := bytes.NewBuffer(fullBody)
*/
reqBodyReader := bytes.NewBuffer(c.Request.Body()) reqBodyReader, err := c.GetReqBodyBuffer()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to read request body: %v", err))
return
}
//bodyReader := c.Request.BodyStream() // 不可替换为此实现 //bodyReader := c.Request.BodyStream() // 不可替换为此实现
@@ -46,7 +52,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
} }
if cfg.GitClone.Mode == "cache" { if cfg.GitClone.Mode == "cache" {
rb := gitclient.NewRequestBuilder(method, u) rb := gitclient.NewRequestBuilder(c.Request.Method, u)
rb.NoDefaultHeaders() rb.NoDefaultHeaders()
rb.SetBody(reqBodyReader) rb.SetBody(reqBodyReader)
rb.WithContext(ctx) rb.WithContext(ctx)
@@ -65,8 +71,9 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return return
} }
defer resp.Body.Close()
} else { } else {
rb := client.NewRequestBuilder(string(c.Request.Method()), u) rb := client.NewRequestBuilder(c.Request.Method, u)
rb.NoDefaultHeaders() rb.NoDefaultHeaders()
rb.SetBody(reqBodyReader) rb.SetBody(reqBodyReader)
rb.WithContext(ctx) rb.WithContext(ctx)
@@ -85,6 +92,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return return
} }
defer resp.Body.Close()
} }
contentLength := resp.Header.Get("Content-Length") contentLength := resp.Header.Get("Content-Length")
@@ -92,21 +100,25 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
size, err := strconv.Atoi(contentLength) size, err := strconv.Atoi(contentLength)
sizelimit := cfg.Server.SizeLimit * 1024 * 1024 sizelimit := cfg.Server.SizeLimit * 1024 * 1024
if err != nil { if err != nil {
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err) c.Warnf("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, err)
} }
if err == nil && size > sizelimit { if err == nil && size > sizelimit {
finalURL := []byte(resp.Request.URL.String()) finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL) c.Redirect(http.StatusMovedPermanently, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), finalURL, size) c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, size)
return return
} }
} }
for key, values := range resp.Header { /*
for _, value := range values { for key, values := range resp.Header {
c.Response.Header.Add(key, value) for _, value := range values {
c.Response.Header.Add(key, value)
}
} }
} */
//copyHeader( resp.Header)
c.SetHeaders(resp.Header)
headersToRemove := map[string]struct{}{ headersToRemove := map[string]struct{}{
"Content-Security-Policy": {}, "Content-Security-Policy": {},
@@ -131,13 +143,17 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
c.Status(resp.StatusCode) c.Status(resp.StatusCode)
if cfg.GitClone.Mode == "cache" { if cfg.GitClone.Mode == "cache" {
c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate") c.SetHeader("Cache-Control", "no-store, no-cache, must-revalidate")
c.Response.Header.Set("Pragma", "no-cache") c.SetHeader("Pragma", "no-cache")
c.Response.Header.Set("Expires", "0") c.SetHeader("Expires", "0")
} }
bodyReader := resp.Body bodyReader := resp.Body
// 读取body内容
//bodyContent, _ := io.ReadAll(bodyReader)
// c.Infof("%s", bodyContent)
if cfg.RateLimit.BandwidthLimit.Enabled { if cfg.RateLimit.BandwidthLimit.Enabled {
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
} }

View File

@@ -1,39 +1,37 @@
package proxy package proxy
import ( import (
"context"
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"ghproxy/rate"
"regexp" "regexp"
"strings" "strings"
"github.com/cloudwego/hertz/pkg/app" "github.com/infinite-iroha/touka"
) )
var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径 var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { func NoRouteHandler(cfg *config.Config) touka.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) { return func(c *touka.Context) {
var ctx = c.Request.Context()
var shoudBreak bool var shoudBreak bool
shoudBreak = rateCheck(cfg, c, limiter, iplimiter) // shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
if shoudBreak { // if shoudBreak {
return // return
} // }
var ( var (
rawPath string rawPath string
matches []string matches []string
) )
rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ rawPath = strings.TrimPrefix(c.GetRequestURI(), "/") // 去掉前缀/
matches = re.FindStringSubmatch(rawPath) // 匹配路径 matches = re.FindStringSubmatch(rawPath) // 匹配路径
// 匹配路径错误处理 // 匹配路径错误处理
if len(matches) < 3 { if len(matches) < 3 {
logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Warnf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto)
ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.Path()))) ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.GetRequestURI())))
return return
} }
@@ -53,9 +51,6 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
return return
} }
logDump("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
logDump("%s", c.Request.Header.Header())
shoudBreak = listCheck(cfg, c, user, repo, rawPath) shoudBreak = listCheck(cfg, c, user, repo, rawPath)
if shoudBreak { if shoudBreak {
return return
@@ -68,11 +63,12 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
// 处理blob/raw路径 // 处理blob/raw路径
if matcher == "blob" { if matcher == "blob" {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) rawPath = rawPath[18:]
rawPath = "https://raw.githubusercontent.com" + rawPath
rawPath = strings.Replace(rawPath, "/blob/", "/", 1)
matcher = "raw"
} }
logDebug("Matched: %v", matcher)
switch matcher { switch matcher {
case "releases", "blob", "raw", "gist", "api": case "releases", "blob", "raw", "gist", "api":
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher) ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
@@ -80,7 +76,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
GitReq(ctx, c, rawPath, cfg, "git") GitReq(ctx, c, rawPath, cfg, "git")
default: default:
ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched")) ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher) c.Errorf("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.GetRequestURIPath(), rawPath, matcher)
return return
} }
} }

View File

@@ -39,7 +39,7 @@ func initHTTPClient(cfg *config.Config) {
proTolcols.SetHTTP1(true) proTolcols.SetHTTP1(true)
proTolcols.SetHTTP2(true) proTolcols.SetHTTP2(true)
proTolcols.SetUnencryptedHTTP2(true) proTolcols.SetUnencryptedHTTP2(true)
if cfg.Httpc.Mode == "auto" { if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" {
tr = &http.Transport{ tr = &http.Transport{
IdleConnTimeout: 30 * time.Second, IdleConnTimeout: 30 * time.Second,
@@ -57,16 +57,7 @@ func initHTTPClient(cfg *config.Config) {
Protocols: proTolcols, Protocols: proTolcols,
} }
} else { } else {
// 错误的模式 panic("unknown httpc mode: " + cfg.Httpc.Mode)
logError("unknown httpc mode: %s", cfg.Httpc.Mode)
fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
logWarning("use Auto to Run HTTP Client")
fmt.Println("use Auto to Run HTTP Client")
tr = &http.Transport{
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
}
} }
if cfg.Outbound.Enabled { if cfg.Outbound.Enabled {
initTransport(cfg, tr) initTransport(cfg, tr)
@@ -86,7 +77,7 @@ func initHTTPClient(cfg *config.Config) {
func initGitHTTPClient(cfg *config.Config) { func initGitHTTPClient(cfg *config.Config) {
if cfg.Httpc.Mode == "auto" { if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" {
gittr = &http.Transport{ gittr = &http.Transport{
IdleConnTimeout: 30 * time.Second, IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB WriteBufferSize: 32 * 1024, // 32KB
@@ -101,17 +92,7 @@ func initGitHTTPClient(cfg *config.Config) {
ReadBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB
} }
} else { } else {
// 错误的模式 panic("unknown httpc mode: " + cfg.Httpc.Mode)
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")
gittr = &http.Transport{
//MaxIdleConns: 160,
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
}
} }
if cfg.Outbound.Enabled { if cfg.Outbound.Enabled {
initTransport(cfg, gittr) initTransport(cfg, gittr)
@@ -157,7 +138,7 @@ func initGhcrHTTPClient(cfg *config.Config) {
var proTolcols = new(http.Protocols) var proTolcols = new(http.Protocols)
proTolcols.SetHTTP1(true) proTolcols.SetHTTP1(true)
proTolcols.SetHTTP2(true) proTolcols.SetHTTP2(true)
if cfg.Httpc.Mode == "auto" { if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" {
ghcrtr = &http.Transport{ ghcrtr = &http.Transport{
IdleConnTimeout: 30 * time.Second, IdleConnTimeout: 30 * time.Second,
@@ -175,16 +156,7 @@ func initGhcrHTTPClient(cfg *config.Config) {
Protocols: proTolcols, Protocols: proTolcols,
} }
} else { } else {
// 错误的模式 panic(fmt.Sprintf("unknown httpc mode: %s", cfg.Httpc.Mode))
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 { if cfg.Outbound.Enabled {
initTransport(cfg, ghcrtr) initTransport(cfg, ghcrtr)

View File

@@ -10,20 +10,23 @@ import (
) )
var ( var (
githubPrefix = "https://github.com/" githubPrefix = "https://github.com/"
rawPrefix = "https://raw.githubusercontent.com/" rawPrefix = "https://raw.githubusercontent.com/"
gistPrefix = "https://gist.github.com/" gistPrefix = "https://gist.github.com/"
apiPrefix = "https://api.github.com/" gistContentPrefix = "https://gist.githubusercontent.com/"
githubPrefixLen int apiPrefix = "https://api.github.com/"
rawPrefixLen int githubPrefixLen int
gistPrefixLen int rawPrefixLen int
apiPrefixLen int gistPrefixLen int
gistContentPrefixLen int
apiPrefixLen int
) )
func init() { func init() {
githubPrefixLen = len(githubPrefix) githubPrefixLen = len(githubPrefix)
rawPrefixLen = len(rawPrefix) rawPrefixLen = len(rawPrefix)
gistPrefixLen = len(gistPrefix) gistPrefixLen = len(gistPrefix)
gistContentPrefixLen = len(gistContentPrefix)
apiPrefixLen = len(apiPrefix) apiPrefixLen = len(apiPrefix)
//log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen) //log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen)
} }
@@ -114,6 +117,23 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro
return user, "", "gist", nil return user, "", "gist", nil
} }
// 匹配 "https://gist.githubusercontent.com/"
if strings.HasPrefix(rawPath, gistContentPrefix) {
remaining := rawPath[gistContentPrefixLen:]
i := strings.IndexByte(remaining, '/')
if i <= 0 {
// case: https://gist.githubusercontent.com/user
// 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的
if len(remaining) > 0 {
return remaining, "", "gist", nil
}
return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user")
}
// case: https://gist.githubusercontent.com/user/gist_id...
user := remaining[:i]
return user, "", "gist", nil
}
// 匹配 "https://api.github.com/" // 匹配 "https://api.github.com/"
if strings.HasPrefix(rawPath, apiPrefix) { if strings.HasPrefix(rawPath, apiPrefix) {
if !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) { if !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) {

View File

@@ -87,6 +87,12 @@ func TestMatcher_Compatibility(t *testing.T) {
config: cfgWithAuth, config: cfgWithAuth,
expectedUser: "user", expectedRepo: "", expectedMatcher: "gist", expectedUser: "user", expectedRepo: "", expectedMatcher: "gist",
}, },
{
name: "Gist UserContent Path",
rawPath: "https://gist.githubusercontent.com/user/abcdef1234567890",
config: cfgWithAuth,
expectedUser: "user", expectedRepo: "", expectedMatcher: "gist",
},
{ {
name: "API Repos Path (with Auth)", name: "API Repos Path (with Auth)",
rawPath: "https://api.github.com/repos/owner/repo/pulls", rawPath: "https://api.github.com/repos/owner/repo/pulls",

View File

@@ -1,7 +1,3 @@
// Copyright 2025 WJQSERVER, WJQSERVER-STUDIO. All rights reserved.
// 使用本源代码受 WSL 2.0(WJQserver Studio License v2.0)与MPL 2.0(Mozilla Public License v2.0)许可协议的约束
// 此段代码使用双重授权许可, 允许用户选择其中一种许可证
package proxy package proxy
import ( import (
@@ -11,6 +7,8 @@ import (
"ghproxy/config" "ghproxy/config"
"io" "io"
"strings" "strings"
"github.com/infinite-iroha/touka"
) )
func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) { func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) {
@@ -56,21 +54,19 @@ func modifyURL(url string, host string, cfg *config.Config) string {
// 去除url内的https://或http:// // 去除url内的https://或http://
matched, err := EditorMatcher(url, cfg) matched, err := EditorMatcher(url, cfg)
if err != nil { if err != nil {
logDump("Invalid URL: %s", url)
return url return url
} }
if matched { if matched {
var u = url var u = url
u = strings.TrimPrefix(u, "https://") u = strings.TrimPrefix(u, "https://")
u = strings.TrimPrefix(u, "http://") u = strings.TrimPrefix(u, "http://")
logDump("Modified URL: %s", "https://"+host+"/"+u)
return "https://" + host + "/" + u return "https://" + host + "/" + u
} }
return url return url
} }
// processLinks 处理链接,返回包含处理后数据的 io.Reader // processLinks 处理链接,返回包含处理后数据的 io.Reader
func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) { func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config, c *touka.Context) (readerOut io.Reader, written int64, err error) {
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
readerOut = pipeReader readerOut = pipeReader
@@ -79,11 +75,11 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config
if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误 if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误
if err != nil { if err != nil {
if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader
logError("pipeWriter close with error failed: %v, original error: %v", closeErr, err) c.Errorf("pipeWriter close with error failed: %v, original error: %v", closeErr, err)
} }
} else { } else {
if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭 if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭
logError("pipeWriter close failed: %v", closeErr) c.Errorf("pipeWriter close failed: %v", closeErr)
if err == nil { // 如果之前没有错误,记录关闭错误 if err == nil { // 如果之前没有错误,记录关闭错误
err = closeErr err = closeErr
} }
@@ -94,7 +90,7 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config
defer func() { defer func() {
if err := input.Close(); err != nil { if err := input.Close(); err != nil {
logError("input close failed: %v", err) c.Errorf("input close failed: %v", err)
} }
}() }()
@@ -131,7 +127,7 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config
if gzipWriter != nil { if gzipWriter != nil {
if closeErr = gzipWriter.Close(); closeErr != nil { if closeErr = gzipWriter.Close(); closeErr != nil {
logError("gzipWriter close failed %v", closeErr) c.Errorf("gzipWriter close failed %v", closeErr)
// 如果已经存在错误,则保留。否则,记录此错误。 // 如果已经存在错误,则保留。否则,记录此错误。
if err == nil { if err == nil {
err = closeErr err = closeErr
@@ -139,7 +135,7 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config
} }
} }
if flushErr := bufWriter.Flush(); flushErr != nil { if flushErr := bufWriter.Flush(); flushErr != nil {
logError("writer flush failed %v", flushErr) c.Errorf("writer flush failed %v", flushErr)
// 如果已经存在错误,则保留。否则,记录此错误。 // 如果已经存在错误,则保留。否则,记录此错误。
if err == nil { if err == nil {
err = flushErr err = flushErr
@@ -160,7 +156,6 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config
// 替换所有匹配的 URL // 替换所有匹配的 URL
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string { modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
logDump("originalURL: %s", originalURL)
return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义 return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义
}) })

View File

@@ -4,7 +4,7 @@ import (
"ghproxy/config" "ghproxy/config"
"net/http" "net/http"
"github.com/cloudwego/hertz/pkg/app" "github.com/infinite-iroha/touka"
) )
var ( var (
@@ -49,27 +49,31 @@ var (
} }
) )
func setRequestHeaders(c *app.RequestContext, req *http.Request, cfg *config.Config, matcher string) { // copyHeader 将所有头部从 src 复制到 dst。
// 对于多值头部,它会为每个值调用 Add从而保留所有值。
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func setRequestHeaders(c *touka.Context, req *http.Request, cfg *config.Config, matcher string) {
if matcher == "raw" && cfg.Httpc.UseCustomRawHeaders { if matcher == "raw" && cfg.Httpc.UseCustomRawHeaders {
// 使用预定义Header // 使用预定义Header
for key, value := range defaultHeaders { for key, value := range defaultHeaders {
req.Header.Set(key, value) req.Header.Set(key, value)
} }
} else if matcher == "clone" { } else if matcher == "clone" {
c.Request.Header.VisitAll(func(key, value []byte) { copyHeader(req.Header, c.Request.Header)
headerKey := string(key) for key := range cloneHeadersToRemove {
headerValue := string(value) req.Header.Del(key)
if _, shouldRemove := cloneHeadersToRemove[headerKey]; !shouldRemove { }
req.Header.Set(headerKey, headerValue)
}
})
} else { } else {
c.Request.Header.VisitAll(func(key, value []byte) { copyHeader(req.Header, c.Request.Header)
headerKey := string(key) for key := range reqHeadersToRemove {
headerValue := string(value) req.Header.Del(key)
if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove { }
req.Header.Set(headerKey, headerValue)
}
})
} }
} }

View File

@@ -1,42 +1,43 @@
package proxy package proxy
import ( import (
"context"
"ghproxy/config" "ghproxy/config"
"ghproxy/rate"
"strings" "strings"
"github.com/cloudwego/hertz/pkg/app" "github.com/infinite-iroha/touka"
) )
func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { func RoutingHandler(cfg *config.Config) touka.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) { return func(c *touka.Context) {
var shoudBreak bool var shoudBreak bool
shoudBreak = rateCheck(cfg, c, limiter, iplimiter) // shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
if shoudBreak { // if shoudBreak {
return // return
} //}
var ( var (
rawPath string rawPath string
) )
rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ rawPath = strings.TrimPrefix(c.GetRequestURI(), "/") // 去掉前缀/
var ( var (
user string user string
repo string repo string
matcher string
) )
user = c.Param("user") user = c.Param("user")
repo = c.Param("repo") repo = c.Param("repo")
matcher = c.GetString("matcher") matcher, exists := c.GetString("matcher")
if !exists {
ErrorPage(c, NewErrorWithStatusLookup(500, "Matcher Not Found in Context"))
c.Errorf("Matcher Not Found in Context Path: %s", c.GetRequestURIPath())
return
}
logDump("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) ctx := c.Request.Context()
logDump("%s", c.Request.Header.Header())
shoudBreak = listCheck(cfg, c, user, repo, rawPath) shoudBreak = listCheck(cfg, c, user, repo, rawPath)
if shoudBreak { if shoudBreak {
@@ -50,14 +51,15 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
// 处理blob/raw路径 // 处理blob/raw路径
if matcher == "blob" { if matcher == "blob" {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) rawPath = rawPath[10:]
rawPath = "raw.githubusercontent.com" + rawPath
rawPath = strings.Replace(rawPath, "/blob/", "/", 1)
matcher = "raw"
} }
// 为rawpath加入https:// 头 // 为rawpath加入https:// 头
rawPath = "https://" + rawPath rawPath = "https://" + rawPath
logDebug("Matched: %v", matcher)
switch matcher { switch matcher {
case "releases", "blob", "raw", "gist", "api": case "releases", "blob", "raw", "gist", "api":
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher) ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
@@ -65,7 +67,7 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
GitReq(ctx, c, rawPath, cfg, "git") GitReq(ctx, c, rawPath, cfg, "git")
default: default:
ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched")) ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher) c.Errorf("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.GetRequestURIPath(), rawPath, matcher)
return return
} }
} }

View File

@@ -4,12 +4,11 @@ import (
"fmt" "fmt"
"ghproxy/auth" "ghproxy/auth"
"ghproxy/config" "ghproxy/config"
"ghproxy/rate"
"github.com/cloudwego/hertz/pkg/app" "github.com/infinite-iroha/touka"
) )
func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) bool { func listCheck(cfg *config.Config, c *touka.Context, user string, repo string, rawPath string) bool {
if cfg.Auth.ForceAllowApi && cfg.Auth.ForceAllowApiPassList { if cfg.Auth.ForceAllowApi && cfg.Auth.ForceAllowApiPassList {
return false return false
} }
@@ -18,7 +17,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
whitelist := auth.CheckWhitelist(user, repo) whitelist := auth.CheckWhitelist(user, repo)
if !whitelist { if !whitelist {
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo))) 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) c.Infof("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, user, repo)
return true return true
} }
} }
@@ -28,7 +27,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
blacklist := auth.CheckBlacklist(user, repo) blacklist := auth.CheckBlacklist(user, repo)
if blacklist { if blacklist {
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo))) 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) c.Infof("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, user, repo)
return true return true
} }
} }
@@ -37,13 +36,13 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
} }
// 鉴权 // 鉴权
func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPath string) bool { func authCheck(c *touka.Context, cfg *config.Config, matcher string, rawPath string) bool {
var err error var err error
if matcher == "api" && !cfg.Auth.ForceAllowApi { if matcher == "api" && !cfg.Auth.ForceAllowApi {
if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
ErrorPage(c, NewErrorWithStatusLookup(403, "Github API Req without AuthHeader is Not Allowed")) ErrorPage(c, NewErrorWithStatusLookup(403, "Github API Req without AuthHeader is Not Allowed"))
logInfo("%s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath) c.Infof("%s %s %s AuthHeader Unavailable", c.ClientIP(), c.Request.Method, rawPath)
return true return true
} }
} }
@@ -54,34 +53,7 @@ func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPat
authcheck, err = auth.AuthHandler(c, cfg) authcheck, err = auth.AuthHandler(c, cfg)
if !authcheck { if !authcheck {
ErrorPage(c, NewErrorWithStatusLookup(401, fmt.Sprintf("Unauthorized: %v", err))) ErrorPage(c, NewErrorWithStatusLookup(401, fmt.Sprintf("Unauthorized: %v", err)))
logInfo("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) c.Infof("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, err)
return true
}
}
return false
}
func rateCheck(cfg *config.Config, c *app.RequestContext, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) bool {
// 限制访问频率
if cfg.RateLimit.Enabled {
var allowed bool
switch cfg.RateLimit.RateMethod {
case "ip":
allowed = iplimiter.Allow(c.ClientIP())
case "total":
allowed = limiter.Allow()
default:
logWarning("Invalid RateLimit Method")
ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid RateLimit Method"))
return true
}
if !allowed {
ErrorPage(c, NewErrorWithStatusLookup(429, fmt.Sprintf("Too Many Requests; Rate Limit is %d per minute", cfg.RateLimit.RatePerMinute)))
logInfo("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
return true return true
} }
} }

View File

@@ -1,107 +0,0 @@
package rate
import (
"sync"
"time"
"github.com/WJQSERVER-STUDIO/logger"
"golang.org/x/time/rate"
)
// 日志模块
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
// RateLimiter 总体限流器
type RateLimiter struct {
limiter *rate.Limiter
}
// New 创建一个总体限流器
func New(limit int, burst int, duration time.Duration) *RateLimiter {
if limit <= 0 {
limit = 1
logWarning("rate limit per minute must be positive, setting to 1")
}
if burst <= 0 {
burst = 1
logWarning("rate limit burst must be positive, setting to 1")
}
rateLimit := rate.Limit(float64(limit) / duration.Seconds())
return &RateLimiter{
limiter: rate.NewLimiter(rateLimit, burst),
}
}
// Allow 检查是否允许请求通过
func (rl *RateLimiter) Allow() bool {
return rl.limiter.Allow()
}
// IPRateLimiter 基于IP的限流器
type IPRateLimiter struct {
limiters map[string]*RateLimiter // 用户级限流器 map
mu sync.RWMutex // 保护 limiters map
limit int // 每 duration 时间段内允许的请求数
burst int // 突发请求数
duration time.Duration // 限流周期
}
// NewIPRateLimiter 创建一个基于IP的限流器
func NewIPRateLimiter(ipLimit int, ipBurst int, duration time.Duration) *IPRateLimiter {
if ipLimit <= 0 {
ipLimit = 1
logWarning("IP rate limit per minute must be positive, setting to 1")
}
if ipBurst <= 0 {
ipBurst = 1
logWarning("IP rate limit burst must be positive, setting to 1")
}
logInfo("IP Rate Limiter initialized with limit: %d, burst: %d, duration: %v", ipLimit, ipBurst, duration)
return &IPRateLimiter{
limiters: make(map[string]*RateLimiter),
limit: ipLimit,
burst: ipBurst,
duration: duration,
}
}
// Allow 检查给定IP的请求是否允许通过
func (rl *IPRateLimiter) Allow(ip string) bool {
if ip == "" {
logWarning("empty ip for rate limiting")
return false
}
// 使用读锁快速查找
rl.mu.RLock()
limiter, found := rl.limiters[ip]
rl.mu.RUnlock()
if found {
return limiter.Allow()
}
// 未找到,获取写锁来创建和添加
rl.mu.Lock()
// 双重检查
limiter, found = rl.limiters[ip]
if !found {
newL := New(rl.limit, rl.burst, rl.duration)
rl.limiters[ip] = newL
limiter = newL
}
rl.mu.Unlock()
return limiter.Allow()
}